const { test } = require('node:test'); const assert = require('node:assert'); const fs = require('node:fs'); const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); // The help pages are HTML too: the same structural walk validates them. const helpPages = ['help.html', 'help-ru.html'].map(f => ({ name: f, text: fs.readFileSync(path.join(__dirname, '..', f), 'utf8'), })); // Tags that never take an end tag. const VOID_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']); // End tags HTML allows to be omitted: a close tag may legally skip these. const OPTIONAL_END = new Set(['p', 'li', 'dt', 'dd', 'option', 'optgroup', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th', 'rt', 'rp', 'colgroup', 'caption']); // Start tags that implicitly close the listed open element (the subset used here). const IMPLIED_END = { li: ['li'], dt: ['dt', 'dd'], dd: ['dt', 'dd'], p: ['p'], option: ['option'], optgroup: ['optgroup'], tr: ['tr'], td: ['td', 'th'], th: ['td', 'th'], thead: ['thead', 'tbody', 'tfoot'], tbody: ['thead', 'tbody', 'tfoot'], tfoot: ['thead', 'tbody', 'tfoot'], }; // The markup without comments and embedded script/style bodies: only this // participates in tag nesting. JavaScript template strings may hold // unbalanced
fragments, which is exactly how a missing
stayed // invisible to the old whole-file count (the Simulator panels were nested // from v3.5.11 to v3.6.0 while the total stayed balanced). function htmlOnly(src) { return src.replace(//g, '') .replace(/]*>[\s\S]*?<\/script>/gi, '') .replace(/]*>[\s\S]*?<\/style>/gi, ''); } // Walk the tag stream and report cross-nesting, stray end tags and unclosed // elements. Run on every HTML page for every change. HTML optional end tags // are honoured so a legitimately omitted

/ is not an error. function htmlStructureProblems(src) { const markup = htmlOnly(src); const problems = []; const stack = []; const re = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?(\/?)>/g; let m; while ((m = re.exec(markup))) { const closing = m[1] === '/'; const tag = m[2].toLowerCase(); const selfClosing = m[3] === '/'; if (VOID_TAGS.has(tag) || selfClosing) continue; if (!closing) { for (const t of (IMPLIED_END[tag] || [])) { while (stack.length && stack[stack.length - 1].tag === t) stack.pop(); } stack.push({ tag, at: m.index }); continue; } if (!stack.length) { problems.push(`stray at offset ${m.index}`); continue; } if (stack[stack.length - 1].tag === tag) { stack.pop(); continue; } let i = stack.length - 1; while (i >= 0 && stack[i].tag !== tag) i--; if (i < 0) { problems.push(`stray at offset ${m.index}`); continue; } const between = stack.slice(i + 1); if (between.every(e => OPTIONAL_END.has(e.tag))) stack.length = i; else { problems.push(`cross-nesting: at offset ${m.index} closes over ` + between.map(e => `<${e.tag}>`).join(' ')); } } for (const e of stack) { if (!OPTIONAL_END.has(e.tag)) problems.push(`unclosed <${e.tag}> at offset ${e.at}`); } return problems; } // Depth of a
in the parsed div stream: sibling panels share // one depth, a nested panel sits one level deeper. function divDepthOf(src, id) { const markup = htmlOnly(src); const re = /<(\/?)div\b[^>]*>/g; let depth = 0, m; while ((m = re.exec(markup))) { if (m[1] === '/') { depth -= 1; continue; } depth += 1; if (m[0].includes('id="' + id + '"')) return depth; } return -1; } test('HTML
tags are balanced in the markup (scripts excluded)', () => { const markup = htmlOnly(html); const opens = (markup.match(//g) || []).length; assert.strictEqual(opens, closes, `Unbalanced divs: ${opens} opens vs ${closes} closes`); }); test('HTML tag nesting is well-formed on every page', () => { for (const page of [{ name: 'index.html', text: html }, ...helpPages]) { assert.deepStrictEqual(htmlStructureProblems(page.text), [], page.name + ' has structure problems'); } }); test('the Simulator panels are siblings of the tab container, never nested', () => { const ids = ['phone-sub-phone', 'phone-sub-tr', 'phone-sub-bip', 'phone-sub-esim', 'phone-sub-test']; const depths = ids.map(id => { const d = divDepthOf(html, id); assert.ok(d > 0, 'panel missing: ' + id); return d; }); assert.strictEqual(new Set(depths).size, 1, 'Simulator panels are nested at different depths: ' + ids.map((id, i) => id + '=' + depths[i]).join(', ')); }); test('top-level tabs match the rearranged views', () => { const tabs = [...html.matchAll(/class="tab-btn[^"]*" data-tab="([^"]+)"/g)].map(m => m[1]); assert.deepStrictEqual(tabs, ['c-apdu', 'scp80', 'scp81', 'cards', 'profiler', 'pysim', 'phone']); assert.match(html, /data-tab="c-apdu">Remote APDU { assert.match(html, /data-l10n="SCP81">SCP81 { const fieldsets = [...html.matchAll(//g)].map(m => m[0]); const scp80 = fieldsets.find(f => /data-l10n="SCP80 \(GSM 03\.48, ETSI TS 102 225\)"/.test(f)); const scp81 = fieldsets.find(f => /data-l10n="SCP81 \(HTTP OTA\)"/.test(f)); assert.ok(scp80, 'SCP80 card fieldset not found'); assert.ok(scp81, 'SCP81 card fieldset not found'); assert.match(scp80, /id="cards-kic"/); assert.match(scp80, /id="cards-kid-key"/); assert.match(scp80, /id="cards-tar"[^>]*value="000000"/); assert.match(scp80, /id="cards-uicc-tar"[^>]*value="B00000"/); assert.match(scp80, /id="cards-usim-tar"[^>]*value="B00001"/); // the TAR fields are labelled and sit to the right of Counter assert.match(scp80, />ISD TAR<\/label>/); assert.match(scp80, />UICC RFM TAR<\/label>/); assert.match(scp80, />ADF RFM TAR<\/label>/); assert.ok(scp80.indexOf('id="cards-cntr"') < scp80.indexOf('id="cards-tar"'), 'TAR fields must follow the Counter field'); assert.match(scp81, /id="cards-psk-id"/); assert.match(scp81, /id="cards-psk-key"/); // the PSK explanation lives inside the SCP81 group, not outside it assert.match(scp81, /data-l10n="SCP81 HTTP OTA: the listener picks the key/); // the ICCID field is not in either group assert.ok(!fieldsets.some(f => f.includes('id="cards-iccid"'))); }); test('the ADM field sits between From card and Add, outside the card fieldsets', () => { const fieldsets = [...html.matchAll(//g)].map(m => m[0]); assert.ok(!fieldsets.some(f => f.includes('id="cards-adm"'))); assert.match(html, /id="cards-adm"[^>]*placeholder="ADM \(optional\)"/); const fromIdx = html.indexOf('id="cards-iccid-from-card"'); const admIdx = html.indexOf('id="cards-adm"'); const addIdx = html.indexOf('id="cards-add-btn"'); assert.ok(fromIdx >= 0 && fromIdx < admIdx && admIdx < addIdx, 'ADM input must sit between the From card and Add buttons'); }); test('card-dependent profiler buttons show the ICCID and need a readable one', () => { assert.match(html, /id="profiler-from-card-btn" data-needs="card-iccid"/); assert.match(html, /id="snapshot-new-btn" data-needs="card-iccid"/); assert.match(html, /data-needs="card-iccid" onclick="profilerCheck/); // both static buttons carry the dynamic ICCID span assert.match(html, /id="profiler-from-card-btn"[\s\S]*?<\/span><\/button>/); assert.match(html, /id="snapshot-new-btn"[\s\S]*?<\/span><\/button>/); }); test('network state monitor panel lives next to the network simulation', () => { assert.match(html, /data-l10n="Network state"/); assert.match(html, /id="netstate-badge"/); assert.match(html, /id="netstate-refresh-btn" data-needs="card"/); assert.match(html, /id="netstate-body"/); assert.match(html, /id="netsim-write-fplmn"/); // the monitor is rendered from the cached server state (no file polling) assert.ok(html.includes("netStateFetch()")); }); test('labelled containers use fieldsets with embedded legends', () => { const legendCls = 'HTTP OTA listener Script results (R-APDUs) \s*\s*/); }); test('PLI qualifier tables cover all standard qualifiers', () => { // ESN (07), MEID (0B) and Supported RATs (1A) must at least be named, in // both the TR Config dictionary and the proactive-log short labels. const pli = /const PLI_QUALIFIERS = \[([\s\S]*?)\];/.exec(html); assert.ok(pli, 'PLI_QUALIFIERS not found'); for (const code of ['07', '0B', '1A']) { assert.ok(pli[1].includes("{code:'" + code + "'"), 'PLI_QUALIFIERS missing ' + code); } const block = /const CMD_QUALIFIER_SHORT = \{([\s\S]*?)\n\};/.exec(html); assert.ok(block, 'CMD_QUALIFIER_SHORT not found'); const short = /'26': \{([^}]*)\}/.exec(block[1]); assert.ok(short, "CMD_QUALIFIER_SHORT['26'] not found"); for (const key of ['0x07', '0x0B', '0x1A']) { assert.ok(short[1].includes(key + ':'), 'CMD_QUALIFIER_SHORT 26 missing ' + key); } }); test('profile rows have a Clone action', () => { assert.match(html, /onclick="profilerClone\(' \+ i \+ '\)"/); assert.match(html, /t\('Clone'\)/); }); test('the C-APDU and R-APDU parsers are sub-pills of the Parser pill', () => { // The Parser pill is a top-level Remote APDU subtab... assert.match(html, /data-sub="parser" onclick="cApduSwitchSubtab\('parser'\)"/); // ...and both parser panels live inside its container, one after the other. const container = html.indexOf('id="c-apdu-sub-parser"'); const parse = html.indexOf('id="c-apdu-sub-parse"'); const response = html.indexOf('id="c-apdu-sub-response"'); assert.ok(container > 0, 'Parser container missing'); assert.ok(parse > container, 'C-APDU panel must be inside the Parser container'); assert.ok(response > parse, 'R-APDU panel must follow the C-APDU panel'); // Their sub-pill buttons exist and switch through parserSwitchSubtab. assert.match(html, /id="parser-btn-parse" onclick="parserSwitchSubtab\('parse'\)"/); assert.match(html, /id="parser-btn-response" onclick="parserSwitchSubtab\('response'\)"/); }); test('the document and its inline script are complete', () => { // A truncated index.html (missing ) makes the whole // inline script fail to parse in the browser: every onclick handler then // reports "function is not defined" while the Node tests still pass, // because they extract functions from the text without ever running the // document's script. assert.ok(html.trimEnd().endsWith(''), 'index.html must end with '); assert.strictEqual((html.match(//g) || []).length, 'every