Production typecheck (tsconfig.json + tsconfig.app.json) was already
clean, but a third config that includes test files surfaced 661 errors
across 60+ test files — accumulated drift between mock fixtures and
production types. Six parallel typescript-pro agents fixed every one
without touching production code.
Per-scope before/after (errors → 0):
ChatView 183
Mailbox + Agent suite (5 files) 156
Task / Modal suite (6 files) 127
App + small components (12 files) 96
Hooks + api/auth (8 files) 48
Long tail (32 files) 51
-----------------------------------------------------------------
Total 661
Major fix categories:
- Untyped state objects inferring `never[]` / `null` literals (root
cause of ~120 errors in ChatView alone — added a single
`UseChatReturn` annotation)
- Mock objects missing fields that became required: `WorkflowStep.mode`,
`ChatMessage.thinkingOutput / metadata`, `ChatSession.projectId`,
`Task.log`, `ProjectHealth` fields, `PtyTerminalSessionInfo.createdAt`,
`Agent.metadata`, `InboxResponse.total`, etc.
- Mock objects with stale fields that no longer exist:
`AgentBudgetStatus.budgetPeriod`, `truncated` on log responses,
`OutboxResponse.unreadCount`, `MergeResult.source/target/details`
- Modal props that became required (e.g. `PlanningModeModal.onTasksCreated`)
- String literals not in narrowed unions (`Column`, `WorkflowStepPhase`,
`InsightStatus`, `AgentLogType`, etc.)
- `querySelector` returning `Element` cast to `HTMLElement` for
`@testing-library/react`'s `within()`
- Vitest mock typing: `.mock.calls` access needing `vi.mocked(...)`,
zero-param tuple handling, generic `vi.fn(() => [])` inferring
`never[]`
Helpers introduced in test files (no shared infra):
- `makeSettings(overrides)` in ModelSelectorTab.test.tsx
- `makePromptOverrides(overrides)` in AgentPromptsManager.test.tsx
- `FileBrowserTestOverrides` type alias in FileBrowser.test.tsx
- `makeInboxResponse / makeOutboxResponse` in MailboxView.test.tsx
Verification:
- tsc -p tsconfig.json: exit 0
- tsc -p tsconfig.app.json: exit 0
- tsc -p tsconfig.test-check.json (new — includes test files): exit 0
- vitest run: 9639 / 9641 (2 pre-existing failures
in terminal-mobile-keyboard-layout.test.ts
unrelated to this work; verified via
`git stash` + run on clean HEAD)
Adds packages/dashboard/tsconfig.test-check.json to keep this regression
guard available locally — same as tsconfig.app.json minus the test
exclude.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
240 lines
7.0 KiB
TypeScript
240 lines
7.0 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||
import { createRef } from "react";
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||
import { PreviewIframe } from "../PreviewIframe";
|
||
|
||
vi.mock("lucide-react", () => ({
|
||
AlertTriangle: () => <span data-testid="alert-icon">⚠️</span>,
|
||
Loader2: () => <span data-testid="loader-icon">⏳</span>,
|
||
ShieldAlert: () => <span data-testid="shield-icon">🛡️</span>,
|
||
}));
|
||
|
||
describe("PreviewIframe", () => {
|
||
const originalWindowOpen = window.open;
|
||
|
||
beforeEach(() => {
|
||
window.open = vi.fn();
|
||
});
|
||
|
||
afterEach(() => {
|
||
window.open = originalWindowOpen;
|
||
});
|
||
|
||
it("renders null when url is null", () => {
|
||
const { container } = render(
|
||
<PreviewIframe
|
||
url={null}
|
||
embedStatus="unknown"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason={null}
|
||
embedContext={null}
|
||
/>,
|
||
);
|
||
|
||
expect(container).toBeEmptyDOMElement();
|
||
});
|
||
|
||
it("renders iframe with correct url, sandbox attributes, and title", () => {
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="embedded"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason={null}
|
||
embedContext={null}
|
||
/>,
|
||
);
|
||
|
||
const iframe = screen.getByTitle("Dev server preview");
|
||
expect(iframe).toBeInTheDocument();
|
||
expect(iframe).toHaveAttribute("src", "http://localhost:3000");
|
||
expect(iframe).toHaveAttribute(
|
||
"sandbox",
|
||
"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox",
|
||
);
|
||
});
|
||
|
||
it("moves unknown status into loading", async () => {
|
||
const onEmbedStatusChange = vi.fn();
|
||
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="unknown"
|
||
onEmbedStatusChange={onEmbedStatusChange}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason={null}
|
||
embedContext={null}
|
||
/>,
|
||
);
|
||
|
||
await waitFor(() => {
|
||
expect(onEmbedStatusChange).toHaveBeenCalledWith("loading");
|
||
});
|
||
});
|
||
|
||
it("iframe load marks status as embedded", () => {
|
||
const onEmbedStatusChange = vi.fn();
|
||
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="loading"
|
||
onEmbedStatusChange={onEmbedStatusChange}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason={null}
|
||
embedContext={null}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.load(screen.getByTitle("Dev server preview"));
|
||
|
||
expect(onEmbedStatusChange).toHaveBeenCalledWith("embedded");
|
||
});
|
||
|
||
it("iframe error marks status as error", () => {
|
||
const onEmbedStatusChange = vi.fn();
|
||
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="loading"
|
||
onEmbedStatusChange={onEmbedStatusChange}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason={null}
|
||
embedContext={null}
|
||
/>,
|
||
);
|
||
|
||
fireEvent(screen.getByTitle("Dev server preview"), new Event("error"));
|
||
|
||
expect(onEmbedStatusChange).toHaveBeenCalledWith("error");
|
||
});
|
||
|
||
it("applies custom className", () => {
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="embedded"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason={null}
|
||
embedContext={null}
|
||
className="custom-class"
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByTitle("Dev server preview")).toHaveClass("custom-class");
|
||
});
|
||
|
||
it("blocked state shows ShieldAlert icon and embed context", () => {
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="blocked"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason="The server may block iframe embedding..."
|
||
embedContext="The server may block iframe embedding..."
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByTestId("shield-icon")).toBeInTheDocument();
|
||
expect(screen.getByText("Preview cannot be embedded")).toBeInTheDocument();
|
||
expect(screen.getByText("The server may block iframe embedding...")).toBeInTheDocument();
|
||
});
|
||
|
||
it("blocked state open in new tab opens external URL", () => {
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="blocked"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason="The server may block iframe embedding..."
|
||
embedContext="The server may block iframe embedding..."
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole("button", { name: "Open in new tab" }));
|
||
|
||
expect(window.open).toHaveBeenCalledWith("http://localhost:3000", "_blank", "noopener,noreferrer");
|
||
});
|
||
|
||
it("blocked state retry calls onRetry", () => {
|
||
const onRetry = vi.fn();
|
||
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="blocked"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason="The server may block iframe embedding..."
|
||
embedContext="The server may block iframe embedding..."
|
||
onRetry={onRetry}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||
|
||
expect(onRetry).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it("error state shows AlertTriangle and contextual message", () => {
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="error"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason="The preview URL could not be loaded..."
|
||
embedContext="The preview URL could not be loaded..."
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByTestId("alert-icon")).toBeInTheDocument();
|
||
expect(screen.getByText("Unable to load preview")).toBeInTheDocument();
|
||
expect(screen.getByText("The preview URL could not be loaded...")).toBeInTheDocument();
|
||
});
|
||
|
||
it("error state retry calls onRetry", () => {
|
||
const onRetry = vi.fn();
|
||
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="error"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason="The preview URL could not be loaded..."
|
||
embedContext="The preview URL could not be loaded..."
|
||
onRetry={onRetry}
|
||
/>,
|
||
);
|
||
|
||
fireEvent.click(screen.getByRole("button", { name: "Retry" }));
|
||
|
||
expect(onRetry).toHaveBeenCalledTimes(1);
|
||
});
|
||
|
||
it("loading state shows loading spinner without action buttons", () => {
|
||
render(
|
||
<PreviewIframe
|
||
url="http://localhost:3000"
|
||
embedStatus="loading"
|
||
onEmbedStatusChange={vi.fn()}
|
||
iframeRef={createRef<HTMLIFrameElement>()}
|
||
blockReason={null}
|
||
embedContext={null}
|
||
/>,
|
||
);
|
||
|
||
expect(screen.getByTestId("loader-icon")).toBeInTheDocument();
|
||
expect(screen.queryByRole("button", { name: "Open in new tab" })).not.toBeInTheDocument();
|
||
expect(screen.queryByRole("button", { name: "Retry" })).not.toBeInTheDocument();
|
||
});
|
||
});
|