From 9e7d2cd4f9f9148916e92b0c4152bd0e64060000 Mon Sep 17 00:00:00 2001 From: dormouse-bot <287024035+dormouse-bot@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:11:24 +0000 Subject: [PATCH 1/2] Settle the notepad prompt's answer inside act so the kill's removal timer wins --- lib/src/components/Wall.test.tsx | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/lib/src/components/Wall.test.tsx b/lib/src/components/Wall.test.tsx index e0908d64..605cd616 100644 --- a/lib/src/components/Wall.test.tsx +++ b/lib/src/components/Wall.test.tsx @@ -1681,11 +1681,18 @@ describe('Wall on the Lath engine', () => { return container.querySelector('[aria-labelledby="notepad-archive-failure-title"]'); } - function clickButton(label: string): void { + /** Answer the prompt and settle the closure it starts. Both answers run an async + * chain that ends on the two-phase kill's deferred removal timer, so the click's + * own async work is awaited BEFORE `flush()` registers the timer that has to fire + * after it. A bare `act(click)` leaves the two `setTimeout(0)`s racing: the + * removal is registered while the test awaits `flush()`, so it lands second and + * the leaf is still mid-fade when the assertion runs. */ + async function clickButton(label: string): Promise { const button = Array.from(container.querySelectorAll('button')) .find((candidate) => candidate.textContent?.trim() === label); expect(button, `no "${label}" button`).toBeDefined(); - act(() => { button!.click(); }); + await act(async () => { button!.click(); }); + await flush(); } /** The pane header's Kill button — a user-visible closure, which does prompt. @@ -1853,9 +1860,7 @@ describe('Wall on the Lath engine', () => { await clickHeaderKill('pane-a'); expect(archiveFailureModal()).not.toBeNull(); setBusy(true); - clickButton('Close anyway'); - await flush(); - await flush(); + await clickButton('Close anyway'); expect(getNotes('pane-a')).toHaveLength(1); expect(dispose).not.toHaveBeenCalled(); expect(container.querySelector('[data-lath-leaf="pane-a"]')).not.toBeNull(); @@ -1925,8 +1930,7 @@ describe('Wall on the Lath engine', () => { act(() => { addPlainNote('pane-a', 'keep me'); }); await clickHeaderKill('pane-a'); - clickButton('Keep open'); - await flush(); + await clickButton('Keep open'); expect(archiveFailureModal()).toBeNull(); expect(container.querySelector('[data-lath-leaf="pane-a"]')).not.toBeNull(); @@ -1942,8 +1946,7 @@ describe('Wall on the Lath engine', () => { act(() => { addPlainNote('pane-a', 'expendable'); }); await clickHeaderKill('pane-a'); - clickButton('Close anyway'); - await flush(); + await clickButton('Close anyway'); expect(archiveFailureModal()).toBeNull(); expect(container.querySelector('[data-lath-leaf="pane-a"]')).toBeNull(); @@ -1972,12 +1975,10 @@ describe('Wall on the Lath engine', () => { // A's prompt is the one on screen; B's is behind it. expect(archiveFailureModal()?.textContent).toContain('a could not be written'); - clickButton('Keep open'); - await flush(); + await clickButton('Keep open'); expect(archiveFailureModal()?.textContent).toContain('b could not be written'); - clickButton('Close anyway'); - await flush(); + await clickButton('Close anyway'); expect(archiveFailureModal()).toBeNull(); expect(container.querySelector('[data-lath-leaf="pane-a"]')).not.toBeNull(); From c1a5ec4a024a8697f3a922885d8d83bbc49779ed Mon Sep 17 00:00:00 2001 From: dormouse-bot <287024035+dormouse-bot@users.noreply.github.com> Date: Mon, 7 Sep 2026 06:19:56 +0000 Subject: [PATCH 2/2] Scope the clickButton doc comment to the answer that starts the timer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Keep open` is `onKeepOpen={shiftArchiveFailure}` — a synchronous queue shift with no `closeSurface` call and no removal timer — so only `Close anyway` depends on the act/flush ordering the helper enforces. --- lib/src/components/Wall.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/components/Wall.test.tsx b/lib/src/components/Wall.test.tsx index 605cd616..8b8379a0 100644 --- a/lib/src/components/Wall.test.tsx +++ b/lib/src/components/Wall.test.tsx @@ -1681,12 +1681,13 @@ describe('Wall on the Lath engine', () => { return container.querySelector('[aria-labelledby="notepad-archive-failure-title"]'); } - /** Answer the prompt and settle the closure it starts. Both answers run an async + /** Answer the prompt and settle the closure it starts. `Close anyway` runs an async * chain that ends on the two-phase kill's deferred removal timer, so the click's * own async work is awaited BEFORE `flush()` registers the timer that has to fire * after it. A bare `act(click)` leaves the two `setTimeout(0)`s racing: the * removal is registered while the test awaits `flush()`, so it lands second and - * the leaf is still mid-fade when the assertion runs. */ + * the leaf is still mid-fade when the assertion runs. (`Keep open` only shifts the + * prompt queue, so it needs no ordering — one helper still covers both.) */ async function clickButton(label: string): Promise { const button = Array.from(container.querySelectorAll('button')) .find((candidate) => candidate.textContent?.trim() === label);