WordPress runs jQuery in noConflict mode, so bare $ is undefined. The clipboard module called $(this) on every click and $.isFunction on the copy result, so any click on a [data-clipboard-text] button threw "$ is not a function" and copied nothing. This was latent: the only prior emitter was the paid card, and the click path only ran when a user actually clicked it. Exposing the copy button on the download box made it reachable on every resource, surfacing the error. Bind through jQuery explicitly, guard notyf so a missing toast lib cannot also throw, and drop the _win.bd dependency (that payload only gains .bd once main.js runs). Contract-lock the module under simulated noConflict. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.5 KiB
JavaScript
100 lines
3.5 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
|
|
|
// Strip string literals so a "$" inside a message is not mistaken for a call.
|
|
const stripStrings = (src) =>
|
|
src.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
|
|
// A jQuery stand-in exposing only what clipboard.min.js touches. Bare $ is
|
|
// deliberately NOT provided, matching WordPress's noConflict mode.
|
|
function makeSandbox() {
|
|
const handlers = [];
|
|
const created = [];
|
|
const toasts = [];
|
|
|
|
const body = {
|
|
on(evt, childSel, ns, fn) {
|
|
handlers.push({ evt, childSel, ns, fn });
|
|
return this;
|
|
},
|
|
};
|
|
|
|
const documentStub = {
|
|
querySelector: (sel) => (sel === '#copy-input' ? created[created.length - 1] || null : null),
|
|
createElement: () => {
|
|
const el = {
|
|
style: {}, value: '', id: '', readOnly: '',
|
|
setSelectionRange() {}, select() {}, blur() {},
|
|
};
|
|
created.push(el);
|
|
return el;
|
|
},
|
|
execCommand: () => true,
|
|
};
|
|
|
|
const jq = (sel) => {
|
|
if (sel === 'body') return body;
|
|
if (sel && sel.attrs) return { attr: (n) => sel.attrs[n] };
|
|
return { on() { return this; } };
|
|
};
|
|
jq.__isElement = true;
|
|
|
|
return { sandbox: { window: {}, document: documentStub, jQuery: jq, notyf: (m, t) => toasts.push({ m, t }) }, handlers, created, toasts, body };
|
|
}
|
|
|
|
test('the lazily loaded clipboard lib survives WordPress noConflict', () => {
|
|
const src = read('assets/js/runtime/libs/clipboard.min.js');
|
|
|
|
// No bare $ calls or $.<method> usage anywhere in the module.
|
|
const bare = stripStrings(src);
|
|
assert.doesNotMatch(bare, /(^|[^.\w$])\$\(/, 'clipboard.min.js uses a bare $(');
|
|
assert.doesNotMatch(bare, /\$\.\w+/, 'clipboard.min.js uses a bare $.<method>');
|
|
|
|
// It must bind through jQuery explicitly on the element that always exists.
|
|
assert.match(src, /jQuery\("body"\)\.on\("click",\s*"\[data-clipboard-text\]"/);
|
|
|
|
const { sandbox, handlers, created, toasts } = makeSandbox();
|
|
sandbox.window.jQuery = sandbox.jQuery;
|
|
|
|
// Evaluating with no global $ must not throw.
|
|
assert.doesNotThrow(() => {
|
|
new Function('window', 'document', 'jQuery', 'notyf', '"use strict";\n' + src)(
|
|
sandbox.window, sandbox.document, sandbox.jQuery, sandbox.notyf
|
|
);
|
|
});
|
|
|
|
assert.equal(handlers.length, 1);
|
|
assert.equal(handlers[0].evt, 'click');
|
|
assert.equal(handlers[0].childSel, '[data-clipboard-text]');
|
|
|
|
// Clicking must copy the copy content and toast the copy name.
|
|
const el = {
|
|
attrs: { 'data-clipboard-text': 'a1b2', 'data-clipboard-tag': '提取码' },
|
|
parentNode: { appendChild() {} },
|
|
};
|
|
assert.doesNotThrow(() => handlers[0].fn.call(el, {}));
|
|
assert.equal(created[created.length - 1].value, 'a1b2');
|
|
assert.deepEqual(toasts, [{ m: '提取码已复制', t: undefined }]);
|
|
});
|
|
|
|
test('the readable clipboard source agrees with the loaded bundle', () => {
|
|
const src = read('assets/js/runtime/libs/clipboard.js');
|
|
const bare = stripStrings(src);
|
|
assert.doesNotMatch(bare, /(^|[^.\w$])\$\(/, 'clipboard.js uses a bare $(');
|
|
assert.match(src, /jQuery\('body'\)\.on\('click'/);
|
|
assert.match(src, /typeof notyf === 'function'/);
|
|
});
|
|
|
|
test('bootstrap is unaffected because it receives jQuery as an IIFE argument', () => {
|
|
const src = read('assets/js/runtime/libs/bootstrap.min.js');
|
|
assert.match(src, /!function\(\w\)\{"use strict"/);
|
|
assert.match(src, /requires jQuery/);
|
|
});
|