From 1a889ea4351b4bddd5f7ce3556cc4726131b2c0f Mon Sep 17 00:00:00 2001 From: th Date: Wed, 15 Apr 2026 19:02:58 +0200 Subject: [PATCH] tests: snapshot and reset view state around runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tests assume a normal board — no super-view, no bucket filter, manual sort, no hides. When the runner fired with the app left in super-view or with filters on, selectors like `.card[data-id]` missed the `.superview-item` DOM and multi-select / due-date assertions read the wrong initial values. Snapshot the relevant bits of window.state before the run, force a clean baseline, then restore the user's view afterwards so opening the runner doesn't silently wipe their context. Co-Authored-By: Claude Opus 4.6 (1M context) --- public/plugins/tests/runner.js | 84 ++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/public/plugins/tests/runner.js b/public/plugins/tests/runner.js index 3d8139e..79b93de 100644 --- a/public/plugins/tests/runner.js +++ b/public/plugins/tests/runner.js @@ -170,6 +170,71 @@ export function csrf() { return getCsrf(); } +// Snapshot the view-affecting bits of window.state so we can force a +// neutral baseline (normal board, no filter, manual sort, no hides) +// for the test run and restore the user's view afterwards. Tests were +// flaky when the runner fired while the board was in super-view: DOM +// selectors like `.card[data-id]` don't match `.superview-item`, so +// multi-select and due-date tests read wrong initial values. +export function snapshotViewState() { + const s = window.state; + if (!s) return null; + return { + superView: s.superView, + activeBucket: s.activeBucket, + activeTags: [...(s.activeTags || [])], + activeGroup: s.activeGroup, + sortMode: s.sortMode, + hideTermine: s.hideTermine, + hideDone: s.hideDone, + hideTagged: s.hideTagged, + boardSplitOpen: s.boardSplitOpen, + }; +} + +export function resetViewState() { + const s = window.state; + if (!s) return; + s.superView = false; + s.activeBucket = 'all'; + s.activeTags = []; + s.activeGroup = null; + s.sortMode = 'manual'; + s.hideTermine = false; + s.hideDone = false; + s.hideTagged = false; + s.boardSplitOpen = false; + s.selectedIds?.clear?.(); + // Sync super-view toggle visuals with the reset state so tests that + // inspect the button see the correct pose. + const btn = document.getElementById('superview-toggle'); + const icon = btn?.querySelector('i'); + if (icon) icon.className = 'ph ph-list text-sm'; + btn?.classList.remove('active'); + window._renderBoard?.(); +} + +export function restoreViewState(snap) { + const s = window.state; + if (!s || !snap) return; + Object.assign(s, { + superView: snap.superView, + activeBucket: snap.activeBucket, + activeTags: snap.activeTags, + activeGroup: snap.activeGroup, + sortMode: snap.sortMode, + hideTermine: snap.hideTermine, + hideDone: snap.hideDone, + hideTagged: snap.hideTagged, + boardSplitOpen: snap.boardSplitOpen, + }); + const btn = document.getElementById('superview-toggle'); + const icon = btn?.querySelector('i'); + if (icon) icon.className = `ph ${snap.superView ? 'ph-squares-four' : 'ph-list'} text-sm`; + btn?.classList.toggle('active', !!snap.superView); + window._renderBoard?.(); +} + export async function closeAll() { document.getElementById('hterm')?.classList.add('hidden'); for (const id of ['settings-overlay', 'help', 'shortcuts', 'search', 'calendar', 'gallery', 'detail', 'modal', 'tasks', 'dialog']) { @@ -452,6 +517,15 @@ async function run() { stopBackgroundSync(); window._testRunning = true; + // 2b. Snapshot + reset view state. Tests assume a normal board with + // no filters / super-view off / manual sort — if the user left the + // app in super-view or with a bucket filter active, assertions on + // .card[data-id], card count, etc. read wrong initial values. + const viewSnap = snapshotViewState(); + await closeAll(); + resetViewState(); + await sleep(50); + // 3. Hide panel so tests run against visible UI panel.style.display = 'none'; await sleep(100); @@ -476,6 +550,11 @@ async function run() { await window._reloadAll?.(); await sleep(200); } catch {} + // Re-reset the view in case a failing test left it in super-view, + // a bucket filter, or a sorted mode — otherwise the retry runs + // against the same wrong baseline that flaked it the first time. + resetViewState(); + await sleep(50); const retryResults = await runAll(failedKeys); const retryMap = new Map(); for (const sr of retryResults) { @@ -508,6 +587,11 @@ async function run() { await sleep(200); } catch {} + // 6b. Put the view back the way the user left it (super-view, sort, + // filters, …) so launching the test runner doesn't silently wipe + // their working context. + restoreViewState(viewSnap); + // 7. Resume background sync (delay to avoid immediate re-sync creating duplicates) window._testRunning = false; setTimeout(() => startBackgroundSync(), 60000);