Files
mission-control/tests/e2e/app.spec.ts
teernisse 480d0817d9 test: add comprehensive frontend tests for components, stores, and utils
Full test coverage for the frontend implementation using Vitest and
Testing Library. Tests are organized by concern with shared fixtures.

Component tests:
- AppShell.test.tsx: Navigation tabs, view switching, batch mode overlay
- FocusCard.test.tsx: Rendering, action buttons, keyboard shortcuts, empty state
- QueueView.test.tsx: Item display, focus promotion, empty state
- QueueItem.test.tsx: Type badges, click handling
- QueueSummary.test.tsx: Count display by type
- QuickCapture.test.tsx: Modal behavior, form submission, error states
- BatchMode.test.tsx: Progress tracking, item advancement, completion
- App.test.tsx: Updated for AppShell integration

Store tests:
- focus-store.test.ts: Item management, act(), setFocus(), reorderQueue()
- nav-store.test.ts: View switching
- capture-store.test.ts: Open/close, submission states
- batch-store.test.ts: Batch lifecycle, status tracking, derived counts

Library tests:
- types.test.ts: Type guards, staleness computation
- transform.test.ts: Lore data transformation, priority ordering
- format.test.ts: IID formatting for MRs vs issues

E2E tests (app.spec.ts):
- Navigation flow
- Focus card interactions
- Queue management
- Quick capture flow

Test infrastructure:
- fixtures.ts: makeFocusItem() factory
- tauri-plugin-shell.ts: Mock for @tauri-apps/plugin-shell
- Updated tauri-api.ts mock with new commands
- vitest.config.ts: Path aliases, jsdom environment
- playwright.config.ts: Removed webServer (run separately)
- package.json: Added @tauri-apps/plugin-shell dependency

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 09:54:59 -05:00

238 lines
7.4 KiB
TypeScript

import { test, expect, type Page } from "@playwright/test";
/**
* Inject mock data into the Zustand focus store via the browser console.
*
* Since we run against the Vite dev server (no Tauri runtime),
* we seed stores directly to test the real React rendering pipeline.
*/
async function seedFocusStore(page: Page): Promise<void> {
await page.evaluate(() => {
// Access Zustand stores via their internal getState/setState
// The stores are module-scoped singletons, accessible from window.__ZUSTAND_STORES__
// We expose them in development via a small shim in main.tsx
const w = window as Record<string, unknown>;
const focusStore = w.__MC_FOCUS_STORE__ as {
setState: (state: Record<string, unknown>) => void;
};
if (focusStore) {
focusStore.setState({
current: {
id: "mr_review:platform/core:847",
title: "Fix authentication token refresh logic",
type: "mr_review",
project: "platform/core",
url: "https://gitlab.com/platform/core/-/merge_requests/847",
iid: 847,
updatedAt: new Date().toISOString(),
contextQuote: "The refresh token logic has a race condition",
requestedBy: "johndoe",
},
queue: [
{
id: "issue:platform/core:42",
title: "Users unable to login after password reset",
type: "issue",
project: "platform/core",
url: "https://gitlab.com/platform/core/-/issues/42",
iid: 42,
updatedAt: new Date(
Date.now() - 5 * 24 * 60 * 60 * 1000
).toISOString(),
contextQuote: null,
requestedBy: null,
},
{
id: "mr_review:platform/api:101",
title: "Add rate limiting to public endpoints",
type: "mr_review",
project: "platform/api",
url: "https://gitlab.com/platform/api/-/merge_requests/101",
iid: 101,
updatedAt: new Date(
Date.now() - 10 * 24 * 60 * 60 * 1000
).toISOString(),
contextQuote: null,
requestedBy: "alice",
},
],
isLoading: false,
error: null,
});
}
});
}
/**
* Expose Zustand stores on the window object for E2E test seeding.
* This is done by evaluating a script that patches the store modules.
*/
async function exposeStores(page: Page): Promise<void> {
await page.evaluate(() => {
// The stores are ES module singletons. We need to wait for them
// to be available. In dev mode, Vite's HMR keeps them accessible.
// We use a polling approach to find them.
return new Promise<void>((resolve) => {
const check = (): void => {
const w = window as Record<string, unknown>;
if (w.__MC_FOCUS_STORE__) {
resolve();
} else {
setTimeout(check, 50);
}
};
check();
});
});
}
test.describe("Mission Control E2E", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/");
// Wait for React to mount
await page.waitForSelector("nav");
});
test.describe("Focus View", () => {
test("shows empty state when no items", async ({ page }) => {
await expect(page.getByText("All Clear")).toBeVisible();
await expect(
page.getByText("Nothing needs your attention right now")
).toBeVisible();
});
test("shows navigation tabs", async ({ page }) => {
await expect(page.getByRole("button", { name: "Focus" })).toBeVisible();
await expect(page.getByRole("button", { name: "Queue" })).toBeVisible();
await expect(page.getByRole("button", { name: "Inbox" })).toBeVisible();
});
test("shows focus item when store has data", async ({ page }) => {
try {
await exposeStores(page);
await seedFocusStore(page);
} catch {
// Stores not exposed -- skip this test in environments without the shim
test.skip();
return;
}
await expect(
page.getByText("Fix authentication token refresh logic")
).toBeVisible();
await expect(page.getByText("MR REVIEW")).toBeVisible();
await expect(page.getByText("!847 in platform/core")).toBeVisible();
});
test("shows action buttons when item is focused", async ({ page }) => {
try {
await exposeStores(page);
await seedFocusStore(page);
} catch {
test.skip();
return;
}
await expect(page.getByText("Start")).toBeVisible();
await expect(page.getByText("1 hour")).toBeVisible();
await expect(page.getByText("Tomorrow")).toBeVisible();
await expect(page.getByText("Skip")).toBeVisible();
});
});
test.describe("Navigation", () => {
test("switches between Focus and Queue views", async ({ page }) => {
// Start in Focus view
await expect(page.getByText("All Clear")).toBeVisible();
// Click Queue tab
await page.getByRole("button", { name: "Queue" }).click();
await expect(page.getByText("No items in the queue")).toBeVisible();
// Click Focus tab
await page.getByRole("button", { name: "Focus" }).click();
await expect(page.getByText("All Clear")).toBeVisible();
});
test("shows Inbox placeholder", async ({ page }) => {
await page.getByRole("button", { name: "Inbox" }).click();
await expect(page.getByText("Inbox view coming in Phase 4b")).toBeVisible();
});
test("Queue tab shows item count badge when store has data", async ({
page,
}) => {
try {
await exposeStores(page);
await seedFocusStore(page);
} catch {
test.skip();
return;
}
// 1 current + 2 queue = 3
await expect(page.getByText("3")).toBeVisible();
});
});
test.describe("Queue View", () => {
test("shows items grouped by type when store has data", async ({
page,
}) => {
try {
await exposeStores(page);
await seedFocusStore(page);
} catch {
test.skip();
return;
}
await page.getByRole("button", { name: "Queue" }).click();
// Should have a Reviews section with 2 items
await expect(page.getByText("REVIEWS (2)")).toBeVisible();
// Should have an Issues section with 1 item
await expect(page.getByText("ISSUES (1)")).toBeVisible();
});
test("clicking item switches to Focus view", async ({ page }) => {
try {
await exposeStores(page);
await seedFocusStore(page);
} catch {
test.skip();
return;
}
await page.getByRole("button", { name: "Queue" }).click();
// Click the issue item
await page
.getByText("Users unable to login after password reset")
.click();
// Should switch to focus view -- wait for the Queue header to disappear
await expect(page.getByText("ISSUES (1)")).not.toBeVisible();
// The clicked item should now be THE ONE THING
await expect(
page.getByRole("heading", {
name: "Users unable to login after password reset",
})
).toBeVisible();
});
});
test.describe("Dark mode", () => {
test("page has dark background", async ({ page }) => {
const body = page.locator("body");
await expect(body).toHaveClass(/bg-surface/);
});
test("HTML element has dark class", async ({ page }) => {
const html = page.locator("html");
await expect(html).toHaveClass(/dark/);
});
});
});