| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- /**
- * 验证「复制」在 **HTTP(非安全上下文)** 下仍可用。
- *
- * 为什么要有它:`navigator.clipboard` **只在安全上下文存在**(HTTPS / localhost)。
- * 用户 2026-09-29 要部署到 HTTP ⇒ 原来的写法
- * `navigator.clipboard.writeText(x).catch(() => { …execCommand 兜底… })`
- * 会在取 `.writeText` 时**同步抛 TypeError**,`.catch()` 根本轮不到 ⇒ 复制按钮直接报错。
- * 现在改成「显式判断 + try/catch + execCommand 兜底」。
- *
- * 怎么跑(纯 node,不需要打包):
- * node harness/tools/verify-clipboard-fallback.mjs
- *
- * 做法:从 `FeedbackWidget.vue` 里**抽出真实的 `writeClipboard` 源码**(用 esbuild 去掉 TS 类型),
- * 配三组桩跑:① HTTP(没有 clipboard)② 正常 HTTPS ③ clipboard 存在但 reject。
- */
- import { readFileSync } from 'node:fs';
- import { transform } from 'esbuild';
- let pass = 0;
- let fail = 0;
- const check = (label, ok, extra = '') => {
- if (ok) {
- pass += 1;
- console.log(` ok ${label}${extra ? ` —— ${extra}` : ''}`);
- } else {
- fail += 1;
- console.log(` FAIL ${label}${extra ? ` —— ${extra}` : ''}`);
- }
- };
- const vue = readFileSync('src/components/Common/FeedbackWidget.vue', 'utf8');
- const start = vue.indexOf('async function writeClipboard');
- if (start === -1) {
- console.error('❌ 没在 FeedbackWidget.vue 里找到 writeClipboard()');
- process.exit(1);
- }
- // 花括号配平取出整个函数体(跳过字符串/模板里的花括号;这个函数很简单,够用)
- let depth = 0;
- let end = -1;
- for (let i = vue.indexOf('{', start); i < vue.length; i += 1) {
- const ch = vue[i];
- if (ch === '{') depth += 1;
- else if (ch === '}') {
- depth -= 1;
- if (depth === 0) { end = i + 1; break; }
- }
- }
- const tsSource = vue.slice(start, end);
- check('从 SFC 里抽到了 writeClipboard 源码', tsSource.includes('execCommand'), `长度 ${tsSource.length}`);
- const jsSource = (await transform(tsSource, { loader: 'ts', format: 'esm' })).code;
- /** 造一组最小的 document 桩,记录 execCommand 是否被调用 */
- const makeDocument = () => {
- const calls = { execCommand: 0, appended: 0, removed: 0 };
- const el = { value: '', style: {}, select: () => {}, };
- const doc = {
- createElement: () => el,
- body: {
- appendChild: () => { calls.appended += 1; },
- removeChild: () => { calls.removed += 1; },
- },
- execCommand: (cmd) => { calls.execCommand += 1; return cmd === 'copy'; },
- };
- return { doc, calls };
- };
- const run = async (navigatorStub) => {
- const { doc, calls } = makeDocument();
- const factory = new Function('navigator', 'document', `${jsSource}\nreturn writeClipboard;`);
- const fn = factory(navigatorStub, doc);
- let threw = null;
- try { await fn('待复制的内容'); } catch (e) { threw = e; }
- return { calls, threw };
- };
- console.log('\n【1】HTTP(非安全上下文):navigator.clipboard 不存在 → 必须走 execCommand 兜底且不抛错');
- {
- const { calls, threw } = await run({});
- check('没有抛错', threw === null, threw ? String(threw).slice(0, 60) : '');
- check('走了 execCommand 兜底', calls.execCommand === 1, `execCommand 调用 ${calls.execCommand} 次`);
- check('临时 textarea 加了又移除(不漏在 DOM 里)', calls.appended === 1 && calls.removed === 1,
- `append ${calls.appended} / remove ${calls.removed}`);
- }
- console.log('\n【2】HTTPS(安全上下文):clipboard.writeText 可用 → 用它,不碰 execCommand');
- {
- let written = '';
- const { calls, threw } = await run({ clipboard: { writeText: async (t) => { written = t; } } });
- check('没有抛错', threw === null);
- check('用了 clipboard.writeText', written === '待复制的内容');
- check('没有走 execCommand', calls.execCommand === 0);
- }
- console.log('\n【3】clipboard 存在但抛错(如权限被拒)→ 回退到 execCommand');
- {
- const { calls, threw } = await run({
- clipboard: { writeText: () => { throw new Error('NotAllowedError'); } },
- });
- check('没有抛错(同步抛错也被接住)', threw === null, threw ? String(threw).slice(0, 60) : '');
- check('回退到 execCommand', calls.execCommand === 1);
- }
- console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
- process.exit(fail ? 1 : 0);
|