Compare commits

...

1 Commits

Author SHA1 Message Date
fullstack-engineer de915098ba fix(canvas/test): wrap render() in act() for SettingsPanel open-state tests
CI / Shellcheck (E2E scripts) (pull_request) Blocked by required conditions
CI / Canvas Deploy Reminder (pull_request) Blocked by required conditions
CI / Python Lint & Test (pull_request) Blocked by required conditions
CI / all-required (pull_request) Blocked by required conditions
E2E API Smoke Test / E2E API Smoke Test (pull_request) Blocked by required conditions
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Blocked by required conditions
Harness Replays / Harness Replays (pull_request) Blocked by required conditions
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Blocked by required conditions
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 3s
CI / Detect changes (pull_request) Successful in 11s
Harness Replays / detect-changes (pull_request) Successful in 11s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 11s
qa-review / approved (pull_request) Successful in 12s
security-review / approved (pull_request) Successful in 12s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 19s
E2E API Smoke Test / detect-changes (pull_request) Successful in 20s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 23s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m13s
gate-check-v3 / gate-check (pull_request) Successful in 22s
sop-tier-check / tier-check (pull_request) Successful in 20s
CI / Platform (Go) (pull_request) Failing after 11m22s
CI / Canvas (Next.js) (pull_request) Successful in 12m48s
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
React state updates triggered by Zustand store subscriptions are not
guaranteed to flush before the next synchronous assertion. On slower CI
runners the Dialog.Root open={isPanelOpen} resolve fires after
getByTestId("secrets-tab") fires, causing a 5000ms timeout.

Fix: wrap every render() call where storeState.isPanelOpen=true is set
before rendering inside await act(async () => { render(...) }) so
React flushes all state before assertions run. 14 tests updated; all
pass (was failing before).

Closes #1183
2026-05-15 14:16:54 +00:00
@@ -15,7 +15,7 @@
* - aria-modal="false" — canvas stays interactive
*/
import React from "react";
import { render, screen, fireEvent, cleanup, act, waitFor } from "@testing-library/react";
import { render, screen, fireEvent, cleanup, act } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SettingsPanel } from "../SettingsPanel";
@@ -120,35 +120,45 @@ describe("SettingsPanel — closed by default", () => {
// ─── Open / close ──────────────────────────────────────────────────────────
describe("SettingsPanel — open / close", () => {
it("renders SecretsTab when panel is open", () => {
it("renders SecretsTab when panel is open", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-xyz" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-xyz" />);
});
expect(screen.getByTestId("secrets-tab")).toBeTruthy();
expect(screen.getByText(/workspaceId=ws-xyz/i)).toBeTruthy();
});
it("renders TokensTab tab in tabs list", () => {
it("renders TokensTab tab in tabs list", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
expect(screen.getByRole("tab", { name: /workspace tokens/i })).toBeTruthy();
});
it("renders Org API Keys tab in tabs list", () => {
it("renders Org API Keys tab in tabs list", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
expect(screen.getByRole("tab", { name: /org api keys/i })).toBeTruthy();
});
it("Secrets tab is default active", () => {
it("Secrets tab is default active", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
expect(screen.getByTestId("secrets-tab")).toBeTruthy();
expect(screen.getByRole("tab", { name: /secrets/i }).getAttribute("data-state")).toBe("active");
});
it("Tokens tab trigger exists with correct aria attributes", () => {
it("Tokens tab trigger exists with correct aria attributes", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
const tab = screen.getByRole("tab", { name: /workspace tokens/i });
// Radix Tabs.Trigger has role="tab" and aria-selected
expect(tab).toBeTruthy();
@@ -159,16 +169,20 @@ describe("SettingsPanel — open / close", () => {
expect(tab.getAttribute("data-state")).not.toBe("active");
});
it("Close button calls closePanel", () => {
it("Close button calls closePanel", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
fireEvent.click(screen.getByRole("button", { name: /close settings/i }));
expect(mockClosePanel).toHaveBeenCalled();
});
it("calls fetchSecrets(workspaceId) when panel opens", () => {
it("calls fetchSecrets(workspaceId) when panel opens", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-fetch-test" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-fetch-test" />);
});
expect(mockFetchSecrets).toHaveBeenCalledWith("ws-fetch-test");
});
});
@@ -176,26 +190,32 @@ describe("SettingsPanel — open / close", () => {
// ─── Unsaved changes guard ──────────────────────────────────────────────────
describe("SettingsPanel — unsaved changes guard", () => {
it("shows guard when panel closing with isAddFormOpen=true", () => {
it("shows guard when panel closing with isAddFormOpen=true", async () => {
storeState.isPanelOpen = true;
storeState.isAddFormOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
fireEvent.click(screen.getByRole("button", { name: /close settings/i }));
expect(screen.getByTestId("unsaved-guard")).toBeTruthy();
});
it("guard shows when editingKey is set (dirty form)", () => {
it("guard shows when editingKey is set (dirty form)", async () => {
storeState.isPanelOpen = true;
storeState.editingKey = "GITHUB_TOKEN";
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
fireEvent.click(screen.getByRole("button", { name: /close settings/i }));
expect(screen.getByTestId("unsaved-guard")).toBeTruthy();
});
it("'Keep editing' closes guard but panel stays open", () => {
it("'Keep editing' closes guard but panel stays open", async () => {
storeState.isPanelOpen = true;
storeState.editingKey = "GITHUB_TOKEN";
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
// Trigger close attempt
fireEvent.click(screen.getByRole("button", { name: /close settings/i }));
expect(screen.getByTestId("unsaved-guard")).toBeTruthy();
@@ -206,28 +226,34 @@ describe("SettingsPanel — unsaved changes guard", () => {
expect(screen.getByTestId("secrets-tab")).toBeTruthy();
});
it("'Discard' button on guard calls closePanel", () => {
it("'Discard' button on guard calls closePanel", async () => {
storeState.isPanelOpen = true;
storeState.isAddFormOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
fireEvent.click(screen.getByRole("button", { name: /close settings/i }));
fireEvent.click(screen.getByTestId("guard-discard"));
expect(mockClosePanel).toHaveBeenCalled();
});
});
// ─── Accessibility ─────────────────────────────────────────────────────────
// ─── Accessibility ─────────────────────────────────────────────────────────
describe("SettingsPanel — accessibility", () => {
it("Dialog.Content has aria-label='Settings: API Keys'", () => {
it("Dialog.Content has aria-label='Settings: API Keys'", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
expect(document.querySelector('[aria-label="Settings: API Keys"]')).toBeTruthy();
});
it("TabList has aria-label='Settings sections'", () => {
it("TabList has aria-label='Settings sections'", async () => {
storeState.isPanelOpen = true;
render(<SettingsPanel workspaceId="ws-1" />);
await act(async () => {
render(<SettingsPanel workspaceId="ws-1" />);
});
expect(document.querySelector('[aria-label="Settings sections"]')).toBeTruthy();
});
});