/* The common attempt policy reads visible scoped DOM, never Angular/network state. */ 'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { chromium } = require('playwright'); (async () => { const browser = await chromium.launch({ channel: process.env.PLAYWRIGHT_CHANNEL || 'chrome', headless: true }); const requests = [], errors = []; try { const context = await browser.newContext(); await context.route('**/*', route => { requests.push(route.request().url()); return route.abort(); }); const page = await context.newPage(); page.on('pageerror', error => errors.push(error.message)); await page.addScriptTag({ content: fs.readFileSync(path.join(__dirname, '..', 'attempt-policy.js'), 'utf8') }); async function read(html, tag = 'app-examdetail') { return page.evaluate(({ html, tag }) => { document.body.innerHTML = '<' + tag + ' id="scope">' + html + ''; return YianAttemptPolicy.read(document.querySelector('#scope')); }, { html, tag }); } const positive = [ '允许答题次数:1次', '总考试次数:1', '最多作答次数为一次', '剩余考试次数:1 次', '可用答题机会:1次', '考试总次数:1次', '答题次数上限:1次', '允许答题次数:仅1次', '考试机会:仅剩1次', '本次仅可答题1次', '仅允许考试一次', '仅有1次答题机会', '本场只剩一次考试机会', '允许作答1次', '总共可考试1次', '还可补考 1 次', '还可补考一次', '剩余答题次数:1次', '剩余 1 次答题机会', '还有一次考试机会', '还可重考1次', '不及格,还可补考 1 次', '(不及格,还可补考 1 次)', '', '', '', '
允许答题次数:1次
', '
剩余考试次数
1 次
' ]; for (const html of positive) { const result = await read(html); assert.equal(result.blocked, true, html); assert.equal(result.known, true, html); assert.equal(result.count, 1, html); assert(result.reason.includes('1 次'), html); } console.log('PASS explicit allowed/total/remaining counts, split inline labels, definition rows and real retake wording'); for (const html of ['剩余考试次数:0次', '还可补考零次', '允许答题次数:0', '重新答题(不及格,还可补考0次)']) { const result = await read(html); assert(result.blocked, html); assert.equal(result.count, 0, html); } for (const html of ['总考试次数:2次', '剩余答题次数:12次', '还可补考十一次', '允许答题次数:无限次', '重新考试(不及格,还可补考3次)']) { const result = await read(html); assert.equal(result.blocked, false, html); assert.equal(result.count, null, html); } assert.equal((await read('

总考试次数:3次

剩余考试次数:1次

')).count, 1); assert.equal((await read('

总考试次数:1次

还可补考0次

')).count, 0); console.log('PASS zero/single attempts stop, larger counts are not blocked, remaining zero wins over total one'); const ignored = [ '已答次数:1次', '已用答题次数:1次', '已考试次数:1次', '已答题数1/40', '已答 1/40', '157人已交卷', '已答题次数:1次,答对 1 题', '答题次数:1', '考试次数:1次', '考试题数:1', '剩余考试次数:1/3', '允许答题次数:1.5次', '允许答题次数:-1次', '还可补考1次是正确答案', '
仅有1次答题机会
', '
(不及格,还可补考1次)是这道题的答案
', '
重新答题(不及格,还可补考1次)
', '
允许答题次数1次
', '
还可补考1次
', '
剩余考试次数1次
', '
总考试次数1次
', '
允许答题次数1次
', '', '', '', '
总考试次数1次
', '
剩余考试次数1次
', '
还可补考1次
', '', '', '
考试已就绪
', '

这里只剩1题要答

' ]; for (const html of ignored) { const result = await read(html); assert.equal(result.blocked, false, html); assert.equal(result.count, null, html); assert.equal(result.signature, '[]', html); assert.equal(result.known, false, html); } console.log('PASS used counts, question totals, people, decimals, hidden/answer/question text and private/data attributes do not infer limits'); for (const tag of ['app-examtest', 'div']) assert.equal((await read('剩余考试次数:1次', tag)).count, 1); const before = await read('

总考试次数:3次

'); const after = await read('

总考试次数:2次

'); assert(before.known && after.known, 'larger explicit count is known even though it does not block'); assert.notEqual(before.signature, after.signature, 'larger explicit count changes invalidate earlier evidence'); const stable = await read('

剩余考试次数:1次

'); assert.equal(stable.signature, (await read('

剩余考试次数:1次

')).signature); await page.evaluate(() => { document.querySelector('#scope').insertAdjacentHTML('afterend', '还可补考0次'); }); assert.equal(await page.evaluate(() => YianAttemptPolicy.read(document.querySelector('#scope')).count), 1, 'another exam outside scope is ignored'); for (const invalid of ['document', 'body', 'null', 'detached']) { const result = await page.evaluate(invalid => YianAttemptPolicy.read(invalid === 'document' ? document : invalid === 'body' ? document.body : invalid === 'detached' ? document.createElement('app-examtest') : null), invalid); assert.equal(result.blocked, false); assert.equal(result.signature, '[]'); } assert.deepEqual(requests, []); assert.deepEqual(errors, []); console.log('PASS scoped containers only, stable signatures, explicit changes, no network or browser errors'); } finally { await browser.close(); } })().catch(error => { console.error(error); process.exitCode = 1; });