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'); const opens = (html.match(//g) || []).length; test('HTML
tags are balanced', () => { assert.strictEqual(opens, closes, `Unbalanced divs: ${opens} opens vs ${closes} closes`); }); 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