From 84cd70c916d65c0b72f1e3b871e0ff41400ea17f Mon Sep 17 00:00:00 2001 From: chriscoveries <212684707+chriscoveries@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:32:27 +0000 Subject: [PATCH 1/2] test(web): settle the lazy Guidance tab inside act() in SettingsModal guidance test The first it.each case renders SettingsModal while the lazily imported GuidanceTab chunk is still loading. The render's act() scope exits with the Suspense boundary pending, so the chunk resolves outside act(): React logs "A suspended resource finished loading inside a test, but the event was not wrapped in act(...)" and the tab's retry render, its mount effect and its prompt load run on the Scheduler while the test polls with vi.waitFor. On a loaded runner the portaled expanded dialog is then not found within vi.waitFor's 1000 ms budget. The second case is unaffected because the lazy component is already resolved. Wait for the tab in back-to-back act() scopes instead of vi.waitFor, so an act() scope is open whenever the chunk resolves and every follow-up render and effect is flushed by act() before the click. A single act() scope around vi.waitFor would not work: act() flushes queued React work only when its outermost scope exits, so the DOM would never update while polling. Test-only change; no product code is touched. --- web/src/components/SettingsModal.guidance.test.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/components/SettingsModal.guidance.test.tsx b/web/src/components/SettingsModal.guidance.test.tsx index 031ec01d7..4bcf53035 100644 --- a/web/src/components/SettingsModal.guidance.test.tsx +++ b/web/src/components/SettingsModal.guidance.test.tsx @@ -84,7 +84,13 @@ it.each([0, 1])( />, ), ); - await vi.waitFor(() => expect(host.querySelectorAll("textarea")).toHaveLength(2)); + // GuidanceTab is a lazy chunk. Wait for it inside act() so its retry render, its + // prompt load and their effects are flushed by act(), not by the Scheduler after the + // test has moved on. act() flushes only when its outermost scope exits, so poll + // between back-to-back scopes rather than with vi.waitFor inside one. + while (host.querySelectorAll("textarea").length !== 2) { + await act(async () => new Promise((resolve) => setTimeout(resolve, 10))); + } const trigger = [...host.querySelectorAll("button")].filter( (button) => button.textContent === "guidance.expand", )[index]; From de391350e9383881fd412d6de7a0efad12248b53 Mon Sep 17 00:00:00 2001 From: chriscoveries <212684707+chriscoveries@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:32:21 +0000 Subject: [PATCH 2/2] test(web): bound Guidance readiness before the test timeout --- web/src/components/SettingsModal.guidance.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/web/src/components/SettingsModal.guidance.test.tsx b/web/src/components/SettingsModal.guidance.test.tsx index 4bcf53035..174b93b44 100644 --- a/web/src/components/SettingsModal.guidance.test.tsx +++ b/web/src/components/SettingsModal.guidance.test.tsx @@ -88,9 +88,12 @@ it.each([0, 1])( // prompt load and their effects are flushed by act(), not by the Scheduler after the // test has moved on. act() flushes only when its outermost scope exits, so poll // between back-to-back scopes rather than with vi.waitFor inside one. - while (host.querySelectorAll("textarea").length !== 2) { + // Bounded under the 5 s test timeout so a lazy-chunk failure ends at this assertion. + const readinessDeadline = Date.now() + 4000; + while (host.querySelectorAll("textarea").length < 2 && Date.now() < readinessDeadline) { await act(async () => new Promise((resolve) => setTimeout(resolve, 10))); } + expect(host.querySelectorAll("textarea")).toHaveLength(2); const trigger = [...host.querySelectorAll("button")].filter( (button) => button.textContent === "guidance.expand", )[index];