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