9a4dc80f11
The browser Back button now walks the app's own view stack:
- a small state object {tab, sub, parser, view, listtab, preset, suite,
script, item} is recorded on every view switch (navRecord/navCurrent/
navStateKey) via history.pushState - session-only, the URL never changes
(pushState throwing on file:// degrades to no history);
- popstate replays through the same switch functions under a _navApplying
guard (they re-render and re-translate): the seven tabs, every pill set
(SCP80 / Remote APDU + parser / Simulator / SCP81), the Cards list/editor
(preset id), the test-suite views (suite/script ids) and the Profiler
views/list tabs (profile/snapshot ids); unresolvable ids fall back to the
area's default view;
- the in-app Cancel/Back buttons (cardsCancelEdit, testScriptBack,
testBackToSuites, profilerBackToList, profilerSaveProfile) call
navReplace() so their entry is replaced instead of stacked, and the
initial entry is seeded with replaceState so the first Back returns to
the initial view.
The reported untranslated suite Run button was the tip of a larger gap: the
sweep found 21 t() keys without a Russian entry (Run/Suite/Move/Copy/Warning/
Text, the respond-result names, the event-data errors, the CAP tooltip, ...)
- all added, and html.test.js now asserts every t('...') literal and every
data-l10n/data-l10n-title attribute has a LANG_RU entry.
Tests: nav.test.js (the recording/replace hooks, the state-key
canonicalisation, the router core), the i18n completeness test, the
extracted-function stubs in the cards/phone/profiler tests.
842 frontend + 819 Python; sw cache simple-v409.
79 lines
3.5 KiB
JavaScript
79 lines
3.5 KiB
JavaScript
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);
|
|
}
|
|
|
|
// The pure state helpers (they only read the NAV_FIELDS constant).
|
|
eval(html.match(/const NAV_FIELDS = \[[\s\S]*?\];/)[0].replace('const ', 'var '));
|
|
eval(extractFunc(html, 'navStateKey'));
|
|
|
|
test('navStateKey canonicalises the view state', () => {
|
|
assert.strictEqual(navStateKey(null), navStateKey({}));
|
|
assert.strictEqual(navStateKey({ tab: 'cards' }),
|
|
navStateKey({ tab: 'cards', preset: '', view: '' }));
|
|
assert.notStrictEqual(navStateKey({ tab: 'cards', view: 'list' }),
|
|
navStateKey({ tab: 'cards', view: 'edit' }));
|
|
// the field order does not matter
|
|
assert.strictEqual(navStateKey({ tab: 'a', suite: 'x' }),
|
|
navStateKey({ suite: 'x', tab: 'a' }));
|
|
// every field participates in the identity
|
|
const base = { tab: 'phone', sub: 'test', view: 'script', suite: 's', script: 'c' };
|
|
const baseKey = navStateKey(base);
|
|
for (const f of ['tab', 'sub', 'parser', 'view', 'listtab', 'preset', 'suite', 'script', 'item']) {
|
|
const st = Object.assign({}, base);
|
|
st[f] = 'other';
|
|
assert.notStrictEqual(navStateKey(st), baseKey, f + ' must change the key');
|
|
}
|
|
});
|
|
|
|
test('the view switches record history, the back buttons replace it', () => {
|
|
// every user-facing switch records its view
|
|
for (const fn of ['switchTab', 'scp80SwitchSubtab', 'cApduSwitchSubtab',
|
|
'parserSwitchSubtab', 'phoneSwitchSubtab', 'scp81SwitchSubtab',
|
|
'cardsShowList', 'cardsShowEditor', 'testViewShow', 'profilerSetView',
|
|
'profilerListSwitch']) {
|
|
assert.match(extractFunc(html, fn), /navRecord\(\)/, fn + ' must record');
|
|
}
|
|
// the view variables the state is built from
|
|
assert.match(extractFunc(html, 'switchTab'), /_activeTab = name/);
|
|
assert.match(extractFunc(html, 'scp80SwitchSubtab'), /_scp80Sub = name/);
|
|
assert.match(extractFunc(html, 'cApduSwitchSubtab'), /_capduSub = name/);
|
|
assert.match(extractFunc(html, 'phoneSwitchSubtab'), /_phoneSub = name/);
|
|
assert.match(extractFunc(html, 'scp81SwitchSubtab'), /_scp81Sub = name/);
|
|
assert.match(extractFunc(html, 'cardsShowList'), /_cardsView = 'list'/);
|
|
assert.match(extractFunc(html, 'cardsShowEditor'), /_cardsView = 'edit'/);
|
|
// the in-app Cancel/Back (and the profiler save) replace their entry
|
|
for (const fn of ['cardsCancelEdit', 'profilerBackToList', 'testScriptBack',
|
|
'profilerSaveProfile']) {
|
|
assert.match(extractFunc(html, fn), /navReplace\(\)/, fn + ' must replace');
|
|
}
|
|
// the router core: seeded, guarded, replayed through the switch functions
|
|
assert.match(html, /window\.addEventListener\('popstate'/);
|
|
assert.match(html, /history\.pushState\(/);
|
|
assert.match(html, /history\.replaceState\(/);
|
|
assert.match(html, /navSeed\(\);/);
|
|
assert.match(extractFunc(html, 'navApply'), /_navApplying = true/);
|
|
assert.match(extractFunc(html, 'navApply'), /switchTab\(/);
|
|
for (const fn of ['navApplyTest', 'navApplyCards', 'navApplyProfiler', 'testBackToSuites']) {
|
|
assert.match(html, new RegExp('function ' + fn + '\\('), fn + ' must exist');
|
|
}
|
|
});
|