v3.22.0 (review fixes): the suite members share the SCP80 counter; drain/PWA nits
The v3.22.0 review found that a suite's SCP80 counter state was rebuilt from the preset snapshot for every member: a second SCP80 script sent the counter the first had consumed (the store persists the advance, the in-memory preset copy does not move) and the card answered cntr_low. Reproduced with a two-member suite (both packets carried CNTR=0000000010). - runner: `_test_run_script` takes a `shared` counter state (created once per suite, passed to every member; a single script passes None) and copies the no-keyset counter back into it, so a suite behaves like one script for counter bookkeeping. Pinned by a two-member test asserting the second packet's counter is the first's + 1 and the final counters. - `_test_drain_pending`: the unknown-length FETCH uses 0xff - `'%02x' % 0x100` emitted an odd-length APDU (a malformed fetch) when a 9100 SW (length 0) reached the drain. - PWA: the suite save adopts the stored suite (the server normalises the setup-first/teardown-last order); the role rows' up/down buttons are disabled (only members move); deleting a suite cancels its pending name debounce (no late 404); `refreshDynamicI18n` redraws the test views, so a language switch updates the suites list/report immediately. - tests: the cross-member counter test (Python) and the save-adoption / move-guard / i18n wiring tests (Node). Tests: 835 frontend + 795 Python; sw cache simple-v398.
This commit is contained in:
+15
-2
@@ -7319,6 +7319,8 @@ async function testSuiteDelete(i) {
|
||||
const s = testSuites()[i];
|
||||
if (!s) return;
|
||||
if (!confirm(t('Delete this test suite?'))) return;
|
||||
// a pending debounced name write must not 404 after the delete
|
||||
if (_testSuiteSaveTimer) { clearTimeout(_testSuiteSaveTimer); _testSuiteSaveTimer = null; }
|
||||
try {
|
||||
const resp = await pysimFetch('/api/test/suites/delete', {id: s.id});
|
||||
if (resp.error) throw new Error(resp.error);
|
||||
@@ -7381,6 +7383,13 @@ function testSuiteSave(immediate) {
|
||||
id: s.id, suite: {name: s.name || '', require_adm: !!s.require_adm,
|
||||
scripts: s.scripts || []}});
|
||||
if (resp.error) throw new Error(resp.error);
|
||||
// Adopt the stored suite: the server normalises the order (setup
|
||||
// first, teardown last) and may adjust the roles.
|
||||
if (resp.suite) {
|
||||
const i = testSuites().findIndex(x => x.id === s.id);
|
||||
if (i >= 0) _testSuites[i] = resp.suite;
|
||||
if (_testView === 'suite') testRenderSuiteScripts();
|
||||
}
|
||||
} catch (e) {
|
||||
ioStatus('test-io-status', t('Could not save the test suite') + ': ' + e.message);
|
||||
}
|
||||
@@ -7420,14 +7429,17 @@ function testRenderSuiteScripts() {
|
||||
entries.forEach((e, i) => {
|
||||
const script = testScriptById(e.script_id);
|
||||
const name = script ? (script.name || script.id) : ('(' + t('unknown script') + ': ' + String(e.script_id || '').slice(0, 8) + ')');
|
||||
// The roles define the order (setup first, teardown last): only the
|
||||
// member rows can be moved.
|
||||
const movable = e.role === 'member';
|
||||
html += '<div class="flex items-center gap-1.5 px-2 py-1.5">'
|
||||
+ '<span class="w-5 text-right text-xs text-gray-400">' + (i + 1) + '</span>'
|
||||
+ roleBadge(e.role)
|
||||
+ '<span class="flex-1 truncate ' + (script ? '' : 'text-red-500') + '">' + esc(name) + '</span>'
|
||||
+ '<select onchange="testSuiteRoleChanged(' + i + ', this.value)" class="' + sel + '">' + testRoleOptions(e.role) + '</select>'
|
||||
+ '<select onchange="testSuiteFailChanged(' + i + ', this.value)" class="' + sel + '" title="' + esc(t('on failure')) + '">' + testFailOptions(e.on_fail) + '</select>'
|
||||
+ '<button onclick="testSuiteMoveScript(' + i + ',-1)" class="px-1.5 text-xs text-gray-500 hover:text-gray-800 disabled:opacity-30" ' + (i === 0 ? 'disabled' : '') + '>↑</button>'
|
||||
+ '<button onclick="testSuiteMoveScript(' + i + ',1)" class="px-1.5 text-xs text-gray-500 hover:text-gray-800 disabled:opacity-30" ' + (i === entries.length - 1 ? 'disabled' : '') + '>↓</button>'
|
||||
+ '<button onclick="testSuiteMoveScript(' + i + ',-1)" class="px-1.5 text-xs text-gray-500 hover:text-gray-800 disabled:opacity-30" ' + (!movable || i === 0 ? 'disabled' : '') + '>↑</button>'
|
||||
+ '<button onclick="testSuiteMoveScript(' + i + ',1)" class="px-1.5 text-xs text-gray-500 hover:text-gray-800 disabled:opacity-30" ' + (!movable || i === entries.length - 1 ? 'disabled' : '') + '>↓</button>'
|
||||
+ (script ? '<button data-needs="card" onclick="testOpenScript(\'' + e.script_id + '\', true)" class="px-2 py-0.5 text-xs rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Run')) + '</button>' : '')
|
||||
+ (script ? '<button onclick="testOpenScript(\'' + e.script_id + '\')" class="' + btn + '">' + esc(t('Edit')) + '</button>' : '')
|
||||
+ '<button onclick="testSuitePickTarget(' + i + ',\'move\')" class="' + btn + '">' + esc(t('Move')) + '</button>'
|
||||
@@ -22505,6 +22517,7 @@ function refreshDynamicI18n() {
|
||||
pysimProactiveLogRender();
|
||||
}
|
||||
if (isViewVisible('phone-sub-tr')) pysimPliRender();
|
||||
if (isViewVisible('phone-sub-test')) testRender();
|
||||
if (isViewVisible('phone-sub-esim')) {
|
||||
esimRenderChip();
|
||||
esimRenderProfiles();
|
||||
|
||||
Reference in New Issue
Block a user