feat: warn when the equipped card and the selected preset disagree (v3.8.0)
Autoselection normally keeps the SCP80/RAM preset in step with the card, so
the mismatch is easy to miss. Both views now carry an amber banner above the
preset selector (the existing warning style - no new Tailwind classes):
- the equipped card matches no preset: "The equipped card <ICCID> does not
match any card preset - the selected preset may belong to another card.
Add it in the Cards tab (From card)"; with no presets at all the banner says
so instead;
- a preset is selected but it is not the equipped card's: a softer line naming
the matching preset ("check that its keys and counters belong to this card").
`spPresetWarning(iccid, presets, selectedIdx)` is pure (unit-tested with the
ICCID forms the store accepts: digits, spaced, raw EF hex), `spPresetWarningRender`
fills both banners and runs on every card-state poll, preset load, preset
selection and page init. No card session or an unreadable ICCID shows nothing
(the card-iccid gating already explains that state); the banner warns only -
it never blocks a send (preparing a packet for another card stays possible).
Tests: frontend/tests/preset_warning.test.js (5), the card-state/preset
harnesses stub the DOM-bound renderer. sw cache simple-v336.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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 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',
|
||||
'spPresetWarning']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
code += 'function t(s){return s;}\n';
|
||||
eval(code);
|
||||
|
||||
const DIGITS = '8970119000004600098';
|
||||
const RAW_HEX = '980711090000640090F8';
|
||||
const OTHER = '89390100000129506903';
|
||||
const CARDS = [
|
||||
{ name: 'Card A', iccid: DIGITS },
|
||||
{ name: 'Card B', iccid: OTHER },
|
||||
];
|
||||
|
||||
test('no warning without a card session or a readable ICCID', () => {
|
||||
assert.strictEqual(spPresetWarning(null, CARDS, 0), '');
|
||||
assert.strictEqual(spPresetWarning('', CARDS, 0), '');
|
||||
assert.strictEqual(spPresetWarning(undefined, CARDS, 0), '');
|
||||
});
|
||||
|
||||
test('no warning when the equipped card matches a preset', () => {
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, 0), '');
|
||||
// the preset's ICCID may be stored in any form
|
||||
assert.strictEqual(spPresetWarning(DIGITS, [{ name: 'A', iccid: RAW_HEX }], 0), '');
|
||||
// the match does not depend on the selection
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, 1) === '', false);
|
||||
});
|
||||
|
||||
test('a card with no matching preset gets the strong warning', () => {
|
||||
const text = spPresetWarning('8945001234567890123', CARDS, 0);
|
||||
assert.match(text, /8945001234567890123/);
|
||||
assert.match(text, /does not match any card preset/);
|
||||
assert.match(text, /From card/);
|
||||
// no presets at all: the other wording
|
||||
const none = spPresetWarning(DIGITS, [], 0);
|
||||
assert.match(none, /No card presets defined/);
|
||||
assert.match(spPresetWarning(DIGITS, null, -1), /No card presets defined/);
|
||||
});
|
||||
|
||||
test('a manually selected preset of another card gets the softer hint', () => {
|
||||
const text = spPresetWarning(DIGITS, CARDS, 1);
|
||||
assert.match(text, /selected preset is not the equipped card/);
|
||||
assert.match(text, /Card A/);
|
||||
// an empty / out-of-range selection is not the softer case (the strong
|
||||
// warning above covers a card with no preset)
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, -1), '');
|
||||
assert.strictEqual(spPresetWarning(DIGITS, CARDS, 5), '');
|
||||
});
|
||||
|
||||
test('both SCP80 views carry the warning banner', () => {
|
||||
assert.match(html, /id="sp-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400/);
|
||||
assert.match(html, /id="ram-preset-warning" class="hidden mb-3 px-3 py-2 rounded border border-amber-400/);
|
||||
// ... and the renderer is wired into the card state, the preset load and
|
||||
// the preset selection
|
||||
assert.ok(html.includes('spPresetWarningRender();'));
|
||||
assert.ok(html.includes("show('sp-preset-warning', spPresetWarning(iccid, cards, spPresetIdx()));"));
|
||||
assert.ok(html.includes("show('ram-preset-warning', spPresetWarning(iccid, cards, ramPresetIdx()));"));
|
||||
});
|
||||
Reference in New Issue
Block a user