-
Notifications
You must be signed in to change notification settings - Fork 0
test: E2E scenario expansion with Playwright (#712) #822
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f32724d
cc4c860
9edf99d
1b667c4
e421abc
0411cd6
5703e10
6397e0d
7c39562
9ec5b9e
1c8fc58
9a9e9fd
9edbd6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| /** | ||
| * E2E: Capture Edge Cases | ||
| * | ||
| * Covers UI-driven capture edge-case scenarios: | ||
| * - Empty text rejection (submit with no input) | ||
| * - Whitespace-only text rejection | ||
| * - Escape dismissal without saving | ||
| * - Board-linked capture from the board action rail | ||
| */ | ||
|
|
||
| import type { Page } from '@playwright/test' | ||
| import { expect, test } from '@playwright/test' | ||
| import { registerAndAttachSession, type AuthResult } from './support/authSession' | ||
| import { createBoardWithColumn } from './support/boardHelpers' | ||
|
|
||
| let auth: AuthResult | ||
|
|
||
| test.beforeEach(async ({ page, request }) => { | ||
| auth = await registerAndAttachSession(page, request, 'capture-edge') | ||
| }) | ||
|
|
||
| // --- Helper --- | ||
|
|
||
| async function openCaptureModalViaHotkey(page: Page) { | ||
| await page.keyboard.press('Control+Shift+C') | ||
| const captureModal = page.getByRole('dialog', { name: 'Capture item' }) | ||
| await expect(captureModal).toBeVisible() | ||
| return captureModal | ||
| } | ||
|
|
||
| // --- Empty text rejection --- | ||
|
|
||
| test('capture modal should not submit empty text', async ({ page }) => { | ||
| await page.goto('/workspace/boards') | ||
| await expect(page.getByRole('button', { name: '+ New Board' })).toBeVisible() | ||
|
|
||
| const captureModal = await openCaptureModalViaHotkey(page) | ||
| const inputField = captureModal.getByPlaceholder('Capture a thought, task, or follow-up...') | ||
|
|
||
| // Leave text empty and try to submit | ||
| await inputField.fill('') | ||
| await inputField.press('Control+Enter') | ||
|
|
||
| // Modal should remain open (no navigation to inbox) | ||
| await expect(captureModal).toBeVisible() | ||
| await expect(page).not.toHaveURL(/\/workspace\/inbox/) | ||
| }) | ||
|
|
||
| test('capture modal should not submit whitespace-only text', async ({ page }) => { | ||
| await page.goto('/workspace/boards') | ||
| await expect(page.getByRole('button', { name: '+ New Board' })).toBeVisible() | ||
|
|
||
| const captureModal = await openCaptureModalViaHotkey(page) | ||
| const inputField = captureModal.getByPlaceholder('Capture a thought, task, or follow-up...') | ||
|
|
||
| // Fill with only whitespace | ||
| await inputField.fill(' \n \t ') | ||
| await inputField.press('Control+Enter') | ||
|
|
||
| // Modal should remain open (whitespace-only should be rejected) | ||
| await expect(captureModal).toBeVisible() | ||
| await expect(page).not.toHaveURL(/\/workspace\/inbox/) | ||
| }) | ||
|
|
||
| // --- Capture modal dismissal --- | ||
|
|
||
| test('capture modal should close without saving when pressing Escape', async ({ page }) => { | ||
| await page.goto('/workspace/boards') | ||
| await expect(page.getByRole('button', { name: '+ New Board' })).toBeVisible() | ||
|
|
||
| const captureModal = await openCaptureModalViaHotkey(page) | ||
|
|
||
| // Type something but do not submit | ||
| await captureModal | ||
| .getByPlaceholder('Capture a thought, task, or follow-up...') | ||
| .fill('This text should not be saved') | ||
|
|
||
| // Press Escape to dismiss | ||
| await page.keyboard.press('Escape') | ||
|
|
||
| // Modal should close | ||
| await expect(captureModal).toHaveCount(0) | ||
|
|
||
| // Navigate to inbox to verify nothing was saved | ||
| await page.goto('/workspace/inbox') | ||
| await expect(page.getByRole('heading', { name: 'Inbox', exact: true })).toBeVisible() | ||
| await expect(page.locator('[data-testid="inbox-item"]').filter({ hasText: 'This text should not be saved' })).toHaveCount(0) | ||
| }) | ||
|
|
||
| // --- Capture from board action rail --- | ||
|
|
||
| test('capture from board action rail should link capture to that board', async ({ page, request }) => { | ||
| test.setTimeout(60_000) | ||
|
|
||
| const seed = `${Date.now()}-${Math.floor(Math.random() * 1_000_000)}` | ||
| const boardId = await createBoardWithColumn(request, auth, seed, { | ||
| boardNamePrefix: 'Rail Capture', | ||
| description: 'board for rail capture test', | ||
| columnNamePrefix: 'Column', | ||
| }) | ||
| const boardName = `Rail Capture ${seed}` | ||
|
|
||
| await page.goto(`/workspace/boards/${boardId}`) | ||
| await expect(page.getByRole('heading', { name: boardName })).toBeVisible() | ||
|
|
||
| const boardActionRail = page.locator('[data-board-action-rail]') | ||
| await expect(boardActionRail.getByRole('button', { name: 'Capture here' })).toBeVisible() | ||
|
|
||
| await boardActionRail.getByRole('button', { name: 'Capture here' }).click() | ||
| const captureModal = page.getByRole('dialog', { name: 'Capture item' }) | ||
| await expect(captureModal).toBeVisible() | ||
|
|
||
| // The capture modal should indicate it is linked to this board | ||
| await expect(captureModal.getByText(boardName)).toBeVisible() | ||
|
|
||
| const captureText = `Rail-linked capture ${seed}` | ||
| const createCaptureResponse = page.waitForResponse((response) => | ||
| response.request().method() === 'POST' | ||
| && /\/api\/capture\/items$/i.test(response.url()) | ||
| && response.ok()) | ||
|
|
||
| await captureModal.getByPlaceholder('Capture a thought, task, or follow-up...').fill(captureText) | ||
| await captureModal.getByRole('button', { name: 'Save Capture' }).click() | ||
| await createCaptureResponse | ||
| await expect(captureModal).toHaveCount(0) | ||
|
|
||
| // Navigate to board-filtered inbox and verify the capture is there | ||
| await page.goto(`/workspace/inbox?boardId=${boardId}`) | ||
| await expect(page.getByRole('heading', { name: 'Inbox', exact: true })).toBeVisible() | ||
| const captureRow = page.locator('[data-testid="inbox-item"]').filter({ hasText: captureText }).first() | ||
| await expect(captureRow).toBeVisible({ timeout: 15_000 }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| /** | ||
| * E2E: Dark Mode Scenarios | ||
| * | ||
| * Extends dark mode coverage beyond the basic toggle test: | ||
| * - Dark mode applies across multiple views (home, boards, inbox, today) | ||
| * - Dark mode with board content (columns, cards) renders without | ||
| * white-on-white or invisible elements | ||
| * - System prefers-color-scheme: dark (stub -- test.fixme until feature ships) | ||
| * - Toggling dark mode off restores light theme | ||
| */ | ||
|
|
||
| import type { Page } from '@playwright/test' | ||
| import { expect, test } from '@playwright/test' | ||
| import { registerAndAttachSession, type AuthResult } from './support/authSession' | ||
| import { createBoardWithColumn } from './support/boardHelpers' | ||
|
|
||
| let auth: AuthResult | ||
|
|
||
| test.beforeEach(async ({ page, request }) => { | ||
| auth = await registerAndAttachSession(page, request, 'dark-mode') | ||
| }) | ||
|
|
||
| // --- Helpers --- | ||
|
|
||
| async function findDarkModeToggle(page: Page) { | ||
| const toggle = page | ||
| .getByRole('button', { name: /dark mode|theme|light|dark/i }) | ||
| .or(page.getByLabel(/dark mode|toggle theme/i)) | ||
| .first() | ||
|
|
||
| if (await toggle.isVisible({ timeout: 5_000 }).catch(() => false)) { | ||
| return toggle | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| async function isDarkMode(page: Page): Promise<boolean> { | ||
| return page.evaluate(() => { | ||
| return ( | ||
| document.documentElement.classList.contains('dark') || | ||
| document.documentElement.dataset.theme === 'dark' || | ||
| document.body.classList.contains('dark') || | ||
| document.body.dataset.theme === 'dark' | ||
| ) | ||
| }) | ||
| } | ||
|
|
||
| async function enableDarkMode(page: Page): Promise<boolean> { | ||
| const toggle = await findDarkModeToggle(page) | ||
| if (!toggle) { | ||
| return false | ||
| } | ||
|
|
||
| const alreadyDark = await isDarkMode(page) | ||
| if (!alreadyDark) { | ||
| await toggle.click() | ||
| } | ||
| return true | ||
| } | ||
|
|
||
| // --- Dark mode across multiple views --- | ||
|
|
||
| test('dark mode should persist when navigating between Home, Boards, and Inbox views', async ({ page }) => { | ||
| await page.goto('/workspace/home') | ||
| await expect(page.getByRole('heading', { name: 'Home', exact: true })).toBeVisible() | ||
|
|
||
| const activated = await enableDarkMode(page) | ||
| if (!activated) { | ||
| test.skip() | ||
| return | ||
| } | ||
|
Comment on lines
+68
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using |
||
|
|
||
| expect(await isDarkMode(page)).toBeTruthy() | ||
|
|
||
| // Navigate to Boards | ||
| await page.goto('/workspace/boards') | ||
| await expect(page.getByRole('button', { name: '+ New Board' })).toBeVisible() | ||
| expect(await isDarkMode(page)).toBeTruthy() | ||
|
|
||
| // Navigate to Inbox | ||
| await page.goto('/workspace/inbox') | ||
| await expect(page.getByRole('heading', { name: 'Inbox', exact: true })).toBeVisible() | ||
| expect(await isDarkMode(page)).toBeTruthy() | ||
|
|
||
| // Navigate to Today | ||
| await page.goto('/workspace/today') | ||
| await expect(page.getByRole('heading', { name: 'Today', exact: true })).toBeVisible() | ||
| expect(await isDarkMode(page)).toBeTruthy() | ||
| }) | ||
|
|
||
| // --- Dark mode with board content --- | ||
|
|
||
| test('dark mode board view should render columns and cards without invisible text', async ({ page, request }) => { | ||
| const seed = `${Date.now()}-${Math.floor(Math.random() * 1_000_000)}` | ||
| const boardId = await createBoardWithColumn(request, auth, seed, { | ||
| boardNamePrefix: 'Dark Mode Board', | ||
| description: 'dark mode board test', | ||
| columnNamePrefix: 'Dark Column', | ||
| }) | ||
|
|
||
| await page.goto(`/workspace/boards/${boardId}`) | ||
| await expect(page.getByRole('heading', { name: `Dark Mode Board ${seed}` })).toBeVisible() | ||
|
|
||
| const activated = await enableDarkMode(page) | ||
| if (!activated) { | ||
| test.skip() | ||
| return | ||
| } | ||
|
|
||
| expect(await isDarkMode(page)).toBeTruthy() | ||
|
|
||
| // Column heading should still be visible (not white-on-white) | ||
| const columnHeading = page.getByRole('heading', { name: `Dark Column ${seed}`, exact: true }) | ||
| await expect(columnHeading).toBeVisible() | ||
|
|
||
| // Verify the column heading occupies real space (not collapsed/invisible) | ||
| const columnHeadingBox = await columnHeading.boundingBox() | ||
| expect(columnHeadingBox).not.toBeNull() | ||
|
|
||
| // The heading text should have non-zero dimensions (not collapsed/invisible) | ||
| expect(columnHeadingBox!.width).toBeGreaterThan(0) | ||
| expect(columnHeadingBox!.height).toBeGreaterThan(0) | ||
| }) | ||
|
|
||
| // --- Toggling dark mode off restores light theme --- | ||
|
|
||
| test('toggling dark mode off should restore light theme', async ({ page }) => { | ||
| await page.goto('/workspace/home') | ||
| await expect(page.getByRole('heading', { name: 'Home', exact: true })).toBeVisible() | ||
|
|
||
| const toggle = await findDarkModeToggle(page) | ||
| if (!toggle) { | ||
| test.skip() | ||
| return | ||
| } | ||
|
|
||
| // Enable dark mode | ||
| const wasDark = await isDarkMode(page) | ||
| if (!wasDark) { | ||
| await toggle.click() | ||
| } | ||
| expect(await isDarkMode(page)).toBeTruthy() | ||
|
|
||
| // Disable dark mode | ||
| await toggle.click() | ||
| expect(await isDarkMode(page)).toBeFalsy() | ||
| }) | ||
|
|
||
| // --- System prefers-color-scheme --- | ||
|
|
||
| test.fixme('system prefers-color-scheme dark should activate dark mode on first visit', async () => { | ||
| // TODO: implement once automatic system dark mode detection is shipped | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allowing
HttpStatusCode.InternalServerErrorhere means this stress test can pass even when concurrent triage requests are crashing on the server; combined with the new>= 1success threshold, a regression that breaks most requests would no longer fail CI. If SQLite contention is expected in this environment, this should be handled with retries or environment-specific gating rather than treating 500s as an acceptable outcome for the core assertion.Useful? React with 👍 / 👎.