diff --git a/ui/src/app/question-prompt.test.ts b/ui/src/app/question-prompt.test.ts index db4a97f24847..babf5f502b9c 100644 --- a/ui/src/app/question-prompt.test.ts +++ b/ui/src/app/question-prompt.test.ts @@ -1,5 +1,6 @@ // Control UI tests cover operator question parsing and lifecycle state. import { afterEach, describe, expect, it, vi } from "vitest"; +import { waitForFast } from "../test-helpers/wait-for.ts"; import { cancelQuestionPrompt, createQuestionPromptState, @@ -367,7 +368,7 @@ describe("refreshPendingQuestions", () => { setQuestionPromptClient(state, client); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("pending")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("pending")); expect(request).toHaveBeenCalledWith("question.list", {}); expect(state.prompts.get("question-1")?.status).toBe("pending"); @@ -423,7 +424,7 @@ describe("refreshPendingQuestions", () => { }, }); finishList({ questions: [requestedPayload()] }); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(state.prompts.get("question-1")?.status).toBe("answered"); }); @@ -456,7 +457,7 @@ describe("refreshPendingQuestions", () => { }, }); finishList({ questions: [] }); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(request).toHaveBeenCalledWith("question.get", { id: "question-1" }); expect(state.prompts.get("question-1")).toMatchObject({ @@ -488,7 +489,7 @@ describe("refreshPendingQuestions", () => { }); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(request).toHaveBeenCalledWith("question.get", { id: "question-1" }); expect(state.prompts.get("question-1")).toMatchObject({ @@ -524,11 +525,11 @@ describe("refreshPendingQuestions", () => { }); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(2)); expect(state.prompts.get("question-1")?.status).toBe("pending"); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(state.prompts.get("question-1")).toMatchObject({ status: "answered", answeredElsewhere: true, @@ -552,7 +553,7 @@ describe("refreshPendingQuestions", () => { }); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(state.prompts.get("question-1")).toMatchObject({ status: "answered", @@ -590,7 +591,7 @@ describe("refreshPendingQuestions", () => { }); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(state.prompts.get("question-1")).toMatchObject({ status: "answered", @@ -621,7 +622,7 @@ describe("refreshPendingQuestions", () => { }); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => + await waitForFast(() => expect(request).toHaveBeenCalledWith("question.get", { id: "question-1" }), ); await vi.advanceTimersByTimeAsync(1_000); @@ -633,7 +634,7 @@ describe("refreshPendingQuestions", () => { }), }); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(state.prompts.get("question-1")).toMatchObject({ status: "answered", locallyExpired: false, @@ -670,7 +671,7 @@ describe("refreshPendingQuestions", () => { await vi.advanceTimersByTimeAsync(1_000); finishList({ questions: [] }); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("answered")); expect(request).toHaveBeenCalledWith("question.get", { id: "question-1" }); expect(state.prompts.get("question-1")).toMatchObject({ status: "answered", @@ -702,7 +703,7 @@ describe("refreshPendingQuestions", () => { onChange.mockClear(); refreshPendingQuestionsWithRetry(state, client); - await vi.waitFor(() => + await waitForFast(() => expect(request).toHaveBeenCalledWith("question.get", { id: "question-1" }), ); @@ -710,6 +711,6 @@ describe("refreshPendingQuestions", () => { expect(onChange).toHaveBeenCalled(); finishGet({ question: requestedPayload({ status: "cancelled" }) }); - await vi.waitFor(() => expect(state.prompts.get("question-1")?.status).toBe("cancelled")); + await waitForFast(() => expect(state.prompts.get("question-1")?.status).toBe("cancelled")); }); }); diff --git a/ui/src/components/agent-select.test.ts b/ui/src/components/agent-select.test.ts index d4ac647da936..c831dfdc9055 100644 --- a/ui/src/components/agent-select.test.ts +++ b/ui/src/components/agent-select.test.ts @@ -3,6 +3,7 @@ import { expect, it, vi } from "vitest"; import type { AgentIdentityResult, GatewayAgentRow } from "../api/types.ts"; import { i18n, t } from "../i18n/index.ts"; +import { waitForFast } from "../test-helpers/wait-for.ts"; import { AgentSelect } from "./agent-select.ts"; const AGENT_SELECT_TEST_TAG = `test-openclaw-agent-select-${crypto.randomUUID()}`; @@ -117,7 +118,7 @@ it("fetches local avatars with the bearer credential when token auth is active", signal: expect.any(AbortSignal), }); - await vi.waitFor(() => { + await waitForFast(() => { expect( element.querySelector("img.agent-select__avatar")?.getAttribute("src"), ).toBe("blob:agent-avatar"); @@ -152,13 +153,13 @@ it("refetches a failed local avatar after the auth credential rotates", async () }); try { - await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); + await waitForFast(() => expect(fetchMock).toHaveBeenCalledTimes(1)); expect(element.querySelector("img.agent-select__avatar")).toBeNull(); element.authToken = "tok2"; await element.updateComplete; - await vi.waitFor(() => { + await waitForFast(() => { expect(fetchMock).toHaveBeenLastCalledWith("/avatar/alpha", { headers: { Authorization: "Bearer tok2" }, signal: expect.any(AbortSignal), @@ -208,7 +209,7 @@ it("aborts the stale request on auth rotation without duplicating the current fe expect(fetchMock).toHaveBeenCalledTimes(1); element.authToken = "tok2"; await element.updateComplete; - await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2)); + await waitForFast(() => expect(fetchMock).toHaveBeenCalledTimes(2)); expect(pending[0]?.signal.aborted).toBe(true); expect( @@ -226,7 +227,7 @@ it("aborts the stale request on auth rotation without duplicating the current fe ok: true, blob: async () => new Blob(["avatar"]), } as Response); - await vi.waitFor(() => { + await waitForFast(() => { expect( element.querySelector("img.agent-select__avatar")?.getAttribute("src"), ).toBe("blob:rotated-avatar"); @@ -272,7 +273,7 @@ it("aborts a stalled local avatar fetch after the request deadline", async () => await vi.advanceTimersByTimeAsync(1); expect(fetchInit?.signal?.aborted).toBe(true); - await vi.waitFor(() => { + await waitForFast(() => { expect(element.querySelector("img.agent-select__avatar")).toBeNull(); expect(element.querySelector(".agent-select__avatar--text")?.textContent?.trim()).toBe("A"); }); @@ -311,12 +312,12 @@ it("aborts a stalled local avatar body after the request deadline", async () => try { expect(fetchMock).toHaveBeenCalledTimes(1); - await vi.waitFor(() => expect(blob).toHaveBeenCalledTimes(1)); + await waitForFast(() => expect(blob).toHaveBeenCalledTimes(1)); const [, fetchInit] = fetchMock.mock.calls[0] ?? []; await vi.advanceTimersByTimeAsync(30_000); expect(fetchInit?.signal?.aborted).toBe(true); - await vi.waitFor(() => { + await waitForFast(() => { expect(element.querySelector("img.agent-select__avatar")).toBeNull(); expect(element.querySelector(".agent-select__avatar--text")?.textContent?.trim()).toBe("A"); }); diff --git a/ui/src/components/terminal/terminal-panel-readiness.test.ts b/ui/src/components/terminal/terminal-panel-readiness.test.ts index 15ab16cb031d..0d95386fd8bf 100644 --- a/ui/src/components/terminal/terminal-panel-readiness.test.ts +++ b/ui/src/components/terminal/terminal-panel-readiness.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { i18n } from "../../i18n/index.ts"; import { createStorageMock } from "../../test-helpers/storage.ts"; +import { waitForFast } from "../../test-helpers/wait-for.ts"; import type { TerminalGatewayClient } from "./terminal-connection.ts"; import { OpenClawTerminalPanel } from "./terminal-panel.ts"; import type { createIsolatedGhosttyTerminal } from "./terminal-runtime.ts"; @@ -110,7 +111,7 @@ describe("terminal panel readiness", () => { }), ); - await vi.waitFor(() => { + await waitForFast(() => { expect(requests).toContainEqual({ method: "terminal.attach", params: { sessionId: "agent-terminal-1" }, @@ -139,7 +140,7 @@ describe("terminal panel readiness", () => { document.body.append(panel); panel.toggle(); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-connecting")?.textContent).toContain( "Connecting to session", ); @@ -149,7 +150,7 @@ describe("terminal panel readiness", () => { }); open.resolve(terminalOpenResult("session-1")); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull(); expect(panel.renderRoot.querySelector(".tabstrip-tab")?.classList.contains("is-live")).toBe( true, @@ -186,7 +187,7 @@ describe("terminal panel readiness", () => { panel.handleToggleRequest(new CustomEvent("openclaw:terminal-toggle", { detail: { catalog } })); - await vi.waitFor(() => { + await waitForFast(() => { expect(requests).toContainEqual({ method: "terminal.open", params: { agentId: undefined, cols: 100, rows: 30, catalog }, @@ -203,7 +204,7 @@ describe("terminal panel readiness", () => { event: "terminal.data", payload: { sessionId: "catalog-terminal-1", seq: 5, data: "ready" }, }); - await vi.waitFor(() => expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull()); + await waitForFast(() => expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull()); expect(new TextDecoder().decode(controller.write.mock.calls[0]?.[0])).toBe("ready"); expect(sessionStorage.getItem("openclaw.terminal.sessions.v1")).toBe( JSON.stringify(["catalog-terminal-1"]), @@ -248,14 +249,16 @@ describe("terminal panel readiness", () => { detail: { catalog: { catalogId: "anthropic", hostId: "node:mac", threadId: "thread" } }, }), ); - await vi.waitFor(() => expect(panel.renderRoot.querySelector(".tp-connecting")).not.toBeNull()); + await waitForFast(() => + expect(panel.renderRoot.querySelector(".tp-connecting")).not.toBeNull(), + ); listener?.({ event: "terminal.data", payload: { sessionId: "catalog-terminal-1", seq: 12, data: "gap" }, }); - await vi.waitFor(() => expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull()); + await waitForFast(() => expect(panel.renderRoot.querySelector(".tp-connecting")).toBeNull()); expect(requests).toContainEqual({ method: "terminal.attach", params: { sessionId: "catalog-terminal-1" }, @@ -290,7 +293,7 @@ describe("terminal panel readiness", () => { }), ); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-error")?.textContent).toContain( "Session did not connect within 30 seconds", ); diff --git a/ui/src/components/terminal/terminal-panel-upload.test.ts b/ui/src/components/terminal/terminal-panel-upload.test.ts index 9a6d66ab8c21..b8f7e2e0bc0e 100644 --- a/ui/src/components/terminal/terminal-panel-upload.test.ts +++ b/ui/src/components/terminal/terminal-panel-upload.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest"; import { i18n } from "../../i18n/index.ts"; import { createStorageMock } from "../../test-helpers/storage.ts"; +import { waitForFast } from "../../test-helpers/wait-for.ts"; import type { TerminalGatewayClient } from "./terminal-connection.ts"; import { OpenClawTerminalPanel } from "./terminal-panel.ts"; @@ -113,7 +114,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { panel.available = true; document.body.append(panel); panel.toggle(); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-upload")?.disabled).toBe(false); }); expect(panel.renderRoot.querySelector(".tp-file-input")?.multiple).toBe(true); @@ -128,7 +129,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { }); panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop); - await vi.waitFor(() => { + await waitForFast(() => { expect(requests).toContainEqual({ method: "terminal.upload", params: { @@ -175,7 +176,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { panel.available = true; document.body.append(panel); panel.toggle(); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-upload")?.disabled).toBe(false); }); @@ -192,7 +193,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { }); panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop); - await vi.waitFor(() => { + await waitForFast(() => { const progress = panel.renderRoot.querySelector(".tp-upload-progress"); expect(progress?.getAttribute("aria-valuenow")).toBe("1"); expect(progress?.getAttribute("aria-valuemax")).toBe("2"); @@ -209,7 +210,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { retryable: false, }), ); - await vi.waitFor(() => { + await waitForFast(() => { const failed = panel.renderRoot.querySelector(".tp-upload-card--failed"); expect(failed?.textContent).toContain("Upload failed"); expect(failed?.textContent).toContain("paired node went offline"); @@ -217,7 +218,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { }); panel.renderRoot.querySelector(".tp-upload-retry")?.click(); - await vi.waitFor(() => { + await waitForFast(() => { expect(controller.terminal.paste).toHaveBeenCalledWith( "'/tmp/openclaw upload/scan final.pdf' '/tmp/openclaw upload/notes.txt'", ); @@ -254,7 +255,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { panel.available = true; document.body.append(panel); panel.toggle(); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-upload")?.disabled).toBe(false); }); @@ -267,7 +268,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { }, }); panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain( "Uploading 1 of 1", ); @@ -315,7 +316,7 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { panel.available = true; document.body.append(panel); panel.toggle(); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-upload")?.disabled).toBe(false); }); @@ -328,14 +329,14 @@ describe("OpenClawTerminalPanel upload lifecycle", () => { }, }); panel.renderRoot.querySelector(".tp-viewport")?.dispatchEvent(drop); - await vi.waitFor(() => { + await waitForFast(() => { expect(panel.renderRoot.querySelector(".tp-upload-card")?.textContent).toContain( "Uploading 1 of 1", ); }); panel.renderRoot.querySelector(".tabstrip-tab__close")?.click(); - await vi.waitFor(() => { + await waitForFast(() => { expect(uploadSignal?.aborted).toBe(true); expect(panel.renderRoot.querySelector(".tp-upload-card")).toBeNull(); }); diff --git a/ui/src/lib/skills/index.test.ts b/ui/src/lib/skills/index.test.ts index db4dbae1f453..0984a002f642 100644 --- a/ui/src/lib/skills/index.test.ts +++ b/ui/src/lib/skills/index.test.ts @@ -1,6 +1,7 @@ // Control UI tests cover skills behavior. import { expectDefined } from "@openclaw/normalization-core"; import { describe, expect, it, vi } from "vitest"; +import { waitForFast } from "../../test-helpers/wait-for.ts"; import { installFromClawHub, installSkill, @@ -317,7 +318,7 @@ describe("loadSkills", () => { ); const load = loadSkills(state); - await vi.waitFor(() => expect(state.skillsLoading).toBe(true)); + await waitForFast(() => expect(state.skillsLoading).toBe(true)); state.connected = false; expect(rejectStatus).toBeDefined(); rejectStatus?.(new Error("gateway disconnected")); @@ -700,7 +701,7 @@ describe("skill mutations", () => { const overlappingLoadAgents = vi.fn(async () => undefined); const refresh = refreshSkills(state, loadAgents); - await vi.waitFor(() => expect(state.skillOperation).toEqual({ kind: "refresh" })); + await waitForFast(() => expect(state.skillOperation).toEqual({ kind: "refresh" })); await refreshSkills(state, overlappingLoadAgents); await updateSkillEnabled(state, "github", true); @@ -727,7 +728,7 @@ describe("skill mutations", () => { state.skillsAgentId = "alpha"; const refresh = refreshSkills(state, async () => undefined); - await vi.waitFor(() => expect(request).toHaveBeenCalledOnce()); + await waitForFast(() => expect(request).toHaveBeenCalledOnce()); setSkillsAgentId(state, "beta"); expectDefined( pending[0], @@ -737,7 +738,7 @@ describe("skill mutations", () => { managedSkillsDir: "/tmp/skills", skills: [], }); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(2)); expectDefined( pending[1], "beta status request", @@ -836,7 +837,7 @@ describe("skill mutations", () => { state.skillEdits.github = "submitted-value"; const first = fixture.start(state); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(1)); expect(state.skillOperation).toEqual(fixture.expectedMutation); await fixture.blocked(state); @@ -875,7 +876,7 @@ describe("skill mutations", () => { oldRequest.mockReturnValue(oldMutationResult.promise); const oldMutation = updateSkillEnabled(state, "github", true); - await vi.waitFor(() => expect(oldRequest).toHaveBeenCalledOnce()); + await waitForFast(() => expect(oldRequest).toHaveBeenCalledOnce()); const currentMutationResult = createDeferred(); const currentRequest = vi.fn((method) => @@ -892,7 +893,7 @@ describe("skill mutations", () => { state.skillOperation = null; const currentMutation = updateSkillEnabled(state, "calendar", true); - await vi.waitFor(() => expect(currentRequest).toHaveBeenCalledOnce()); + await waitForFast(() => expect(currentRequest).toHaveBeenCalledOnce()); const currentOperation = state.skillOperation; oldMutationResult.resolve({}); @@ -947,7 +948,7 @@ describe("skill mutations", () => { expect(pendingRequests).toHaveLength(1); expectDefined(pendingRequests[0], "skills update request").resolve({}); - await vi.waitFor(() => { + await waitForFast(() => { expect(pendingRequests).toHaveLength(2); }); expectDefined(pendingRequests[1], "beta skills request after update").resolve({ @@ -983,7 +984,7 @@ describe("skill mutations", () => { state.skillsAgentId = "alpha"; const mutation = updateSkillEnabled(state, "github", true); - await vi.waitFor(() => expect(request).toHaveBeenCalledOnce()); + await waitForFast(() => expect(request).toHaveBeenCalledOnce()); setSkillsAgentId(state, "beta"); expect(rejectUpdate).toBeDefined(); rejectUpdate?.(new Error("stale update failed")); @@ -1161,7 +1162,7 @@ describe("skill mutations", () => { await Promise.resolve(); setSkillsAgentId(state, "beta"); queue.resolveNext({ message: "Installed" }); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(2)); queue.resolveNext({ workspaceDir: "/tmp/beta-after-install", managedSkillsDir: "/tmp/skills", diff --git a/ui/src/lib/workboard/index.test.ts b/ui/src/lib/workboard/index.test.ts index 5afadfb884d3..60a630f5ffcd 100644 --- a/ui/src/lib/workboard/index.test.ts +++ b/ui/src/lib/workboard/index.test.ts @@ -3,6 +3,7 @@ import { expectDefined } from "@openclaw/normalization-core"; import { afterEach, describe, expect, it, vi } from "vitest"; import { GatewayRequestError } from "../../api/gateway.ts"; import type { GatewaySessionRow } from "../../api/types.ts"; +import { waitForFast } from "../../test-helpers/wait-for.ts"; import { addWorkboardCardComment, archiveWorkboardCard, @@ -268,7 +269,7 @@ describe("workboard controller", () => { client: client as never, sessions: [sampleSession], }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith( "workboard.cards.update", expect.objectContaining({ id: linkedCard.id }), @@ -1863,7 +1864,7 @@ describe("workboard controller", () => { const refresh = loadWorkboard({ host, client: client as never, force: true }); await Promise.resolve(); const save = saveWorkboardCardDraft({ host, client: client as never }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("workboard.cards.update", expect.anything()); }); refreshList.resolve({ cards: [sampleCard], statuses: ["todo", "done"] }); @@ -2121,7 +2122,7 @@ describe("workboard controller", () => { state.draftTitle = "Unsaved edit"; const save = saveWorkboardCardDraft({ host, client: client as never }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("workboard.cards.update", expect.anything()); }); stopWorkboardLifecycleRefresh(host); @@ -2156,7 +2157,7 @@ describe("workboard controller", () => { state.cards = [sampleCard]; const dispatch = dispatchWorkboard({ host, client: client as never }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("workboard.cards.dispatch", {}); }); stopWorkboardLifecycleRefresh(host); @@ -3302,7 +3303,7 @@ describe("workboard controller", () => { client: client as never, session: firstSession, }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith( "workboard.cards.create", expect.objectContaining({ sessionKey: firstSession.key }), @@ -3353,7 +3354,7 @@ describe("workboard controller", () => { session: sampleSession, }); list.resolve({ cards: [], statuses: ["todo"] }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith( "workboard.cards.create", expect.objectContaining({ sessionKey: sampleSession.key }), @@ -3488,7 +3489,7 @@ describe("workboard controller", () => { client: client as never, sessions: [sampleSession], }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("workboard.cards.update", expect.anything()); }); stopWorkboardLifecycleRefresh(host); @@ -5150,7 +5151,7 @@ describe("workboard controller", () => { }); const sync = syncWorkboardLifecycle({ host, client: client as never, sessions: [] }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("tasks.list", { limit: 500 }); }); stopWorkboardLifecycleRefresh(host); @@ -5188,7 +5189,7 @@ describe("workboard controller", () => { ]; const syncing = syncWorkboardLifecycle({ host, client: client as never, sessions }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith( "workboard.cards.update", expect.objectContaining({ id: first.id }), @@ -5231,7 +5232,7 @@ describe("workboard controller", () => { }); const first = syncWorkboardLifecycle({ host, client: client as never, sessions: [] }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("tasks.list", { limit: 500 }); }); const second = syncWorkboardLifecycle({ host, client: client as never, sessions: [] }); @@ -5288,7 +5289,7 @@ describe("workboard controller", () => { sessions: [], requestUpdate, }); - await vi.waitFor(() => { + await waitForFast(() => { expect(client.request).toHaveBeenCalledWith("tasks.list", { limit: 500 }); }); await addWorkboardCardComment({ diff --git a/ui/src/lib/workboard/live-refresh.test.ts b/ui/src/lib/workboard/live-refresh.test.ts index a04b38cc78cc..10b968376856 100644 --- a/ui/src/lib/workboard/live-refresh.test.ts +++ b/ui/src/lib/workboard/live-refresh.test.ts @@ -1,4 +1,5 @@ import { afterEach, describe, expect, it, vi } from "vitest"; +import { waitForFast } from "../../test-helpers/wait-for.ts"; import { normalizeWorkboardChange } from "./change-payload.ts"; import { configureWorkboardLiveRefresh, @@ -61,7 +62,7 @@ describe("Workboard live refresh", () => { configureWorkboardLiveRefresh({ host, client: client as never }); expect(handleWorkboardChanged(host, { epoch: "epoch-a", revision: 2 })).toBe(true); - await vi.waitFor(() => + await waitForFast(() => expect(getWorkboardState(host).cards[0]?.title).toBe("Updated elsewhere"), ); expect(handleWorkboardChanged(host, { epoch: "epoch-a", revision: 1 })).toBe(false); @@ -98,12 +99,12 @@ describe("Workboard live refresh", () => { configureWorkboardLiveRefresh({ host, client: client as never }); handleWorkboardChanged(host, { epoch: "epoch-a", revision: 1 }); - await vi.waitFor(() => expect(listCalls).toBe(1)); + await waitForFast(() => expect(listCalls).toBe(1)); handleWorkboardChanged(host, { epoch: "epoch-a", revision: 2 }); handleWorkboardChanged(host, { epoch: "epoch-a", revision: 3 }); firstList.resolve({ cards: [], statuses: ["todo", "done"] }); - await vi.waitFor(() => expect(listCalls).toBe(2)); + await waitForFast(() => expect(listCalls).toBe(2)); await Promise.resolve(); expect(listCalls).toBe(2); }); @@ -127,7 +128,9 @@ describe("Workboard live refresh", () => { state.draftOpen = false; state.editingCardId = null; resumeWorkboardLiveRefresh(host); - await vi.waitFor(() => expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {})); + await waitForFast(() => + expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {}), + ); }); it("retries transient failures and treats a new epoch as authoritative", async () => { @@ -146,7 +149,9 @@ describe("Workboard live refresh", () => { configureWorkboardLiveRefresh({ host, client: client as never }); handleWorkboardChanged(host, { epoch: "epoch-a", revision: 9 }); - await vi.waitFor(() => expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {})); + await waitForFast(() => + expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {}), + ); resumeWorkboardLiveRefresh(host); configureWorkboardLiveRefresh({ host, client: client as never }); await Promise.resolve(); @@ -160,7 +165,7 @@ describe("Workboard live refresh", () => { ).toHaveLength(2); expect(handleWorkboardChanged(host, { epoch: "epoch-b", revision: 1 })).toBe(true); - await vi.waitFor(() => + await waitForFast(() => expect( client.request.mock.calls.filter(([method]) => method === "workboard.cards.list"), ).toHaveLength(3), @@ -176,7 +181,9 @@ describe("Workboard live refresh", () => { ); configureWorkboardLiveRefresh({ host, client: client as never }); handleWorkboardChanged(host, { epoch: "epoch-a", revision: 1 }); - await vi.waitFor(() => expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {})); + await waitForFast(() => + expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {}), + ); stopWorkboardLiveRefresh(host); list.resolve({ @@ -210,7 +217,9 @@ describe("Workboard live refresh", () => { ); configureWorkboardLiveRefresh({ host, client: client as never }); const loading = loadWorkboard({ host, client: client as never, force: true }); - await vi.waitFor(() => expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {})); + await waitForFast(() => + expect(client.request).toHaveBeenCalledWith("workboard.cards.list", {}), + ); stopWorkboardLiveRefresh(host); list.resolve({ diff --git a/ui/src/pages/gateway-source-replacement.test.ts b/ui/src/pages/gateway-source-replacement.test.ts index 85f54929a5b3..270091929028 100644 --- a/ui/src/pages/gateway-source-replacement.test.ts +++ b/ui/src/pages/gateway-source-replacement.test.ts @@ -4,6 +4,7 @@ import { nothing } from "lit"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { GatewayBrowserClient } from "../api/gateway.ts"; import type { ApplicationContext, ApplicationGatewaySnapshot } from "../app/context.ts"; +import { waitForFast } from "../test-helpers/wait-for.ts"; import type { SessionsRouteData } from "./sessions/sessions-page.ts"; import type { SkillsRouteData } from "./skills/skills-page.ts"; import { USAGE_PAYLOAD_TTL_MS, type UsageRefreshReason } from "./usage/refresh-policy.ts"; @@ -235,7 +236,7 @@ describe("gateway source replacement across reconnect with a reused client", () document.body.append(page); await page.updateComplete; - await vi.waitFor(() => expect(page.usageResult).toBe(freshResult)); + await waitForFast(() => expect(page.usageResult).toBe(freshResult)); expect(request).toHaveBeenCalledWith("sessions.usage", expect.any(Object)); expect(page.usageResult).not.toBe(staleResult); @@ -324,7 +325,7 @@ describe("gateway source replacement across reconnect with a reused client", () }); page.refreshRuntime.applyGatewaySnapshot(context.gateway.snapshot); - await vi.waitFor(() => + await waitForFast(() => expect(request).toHaveBeenCalledWith("sessions.usage", expect.any(Object)), ); }); @@ -385,7 +386,7 @@ describe("gateway source replacement across reconnect with a reused client", () visibility.mockReturnValue("visible"); document.dispatchEvent(new Event("visibilitychange")); window.dispatchEvent(new Event("focus")); - await vi.waitFor(() => expect(page.usageLoading).toBe(false)); + await waitForFast(() => expect(page.usageLoading).toBe(false)); expect(request.mock.calls.map(([method]) => method)).toEqual([ "sessions.usage", "usage.cost", @@ -394,7 +395,7 @@ describe("gateway source replacement across reconnect with a reused client", () page.usageLoading = true; page.refreshRuntime.request("manual"); - await vi.waitFor(() => expect(page.usageLoading).toBe(false)); + await waitForFast(() => expect(page.usageLoading).toBe(false)); expect(request).toHaveBeenCalledTimes(6); const failedRefresh = deferred(); @@ -404,8 +405,8 @@ describe("gateway source replacement across reconnect with a reused client", () expect(request).toHaveBeenCalledTimes(9); page.refreshRuntime.request("focus"); failedRefresh.reject(new Error("connection interrupted")); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(12)); - await vi.waitFor(() => expect(page.usageLoading).toBe(false)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(12)); + await waitForFast(() => expect(page.usageLoading).toBe(false)); }); it("preserves matching skills route data on the first bind", async () => { const request = vi.fn(); @@ -460,7 +461,7 @@ describe("gateway source replacement across reconnect with a reused client", () document.body.append(page); await page.updateComplete; - await vi.waitFor(() => expect(page.skillsReport).toBe(freshReport)); + await waitForFast(() => expect(page.skillsReport).toBe(freshReport)); expect(page.skillsReport).not.toBe(staleReport); }); @@ -542,7 +543,7 @@ describe("gateway source replacement across reconnect with a reused client", () page.connected = true; const load = page.loadAgents(); - await vi.waitFor(() => expect(ensureList).toHaveBeenCalledOnce()); + await waitForFast(() => expect(ensureList).toHaveBeenCalledOnce()); const replacementAgents = { defaultId: "fresh", mainKey: "agent:fresh:main", @@ -622,7 +623,7 @@ describe("gateway source replacement across reconnect with a reused client", () page.connected = true; const load = page.loadDiagnostics(); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(4)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(4)); await replaceContext(page, client); pending.resolve({ models: [{ id: "stale" }], stale: true }); await load; diff --git a/ui/src/pages/plugin/workspace-view.test.ts b/ui/src/pages/plugin/workspace-view.test.ts index 1f91508d4898..549a5f01cbb3 100644 --- a/ui/src/pages/plugin/workspace-view.test.ts +++ b/ui/src/pages/plugin/workspace-view.test.ts @@ -2,6 +2,7 @@ import { render } from "lit"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { GatewayBrowserClient } from "../../api/gateway.ts"; import { getWorkspaceState } from "../../lib/workspace/index.ts"; +import { waitForFast } from "../../test-helpers/wait-for.ts"; import { stopWorkspace } from "./workspace-controller.ts"; import { renderWorkspace as renderWorkspaceView } from "./workspace-view.ts"; @@ -165,7 +166,7 @@ describe("renderWorkspace", () => { render(renderWorkspace({ host, client, connected: true }), host); resolveWorkspace({ doc, workspaceVersion: doc.workspaceVersion }); - await vi.waitFor(() => expect(state.loaded).toBe(true)); + await waitForFast(() => expect(state.loaded).toBe(true)); expect(state.activeSlug).toBe("main"); } finally { stopWorkspace(host); @@ -226,7 +227,7 @@ describe("renderWorkspace", () => { resolveFresh(2); await freshResult; - await vi.waitFor(() => { + await waitForFast(() => { render(renderWorkspace({ host, client, connected: true }), host); expect(host.querySelector(".workspace-stat__value")?.textContent).toBe("2"); }); @@ -293,8 +294,8 @@ describe("renderWorkspace", () => { try { render(renderWorkspace({ host, client, connected: true }), host); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2)); - await vi.waitFor(() => { + await waitForFast(() => expect(request).toHaveBeenCalledTimes(2)); + await waitForFast(() => { render(renderWorkspace({ host, client, connected: true }), host); expect( host.querySelector("[data-test-id='workspace-agent-status']")?.textContent, @@ -303,10 +304,10 @@ describe("renderWorkspace", () => { active = true; render(renderWorkspace({ host, client, connected: true, sessionListRevision: 1 }), host); - await vi.waitFor(() => { + await waitForFast(() => { expect(request.mock.calls.filter(([method]) => method === "sessions.list")).toHaveLength(2); }); - await vi.waitFor(() => { + await waitForFast(() => { render(renderWorkspace({ host, client, connected: true, sessionListRevision: 1 }), host); expect( host.querySelector("[data-test-id='workspace-agent-status']")?.textContent, @@ -371,24 +372,24 @@ describe("renderWorkspace", () => { try { state.workspace = workspace(1); render(renderWorkspace({ host, client, connected: true }), host); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1)); - await vi.waitFor(() => { + await waitForFast(() => expect(request).toHaveBeenCalledTimes(1)); + await waitForFast(() => { render(renderWorkspace({ host, client, connected: true }), host); expect(host.querySelector("iframe")?.getAttribute("src")).toContain("old.html"); }); state.workspace = workspace(2); render(renderWorkspace({ host, client, connected: true }), host); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2)); - await vi.waitFor(() => { + await waitForFast(() => expect(request).toHaveBeenCalledTimes(2)); + await waitForFast(() => { render(renderWorkspace({ host, client, connected: true }), host); expect(host.querySelector("iframe")?.getAttribute("src")).toContain("new.html"); }); render(renderWorkspace({ host, client, connected: false }), host); render(renderWorkspace({ host, client, connected: true }), host); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(3)); - await vi.waitFor(() => { + await waitForFast(() => expect(request).toHaveBeenCalledTimes(3)); + await waitForFast(() => { render(renderWorkspace({ host, client, connected: true }), host); expect(host.querySelector("iframe")?.getAttribute("src")).toContain( "3333333333333333333333333333333333333333333", diff --git a/ui/src/pages/profile/profile-page.test.ts b/ui/src/pages/profile/profile-page.test.ts index c4e543da9c28..d91fbfd084e0 100644 --- a/ui/src/pages/profile/profile-page.test.ts +++ b/ui/src/pages/profile/profile-page.test.ts @@ -7,6 +7,7 @@ import type { RouteId } from "../../app-route-paths.ts"; import type { ApplicationContext, ApplicationGatewaySnapshot } from "../../app/context.ts"; import { i18n, t } from "../../i18n/index.ts"; import { createApplicationContextProvider } from "../../test-helpers/application-context.ts"; +import { waitForFast } from "../../test-helpers/wait-for.ts"; import { USAGE_PAYLOAD_TTL_MS, type UsageRefreshReason } from "../usage/refresh-policy.ts"; import { ProfilePage } from "./profile-page.ts"; @@ -189,8 +190,8 @@ it("gates profile usage refreshes by payload age and page visibility", async () }; provider.append(page); document.body.append(provider); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(2)); - await vi.waitFor(() => expect(page.loading).toBe(false)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(2)); + await waitForFast(() => expect(page.loading).toBe(false)); harness.emitConnected(false); harness.emitConnected(true); @@ -226,12 +227,12 @@ it("gates profile usage refreshes by payload age and page visibility", async () visibility.mockReturnValue("visible"); document.dispatchEvent(new Event("visibilitychange")); window.dispatchEvent(new Event("focus")); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(4)); - await vi.waitFor(() => expect(page.loading).toBe(false)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(4)); + await waitForFast(() => expect(page.loading).toBe(false)); page.querySelector(".profile-refresh")?.click(); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(6)); - await vi.waitFor(() => expect(page.loading).toBe(false)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(6)); + await waitForFast(() => expect(page.loading).toBe(false)); let settlePoll: TimerHandler | null = null; let settleDelayMs: number | undefined; @@ -264,5 +265,5 @@ it("gates profile usage refreshes by payload age and page visibility", async () setTimeoutSpy.mockRestore(); visibility.mockReturnValue("visible"); window.dispatchEvent(new Event("focus")); - await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(8)); + await waitForFast(() => expect(request).toHaveBeenCalledTimes(8)); });