Files
simple/frontend/tests/cards_iccid.test.js
T
catarrh 9a688207ba v3.30.4: the in-app dialogs (native alert/confirm/prompt replaced)
The native browser dialogs block the page and look foreign inside the
app.  One shared modal (#pysim-dialog-modal) with the promise-based
helpers pysimAlert/pysimConfirm/pysimPrompt replaces all 71 call sites
(60 alert, 10 confirm, 1 prompt in profilerNew); deferredPrompt.prompt()
is the PWA install prompt and is untouched.

The helpers serialize through ONE promise queue (overlapping dialogs
appear in order, no message is overwritten).  Escape cancels; Enter
accepts except with the Cancel button focused (native behaviour); key
repeats/IME are ignored; a POINTER answer within 500 ms of a dialog
opening is dropped (the buttons keep the same position, so a
double-click's second click would otherwise answer the next queued
dialog - e.g. an alert queued before a destructive confirm).  Backdrop
clicks are ignored.  The dialog is role="dialog" aria-modal="true" and
traps Tab (the handler cycles the controls the dialog shows: alert OK;
confirm Cancel/OK; prompt Cancel/input/OK), so the focus can neither
leave the modal nor reach a background control (Space would activate
it).  When the modal is absent the helpers fall back to the native
dialogs - the Node tests extract single functions without the markup.

Alerts are fire-and-forget (a notification, not a decision); the 10
confirms and the prompt are awaited, which made five functions async
(scriptsDelete, pysimCustomRemove, profilerNew, profilerDelete,
snapshotDelete).  Because the modal no longer freezes the page, the
delete flows that mutated by index after the confirmation mutate by
identity now (profilerDelete/snapshotDelete/pysimCustomRemove/
testSuiteDeleteScript/testSuiteDelete): a list reorder during the open
dialog would otherwise delete the wrong entry.

The nine dialog messages that were still raw English literals are
t()-wrapped - three of them (No secured packet to verify/send, Card
reader server is not connected) activated RU entries that had existed
unused because they were never wrapped; Import error and Snapshot save
failed are new keys.

Drive-by: the batch modal's #ram-op-batch-info used whitespace-pre-line,
which has no rule in the prebuilt style.css (the v3.30.0 stale-style
trap), so its notice never line-broke - now whitespace-pre-wrap.

Tests: a new frontend/tests/dialogs.test.js (15 tests: the modal path,
the queue, the double-answer guard, the keyboard, the grace, the trap,
the fallback, the sweep pin, the stylesheet pin) plus the async/helper
updates in 8 existing test files.  945 frontend + 952 Python;
sw cache simple-v454.
2026-10-18 00:30:06 +03:00

228 lines
9.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 extractConst(src, name) {
const m = new RegExp('const\\s+' + name + '\\s*=\\s*([^;]+);').exec(src);
if (!m) throw new Error('const ' + name + ' not found');
return 'var ' + name + ' = ' + m[1] + ';\n';
}
function extractFunc(src, name) {
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 src.slice(m.index, i + 1);
}
let code = '';
for (const fn of ['swapNibbles', 'encIccid', 'decIccid',
'cardsNormIccid', 'cardsFindByIccid', 'cardsFindDuplicateIccid',
'cardsIccidFromCard', 'cardsAutoSelectByIccid', 'cardsRender',
'esc', 'spKeysetList', 'spKeysetKvnOf',
'cardsTarList', 'cardsRoleDefault', 'cardsRoleTar',
'cardsTarEntry', 'cardsTarMsl', 'cardsTarText', 'cardsScp81Complete']) {
code += extractFunc(html, fn) + '\n';
}
code += extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_ROLES')
+ extractConst(html, 'CARDS_MSL_DEFAULT');
eval(code);
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs each test installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
// live-card style raw EF.ICCID content (nibble-swapped digits + 'F' pad)
const RAW_HEX = '980711090000640090F8';
const DIGITS = '8970119000004600098';
function fakeDoc() {
const els = { 'sp-card-sel': { value: '' }, 'ram-card-sel': { value: '' },
'tar-card-sel': { value: '' }, 'cards-iccid': { value: '' } };
globalThis.document = { getElementById: id => els[id] || null };
return els;
}
function fakeAlert() {
const seen = [];
globalThis.alert = msg => seen.push(msg);
globalThis.t = s => s;
return seen;
}
test('esc escapes quotes as well as the angle brackets', () => {
// the output is used inside attribute values (value="…", title="…",
// label="…") as well as in text contexts (v3.10.0 review fix)
assert.strictEqual(esc('a"b\'c<d>&'), 'a&quot;b&#39;c&lt;d&gt;&amp;');
});
test('cardsNormIccid accepts digits, separators and raw EF hex', () => {
assert.strictEqual(cardsNormIccid(DIGITS), DIGITS);
assert.strictEqual(cardsNormIccid(' 89 70 1190-0000 4600 098 '), DIGITS);
assert.strictEqual(cardsNormIccid(RAW_HEX), DIGITS);
assert.strictEqual(cardsNormIccid(RAW_HEX.toLowerCase()), DIGITS);
assert.strictEqual(cardsNormIccid(''), '');
assert.strictEqual(cardsNormIccid(null), '');
});
test('cardsNormIccid accepts the outfile digits+F form (v3.27.0)', () => {
// the vendors' outfiles print the ICCID as digits + an odd-nibble F pad -
// not the raw EF form (which never ends in F)
assert.strictEqual(cardsNormIccid(DIGITS + 'F'), DIGITS);
assert.strictEqual(cardsNormIccid(DIGITS + 'FF'), DIGITS);
assert.strictEqual(cardsNormIccid(DIGITS + 'f'), DIGITS);
assert.strictEqual(cardsNormIccid('FFFF'), '');
});
test('cardsFindByIccid matches regardless of the stored form', () => {
globalThis.cards = [
{ name: 'A', iccid: '1111111111111111111' },
{ name: 'B', iccid: RAW_HEX },
{ name: 'C', iccid: '89 70 1190 0000 4600 098' },
];
assert.strictEqual(cardsFindByIccid(DIGITS), 1);
assert.strictEqual(cardsFindByIccid('1111111111111111111'), 0);
assert.strictEqual(cardsFindByIccid('2222222222222222222'), -1);
assert.strictEqual(cardsFindByIccid(''), -1);
});
test('cardsAutoSelectByIccid selects the preset in both SCP80 views', () => {
const els = fakeDoc();
const applied = [];
globalThis.cardsApply = idx => applied.push(idx);
globalThis.cards = [{ name: 'A', iccid: RAW_HEX }, { name: 'B', iccid: '' }];
globalThis._ramCardIdx = null;
globalThis._cardsAutoIccid = null;
const idx = cardsAutoSelectByIccid(DIGITS);
assert.strictEqual(idx, 0);
assert.strictEqual(els['sp-card-sel'].value, '0');
assert.strictEqual(els['ram-card-sel'].value, '0');
assert.strictEqual(els['tar-card-sel'].value, '0',
'the TAR probe view gets the equipped card\'s preset too');
assert.deepStrictEqual(applied, ['0']);
assert.strictEqual(globalThis._ramCardIdx, 0);
});
test('cardsAutoSelectByIccid does not override a manual choice for the same card', () => {
const els = fakeDoc();
const applied = [];
globalThis.cardsApply = idx => applied.push(idx);
globalThis.cards = [{ name: 'A', iccid: DIGITS }, { name: 'B', iccid: '' }];
globalThis._ramCardIdx = null;
globalThis._cardsAutoIccid = null;
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
// user picks the empty preset B for this card, then status polls repeat
els['sp-card-sel'].value = '1';
els['ram-card-sel'].value = '1';
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), -1);
assert.strictEqual(els['sp-card-sel'].value, '1');
assert.deepStrictEqual(applied, ['0']);
});
test('cardsAutoSelectByIccid reacts to a card swap and to unreadable ICCIDs', () => {
fakeDoc();
globalThis.cardsApply = () => {};
globalThis.cards = [{ name: 'A', iccid: DIGITS }, { name: 'B', iccid: '89390100000129506903' }];
globalThis._cardsAutoIccid = null;
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
assert.strictEqual(cardsAutoSelectByIccid('89390100000129506903'), 1);
// ICCID not readable: reset the guard, keep the current selection
assert.strictEqual(cardsAutoSelectByIccid(null), -1);
assert.strictEqual(globalThis._cardsAutoIccid, null);
assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0);
assert.strictEqual(cardsAutoSelectByIccid('1234567890123456789'), -1);
});
test('cardsFindDuplicateIccid catches duplicates across stored formats', () => {
globalThis.cards = [
{ name: 'A', iccid: '1111111111111111111' },
{ name: 'B', iccid: RAW_HEX },
{ name: 'C', iccid: '' },
];
assert.strictEqual(cardsFindDuplicateIccid(DIGITS), 1);
assert.strictEqual(cardsFindDuplicateIccid('89 70 1190-0000 4600 098'), 1);
assert.strictEqual(cardsFindDuplicateIccid('1111111111111111111'), 0);
assert.strictEqual(cardsFindDuplicateIccid('2222222222222222222'), -1);
// empty ICCID is never a duplicate (the field is optional)
assert.strictEqual(cardsFindDuplicateIccid(''), -1);
assert.strictEqual(cardsFindDuplicateIccid(' '), -1);
});
test('cardsFindDuplicateIccid skips the row being edited', () => {
globalThis.cards = [{ name: 'A', iccid: DIGITS }, { name: 'B', iccid: '' }];
assert.strictEqual(cardsFindDuplicateIccid(DIGITS, 0), -1);
assert.strictEqual(cardsFindDuplicateIccid(RAW_HEX, 0), -1);
assert.strictEqual(cardsFindDuplicateIccid(RAW_HEX, 1), 0);
});
test('cardsIccidFromCard fills the field from the equipped card', () => {
const els = fakeDoc();
const seen = fakeAlert();
globalThis._pysimCardIccid = DIGITS;
cardsIccidFromCard();
assert.strictEqual(els['cards-iccid'].value, DIGITS);
assert.deepStrictEqual(seen, []);
// no readable ICCID (state raced the click): alert, leave the field alone
els['cards-iccid'].value = 'keep-me';
globalThis._pysimCardIccid = null;
cardsIccidFromCard();
assert.strictEqual(els['cards-iccid'].value, 'keep-me');
assert.deepStrictEqual(seen, ['Card equipped but its ICCID is not readable']);
});
test('the From card button and the duplicate refusal are wired into the form', () => {
assert.match(html, /<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard\(\)"/);
assert.ok(html.includes('data-l10n="From card"'));
// normalized duplicate check with the conflicting preset named
assert.ok(html.includes('cardsFindDuplicateIccid(v.iccid, _cardsEditIdx)'));
assert.ok(html.includes("t('Card with this ICCID already exists') + ': ' + (cards[dupIdx].name"));
assert.ok(html.includes("'From card': 'С карты'"));
assert.ok(html.includes("'Card with this ICCID already exists': 'Карта с таким ICCID уже есть в списке'"));
});
test('the card-state update wires the ICCID into the preset selection', () => {
assert.ok(html.includes('cardsAutoSelectByIccid(status.iccid);'));
assert.ok(html.includes('_cardsAutoIccid = null;'));
assert.ok(html.includes("getElementById('tar-card-sel')"),
'the auto-selection must cover the TAR probe view');
assert.ok(html.includes("(data.iccid ? ' | ICCID: <b>' + esc(data.iccid) + '</b>' : '')"));
});
test('cardsRender highlights the equipped card preset', () => {
globalThis.t = s => s;
const els = { 'cards-tbody': { innerHTML: '' } };
globalThis.document = { getElementById: id => els[id] || null };
globalThis.cards = [
{ name: 'other', iccid: '1111111111111111111', keysets: [] },
{ name: 'live', iccid: DIGITS, keysets: [] },
];
globalThis._pysimCardIccid = DIGITS;
cardsRender();
let rows = els['cards-tbody'].innerHTML.split('</tr>');
assert.ok(!/bg-emerald-50/.test(rows[0]), 'the other preset must stay plain');
assert.match(rows[1], /bg-emerald-50/);
assert.match(rows[1], /The equipped card&#39;s preset/);
// the same card stored as raw EF hex matches too
globalThis.cards[1].iccid = RAW_HEX;
cardsRender();
rows = els['cards-tbody'].innerHTML.split('</tr>');
assert.match(rows[1], /bg-emerald-50/);
// no readable ICCID -> no highlight
globalThis._pysimCardIccid = null;
cardsRender();
assert.ok(!/bg-emerald-50/.test(els['cards-tbody'].innerHTML));
});