fix(runtime): use jQuery, not bare $, in the lazily loaded clipboard lib
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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
'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/);
|
||||
});
|
||||
Reference in New Issue
Block a user