Files
fusion/packages/dashboard/app/components/QuickScriptsDropdown.test.tsx
gsxdsm 30b9bc6e6e feat(FN-685): add QuickScriptsDropdown component to dashboard header
- Create QuickScriptsDropdown component with script listing, one-click execution, and keyboard navigation (arrow keys, Enter, Escape)
- Replace static Scripts button in Header with dropdown menu showing all configured scripts
- Add loading, empty, and error states with 'Manage Scripts...' footer link
- Add full CSS styles with theme support, animations, and mobile responsiveness
- Add comprehensive tests (510 lines) covering rendering, interactions, keyboard nav, and edge cases
2026-04-01 09:19:28 -07:00

511 lines
16 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { QuickScriptsDropdown } from "./QuickScriptsDropdown";
// Mock the API functions
const mockFetchScripts = vi.fn();
vi.mock("../api", () => ({
fetchScripts: () => mockFetchScripts(),
}));
const mockOnOpenScripts = vi.fn();
const mockOnRunScript = vi.fn();
function renderDropdown(props = {}) {
return render(
<QuickScriptsDropdown
onOpenScripts={mockOnOpenScripts}
onRunScript={mockOnRunScript}
{...props}
/>
);
}
describe("QuickScriptsDropdown", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("rendering", () => {
it("renders the trigger button", () => {
renderDropdown();
expect(screen.getByTestId("scripts-btn")).toBeDefined();
expect(screen.getByTitle("Scripts")).toBeDefined();
});
it("does not show dropdown menu initially", () => {
renderDropdown();
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
describe("dropdown open/close", () => {
it("opens dropdown when trigger is clicked", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
});
it("closes dropdown when clicking outside", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
fireEvent.mouseDown(document.body);
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
it("closes dropdown on Escape key", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
it("closes dropdown when trigger is clicked again", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
});
describe("fetching and displaying scripts", () => {
it("shows loading state while fetching", async () => {
mockFetchScripts.mockImplementation(() => new Promise(() => {}));
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
expect(screen.getByTestId("quick-scripts-loading")).toBeDefined();
});
it("fetches and displays scripts", async () => {
mockFetchScripts.mockResolvedValue({
build: "npm run build",
test: "npm test",
lint: "npm run lint",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-build")).toBeDefined();
expect(screen.getByTestId("quick-script-item-test")).toBeDefined();
expect(screen.getByTestId("quick-script-item-lint")).toBeDefined();
});
});
it("displays script names and truncated commands", async () => {
mockFetchScripts.mockResolvedValue({
"long-command": "this is a very long command that should be truncated",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
const item = screen.getByTestId("quick-script-item-long-command");
expect(item.textContent).toContain("long-command");
expect(item.textContent).toContain("this is a very long command that should be truncat...");
});
});
it("handles short commands without truncation", async () => {
mockFetchScripts.mockResolvedValue({
short: "echo hi",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
const item = screen.getByTestId("quick-script-item-short");
expect(item.textContent).toContain("short");
expect(item.textContent).toContain("echo hi");
});
});
it("sorts scripts alphabetically", async () => {
mockFetchScripts.mockResolvedValue({
zebra: "echo zebra",
alpha: "echo alpha",
beta: "echo beta",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
const items = screen.getAllByRole("option");
expect(items[0].textContent).toContain("alpha");
expect(items[1].textContent).toContain("beta");
expect(items[2].textContent).toContain("zebra");
});
});
});
describe("running scripts", () => {
it("calls onRunScript when a script is clicked", async () => {
mockFetchScripts.mockResolvedValue({
build: "npm run build",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-build")).toBeDefined();
});
fireEvent.click(screen.getByTestId("quick-script-item-build"));
expect(mockOnRunScript).toHaveBeenCalledWith("build", "npm run build");
});
it("closes dropdown after running script", async () => {
mockFetchScripts.mockResolvedValue({
test: "npm test",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-test")).toBeDefined();
});
fireEvent.click(screen.getByTestId("quick-script-item-test"));
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
});
describe("manage scripts link", () => {
it("shows 'Manage Scripts...' link when scripts exist", async () => {
mockFetchScripts.mockResolvedValue({
build: "npm run build",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-manage")).toBeDefined();
});
});
it("calls onOpenScripts when 'Manage Scripts...' is clicked", async () => {
mockFetchScripts.mockResolvedValue({
build: "npm run build",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-manage")).toBeDefined();
});
fireEvent.click(screen.getByTestId("quick-scripts-manage"));
expect(mockOnOpenScripts).toHaveBeenCalled();
});
it("closes dropdown when 'Manage Scripts...' is clicked", async () => {
mockFetchScripts.mockResolvedValue({
build: "npm run build",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-manage")).toBeDefined();
});
fireEvent.click(screen.getByTestId("quick-scripts-manage"));
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
});
describe("empty state", () => {
it("shows empty state when no scripts configured", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-empty")).toBeDefined();
});
});
it("empty state shows 'Add your first script' button", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByText("Add your first script")).toBeDefined();
});
});
it("clicking 'Add your first script' calls onOpenScripts", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByText("Add your first script")).toBeDefined();
});
fireEvent.click(screen.getByText("Add your first script"));
expect(mockOnOpenScripts).toHaveBeenCalled();
});
it("closes dropdown when empty state action is clicked", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByText("Add your first script")).toBeDefined();
});
fireEvent.click(screen.getByText("Add your first script"));
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
});
describe("keyboard navigation", () => {
it("supports ArrowDown to highlight items", async () => {
mockFetchScripts.mockResolvedValue({
alpha: "echo alpha",
beta: "echo beta",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-alpha")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
// First ArrowDown highlights first item
fireEvent.keyDown(menu, { key: "ArrowDown" });
expect(screen.getByTestId("quick-script-item-alpha").className).toContain("highlighted");
// Second ArrowDown highlights second item
fireEvent.keyDown(menu, { key: "ArrowDown" });
expect(screen.getByTestId("quick-script-item-beta").className).toContain("highlighted");
});
it("supports ArrowUp to highlight items", async () => {
mockFetchScripts.mockResolvedValue({
alpha: "echo alpha",
beta: "echo beta",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-alpha")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
// Go to bottom first with End key
fireEvent.keyDown(menu, { key: "End" });
// ArrowUp moves to previous item
fireEvent.keyDown(menu, { key: "ArrowUp" });
expect(screen.getByTestId("quick-script-item-beta").className).toContain("highlighted");
});
it("wraps around with arrow keys", async () => {
mockFetchScripts.mockResolvedValue({
alpha: "echo alpha",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-alpha")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
// ArrowUp from start wraps to end (Manage Scripts...)
fireEvent.keyDown(menu, { key: "ArrowUp" });
expect(screen.getByTestId("quick-scripts-manage").className).toContain("highlighted");
// ArrowDown from end wraps to start
fireEvent.keyDown(menu, { key: "ArrowDown" });
expect(screen.getByTestId("quick-script-item-alpha").className).toContain("highlighted");
});
it("runs script with Enter key", async () => {
mockFetchScripts.mockResolvedValue({
build: "npm run build",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-build")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
// Highlight and press Enter
fireEvent.keyDown(menu, { key: "ArrowDown" });
fireEvent.keyDown(menu, { key: "Enter" });
expect(mockOnRunScript).toHaveBeenCalledWith("build", "npm run build");
});
it("opens manage scripts with Enter key on manage button", async () => {
mockFetchScripts.mockResolvedValue({
build: "npm run build",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-manage")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
// Navigate to last item (Manage Scripts...) and press Enter
fireEvent.keyDown(menu, { key: "End" });
fireEvent.keyDown(menu, { key: "Enter" });
expect(mockOnOpenScripts).toHaveBeenCalled();
});
it("supports Home key to go to first item", async () => {
mockFetchScripts.mockResolvedValue({
alpha: "echo alpha",
beta: "echo beta",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-alpha")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
// Go to end first
fireEvent.keyDown(menu, { key: "End" });
// Home goes to first
fireEvent.keyDown(menu, { key: "Home" });
expect(screen.getByTestId("quick-script-item-alpha").className).toContain("highlighted");
});
it("supports End key to go to last item", async () => {
mockFetchScripts.mockResolvedValue({
alpha: "echo alpha",
beta: "echo beta",
});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-script-item-alpha")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
fireEvent.keyDown(menu, { key: "End" });
expect(screen.getByTestId("quick-scripts-manage").className).toContain("highlighted");
});
});
describe("error handling", () => {
it("handles fetch errors gracefully", async () => {
mockFetchScripts.mockRejectedValue(new Error("Failed to fetch"));
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
// Should show empty state since scripts will be empty object on error
expect(screen.getByTestId("quick-scripts-empty")).toBeDefined();
});
});
});
describe("focus management", () => {
it("menu is focusable with tabIndex", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
const menu = screen.getByTestId("quick-scripts-dropdown");
expect(menu).toHaveAttribute("tabIndex", "-1");
});
it("focus moves to trigger when Escape is pressed", async () => {
mockFetchScripts.mockResolvedValue({});
renderDropdown();
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
fireEvent.keyDown(document, { key: "Escape" });
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
// Trigger should have focus
expect(document.activeElement).toBe(screen.getByTestId("scripts-btn"));
});
});
});