diff --git a/docs/ui-lab.md b/docs/ui-lab.md index df08170..f10a6e7 100644 --- a/docs/ui-lab.md +++ b/docs/ui-lab.md @@ -22,7 +22,7 @@ http://127.0.0.1:1422 The script runs Vite in the custom `ui` mode: ```bash -vite --mode ui --host 127.0.0.1 --port 1422 --strictPort --open +vite --mode ui --host 127.0.0.1 --port 1422 --strictPort ``` The normal app development commands are unchanged: diff --git a/package.json b/package.json index ce68a51..115ce15 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "clean:app": "cd src-tauri && cargo clean", "dev:app": "tauri dev", "dev:app:cpu": "tauri dev -- --no-default-features", - "dev:ui": "vite --mode ui --host 127.0.0.1 --port 1422 --strictPort --open", + "dev:ui": "vite --mode ui --host 127.0.0.1 --port 1422 --strictPort", "dev:vite": "vite", "dev:web": "cd website && pnpm dev", "format": "prettier --write .", @@ -22,7 +22,8 @@ "format:rust": "cd src-tauri && cargo fmt", "format:rust:check": "cd src-tauri && cargo fmt --check", "preview": "vite preview", - "tauri": "tauri" + "tauri": "tauri", + "test:e2e": "playwright test" }, "dependencies": { "@tanstack/react-virtual": "^3.13.23", diff --git a/playwright.config.ts b/playwright.config.ts index 53cd798..3b6714f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -20,13 +20,13 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + workers: process.env.CI ? 1 : 4, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('')`. */ - // baseURL: 'http://localhost:3000', + baseURL: 'http://127.0.0.1:1422', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', @@ -39,11 +39,6 @@ export default defineConfig({ use: { ...devices['Desktop Chrome'] }, }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - { name: 'webkit', use: { ...devices['Desktop Safari'] }, @@ -70,10 +65,11 @@ export default defineConfig({ // }, ], - /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://localhost:3000', - // reuseExistingServer: !process.env.CI, - // }, + /* Run the Phokus UI Lab (browser-only dev mode, see docs/ui-lab.md) before starting the tests */ + webServer: { + command: 'pnpm exec vite --mode ui --host 127.0.0.1 --port 1422 --strictPort', + url: 'http://127.0.0.1:1422', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + }, }) diff --git a/tests/example.spec.ts b/tests/example.spec.ts deleted file mode 100644 index a5f148e..0000000 --- a/tests/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test' - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/') - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/) -}) - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/') - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click() - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible() -}) diff --git a/tests/ui-lab.spec.ts b/tests/ui-lab.spec.ts new file mode 100644 index 0000000..5381bbc --- /dev/null +++ b/tests/ui-lab.spec.ts @@ -0,0 +1,93 @@ +import { test, expect, type Page } from '@playwright/test' + +// Smoke tests against the Phokus UI Lab (docs/ui-lab.md) — a browser-only dev +// mode that runs the real App.tsx/store/components against seeded, in-memory +// mock fixtures selected via the `?scenario=` query param. + +const browserErrors = new WeakMap() + +test.beforeEach(async ({ page }) => { + const errors: string[] = [] + browserErrors.set(page, errors) + + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()) + }) + page.on('pageerror', (error) => errors.push(error.message)) +}) + +test.afterEach(async ({ page }) => { + expect(browserErrors.get(page) ?? []).toEqual([]) +}) + +test.describe('gallery scenarios', () => { + test('rich scenario renders the gallery grid with folders in the sidebar', async ({ page }) => { + await page.goto('/?scenario=rich') + + await expect(page.getByText('Camera Roll', { exact: true })).toBeVisible() + await expect(page.getByText('Client Selects', { exact: true })).toBeVisible() + await expect(page.getByText('Video Archive', { exact: true })).toBeVisible() + + await expect(page.getByRole('button', { name: /^Open / }).first()).toBeVisible() + }) + + test('filename search filters the visible gallery results', async ({ page }) => { + await page.goto('/?scenario=rich') + + const search = page.getByPlaceholder('Search files, or use /s /t') + await search.fill('coastal-walk-003') + + await expect(page.getByText('Filename')).toBeVisible() + await expect(page.getByRole('button', { name: 'Open coastal-walk-003.jpg' })).toBeVisible() + await expect(page.getByRole('button', { name: /^Open / })).toHaveCount(1) + }) + + test('empty scenario shows the empty library without the onboarding tour', async ({ page }) => { + await page.goto('/?scenario=empty') + + await expect(page.getByText('No media found')).toBeVisible() + await expect(page.getByRole('heading', { name: 'Welcome' })).not.toBeVisible() + }) + + test('new-user scenario opens the onboarding tour on step 1', async ({ page }) => { + await page.goto('/?scenario=new-user') + + await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible() + await expect(page.getByText('Step 1 of 8')).toBeVisible() + }) + + test('duplicates fixture data shows cached groups in Duplicate Finder', async ({ page }) => { + await page.goto('/?scenario=duplicates') + + await page.getByText('Duplicates', { exact: true }).click() + + await expect(page.getByRole('heading', { name: 'Duplicate Finder' })).toBeVisible() + await expect(page.getByText(/\d+ groups? · .* reclaimable/)).toBeVisible() + await expect(page.getByRole('button', { name: 'Select all duplicates' })).toBeVisible() + }) +}) + +test.describe("what's new", () => { + test('just-updated scenario shows the toast and opens the modal on click', async ({ page }) => { + await page.goto('/?scenario=just-updated') + + const toastButton = page.getByRole('button', { name: "What's new" }) + await expect(toastButton).toBeVisible() + await toastButton.click() + + await expect(page.getByRole('heading', { name: /^Phokus v/ })).toBeVisible() + }) + + test('just-updated + unreleased changelog opens the modal with the section nav rail', async ({ + page, + }) => { + await page.goto('/?scenario=just-updated&changelog=unreleased') + + await page.getByRole('button', { name: "What's new" }).click() + + await expect(page.getByRole('heading', { name: /^Phokus v/ })).toBeVisible() + const sectionRail = page.getByRole('navigation') + await expect(sectionRail.getByRole('button', { name: /Added/ })).toBeVisible() + await expect(sectionRail.getByRole('button', { name: /Changed/ })).toBeVisible() + }) +})