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');
function extractBlock(startMarker, endMarker) {
const start = html.indexOf(startMarker);
const end = html.indexOf(endMarker, start);
if (start < 0 || end < 0) throw new Error('block not found: ' + startMarker);
return html.slice(start, end);
}
function extractFunc(src, name, asyncFn) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
}
eval(extractBlock('const CMD_NAMES = {', 'function cmdQualifierShort').replace(/^const /gm, 'var '));
eval(extractBlock('const EVENT_NAMES = {', 'const EVENT_FORMS = {').replace(/^const /gm, 'var '));
eval(extractBlock('const EVENT_FORMS = {', 'const PLI_QUALIFIERS = [').replace(/^const /gm, 'var '));
// the builders' pure helpers (eventFormFieldsHtml/Values are the shared form)
eval(extractFunc(html, 'bytesToHex'));
eval(extractFunc(html, 'eventAddressTlv'));
eval(extractFunc(html, 'encPlmn'));
eval(extractFunc(html, 'eventFormFieldsHtml'));
eval(extractFunc(html, 'eventFormValues'));
eval(extractFunc(html, 'eventFormPatternError'));
eval(extractFunc(html, 'eventFormSrc'));
eval(html.match(/const EVENT_INNER_MAX = \d+;/)[0].replace('const ', 'var '));
eval(extractFunc(html, 'eventEnvelopeSize'));
eval(extractFunc(html, 'eventFitsOneEnvelope'));
eval(extractBlock('const TEST_ACTION_KINDS = [', 'let _testScripts').replace(/^const /gm, 'var '));
eval(extractFunc(html, 'testTemplate'));
eval(extractFunc(html, 'testMenuSelectProblem'));
eval(extractFunc(html, 'testScriptProblem'));
eval(extractFunc(html, 'testPorSpecSummary'));
eval(extractFunc(html, 'testCheckSummary'));
eval(extractFunc(html, 'testStepSummary'));
eval(extractFunc(html, 'testCommandOptions'));
globalThis.t = s => s;
globalThis.esc = s => String(s);
globalThis.testScripts = () => _testScripts || [];
globalThis.testSuites = () => _testSuites || [];
eval(extractFunc(html, 'testFormRow'));
eval(extractFunc(html, 'testFormInput'));
eval(extractFunc(html, 'testFormSelect'));
eval(extractFunc(html, 'testStepRender'));
eval(extractFunc(html, 'testRenderChecks'));
eval(extractFunc(html, 'testStepCollect'));
eval(extractFunc(html, 'testStepFormError'));
eval(extractFunc(html, 'testWriteBackCounter'));
eval(extractFunc(html, 'testRunPreset'));
eval(extractFunc(html, 'testScriptsWriteback', true));
eval(extractFunc(html, 'testScriptsWritebackQueued'));
eval(extractFunc(html, 'testScriptsSave'));
eval(extractFunc(html, 'testRunStart', true));
eval(extractFunc(html, 'testSuiteDeleteScript', true));
eval(extractFunc(html, 'testSuitesRefetch', true));
eval(extractFunc(html, 'testChecksCollect'));
eval('var _testEditStep = null; var _testEditChecks = []; var _testEditStepIndex = -1;'
+ ' var _testScripts = null; var _testSuites = []; var _testScriptId = null;'
+ ' var _testScriptDraft = null; var _testRunState = null; var _testSaveTimer = null;'
+ ' var _testWriteChains = new WeakMap(); var _testLastPresetIdx = -1;'
+ ' var _testRunScriptRef = null; var _testSuiteIdx = -1;');
globalThis.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
function fakeForm(values) {
globalThis.document = { getElementById: id => (id in values ? { value: values[id] } : null) };
}
test('the templates contain usable steps', () => {
const menu = testTemplate('menu');
assert.strictEqual(menu.steps.length, 3);
assert.strictEqual(menu.steps[0].kind, 'menu-select');
assert.deepStrictEqual(menu.steps[0].check.sw, { mode: 'mask', value: '91??' });
assert.strictEqual(menu.steps[1].command, 'SELECT ITEM');
assert.strictEqual(menu.steps[1].checks[0].kind, 'item');
assert.strictEqual(menu.steps[2].command, 'DISPLAY TEXT');
const scp80 = testTemplate('scp80');
assert.strictEqual(scp80.steps[0].kind, 'scp80');
assert.strictEqual(scp80.steps[0].check.por, 'none');
assert.ok(scp80.steps[0].params.tar);
assert.strictEqual(testTemplate('empty').steps.length, 0);
});
test('testScriptProblem accepts good scripts and names bad ones', () => {
assert.strictEqual(testScriptProblem(testTemplate('menu')), '');
assert.strictEqual(testScriptProblem(testTemplate('scp80')), '');
assert.match(testScriptProblem(null), /object/);
assert.match(testScriptProblem({ steps: [] }), /no steps/);
assert.match(testScriptProblem({ steps: [{ type: 'nope' }] }), /action or an expectation/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'nope' }] }), /unknown action kind/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'apdu', params: {} }] }), /APDU is empty/);
assert.match(testScriptProblem({
steps: [{ type: 'action', kind: 'scp80', params: { apdu: '' } }] }), /SCP80/);
assert.match(testScriptProblem({
steps: [{ type: 'action', kind: 'menu-select', params: { item_id: 0 } }] }), /item id/);
// the item can be selected by its text instead of the install-dependent id
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'menu-select',
params: { text: 'My menu', mode: 'contains' } }] }), '');
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'menu-select',
params: {} }] }), /item id or text/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'menu-select',
params: { item_id: 1, text: 'My menu' } }] }), /not both/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
params: {} }] }), /event is required/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
params: { event: '03', fields: [] } }] }), /object/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
params: { event: '00' } }] }), /event data is required/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
params: { event: '00', data: 'ZZ' } }] }), /hex/);
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
params: { event: '03', fields: { status: 0 } } }] }), '');
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'event',
params: { event: '00', data: '1C0101', src: '83' } }] }), '');
assert.match(testScriptProblem({
steps: [{ type: 'action', kind: 'file-read', params: {} }] }), /file path/);
assert.match(testScriptProblem({ steps: [{ type: 'expect' }] }), /command is required/);
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'proactive',
params: { attempts: 0 } }] }), /attempts/);
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'proactive',
params: {} }] }), '');
// the selected SCP80 source decides which value is required
assert.match(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
params: { source: 'apdu', sp: 'AA' } }] }), /SCP80/);
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
params: { source: 'apdu', apdu: '80E2' } }] }), '');
assert.strictEqual(testScriptProblem({ steps: [{ type: 'action', kind: 'scp80',
params: { source: 'sp', sp: 'AA', apdu: '80E2' } }] }), '');
});
test('the proactive cleanup step collects its fields', () => {
_testEditStep = { type: 'action', kind: 'proactive', params: {} };
_testEditChecks = [];
_testEditStepIndex = 0;
fakeForm({ 'test-step-kind': 'proactive', 'test-f-presult': 'ok', 'test-f-pfirst': 'cancel',
'test-f-pattempts': '5', 'test-f-pinterval': '0', 'test-f-prequire': 'refresh',
'test-f-prequal': '00', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' });
testStepCollect();
const p = _testEditStep.params;
assert.strictEqual(p.respond.result, 'ok');
assert.strictEqual(p.first.result, 'cancel');
assert.strictEqual(p.attempts, 5);
assert.strictEqual(p.interval_ms, 0);
assert.deepStrictEqual(p.require, { command: 'REFRESH', qualifier: '00' });
// an empty require field drops the assertion
fakeForm({ 'test-step-kind': 'proactive', 'test-f-presult': 'ok', 'test-f-pfirst': '',
'test-f-pattempts': '3', 'test-f-pinterval': '200', 'test-f-prequire': '',
'test-f-prequal': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-fail': 'error' });
testStepCollect();
assert.ok(!('first' in _testEditStep.params));
assert.ok(!('require' in _testEditStep.params));
});
test('testStepSummary renders actions', () => {
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'menu-select', params: { item_id: 3 },
check: { sw: { mode: 'mask', value: '91??' } } }),
'ENVELOPE(Menu Selection) item=3 · SW ~91??');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'menu-select',
params: { text: 'My menu', mode: 'exact' } }),
'ENVELOPE(Menu Selection) text ="My menu"');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'apdu', params: { apdu: '00A4' } }),
'APDU 00A4');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'scp80', params: { apdu: '80E2', tar: 'B00000' },
check: { sw: { mode: 'mask', value: '91??' }, por: 'none' } }),
'SCP80 80E2 TAR=B00000 · SW ~91??, PoR none');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'status', params: { attempts: 5 } }),
'STATUS x5 (poll)');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'proactive',
params: { attempts: 5, respond: { result: 'ok' },
require: { command: 'REFRESH', qualifier: '00' } } }),
'PROACTIVE x5 require REFRESH q=00 TR ok');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'proactive',
params: { attempts: 3, first: { result: 'cancel' } } }),
'PROACTIVE x3 TR ok first cancel');
assert.strictEqual(
testStepSummary({ type: 'action', kind: 'file-read', params: { path: 'MF/2FE2' } }),
'READ MF/2FE2');
assert.ok(testStepSummary({ type: 'action', kind: 'status', params: {},
check: { sw: { mode: 'exact', value: '9000' } }, on_fail: 'warning' }).includes('⚠'));
});
test('testStepSummary renders expectations with checks and the response', () => {
assert.strictEqual(
testStepSummary({ type: 'expect', command: 'SELECT ITEM',
checks: [{ kind: 'item', id: 1, text: 'test', mode: 'contains' }],
respond: { result: 'ok', item_id: 1 } }),
'EXPECT SELECT ITEM · item #1 ~"test" · TR item=1');
assert.strictEqual(
testStepSummary({ type: 'expect', command: 'DISPLAY TEXT',
checks: [{ kind: 'text', mode: 'contains', value: 'hello' }], respond: { result: 'ok' } }),
'EXPECT DISPLAY TEXT · text ~"hello" · TR ok');
assert.ok(testStepSummary({ type: 'expect', command: 'DISPLAY TEXT',
checks: [{ kind: 'text', value: 'x' }], respond: { result: 'cancel' } }).includes('TR cancel'));
assert.ok(testStepSummary({ type: 'expect', command: 'GET INPUT', respond: { result: 'ok', text: 'hi' } })
.includes('text="hi"'));
assert.strictEqual(testCheckSummary({ kind: 'raw', mode: 'mask', value: 'AA??' }), 'raw ~AA??');
});
test('testCommandOptions covers the proactive names and keeps custom values', () => {
const opts = testCommandOptions('DISPLAY TEXT');
assert.ok(opts.some(o => o.v === 'ANY'));
assert.ok(opts.some(o => o.v === 'DISPLAY TEXT'));
assert.ok(opts.some(o => o.v === 'SELECT ITEM'));
const custom = testCommandOptions('0x74');
assert.ok(custom.some(o => o.v === '0x74'));
});
test('the SCP80 source switch sticks and preserves the other value', () => {
_testEditStep = { type: 'action', kind: 'scp80', params: { apdu: '80E2900000' } };
_testEditChecks = [];
_testEditStepIndex = 0;
// switch to the pre-built packet: the packet field is not rendered yet
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'sp', 'test-f-apdu': '80E2900000',
'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'any', 'test-f-fail': 'error' });
testStepCollect();
assert.strictEqual(_testEditStep.params.source, 'sp');
assert.strictEqual(_testEditStep.params.sp, '');
assert.strictEqual(_testEditStep.params.apdu, '80E2900000'); // preserved
assert.ok(!('sw' in _testEditStep.check)); // server default
// the packet field is rendered now and gets a value
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'sp', 'test-f-sp': 'aabbcc',
'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'none', 'test-f-fail': 'error' });
testStepCollect();
assert.strictEqual(_testEditStep.params.sp, 'AABBCC');
assert.strictEqual(_testEditStep.params.source, 'sp');
assert.strictEqual(_testEditStep.check.por, 'none');
// switching back keeps both values
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000',
'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'any', 'test-f-fail': 'error' });
testStepCollect();
assert.strictEqual(_testEditStep.params.source, 'apdu');
assert.strictEqual(_testEditStep.params.sp, 'AABBCC');
});
test('the SCP80 step collects the format and the custom PoR object', () => {
_testEditStep = { type: 'action', kind: 'scp80', params: { apdu: '80E2900000' } };
_testEditChecks = [];
_testEditStepIndex = 0;
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000',
'test-f-fmt': 'expanded', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '',
'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'custom',
'test-f-porstatus': 'por_ok', 'test-f-por-sw': '6a8?', 'test-f-por-data': 'aa??',
'test-f-fail': 'error' });
testStepCollect();
assert.strictEqual(_testEditStep.params.format, 'expanded');
assert.deepStrictEqual(_testEditStep.check.por,
{ status: 'por_ok', sw: { mode: 'mask', value: '6A8?' },
data: { mode: 'mask', value: 'AA??' } });
// a custom PoR check with no fields refuses to save
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000',
'test-f-fmt': 'compact', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '',
'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'custom',
'test-f-fail': 'error' });
assert.match(testStepFormError(), /PoR check/);
assert.ok(!('format' in _testEditStep.params)); // compact is the default
// the plain string forms stay and their object is dropped on switch
fakeForm({ 'test-step-kind': 'scp80', 'test-f-src': 'apdu', 'test-f-apdu': '80E2900000',
'test-f-fmt': '', 'test-f-kvn': '', 'test-f-tar': '', 'test-f-spi1': '',
'test-f-spi2': '', 'test-f-sw': '', 'test-f-cdata': '', 'test-f-por': 'none',
'test-f-fail': 'error' });
testStepCollect();
assert.strictEqual(_testEditStep.check.por, 'none');
// the summary renders the effective format and the object's fields
const s = testStepSummary({ type: 'action', kind: 'scp80',
params: { apdu: '80E2', format: 'expanded-ae' },
check: { por: { status: 'por_ok', sw: { mode: 'mask', value: '6A8?' } } } });
assert.ok(s.includes('fmt=expanded-ae'), s);
assert.ok(s.includes('PoR status por_ok SW ~6A8?'), s);
});
test('the SCP80 step form renders the format select and the custom PoR fields', () => {
const els = {
'test-step-modal': { classList: { add: () => {}, remove: () => {} } },
'test-step-title': {}, 'test-step-body': {},
'test-step-error': { classList: { add: () => {}, remove: () => {} } },
};
globalThis.document = { getElementById: id => els[id] || null };
_testEditStep = { type: 'action', kind: 'scp80',
params: { apdu: '80E2900000', format: 'expanded' },
check: { por: { status: 'por_ok' } } };
_testEditChecks = [];
_testEditStepIndex = 0;
testStepRender();
const body = els['test-step-body'].innerHTML;
assert.match(body, /id="test-f-fmt"/);
assert.match(body, /