Files
fusion/packages/dashboard/app/components/__tests__/ScriptsModal.test.tsx
gsxdsm 01b27b13f1 test(dashboard): add per-character typing coverage to uncovered field surfaces
Sweeping the other views for the FN-8606 typing bug found no further breakage,
but it did expose why the bug shipped: almost all field coverage uses
fireEvent.change, which sets a value in one shot on a node it already holds and
never needs the input to stay mounted. A remount is invisible to it.

Adds expectStableTyping (types character by character via userEvent, then asserts
DOM node identity, accumulated value, and retained focus) and applies it to the
uncovered surfaces: the FN-8606-migrated AddNode, ConnectNode, NodeDetail,
Scripts, and WorkflowAddStep modals, plus the board's QuickEntryBox composer and
SubtaskBreakdownModal title editing. All pass — this is a detection floor, not a
fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:42:39 -07:00

439 lines
15 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { ScriptsModal } from "../ScriptsModal";
import { assertModalGeometryRecoveryAndSheetContracts, assertRenderedModalTouchGeometry } from "./floatingWindowMigration.test-helpers";
import { expectStableTyping } from "./typingStability.test-helpers";
import type { ScriptEntry } from "../../api";
const mockScripts: Record<string, string> = {
build: "npm run build",
test: "pnpm test",
lint: "eslint src --ext .ts,.tsx",
};
vi.mock("../../api", () => ({
fetchScripts: vi.fn(() => Promise.resolve({})),
addScript: vi.fn(() => Promise.resolve({ name: "new-script", command: "echo hello" })),
removeScript: vi.fn(() => Promise.resolve()),
}));
import {
fetchScripts,
addScript,
removeScript,
} from "../../api";
const onClose = vi.fn();
const addToast = vi.fn();
const onRunScript = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
});
describe("ScriptsModal", () => {
it("renders inside its FloatingWindow when visible", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce(mockScripts);
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
expect(screen.getByTestId("floating-window-scripts")).toBeInTheDocument();
});
/*
FNXC:TypingStability 2026-07-26-22:15:
Per-character typing guard. The FN-8606 floating-window migration made Planning Mode and Settings
untypable and no test noticed, because field coverage here uses fireEvent.change, which never needs
the input to stay mounted. This asserts the field keeps its DOM node, value, and focus while typed.
*/
it("keeps the script name field mounted and focused while typing", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce(mockScripts);
render(<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />);
fireEvent.click(await screen.findByTestId("add-script-btn"));
const field = await screen.findByTestId("script-name-input") as HTMLInputElement;
await expectStableTyping(field, "lint", () => screen.getByTestId("script-name-input"));
});
it("does not render when closed", () => {
const { container } = render(
<ScriptsModal isOpen={false} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
expect(container.innerHTML).toBe("");
});
it("renders list of scripts", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce(mockScripts);
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByText("build")).toBeInTheDocument();
expect(screen.getByText("test")).toBeInTheDocument();
expect(screen.getByText("lint")).toBeInTheDocument();
});
});
it("shows empty state when no scripts exist", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("empty-state")).toBeInTheDocument();
});
// Use getAllByText since the header also shows "No scripts defined" text
expect(screen.getAllByText(/No scripts defined/).length).toBeGreaterThan(0);
});
it("opens create form when Add Script button is clicked", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
expect(screen.getByTestId("script-name-input")).toBeInTheDocument();
expect(screen.getByTestId("script-command-input")).toBeInTheDocument();
});
it("submits new script", async () => {
vi.mocked(fetchScripts)
.mockResolvedValueOnce({})
.mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
const nameInput = screen.getByTestId("script-name-input");
const commandInput = screen.getByTestId("script-command-input");
fireEvent.change(nameInput, { target: { value: "new-script" } });
fireEvent.change(commandInput, { target: { value: "echo hello" } });
fireEvent.click(screen.getByTestId("script-save-btn"));
await waitFor(() => {
expect(addScript).toHaveBeenCalledWith("new-script", "echo hello", undefined);
expect(addToast).toHaveBeenCalledWith("Script created", "success");
});
});
it("validates script name (alphanumeric, hyphens, underscores only)", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
const nameInput = screen.getByTestId("script-name-input");
fireEvent.change(nameInput, { target: { value: "invalid name with spaces" } });
await waitFor(() => {
expect(screen.getByTestId("script-name-error")).toBeInTheDocument();
});
// Verify the error message contains expected text
expect(screen.getByTestId("script-name-error").textContent).toContain("letters");
});
it("allows valid script names with hyphens and underscores", async () => {
vi.mocked(fetchScripts)
.mockResolvedValueOnce({})
.mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
const nameInput = screen.getByTestId("script-name-input");
const commandInput = screen.getByTestId("script-command-input");
fireEvent.change(nameInput, { target: { value: "my-script_v2" } });
fireEvent.change(commandInput, { target: { value: "echo test" } });
fireEvent.click(screen.getByTestId("script-save-btn"));
await waitFor(() => {
expect(addScript).toHaveBeenCalledWith("my-script_v2", "echo test", undefined);
});
});
it("runs script when Run button is clicked", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce(mockScripts);
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("run-script-build")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("run-script-build"));
expect(onRunScript).toHaveBeenCalledWith("build", "npm run build");
});
it("shows delete confirmation", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce(mockScripts);
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("run-script-build")).toBeInTheDocument();
});
// Click delete button
fireEvent.click(screen.getByTestId("delete-script-build"));
// Confirm delete buttons should appear
await waitFor(() => {
expect(screen.getByTestId("confirm-delete-script-build")).toBeInTheDocument();
expect(screen.getByTestId("cancel-delete-script-build")).toBeInTheDocument();
});
});
it("deletes script when confirmed", async () => {
vi.mocked(fetchScripts)
.mockResolvedValueOnce(mockScripts)
.mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("delete-script-build")).toBeInTheDocument();
});
// Click delete button
fireEvent.click(screen.getByTestId("delete-script-build"));
// Confirm delete
await waitFor(() => {
expect(screen.getByTestId("confirm-delete-script-build")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("confirm-delete-script-build"));
await waitFor(() => {
expect(removeScript).toHaveBeenCalledWith("build", undefined);
expect(addToast).toHaveBeenCalledWith("Script deleted", "success");
});
});
it("cancels delete when cancel is clicked", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce(mockScripts);
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("delete-script-build")).toBeInTheDocument();
});
// Click delete button
fireEvent.click(screen.getByTestId("delete-script-build"));
// Cancel delete
await waitFor(() => {
expect(screen.getByTestId("cancel-delete-script-build")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("cancel-delete-script-build"));
// Script should still be visible, delete button back to normal
await waitFor(() => {
expect(screen.getByTestId("delete-script-build")).toBeInTheDocument();
expect(removeScript).not.toHaveBeenCalled();
});
});
it("shows error toast when API call fails", async () => {
vi.mocked(fetchScripts).mockRejectedValueOnce(new Error("Network error"));
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith(expect.stringContaining("Network error"), "error");
});
});
it("cancels form when Cancel button is clicked", async () => {
vi.mocked(fetchScripts).mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
await waitFor(() => {
expect(screen.getByTestId("script-save-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("script-cancel-btn"));
// Form should be closed, back to list view
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
});
it("edits existing script", async () => {
vi.mocked(fetchScripts)
.mockResolvedValueOnce(mockScripts)
.mockResolvedValueOnce(mockScripts);
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("edit-script-build")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("edit-script-build"));
await waitFor(() => {
expect(screen.getByTestId("script-name-input")).toBeInTheDocument();
expect(screen.getByTestId("script-command-input")).toBeInTheDocument();
});
const commandInput = screen.getByTestId("script-command-input");
fireEvent.change(commandInput, { target: { value: "npm run build:prod" } });
fireEvent.click(screen.getByTestId("script-save-btn"));
await waitFor(() => {
expect(addScript).toHaveBeenCalledWith("build", "npm run build:prod", undefined);
expect(addToast).toHaveBeenCalledWith("Script updated", "success");
});
});
it("shows validation error when script name is empty", async () => {
vi.mocked(fetchScripts)
.mockResolvedValueOnce({})
.mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
const commandInput = screen.getByTestId("script-command-input");
fireEvent.change(commandInput, { target: { value: "echo test" } });
// Try to save with empty name
fireEvent.click(screen.getByTestId("script-save-btn"));
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Script name is required", "error");
});
});
it("shows validation error when command is empty", async () => {
vi.mocked(fetchScripts)
.mockResolvedValueOnce({})
.mockResolvedValueOnce({});
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
const nameInput = screen.getByTestId("script-name-input");
fireEvent.change(nameInput, { target: { value: "test-script" } });
// Try to save with empty command
fireEvent.click(screen.getByTestId("script-save-btn"));
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("Script command is required", "error");
});
});
it("handles duplicate script name error", async () => {
vi.mocked(fetchScripts)
.mockResolvedValueOnce({})
.mockResolvedValueOnce({});
vi.mocked(addScript).mockRejectedValueOnce(new Error("A script with this name already exists"));
render(
<ScriptsModal isOpen={true} onClose={onClose} addToast={addToast} onRunScript={onRunScript} />
);
await waitFor(() => {
expect(screen.getByTestId("add-script-btn")).toBeInTheDocument();
});
fireEvent.click(screen.getByTestId("add-script-btn"));
fireEvent.change(screen.getByTestId("script-name-input"), { target: { value: "test-script" } });
fireEvent.change(screen.getByTestId("script-command-input"), { target: { value: "echo test" } });
fireEvent.click(screen.getByTestId("script-save-btn"));
await waitFor(() => {
expect(addToast).toHaveBeenCalledWith("A script with this name already exists", "error");
});
});
});
describe("ScriptsModal floating geometry", () => {
it("uses its production header for touch drag and resize", () => {
render(<ScriptsModal isOpen onClose={onClose} addToast={addToast} onRunScript={onRunScript} />);
assertRenderedModalTouchGeometry("scripts", screen.getByText("Scripts").closest(".modal-header") as HTMLElement);
assertModalGeometryRecoveryAndSheetContracts("scripts", () => render(<ScriptsModal isOpen onClose={onClose} addToast={addToast} onRunScript={onRunScript} />));
});
});