diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..487f3fd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright configuration for Mission Control E2E tests + * + * For Tauri desktop app testing, we use the webview URL directly + * rather than launching the full native app wrapper. + */ +export default defineConfig({ + testDir: "./tests/e2e", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: "html", + + use: { + // Base URL for webview testing + baseURL: "http://localhost:1420", + trace: "on-first-retry", + screenshot: "only-on-failure", + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + // Note: Tauri uses WebKit on macOS, so we test against Safari-like behavior + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + ], + + // Run the Vite dev server before tests + webServer: { + command: "npm run dev", + url: "http://localhost:1420", + reuseExistingServer: !process.env.CI, + timeout: 120000, + }, +}); diff --git a/tests/components/App.test.tsx b/tests/components/App.test.tsx new file mode 100644 index 0000000..6f1d517 --- /dev/null +++ b/tests/components/App.test.tsx @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import App from "@/App"; + +describe("App", () => { + it("renders the main heading", () => { + render(); + + expect(screen.getByText("Mission Control")).toBeInTheDocument(); + }); + + it("renders the tagline", () => { + render(); + + expect( + screen.getByText("What should you be doing right now?") + ).toBeInTheDocument(); + }); + + it("renders the focus placeholder", () => { + render(); + + expect( + screen.getByText("THE ONE THING will appear here") + ).toBeInTheDocument(); + }); +}); diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts new file mode 100644 index 0000000..d657bfd --- /dev/null +++ b/tests/e2e/app.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Mission Control App", () => { + test("displays the main heading", async ({ page }) => { + await page.goto("/"); + + await expect(page.getByText("Mission Control")).toBeVisible(); + }); + + test("displays the tagline", async ({ page }) => { + await page.goto("/"); + + await expect( + page.getByText("What should you be doing right now?") + ).toBeVisible(); + }); + + test("has dark mode styling", async ({ page }) => { + await page.goto("/"); + + // Check that the page has dark background (zinc-900) + const body = page.locator("body"); + await expect(body).toHaveClass(/bg-surface/); + }); +}); diff --git a/tests/mocks/tauri-api.ts b/tests/mocks/tauri-api.ts new file mode 100644 index 0000000..adf5d35 --- /dev/null +++ b/tests/mocks/tauri-api.ts @@ -0,0 +1,78 @@ +/** + * Mock implementation of @tauri-apps/api for testing + * + * This allows tests to run without a Tauri runtime. + */ + +import { vi } from "vitest"; + +// Store for mock responses - tests can override these +export const mockResponses: Record = {}; + +// Mock invoke function +export const invoke = vi.fn(async (cmd: string, _args?: unknown) => { + if (cmd in mockResponses) { + return mockResponses[cmd]; + } + + // Default responses + switch (cmd) { + case "greet": + return "Hello from mock Tauri!"; + case "get_lore_status": + return { + last_sync: null, + is_healthy: true, + message: "Mock lore status", + }; + default: + throw new Error(`Mock not implemented for command: ${cmd}`); + } +}); + +// Helper to set mock responses in tests +export function setMockResponse(cmd: string, response: unknown): void { + mockResponses[cmd] = response; +} + +// Helper to reset all mocks +export function resetMocks(): void { + invoke.mockClear(); + Object.keys(mockResponses).forEach((key) => delete mockResponses[key]); +} + +// Mock event listener +export const listen = vi.fn( + async (_event: string, _handler: (payload: unknown) => void) => { + // Return unlisten function + return vi.fn(); + } +); + +// Mock event emitter +export const emit = vi.fn(async (_event: string, _payload?: unknown) => {}); + +// Core module exports +export const core = { + invoke, +}; + +// Event module exports +export const event = { + listen, + emit, +}; + +// Window module mock +export const window = { + getCurrent: vi.fn(() => ({ + label: "main", + listen: vi.fn(), + emit: vi.fn(), + close: vi.fn(), + hide: vi.fn(), + show: vi.fn(), + isVisible: vi.fn(async () => true), + setTitle: vi.fn(), + })), +}; diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..980484b --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,25 @@ +import "@testing-library/jest-dom/vitest"; + +// Mock window.matchMedia for components that use media queries +Object.defineProperty(window, "matchMedia", { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), +}); + +// Mock ResizeObserver for components that use it +class MockResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +window.ResizeObserver = MockResizeObserver as typeof ResizeObserver; diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..59ce37d --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,30 @@ +/// +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import path from "path"; + +export default defineConfig({ + plugins: [react()], + test: { + globals: true, + environment: "jsdom", + setupFiles: ["./tests/setup.ts"], + include: ["tests/**/*.{test,spec}.{ts,tsx}"], + exclude: ["tests/e2e/**", "**/node_modules/**"], + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.{ts,tsx}"], + exclude: ["src/main.tsx", "src/**/*.d.ts"], + }, + // Mock Tauri APIs + alias: { + "@tauri-apps/api": path.resolve(__dirname, "./tests/mocks/tauri-api.ts"), + }, + }, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +});