From add41a09d0a56b50b5060320fd05e81e682de160 Mon Sep 17 00:00:00 2001 From: dborup Date: Mon, 5 Oct 2026 22:36:53 +0200 Subject: [PATCH 1/2] test(e2e): make the #180 packets URL reload step independent of fixture age (#271) Relates to #271 The step compared row COUNTS at the default 15-min window across two separate renders (Clear Filters' in-SPA list, then a full page reload). Both fetches recompute "last 15 minutes" from the browser's live clock, so a fixture packet can cross that boundary in the real-time gap between the two fetches -- same class of problem #252 fixed for the Details test. After the Clear-itself assertions (bare #/packets URL, no detail pane -- the ones a "Clear keeps ?timeWindow=/the detail" mutant breaks), pin a wide 24h window for the list-identity check and compare the actual set of packet hashes instead of a count, so the comparison cannot depend on how old the fixture is. --- test-issue-180-packets-url-modal-e2e.js | 41 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/test-issue-180-packets-url-modal-e2e.js b/test-issue-180-packets-url-modal-e2e.js index 5dadbec3f..26b117b89 100644 --- a/test-issue-180-packets-url-modal-e2e.js +++ b/test-issue-180-packets-url-modal-e2e.js @@ -49,11 +49,6 @@ async function fresh(page, url) { await page.reload({ waitUntil: 'load' }); } -// The "Latest Packets (N)" count, once the list has rendered. -async function listCount(page) { - await page.waitForSelector('#pktLeft .count', { timeout: 15000 }); - return page.evaluate(() => Number((document.querySelector('#pktLeft .count').textContent.match(/\d+/) || [])[0])); -} function clearShown(page) { return page.evaluate(() => { const b = document.getElementById('clearFiltersBtn'); @@ -102,25 +97,45 @@ function detailPaneOpen(page) { await waitFor(page, () => /\(1\)/.test(document.querySelector('#pktLeft .count').textContent), 'detail URL did not filter the list to 1 packet'); assert(await clearShown(page), 'Clear button hidden on the filtered detail URL'); - // The fixture's packets may have aged out of the default 15 min window - // that Clear restores, so compare what Clear showed with what the - // reload shows rather than expecting a number of rows. const listLoaded = () => page.waitForResponse((r) => /\/api\/packets\?/.test(r.url()), { timeout: 15000 }); await Promise.all([listLoaded(), page.click('#clearFiltersBtn')]); await page.waitForTimeout(500); let h = await page.evaluate(() => location.hash); + // These three assertions are what Clear itself must do, and are the + // ones a "Clear keeps ?timeWindow= / keeps the detail" mutant breaks: + // the bare list URL, no detail pane, the saved window back at default. assert(h === '#/packets', 'URL after Clear: ' + h); - const shown = await listCount(page); assert(!(await clearShown(page)), 'Clear button still visible after Clear'); assert(!(await detailPaneOpen(page)), 'detail pane still open after Clear'); + // #271: comparing row COUNTS at the default 15-min window across two + // renders (Clear's in-SPA list, then a full reload) is a race against + // the fixture's age — the CI fixture ages while the job runs, and a + // packet can cross the 15-min boundary in the gap between the two + // fetches (same class of problem #252 fixed for the Details test). + // Pin a wide window (24h; the widest option, same as #252) for the + // list-identity check below, and compare the actual set of packet + // hashes rather than a count, so the comparison cannot depend on how + // old the fixture is. + await Promise.all([listLoaded(), page.evaluate(() => { + const sel = document.getElementById('fTimeWindow'); + sel.value = '1440'; + sel.dispatchEvent(new Event('change', { bubbles: true })); + })]); + await page.waitForTimeout(500); + const hashSet = async () => { + await page.waitForSelector('#pktLeft .count', { timeout: 15000 }); + return page.evaluate(() => Array.from(document.querySelectorAll('#pktBody > tr:not(.group-child)')).map((tr) => tr.dataset.hash).sort()); + }; + const shown = await hashSet(); + assert(shown.length > 0, 'no rows to compare at the pinned 24h window'); + await Promise.all([listLoaded(), page.reload({ waitUntil: 'load' })]); await page.waitForTimeout(500); h = await page.evaluate(() => location.hash); - assert(h === '#/packets', 'URL after reload: ' + h); - const reloaded = await listCount(page); - assert(reloaded === shown, 'reload shows ' + reloaded + ' packets, Clear showed ' + shown); - assert(!(await clearShown(page)), 'Clear button back after reload'); + assert(h === '#/packets?timeWindow=1440', 'URL after reload: ' + h); + const reloaded = await hashSet(); + assert(JSON.stringify(reloaded) === JSON.stringify(shown), 'reload shows ' + JSON.stringify(reloaded) + ', pinned-window Clear state showed ' + JSON.stringify(shown)); assert(!(await detailPaneOpen(page)), 'detail pane open after reload'); assert(await page.evaluate(() => document.getElementById('fHash').value === ''), 'hash filter input filled after reload'); }); From a032471a8f52c7471e1c7d700cd47bf48350c58d Mon Sep 17 00:00:00 2001 From: dborup Date: Tue, 6 Oct 2026 08:34:52 +0200 Subject: [PATCH 2/2] reviewfix(e2e): reload Clear's own URL and compare list state, not spacer rows (#271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-2 review findings on PR #280: 1. The step pinned 24h before its only reload, so it reloaded `#/packets?timeWindow=1440` instead of the bare `#/packets` Clear wrote, and nothing replaced the removed "Clear button back after reload" check. Reload what Clear wrote first and compare state — URL, Clear button, the saved window value, the detail pane, the hash input — all of which are clock-independent; then do the pinned-window list comparison as before. 2. The table is virtualised, so `#pktBody > tr:not(.group-child)` also matches the two #vscroll-* spacer rows, whose `dataset.hash` is undefined. Drop rows without a hash, assert at least one real row is rendered, and also compare the "Latest Packets (N)" total, which is the whole filtered set and clock-independent at 24h. 3. Corrected the comment that claimed the three post-Clear assertions include the saved window going back to the default; they are the URL, the Clear button and the detail pane. Test-only; no product change. --- test-issue-180-packets-url-modal-e2e.js | 73 ++++++++++++++++++------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/test-issue-180-packets-url-modal-e2e.js b/test-issue-180-packets-url-modal-e2e.js index 26b117b89..efecc461a 100644 --- a/test-issue-180-packets-url-modal-e2e.js +++ b/test-issue-180-packets-url-modal-e2e.js @@ -101,43 +101,74 @@ function detailPaneOpen(page) { await Promise.all([listLoaded(), page.click('#clearFiltersBtn')]); await page.waitForTimeout(500); let h = await page.evaluate(() => location.hash); - // These three assertions are what Clear itself must do, and are the - // ones a "Clear keeps ?timeWindow= / keeps the detail" mutant breaks: - // the bare list URL, no detail pane, the saved window back at default. + // What Clear itself must do, asserted right after the click: the bare + // list URL, the Clear button gone and the detail pane closed. These are + // what a "Clear keeps ?timeWindow=" or "Clear keeps the detail" mutant + // breaks. (The window reset to the default is not visible here beyond + // Clear being hidden; it is asserted across the reload below.) assert(h === '#/packets', 'URL after Clear: ' + h); assert(!(await clearShown(page)), 'Clear button still visible after Clear'); assert(!(await detailPaneOpen(page)), 'detail pane still open after Clear'); - // #271: comparing row COUNTS at the default 15-min window across two - // renders (Clear's in-SPA list, then a full reload) is a race against - // the fixture's age — the CI fixture ages while the job runs, and a - // packet can cross the 15-min boundary in the gap between the two - // fetches (same class of problem #252 fixed for the Details test). - // Pin a wide window (24h; the widest option, same as #252) for the - // list-identity check below, and compare the actual set of packet - // hashes rather than a count, so the comparison cannot depend on how - // old the fixture is. + // Reloading the URL Clear wrote must give Clear's own state back — + // that is the "a reload shows the same list" half of this step, and the + // one a mutant where Clear's default no longer matches the default a + // cold load restores breaks. Compare STATE (URL, Clear button, the + // saved window, the detail pane, the hash input), all of which are + // clock-independent; the list contents are compared separately below at + // a pinned window, because row counts at the default window are not + // (#271). + const windowValue = () => page.evaluate(() => document.getElementById('fTimeWindow').value); + const twAfterClear = await windowValue(); + await Promise.all([listLoaded(), page.reload({ waitUntil: 'load' })]); + await page.waitForTimeout(500); + h = await page.evaluate(() => location.hash); + assert(h === '#/packets', 'URL after reloading the Clear state: ' + h); + assert(!(await clearShown(page)), 'Clear button back after reload'); + const twAfterReload = await windowValue(); + assert(twAfterReload === twAfterClear, 'time window after reload ' + twAfterReload + ', after Clear ' + twAfterClear); + assert(!(await detailPaneOpen(page)), 'detail pane open after reloading the Clear state'); + assert(await page.evaluate(() => document.getElementById('fHash').value === ''), 'hash filter input filled after reload'); + + // #271: the list-contents half of this step used to compare row COUNTS + // at the default 15-min window across two renders (Clear's in-SPA list, + // then a full reload). Both compute `since = now - 15min` from the + // browser's live clock, so a fixture packet can cross that boundary in + // the real-time gap between the two fetches, and the CI fixture ages + // while the job runs (same class of problem #252 fixed for the Details + // test). Pin a wide window (24h; the widest option, same as #252) and + // compare the "Latest Packets (N)" total — the whole filtered set — + // plus the hashes of the rows actually rendered, so the comparison + // cannot depend on how old the fixture is. await Promise.all([listLoaded(), page.evaluate(() => { const sel = document.getElementById('fTimeWindow'); sel.value = '1440'; sel.dispatchEvent(new Event('change', { bubbles: true })); })]); await page.waitForTimeout(500); - const hashSet = async () => { + // The table is virtualised, so only a window of rows is in the DOM at + // any time: `total` is the full filtered count, `hashes` is what is + // rendered. The two #vscroll-* spacer rows carry no data-hash and are + // dropped, so an empty table cannot pass as "a set of hashes". + const listState = async () => { await page.waitForSelector('#pktLeft .count', { timeout: 15000 }); - return page.evaluate(() => Array.from(document.querySelectorAll('#pktBody > tr:not(.group-child)')).map((tr) => tr.dataset.hash).sort()); + return page.evaluate(() => ({ + total: Number((document.querySelector('#pktLeft .count').textContent.match(/\d+/) || [])[0]), + hashes: Array.from(document.querySelectorAll('#pktBody > tr:not(.group-child)')).map((tr) => tr.dataset.hash).filter(Boolean).sort(), + })); }; - const shown = await hashSet(); - assert(shown.length > 0, 'no rows to compare at the pinned 24h window'); + const shown = await listState(); + assert(shown.total > 0, 'no packets in the pinned 24h window'); + assert(shown.hashes.length > 0, 'no rendered rows with a hash to compare at the pinned 24h window'); await Promise.all([listLoaded(), page.reload({ waitUntil: 'load' })]); await page.waitForTimeout(500); h = await page.evaluate(() => location.hash); - assert(h === '#/packets?timeWindow=1440', 'URL after reload: ' + h); - const reloaded = await hashSet(); - assert(JSON.stringify(reloaded) === JSON.stringify(shown), 'reload shows ' + JSON.stringify(reloaded) + ', pinned-window Clear state showed ' + JSON.stringify(shown)); - assert(!(await detailPaneOpen(page)), 'detail pane open after reload'); - assert(await page.evaluate(() => document.getElementById('fHash').value === ''), 'hash filter input filled after reload'); + assert(h === '#/packets?timeWindow=1440', 'URL after reloading the pinned window: ' + h); + const reloaded = await listState(); + assert(reloaded.total === shown.total, 'reload shows ' + reloaded.total + ' packets at the pinned window, before the reload it showed ' + shown.total); + assert(JSON.stringify(reloaded.hashes) === JSON.stringify(shown.hashes), 'reload renders ' + JSON.stringify(reloaded.hashes) + ' at the pinned window, before the reload it rendered ' + JSON.stringify(shown.hashes)); + assert(!(await detailPaneOpen(page)), 'detail pane open after reloading the pinned window'); }); await context.close(); }