Stabilize dashboard composer tests when Vitest launches from the workspace root. - Resolve dashboard test source fixtures relative to the app directory. - Migrate affected component tests away from cwd-relative CSS reads. - Enforce the fixture convention in test hooks and document it. Files changed: docs/testing.md | 4 +++ package.json | 6 ++-- .../__tests__/AuthTokenRecoveryDialog.test.tsx | 3 +- .../components/__tests__/ChatView.mobile.test.tsx | 5 +-- .../__tests__/EngineControlMenu.test.tsx | 11 ++---- .../components/__tests__/FloatingWindow.test.tsx | 5 +-- .../app/components/__tests__/ListView.test.tsx | 5 +-- .../__tests__/MissionInterviewModal.test.tsx | 3 +- .../app/components/__tests__/MobileNavBar.test.tsx | 3 +- .../app/components/__tests__/NewTaskModal.test.tsx | 3 +- .../__tests__/PlanningModeModal.initial.test.tsx | 3 +- .../PlanningModeModal.ui-interactions.test.tsx | 7 ++-- .../components/__tests__/PrCreateModal.test.tsx | 3 +- .../__tests__/QuickChat.persist.test.tsx | 3 +- .../components/__tests__/QuickEntryBox.test.tsx | 3 +- .../components/__tests__/ReportActionMenu.test.tsx | 9 ++--- .../app/components/__tests__/ReportModal.test.tsx | 3 +- .../__tests__/ShadcnColorPicker.test.tsx | 3 +- .../components/__tests__/TerminalModal.test.tsx | 3 +- .../components/__tests__/ThemeDropdown.test.tsx | 9 ++--- .../__tests__/WorkflowNodeEditor.test.tsx | 5 +-- .../WorkflowOptionalStepsDropdown.test.tsx | 3 +- .../components/__tests__/WorkflowSwitcher.test.tsx | 5 +-- .../app/components/__tests__/board-mobile.test.tsx | 4 +-- .../__tests__/CommandCenterControls.test.tsx | 6 ++-- .../__tests__/SystemControlsArea.test.tsx | 5 +-- .../__tests__/SystemStatsArea.test.tsx | 3 +- .../command-center/areas/__tests__/areas.test.tsx | 3 +- .../__tests__/KeyboardShortcutsSection.test.tsx | 3 +- .../app/test/__tests__/cssFixture.test.ts | 35 +++++++++++++++++++ packages/dashboard/app/test/cssFixture.ts | 12 +++++++ .../check-no-cwd-relative-dashboard-test-reads.mjs | 39 ++++++++++++++++++++++ 32 files changed, 162 insertions(+), 55 deletions(-) Fusion-Task-Id: FN-8585 Fusion-Task-Lineage: 83a35fb6-a29d-4e97-b282-1054c68b8cc9 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { AuthTokenRecoveryDialog } from "../AuthTokenRecoveryDialog";
|
|
import { clearAuthToken, setAuthToken } from "../../auth";
|
|
import { readAppFile } from "../../test/cssFixture";
|
|
|
|
vi.mock("../../auth", () => ({
|
|
setAuthToken: vi.fn(),
|
|
clearAuthToken: vi.fn(),
|
|
}));
|
|
|
|
describe("AuthTokenRecoveryDialog", () => {
|
|
const originalLocation = window.location;
|
|
let reloadSpy: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
reloadSpy = vi.fn();
|
|
Object.defineProperty(window, "location", {
|
|
configurable: true,
|
|
value: { ...originalLocation, reload: reloadSpy },
|
|
});
|
|
});
|
|
|
|
it("does not render when closed", () => {
|
|
render(<AuthTokenRecoveryDialog open={false} />);
|
|
expect(screen.queryByRole("dialog", { name: "Authentication token required" })).toBeNull();
|
|
});
|
|
|
|
it("renders a blocking shared-modal dialog, focuses the token input, and disables set until input is populated", () => {
|
|
render(<AuthTokenRecoveryDialog open={true} />);
|
|
|
|
const dialog = screen.getByRole("dialog", { name: "Authentication token required" });
|
|
expect(dialog).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: /close/i })).toBeNull();
|
|
|
|
const overlay = dialog.closest(".auth-token-recovery-overlay");
|
|
expect(overlay).toBeTruthy();
|
|
|
|
if (!overlay) {
|
|
throw new Error("Expected auth token recovery overlay");
|
|
}
|
|
|
|
expect(overlay.classList.contains("modal-overlay")).toBe(true);
|
|
expect(overlay.classList.contains("open")).toBe(true);
|
|
expect(dialog.classList.contains("modal")).toBe(true);
|
|
expect(dialog.classList.contains("modal-md")).toBe(true);
|
|
|
|
const input = screen.getByLabelText("Replacement token");
|
|
expect(document.activeElement).toBe(input);
|
|
|
|
const setTokenButton = screen.getByRole("button", { name: "Set token and reload" });
|
|
expect(setTokenButton).toBeDisabled();
|
|
|
|
fireEvent.change(input, { target: { value: " " } });
|
|
expect(setTokenButton).toBeDisabled();
|
|
|
|
fireEvent.change(input, { target: { value: "abc123" } });
|
|
expect(setTokenButton).toBeEnabled();
|
|
});
|
|
|
|
it("focuses the token input when recovery opens after the app shell was already mounted", () => {
|
|
const { rerender } = render(<AuthTokenRecoveryDialog open={false} />);
|
|
|
|
rerender(<AuthTokenRecoveryDialog open={true} />);
|
|
|
|
const tokenInput = screen.getByLabelText("Replacement token");
|
|
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
|
|
expect(tokenInput).toBe(document.activeElement);
|
|
});
|
|
|
|
it("keeps a single blocking dialog when duplicate open signals rerender it", () => {
|
|
const { rerender } = render(<AuthTokenRecoveryDialog open={true} />);
|
|
|
|
rerender(<AuthTokenRecoveryDialog open={true} />);
|
|
|
|
expect(screen.getAllByRole("dialog", { name: "Authentication token required" })).toHaveLength(1);
|
|
expect(document.querySelectorAll(".auth-token-recovery-overlay")).toHaveLength(1);
|
|
});
|
|
|
|
it("does not define auth-specific modal layering outside the shared modal classes", () => {
|
|
const css = readAppFile("components/AuthTokenRecoveryDialog.css");
|
|
|
|
expect(css).not.toMatch(/auth-token-recovery-overlay\s*\{[^}]*z-index/s);
|
|
});
|
|
|
|
it("trims and stores replacement token before reloading", () => {
|
|
render(<AuthTokenRecoveryDialog open={true} />);
|
|
|
|
fireEvent.change(screen.getByLabelText("Replacement token"), { target: { value: " new-token " } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Set token and reload" }));
|
|
|
|
expect(setAuthToken).toHaveBeenCalledWith("new-token");
|
|
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("clears token and reloads when user retries without replacement token", () => {
|
|
render(<AuthTokenRecoveryDialog open={true} />);
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "Clear token and retry" }));
|
|
|
|
expect(clearAuthToken).toHaveBeenCalledTimes(1);
|
|
expect(reloadSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("does not dismiss on Escape key", () => {
|
|
render(<AuthTokenRecoveryDialog open={true} />);
|
|
|
|
const overlay = document.querySelector(".auth-token-recovery-overlay");
|
|
expect(overlay).toBeTruthy();
|
|
|
|
if (!overlay) {
|
|
throw new Error("Expected auth token recovery overlay");
|
|
}
|
|
|
|
fireEvent.keyDown(overlay, { key: "Escape" });
|
|
|
|
expect(screen.getByRole("dialog", { name: "Authentication token required" })).toBeInTheDocument();
|
|
});
|
|
});
|