From c33b59a93a8491a12dd297fee77e99d47a7a97a7 Mon Sep 17 00:00:00 2001 From: Canvas Agent Date: Thu, 16 Apr 2026 10:53:52 +0000 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20QA=20blockers=20=E2=80=94=20Chat?= =?UTF-8?q?Tab=20aria-controls,=20AuthGate=20test,=20CommunicationOverlay?= =?UTF-8?q?=20status=20icons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BLOCKER 1 (ChatTab.tsx): Replace ternary rendering with always-in-DOM panels using `hidden` attribute so `aria-controls` targets always exist (WCAG 4.1.2). Add `id` to tab buttons for `aria-labelledby` back-reference. Non-blocking: change `key={i}` → `key={line + i}` on activity log items. BLOCKER 2 (AuthGate.test.tsx): Create test file asserting the loading state renders a `.bg-zinc-950.fixed.inset-0` overlay with `aria-hidden="true"` — covers the zinc-950 flash-prevention overlay added in the prior commit. BLOCKER 3 (CommunicationOverlay.tsx): Add `aria-hidden="true"` to the status icon span so decorative glyphs (✓ ✕ ⏱) are not announced by screen readers. Tests: 490/490 passing. Build: clean. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/CommunicationOverlay.tsx | 2 +- .../components/__tests__/AuthGate.test.tsx | 117 ++++++++++++++++++ canvas/src/components/tabs/ChatTab.tsx | 23 ++-- 3 files changed, 129 insertions(+), 13 deletions(-) create mode 100644 canvas/src/components/__tests__/AuthGate.test.tsx diff --git a/canvas/src/components/CommunicationOverlay.tsx b/canvas/src/components/CommunicationOverlay.tsx index ee69531a..7b0c49c7 100644 --- a/canvas/src/components/CommunicationOverlay.tsx +++ b/canvas/src/components/CommunicationOverlay.tsx @@ -150,7 +150,7 @@ export function CommunicationOverlay() { {c.targetName}
- {statusIcon} + {age}
diff --git a/canvas/src/components/__tests__/AuthGate.test.tsx b/canvas/src/components/__tests__/AuthGate.test.tsx new file mode 100644 index 00000000..7f581769 --- /dev/null +++ b/canvas/src/components/__tests__/AuthGate.test.tsx @@ -0,0 +1,117 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi, afterEach, beforeEach } from "vitest"; +import { render, cleanup, act } from "@testing-library/react"; + +afterEach(() => { + cleanup(); + vi.restoreAllMocks(); +}); + +// ── Mocks (defined before dynamic import of component) ─────────────────────── +let mockFetchSession: ReturnType; +let mockRedirectToLogin: ReturnType; +let mockGetTenantSlug: ReturnType; + +beforeEach(() => { + mockFetchSession = vi.fn(); + mockRedirectToLogin = vi.fn(); + mockGetTenantSlug = vi.fn(() => null); // default: non-SaaS (pass-through) +}); + +vi.mock("@/lib/auth", () => ({ + fetchSession: (...args: unknown[]) => mockFetchSession(...args), + redirectToLogin: (...args: unknown[]) => mockRedirectToLogin(...args), +})); + +vi.mock("@/lib/tenant", () => ({ + getTenantSlug: (...args: unknown[]) => mockGetTenantSlug(...args), +})); + +// Import after mocks are set up +import { AuthGate } from "../AuthGate"; + +// ───────────────────────────────────────────────────────────────────────────── + +describe("AuthGate — loading state", () => { + it("renders a blank overlay while session fetch is in-flight (prevents flash of unauth'd content)", () => { + // getTenantSlug returns a slug so the session fetch is triggered + mockGetTenantSlug.mockReturnValue("acme"); + // fetchSession never resolves — keeps the component in loading state + mockFetchSession.mockReturnValue(new Promise(() => {})); + + const { container } = render( + +
Protected content
+
+ ); + + const overlay = container.querySelector(".bg-zinc-950.fixed.inset-0"); + expect(overlay).not.toBeNull(); + expect(overlay?.getAttribute("aria-hidden")).toBe("true"); + }); + + it("does not render children while in loading state", () => { + mockGetTenantSlug.mockReturnValue("acme"); + mockFetchSession.mockReturnValue(new Promise(() => {})); + + const { queryByTestId } = render( + +
Protected content
+
+ ); + + expect(queryByTestId("child")).toBeNull(); + }); +}); + +describe("AuthGate — non-SaaS / pass-through mode", () => { + it("renders children immediately when there is no tenant slug", async () => { + mockGetTenantSlug.mockReturnValue(null); + + let result: ReturnType; + await act(async () => { + result = render( + +
Protected content
+
+ ); + }); + + expect(result!.getByTestId("child")).toBeTruthy(); + }); +}); + +describe("AuthGate — authenticated state", () => { + it("renders children after a successful session fetch", async () => { + mockGetTenantSlug.mockReturnValue("acme"); + mockFetchSession.mockResolvedValue({ userId: "u1", email: "a@b.com" }); + + let result: ReturnType; + await act(async () => { + result = render( + +
Protected content
+
+ ); + }); + + expect(result!.getByTestId("child")).toBeTruthy(); + }); +}); + +describe("AuthGate — anonymous / redirect state", () => { + it("calls redirectToLogin when session fetch returns null", async () => { + mockGetTenantSlug.mockReturnValue("acme"); + mockFetchSession.mockResolvedValue(null); + + await act(async () => { + render( + +
Protected content
+
+ ); + }); + + expect(mockRedirectToLogin).toHaveBeenCalledWith("sign-in"); + }); +}); diff --git a/canvas/src/components/tabs/ChatTab.tsx b/canvas/src/components/tabs/ChatTab.tsx index 2c402833..f1b8bbb0 100644 --- a/canvas/src/components/tabs/ChatTab.tsx +++ b/canvas/src/components/tabs/ChatTab.tsx @@ -109,6 +109,7 @@ export function ChatTab({ workspaceId, data }: Props) { }} >