From 30a742618e33c62c8ee5804e2d59f6aa9ad0f001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sun, 18 Oct 2026 00:35:45 +0300 Subject: [PATCH] v3.30.4 follow-up: the suite delete re-resolves the selection by id The independent re-check of the v3.30.4 identity fixes found a pre-existing gap in testSuiteDelete: it cleared the selection only when the deleted suite WAS the selected one, so deleting a suite BEFORE the selection left _testSuiteIdx pointing at whatever moved into that slot (or out of range when the list shrank). The delete now captures the selected suite's id just before the store refresh and re-resolves the index after it, which also keeps a selection the user made while the dialog was open (the modal no longer freezes the page). Test: a behavioural test drives the real function through the four cases (the selected suite deleted; a suite before it deleted; the selection moved during the dialog; nothing selected) - mutation-checked to fail without the re-resolution. 946 frontend + 952 Python. --- frontend/index.html | 13 +++++----- frontend/tests/testsuites.test.js | 41 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 216eda0..2439459 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7598,20 +7598,19 @@ function testSuiteOpen(i) { async function testSuiteDelete(i) { const s = testSuites()[i]; if (!s) return; - // resolve the selection before the await: the list may be reordered while - // the confirmation is open (the modal does not block the page), so the - // reset below re-checks by id that the selection still points here - const wasSelected = _testSuiteIdx === i; - const suiteId = s.id; if (!(await pysimConfirm(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); + // The list may have been reordered while the confirmation was open (the + // modal does not block the page) and the delete itself shifts every + // index after it: re-resolve the selection by id, not by position. + const selId = (testCurrentSuite() || {}).id; await testStoresFetch(); - const cur = testCurrentSuite(); - if (wasSelected && (!cur || cur.id === suiteId)) _testSuiteIdx = -1; + const at = testSuites().findIndex(x => x.id === selId); + _testSuiteIdx = at >= 0 ? at : -1; testViewShow('suites'); } catch (e) { ioStatus('test-io-status', t('Could not delete the test suite') + ': ' + e.message); diff --git a/frontend/tests/testsuites.test.js b/frontend/tests/testsuites.test.js index f7f2d59..2e727c7 100644 --- a/frontend/tests/testsuites.test.js +++ b/frontend/tests/testsuites.test.js @@ -191,6 +191,47 @@ test('role rows cannot be moved and the i18n refresh redraws the test views', () assert.match(i18n, /isViewVisible\('phone-sub-test'\)\) testRender\(\)/); }); +test('deleting a suite re-resolves the selection by id, not by index', async () => { + // The delete shifts every index after it and the list may be reordered + // while the confirmation is open (the modal does not block the page). + const del = extractFunc(html, 'testSuiteDelete', true); + const make = state => new Function('state', ` + var _testSuiteIdx = state.selIdx; + var _testSuiteSaveTimer = null; + var testSuites = () => state.suites; + var testCurrentSuite = () => state.suites[_testSuiteIdx]; + var pysimConfirm = async () => { + if (state.moveTo !== undefined && state.moveTo !== null) _testSuiteIdx = state.moveTo; + return true; + }; + var pysimFetch = async (path, body) => { state.deleted = body.id; return {}; }; + var testStoresFetch = async () => { + state.suites = state.suites.filter(x => x.id !== state.deleted); + }; + var testViewShow = v => { state.view = v; }; + var ioStatus = (id, msg) => { state.status = msg; }; + var t = x => x; + ${del} + return { run: testSuiteDelete, idx: () => _testSuiteIdx }; + `)(state); + // deleting the SELECTED suite clears the selection + let app = make({ suites: [{id: 'SA'}, {id: 'SB'}, {id: 'SC'}], selIdx: 2 }); + await app.run(2); + assert.strictEqual(app.idx(), -1); + // deleting a suite BEFORE the selection keeps it selected (the index shifts) + app = make({ suites: [{id: 'SA'}, {id: 'SB'}, {id: 'SC'}], selIdx: 2 }); + await app.run(0); + assert.strictEqual(app.idx(), 1); + // a selection made while the dialog was open wins + app = make({ suites: [{id: 'SA'}, {id: 'SB'}, {id: 'SC'}], selIdx: 0, moveTo: 1 }); + await app.run(2); + assert.strictEqual(app.idx(), 1); + // nothing selected stays nothing selected + app = make({ suites: [{id: 'SA'}], selIdx: -1 }); + await app.run(0); + assert.strictEqual(app.idx(), -1); +}); + test('the report keeps an expanded run log open across the live polls', () => { // A live suite run re-renders the member blocks on every poll; replacing // innerHTML creates fresh
, so the open state is carried over