feat: add draggable panel and restrict assistant to site directory
This commit is contained in:
+180
-13
@@ -1,14 +1,14 @@
|
||||
/* 完整面板与跨网址存储验证;GM 桥接仅用于本机模拟,不请求业务服务器。 */
|
||||
/* 指定主站范围、完整面板与同站跨路径存储验证;所有主站请求均由本机模拟。 */
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const http = require('node:http');
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { chromium } = require('playwright');
|
||||
const workspace = path.resolve(__dirname, '..');
|
||||
const script = fs.readFileSync(path.join(workspace, '谊安学助手 2.0.user.js'), 'utf8');
|
||||
assert(script.includes('// @match *://*/*'));
|
||||
assert(!/\/\/ @match\s+https?:\/\/10\./.test(script));
|
||||
assert.deepEqual(Array.from(script.matchAll(/^\/\/ @match\s+(.+)$/gm), match => match[1].trim()), ['http://10.119.67.7/clientcn/*']);
|
||||
assert(script.includes('// @name 谊安学助手 2.0'));
|
||||
const qaDir = process.env.YIAN_QA_DIR || path.join(os.tmpdir(), 'yianxue-assistant-qa');
|
||||
fs.mkdirSync(qaDir, { recursive: true });
|
||||
@@ -42,7 +42,7 @@ function practiceHTML() {
|
||||
`<div app-exam-question class="pc_exam_list"><a id="examtest${index}"></a><div class="pc_examtest_content" data-kind="${q.type}" data-number="${index + 1}"><div class="pc_examtest_content_question"><p>[${q.type}] ${index + 1}. ${q.stem}<span class="pc_examtest_content_question_showanswer">查看答案</span></p></div><div class="pc_examtest_content_list">${q.options.map((text, i) => `<div class="pc_examtest_content_list_child1"><div class="pc_examtest_content_list_des${index === 1 && i === 0 ? ' examtest_content_list_des_checked' : ''}"><span class="examtest_content_list_des_optionsort">${String.fromCharCode(65 + i)}</span>${text}</div></div>`).join('')}</div><div class="hidden"><div class="examtest_content_answer_left">参考答案:<span class="answer"></span></div><div class="pc_examtest_content_resolve">题目解析:</div></div></div></div>`).join('')}<div class="pc_examtest_card">${questions.map((_, i) => `<div class="examtestcard_count_defult">${i + 1}</div>`).join('')}</div><div class="pc_examtest_commit">重新练习</div><button id="submit">交卷</button></app-examtest>`;
|
||||
}
|
||||
function documentHTML(practice) {
|
||||
return `<!doctype html><meta charset="utf-8"><title>${practice ? '助手练习验证' : '普通网页验证'}</title><style>body{font:16px system-ui;margin:32px;background:#f4f7fa;user-select:none}.pc_examtest_content{padding:16px;background:white;border-bottom:1px solid #ddd;max-width:750px}.pc_examtest_content_list_des{padding:8px;cursor:pointer}.examtest_content_list_des_checked{background:#dff1e5}.hidden{display:none}.pc_examtest_title{font-size:24px;margin:18px 0}.pc_examtest_content_question_showanswer{margin-left:12px;color:#3268ac}</style><body onselectstart="return false" oncontextmenu="return false"><h1>${practice ? '练习页面' : '任意网址的题库搜索'}</h1><p id="copy-target">广西华谊新材料有限公司涉及几种高危工艺</p>${practice ? practiceHTML() : '<p>网页没有练习题目,仍可使用岗位题库搜索与复制开关。</p>'}<script>
|
||||
return `<!doctype html><meta charset="utf-8"><title>${practice ? '助手练习验证' : '主站主页验证'}</title><style>body{font:16px system-ui;margin:32px;background:#f4f7fa;user-select:none}.pc_examtest_content{padding:16px;background:white;border-bottom:1px solid #ddd;max-width:750px}.pc_examtest_content_list_des{padding:8px;cursor:pointer}.examtest_content_list_des_checked{background:#dff1e5}.hidden{display:none}.pc_examtest_title{font-size:24px;margin:18px 0}.pc_examtest_content_question_showanswer{margin-left:12px;color:#3268ac}</style><body onselectstart="return false" oncontextmenu="return false"><h1>${practice ? '练习页面' : '主站主页的题库搜索'}</h1><p id="copy-target">广西华谊新材料有限公司涉及几种高危工艺</p>${practice ? practiceHTML() : '<p>网页没有练习题目,仍可使用岗位题库搜索与复制开关。</p>'}<script>
|
||||
window.optionClicks=[];window.parseClicks=0;window.submitClicks=0;
|
||||
document.addEventListener('click',function(event){const n=event.target.closest('.pc_examtest_content_list_des');if(n){const box=n.closest('.pc_examtest_content');if(box.dataset.showanswer==='true')return;const label=n.querySelector('.examtest_content_list_des_optionsort').textContent;window.optionClicks.push(box.dataset.number+':'+label);if(box.dataset.kind==='多选')n.classList.toggle('examtest_content_list_des_checked');else{box.querySelectorAll('.pc_examtest_content_list_des').forEach(x=>x.classList.remove('examtest_content_list_des_checked'));n.classList.add('examtest_content_list_des_checked');box.dataset.showanswer='true';box.querySelector('.pc_examtest_content_question_showanswer').remove();box.querySelectorAll('.pc_examtest_content_list_child1').forEach(x=>x.className='pc_examtest_content_list_child2');}}if(event.target.closest('.pc_examtest_content_question_showanswer'))window.parseClicks++;if(event.target.id==='submit')window.submitClicks++;});
|
||||
</script></body>`;
|
||||
@@ -55,13 +55,55 @@ function documentHTML(practice) {
|
||||
res.writeHead(200, { 'Content-Type': 'text/html;charset=utf-8' });
|
||||
res.end(documentHTML(req.url.includes('exercise') || req.url.includes('flag=test')));
|
||||
});
|
||||
await new Promise(resolve => server.listen(0, resolve));
|
||||
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
const port = server.address().port;
|
||||
const localOrigin = 'http://127.0.0.1:' + port;
|
||||
const origin = 'http://10.119.67.7';
|
||||
let browser;
|
||||
try {
|
||||
browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || 'chrome', headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1366, height: 900 }, acceptDownloads: true });
|
||||
await context.exposeBinding('__gmRead', (source, key, fallback) => memory.has(key) ? structuredClone(memory.get(key)) : fallback);
|
||||
const externalRequests = [];
|
||||
const rejectedURLs = [
|
||||
'https://10.119.67.7/clientcn/main',
|
||||
'http://10.119.67.8/clientcn/main',
|
||||
'http://localhost/clientcn/main',
|
||||
'http://10.119.67.7:8080/clientcn/main',
|
||||
origin + '/outside/main',
|
||||
origin + '/clientcn-other/main',
|
||||
origin + '/clientcn'
|
||||
];
|
||||
const rejectedNavigation = new Set(rejectedURLs);
|
||||
await context.route('**/*', async route => {
|
||||
const request = route.request(), url = new URL(request.url());
|
||||
// Even the exact business origin is never continued onto the network.
|
||||
if (url.origin === origin) {
|
||||
const response = await route.fetch({ url: localOrigin + url.pathname + url.search });
|
||||
return route.fulfill({ response });
|
||||
}
|
||||
// Deliberate negative navigation fixtures are fulfilled without reaching those hosts.
|
||||
if (request.isNavigationRequest() && rejectedNavigation.has(url.href)) {
|
||||
return route.fulfill({ status: 200, contentType: 'text/html;charset=utf-8', body: documentHTML(false) });
|
||||
}
|
||||
if (url.origin === localOrigin || url.protocol === 'file:' && url.href.startsWith(pathToFileURL(qaDir).href + '/')) return route.continue();
|
||||
externalRequests.push(url.href); return route.abort();
|
||||
});
|
||||
let delayedPositionPage = null;
|
||||
let delayedPositionRead = null;
|
||||
let signalPositionRead = null;
|
||||
const positionKey = 'yianxue.assistant.setting.v2.panelPosition';
|
||||
await context.exposeBinding('__gmRead', (source, key, fallback) => {
|
||||
const captured = memory.has(key) ? structuredClone(memory.get(key)) : fallback;
|
||||
// Hold only the first position read from one dedicated page; subsequent reads
|
||||
// and all other pages/keys remain immediate, including verification of new writes.
|
||||
if (source.page === delayedPositionPage && key === positionKey && !delayedPositionRead) {
|
||||
return new Promise(resolve => {
|
||||
delayedPositionRead = { captured, release: () => resolve(captured) };
|
||||
if (signalPositionRead) signalPositionRead();
|
||||
});
|
||||
}
|
||||
return captured;
|
||||
});
|
||||
await context.exposeBinding('__gmWrite', (source, key, value) => { memory.set(key, structuredClone(value)); });
|
||||
await context.exposeBinding('__gmCopy', (source, text) => { clipboard = text; });
|
||||
// API 仅作为沙箱全局变量存在,刻意不放到 window 上。
|
||||
@@ -71,17 +113,17 @@ function documentHTML(practice) {
|
||||
const errors = [];
|
||||
function capture(page) { page.on('pageerror', e => errors.push(e.message)); page.on('console', m => { if (m.type() === 'error') errors.push(m.text()); }); }
|
||||
const page = await context.newPage(); capture(page);
|
||||
const startURL = `http://127.0.0.1:${port}/completely-unrelated-path`;
|
||||
const startURL = origin + '/clientcn/main?from=assistant-test';
|
||||
await page.goto(startURL); await page.addScriptTag({ content: sandboxScript });
|
||||
await page.evaluate(() => YianAssistant.ready);
|
||||
assert.equal(page.url(), startURL); assert.equal(await page.title(), '普通网页验证');
|
||||
assert(await page.getByRole('heading', { name: '任意网址的题库搜索' }).isVisible());
|
||||
assert.equal(page.url(), startURL); assert.equal(await page.title(), '主站主页验证');
|
||||
assert(await page.getByRole('heading', { name: '主站主页的题库搜索' }).isVisible());
|
||||
await page.getByRole('button', { name: '展开题库助手' }).click();
|
||||
assert(await page.locator('#yian-question-assistant .brand').textContent() === '谊安学助手 2.0');
|
||||
assert.equal(await page.getByRole('switch', { name: '导出题库', exact: true }).isChecked(), false);
|
||||
assert(await page.getByRole('switch', { name: '自动选择答案' }).isDisabled());
|
||||
assert.equal(await page.evaluate(() => typeof window.GM_getValue), 'undefined');
|
||||
console.log('PASS 任意路径显示搜索面板,非练习页禁止自动选择,沙箱API兼容');
|
||||
console.log('PASS 主站 /clientcn/main 查询参数下显示搜索面板,非练习页禁止自动选择,沙箱 API 兼容');
|
||||
|
||||
const input = await bankInput();
|
||||
await page.locator('#yian-question-assistant input[type=file]').setInputFiles(input.file);
|
||||
@@ -134,7 +176,7 @@ function documentHTML(practice) {
|
||||
console.log('PASS 无效导入提示与原题库保留');
|
||||
|
||||
const second = await context.newPage(); capture(second);
|
||||
const secondURL = `http://localhost:${port}/different/application/route?flag=exercise`;
|
||||
const secondURL = origin + '/clientcn/exam/nested/fixture/questions?flag=exercise';
|
||||
await second.goto(secondURL); await second.addScriptTag({ content: sandboxScript }); await second.evaluate(() => YianAssistant.ready);
|
||||
await second.waitForFunction(() => YianAssistant.getState().pageReady);
|
||||
assert.equal(await second.getByRole('switch', { name: '导出题库', exact: true }).isChecked(), false, '旧版导出偏好不能开启新版开关');
|
||||
@@ -151,9 +193,9 @@ function documentHTML(practice) {
|
||||
await second.getByRole('switch', { name: '自动选择答案' }).check();
|
||||
await second.waitForTimeout(500); assert.equal(await second.evaluate(() => window.optionClicks.length), 5);
|
||||
await second.getByRole('switch', { name: '自动选择答案' }).uncheck();
|
||||
console.log('PASS 更换域名/路径后题库岗位共用,实际选项点击、换序、多选差集与无重复');
|
||||
console.log('PASS 同站不同 /clientcn 路径题库岗位共用,实际选项点击、换序、多选差集与无重复');
|
||||
|
||||
await second.evaluate(html => { history.pushState({}, '', '/another/random-route?flag=exercise'); const doc = new DOMParser().parseFromString(html, 'text/html'); document.querySelector('app-examtest').replaceWith(doc.querySelector('app-examtest')); window.optionClicks = []; }, practiceHTML());
|
||||
await second.evaluate(html => { history.pushState({}, '', '/clientcn/exam/another/random-route?flag=exercise'); const doc = new DOMParser().parseFromString(html, 'text/html'); document.querySelector('app-examtest').replaceWith(doc.querySelector('app-examtest')); window.optionClicks = []; }, practiceHTML());
|
||||
await second.waitForFunction(() => YianAssistant.getContext().ready);
|
||||
await second.getByRole('switch', { name: '自动选择答案' }).check();
|
||||
await second.waitForFunction(() => window.optionClicks.length > 0);
|
||||
@@ -177,7 +219,132 @@ function documentHTML(practice) {
|
||||
await page.setViewportSize({ width: 1366, height: 900 });
|
||||
await page.locator('#yian-question-assistant .body').evaluate(node => { node.scrollTop = 0; });
|
||||
await page.locator('#yian-question-assistant').screenshot({ path: path.join(qaDir, 'assistant-panel.png') });
|
||||
|
||||
// Drag the real header with browser input; persist the released coordinates through GM.
|
||||
const beforeDrag = await page.locator('#yian-question-assistant').boundingBox();
|
||||
const brand = await page.locator('#yian-question-assistant .brand').boundingBox();
|
||||
assert(beforeDrag && brand);
|
||||
await page.mouse.move(brand.x + brand.width / 2, brand.y + brand.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(brand.x + brand.width / 2 - 260, brand.y + brand.height / 2 + 35, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
const draggedPosition = await page.evaluate(() => YianAssistant.getState().panelPosition);
|
||||
assert(draggedPosition && Number.isFinite(draggedPosition.left) && Number.isFinite(draggedPosition.top));
|
||||
assert(draggedPosition.left < beforeDrag.x - 100, '真实拖拽应改变面板位置');
|
||||
assert(await page.locator('#yian-question-assistant .body').isVisible(), '拖动标题不能误触收起');
|
||||
await page.waitForFunction(async position => JSON.stringify(await YianBankStore.getSetting('panelPosition', null)) === JSON.stringify(position), draggedPosition);
|
||||
await page.reload(); await page.addScriptTag({ content: sandboxScript }); await page.evaluate(() => YianAssistant.ready);
|
||||
assert.deepEqual(await page.evaluate(() => YianAssistant.getState().panelPosition), draggedPosition, '刷新主站后应恢复 GM 保存位置');
|
||||
const restoredPosition = await page.locator('#yian-question-assistant').boundingBox();
|
||||
assert(Math.abs(restoredPosition.x - draggedPosition.left) < 2 && Math.abs(restoredPosition.y - draggedPosition.top) < 2);
|
||||
await page.getByRole('button', { name: '展开题库助手' }).click();
|
||||
assert(await page.locator('#yian-question-assistant .body').isVisible(), '恢复位置后仍可展开面板');
|
||||
console.log('PASS 标题真实鼠标拖拽、不误折叠、GM 保存坐标与刷新主站后的面板位置恢复');
|
||||
|
||||
// A late initial settings response cannot overwrite a new position chosen by the user.
|
||||
const positionReadStarted = new Promise(resolve => { signalPositionRead = resolve; });
|
||||
memory.set(positionKey, { left: 100, top: 100 });
|
||||
delayedPositionPage = await context.newPage(); capture(delayedPositionPage);
|
||||
await delayedPositionPage.goto(origin + '/clientcn/main?scope=position-read-race');
|
||||
await delayedPositionPage.addScriptTag({ content: sandboxScript });
|
||||
await positionReadStarted;
|
||||
assert.deepEqual(delayedPositionRead.captured, { left: 100, top: 100 }, '迟到读取必须返回读取时捕获的旧坐标');
|
||||
await delayedPositionPage.evaluate(() => { window.__qaReadyComplete = false; YianAssistant.ready.then(() => { window.__qaReadyComplete = true; }); });
|
||||
assert.equal(await delayedPositionPage.evaluate(() => window.__qaReadyComplete), false);
|
||||
const raceHost = await delayedPositionPage.locator('#yian-question-assistant').boundingBox();
|
||||
const raceBrand = await delayedPositionPage.locator('#yian-question-assistant .brand').boundingBox();
|
||||
assert(raceHost && raceBrand);
|
||||
assert(await delayedPositionPage.getByRole('button', { name: '展开题库助手' }).isVisible(), '加载期间仍为可拖动的收起标题');
|
||||
const grabOffset = { x: raceBrand.x + raceBrand.width / 2 - raceHost.x, y: raceBrand.y + raceBrand.height / 2 - raceHost.y };
|
||||
await delayedPositionPage.mouse.move(raceHost.x + grabOffset.x, raceHost.y + grabOffset.y);
|
||||
await delayedPositionPage.mouse.down();
|
||||
await delayedPositionPage.mouse.move(220 + grabOffset.x, 60 + grabOffset.y, { steps: 14 });
|
||||
await delayedPositionPage.mouse.up();
|
||||
const newPosition = await delayedPositionPage.evaluate(() => YianAssistant.getState().panelPosition);
|
||||
assert.deepEqual(newPosition, { left: 220, top: 60 }, '加载完成前的真实拖动应记录新位置');
|
||||
await delayedPositionPage.waitForFunction(async position => JSON.stringify(await YianBankStore.getSetting('panelPosition', null)) === JSON.stringify(position), newPosition);
|
||||
assert.deepEqual(memory.get(positionKey), newPosition, '释放旧读取前,新位置已写入 GM');
|
||||
assert.equal(await delayedPositionPage.evaluate(() => window.__qaReadyComplete), false, '拖动和写入必须发生在 ready 等待期间');
|
||||
delayedPositionRead.release();
|
||||
await delayedPositionPage.evaluate(() => YianAssistant.ready);
|
||||
assert.equal(await delayedPositionPage.evaluate(() => window.__qaReadyComplete), true);
|
||||
assert.deepEqual(await delayedPositionPage.evaluate(() => YianAssistant.getState().panelPosition), newPosition, '迟到旧设置不得覆盖当前用户坐标');
|
||||
assert.deepEqual(memory.get(positionKey), newPosition, 'GM 中仍应保留新坐标');
|
||||
const settledRacePosition = await delayedPositionPage.locator('#yian-question-assistant').boundingBox();
|
||||
assert(Math.abs(settledRacePosition.x - newPosition.left) < 2 && Math.abs(settledRacePosition.y - newPosition.top) < 2, 'ready 后的实际面板应停留在用户新位置');
|
||||
await delayedPositionPage.close(); delayedPositionPage = null; signalPositionRead = null;
|
||||
console.log('PASS 初始 GM 位置读取延迟期间真实拖动,迟到旧坐标不覆盖新配置、GM 写值或实际面板位置');
|
||||
|
||||
// A running SPA session must suspend its UI and copy controller immediately on leaving scope.
|
||||
await second.evaluate(html => {
|
||||
history.replaceState({}, '', '/clientcn/exam/scope-start/questions?flag=exercise');
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
document.querySelector('app-examtest').replaceWith(doc.querySelector('app-examtest'));
|
||||
window.optionClicks = [];
|
||||
dispatchEvent(new PopStateEvent('popstate'));
|
||||
}, practiceHTML());
|
||||
await second.waitForFunction(() => YianAssistant.getContext().ready);
|
||||
const leftScope = await second.evaluate(() => {
|
||||
YianAssistant.setCopy(true);
|
||||
const enabled = YianAssistant.setAutoAnswer(true);
|
||||
YianAssistant.setExportEnabled(true);
|
||||
const before = YianAssistant.getState();
|
||||
history.pushState({}, '', '/outside/spa-scope?flag=exercise');
|
||||
dispatchEvent(new PopStateEvent('popstate'));
|
||||
return { enabled, before, after: YianAssistant.getState() };
|
||||
});
|
||||
assert.equal(leftScope.enabled, true);
|
||||
assert.equal(leftScope.before.autoAnswer, true); assert.equal(leftScope.before.exportEnabled, true);
|
||||
await second.waitForFunction(() => getComputedStyle(document.querySelector('#yian-question-assistant')).display === 'none');
|
||||
assert.equal(leftScope.after.autoAnswer, false); assert.equal(leftScope.after.exportEnabled, false);
|
||||
assert.equal(await second.evaluate(() => YianAssistant.getContext().ready), false);
|
||||
assert.deepEqual(await second.evaluate(() => ({ select: document.body.getAttribute('onselectstart'), context: document.body.getAttribute('oncontextmenu'), style: getComputedStyle(document.querySelector('#copy-target')).userSelect })),
|
||||
{ select: 'return false', context: 'return false', style: 'none' }, '离开范围应恢复网页复制限制');
|
||||
assert.equal(await second.evaluate(() => YianAssistant.setAutoAnswer(true)), false);
|
||||
assert((await second.evaluate(() => { try { YianAssistant.exportCurrent('csv'); return ''; } catch (error) { return error.message; } })).includes('范围'));
|
||||
await second.waitForTimeout(400);
|
||||
assert.equal(await second.evaluate(() => window.optionClicks.length), 0, '离开范围后不能产生后续选项点击');
|
||||
await second.evaluate(() => { history.pushState({}, '', '/clientcn/exam/scope-start/questions?flag=exercise'); dispatchEvent(new PopStateEvent('popstate')); });
|
||||
await second.waitForFunction(() => YianAssistant.getContext().ready);
|
||||
assert(await second.locator('#yian-question-assistant').isVisible());
|
||||
assert.equal(await second.evaluate(() => YianAssistant.getState().autoAnswer), false);
|
||||
assert.equal(await second.evaluate(() => YianAssistant.getState().exportEnabled), false);
|
||||
assert.deepEqual(await second.evaluate(() => ({ select: document.body.getAttribute('onselectstart'), context: document.body.getAttribute('oncontextmenu'), style: getComputedStyle(document.querySelector('#copy-target')).userSelect })),
|
||||
{ select: null, context: null, style: 'text' }, '返回主站范围应恢复当前复制开关');
|
||||
assert.equal(await second.evaluate(() => window.optionClicks.length), 0);
|
||||
console.log('PASS SPA 离开 /clientcn/ 立即隐藏面板、停止答题/导出并恢复复制限制;返回后恢复面板与复制、自动开关保持关闭');
|
||||
|
||||
// The outer bundle guard is tested even when code is injected manually, outside metadata scope.
|
||||
for (const allowedURL of [origin + '/clientcn/main', origin + '/clientcn/main/deep/path?scope=query', 'http://10.119.67.7:80/clientcn/main?scope=default-port']) {
|
||||
const allowed = await context.newPage(); capture(allowed);
|
||||
await allowed.goto(allowedURL); await allowed.addScriptTag({ content: sandboxScript });
|
||||
await allowed.evaluate(() => YianAssistant.ready);
|
||||
assert.equal(await allowed.locator('#yian-question-assistant').count(), 1);
|
||||
assert.equal(await allowed.evaluate(() => typeof JSZip), 'function');
|
||||
await allowed.close();
|
||||
}
|
||||
const localPath = path.join(qaDir, 'scope-rejected.html');
|
||||
fs.writeFileSync(localPath, documentHTML(false));
|
||||
for (const rejectedURL of rejectedURLs.concat(pathToFileURL(localPath).href)) {
|
||||
const rejected = await context.newPage(); capture(rejected);
|
||||
await rejected.goto(rejectedURL);
|
||||
const inspect = () => rejected.evaluate(() => ({
|
||||
select: document.body.getAttribute('onselectstart'), context: document.body.getAttribute('oncontextmenu'),
|
||||
style: document.body.getAttribute('style'), userSelect: getComputedStyle(document.querySelector('#copy-target')).userSelect,
|
||||
blocked: !document.body.dispatchEvent(new Event('selectstart', { bubbles: true, cancelable: true })),
|
||||
body: document.body.innerHTML
|
||||
}));
|
||||
const before = await inspect();
|
||||
await rejected.addScriptTag({ content: sandboxScript });
|
||||
assert.equal(await rejected.locator('#yian-question-assistant').count(), 0, rejectedURL);
|
||||
assert.deepEqual(await rejected.evaluate(() => ['JSZip', 'QuestionBankCore', 'YianBankStore', 'YianAnswerMatcher', 'YianCopyController', 'YianAssistantPanel', 'YianAnswerRunner', 'YianAssistant', 'YianQuestionBankExporter'].map(key => [key, typeof window[key]])),
|
||||
['JSZip', 'QuestionBankCore', 'YianBankStore', 'YianAnswerMatcher', 'YianCopyController', 'YianAssistantPanel', 'YianAnswerRunner', 'YianAssistant', 'YianQuestionBankExporter'].map(key => [key, 'undefined']), rejectedURL + ' 整个 bundle 必须不注入');
|
||||
assert.deepEqual(await inspect(), before, rejectedURL + ' 不得改变复制限制或正文');
|
||||
await rejected.close();
|
||||
}
|
||||
assert.deepEqual(externalRequests, [], '所有非测试目标请求应被阻断;业务 origin 只允许模拟响应');
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS HTTP 主站 main / 子目录 / 查询参数有效;HTTPS、其他 host、非80端口、范围外路径、相似目录和 file 均无面板/API/JSZip且不改变复制限制');
|
||||
console.log('PASS 桌面/手机面板、页面身份与内容、无错误覆盖层/控制台错误');
|
||||
console.log('截图:' + qaDir);
|
||||
} finally { if (browser) await browser.close(); await new Promise(resolve => server.close(resolve)); }
|
||||
|
||||
Reference in New Issue
Block a user