feat(FN-2655): inline quick scripts into header terminal split-button

- Replace QuickScriptsDropdown with an inline Header implementation for terminal script actions
- Add split-button interaction logic in Header to support default-run and dropdown behavior
- Update Header CSS for split-button layout and mobile overflow handling
- Expand Header tests to cover split-button behavior and remove obsolete QuickScriptsDropdown tests
This commit is contained in:
Fusion
2026-04-27 00:39:28 -07:00
committed by gsxdsm
parent cf12664e5a
commit 85bbf2cd43
5 changed files with 495 additions and 939 deletions

View File

@@ -177,10 +177,11 @@ describe("Header", () => {
});
});
describe("terminal button", () => {
it("renders terminal button with correct title on desktop", () => {
renderHeader({ onToggleTerminal: noop }, "desktop");
describe("terminal split button", () => {
it("renders terminal main button and scripts chevron on desktop", () => {
renderHeader({ onToggleTerminal: noop, onOpenScripts: noop, onRunScript: noop }, "desktop");
expect(screen.getByTitle("Open Terminal")).toBeDefined();
expect(screen.getByTestId("scripts-btn")).toBeDefined();
});
it("does not render terminal button inline on mobile", () => {
@@ -188,11 +189,91 @@ describe("Header", () => {
expect(screen.queryByTitle("Open Terminal")).toBeNull();
});
it("calls onToggleTerminal when terminal button is clicked", () => {
it("clicking main button calls onToggleTerminal without opening scripts dropdown", () => {
const onToggleTerminal = vi.fn();
renderHeader({ onToggleTerminal }, "desktop");
fireEvent.click(screen.getByTitle("Open Terminal"));
expect(onToggleTerminal).toHaveBeenCalled();
renderHeader({ onToggleTerminal, onOpenScripts: noop, onRunScript: noop }, "desktop");
fireEvent.click(screen.getByTestId("terminal-toggle-btn"));
expect(onToggleTerminal).toHaveBeenCalledTimes(1);
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
it("clicking scripts chevron opens dropdown without calling onToggleTerminal", async () => {
const onToggleTerminal = vi.fn();
renderHeader({ onToggleTerminal, onOpenScripts: noop, onRunScript: noop }, "desktop");
fireEvent.click(screen.getByTestId("scripts-btn"));
expect(onToggleTerminal).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
});
it("fetches scripts and runs selected script from dropdown", async () => {
mockFetchScripts.mockResolvedValue({ build: "pnpm build" });
const onRunScript = vi.fn();
renderHeader({ onToggleTerminal: noop, onOpenScripts: noop, onRunScript, projectId: "proj-1" }, "desktop");
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(onRunScript).toHaveBeenCalledWith("build", "pnpm build");
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
it("shows loading state while scripts are fetching", () => {
mockFetchScripts.mockImplementation(() => new Promise(() => {}));
renderHeader({ onToggleTerminal: noop, onOpenScripts: noop, onRunScript: noop }, "desktop");
fireEvent.click(screen.getByTestId("scripts-btn"));
expect(screen.getByTestId("quick-scripts-loading")).toBeDefined();
});
it("shows empty state when no scripts are configured", async () => {
mockFetchScripts.mockResolvedValue({});
renderHeader({ onToggleTerminal: noop, onOpenScripts: noop, onRunScript: noop }, "desktop");
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-empty")).toBeDefined();
});
});
it("shows manage scripts footer when scripts exist", async () => {
mockFetchScripts.mockResolvedValue({ test: "pnpm test" });
renderHeader({ onToggleTerminal: noop, onOpenScripts: noop, onRunScript: noop }, "desktop");
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-manage")).toBeDefined();
});
});
it("supports keyboard navigation in scripts dropdown", async () => {
mockFetchScripts.mockResolvedValue({ alpha: "echo alpha", beta: "echo beta" });
const onRunScript = vi.fn();
renderHeader({ onToggleTerminal: noop, onOpenScripts: noop, onRunScript }, "desktop");
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: "ArrowDown" });
fireEvent.keyDown(menu, { key: "Enter" });
expect(onRunScript).toHaveBeenCalledWith("alpha", "echo alpha");
fireEvent.click(screen.getByTestId("scripts-btn"));
await waitFor(() => {
expect(screen.getByTestId("quick-scripts-dropdown")).toBeDefined();
});
fireEvent.keyDown(screen.getByTestId("quick-scripts-dropdown"), { key: "Escape" });
await waitFor(() => {
expect(screen.queryByTestId("quick-scripts-dropdown")).toBeNull();
});
});
it("is always enabled regardless of task state", () => {

View File

@@ -1,510 +0,0 @@
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"));
});
});
});