diff --git a/e2e/CLAUDE.md b/e2e/CLAUDE.md index 3bc685e2c..e1d9baa5c 100644 --- a/e2e/CLAUDE.md +++ b/e2e/CLAUDE.md @@ -10,7 +10,7 @@ npm run e2e:playwright # All tests ## Test Template ```typescript -// Import path depends on test depth: tests/feature/test.spec.ts → ../../fixtures/test.fixture +// Import path depends on test depth (see Import Paths below) import { expect, test } from '../../fixtures/test.fixture'; test.describe('Feature', () => { @@ -24,17 +24,91 @@ test.describe('Feature', () => { }); ``` -## Fixtures +## Import Paths -| Fixture | Description | +| Test Location | Import Path | +| -------------------------------- | -------------------------------- | +| `tests/feature/test.spec.ts` | `../../fixtures/test.fixture` | +| `tests/feature/sub/test.spec.ts` | `../../../fixtures/test.fixture` | + +## All Fixtures + +| Fixture | Use For | | -------------- | ------------------------------------------------ | -| `workViewPage` | Add tasks, wait for task list | -| `taskPage` | Get/modify individual tasks | -| `settingsPage` | Navigate settings, manage plugins | -| `dialogPage` | Interact with dialogs | -| `projectPage` | Create/navigate projects | +| `workViewPage` | Task list, adding tasks | +| `taskPage` | Task operations (get, edit, mark done) | +| `projectPage` | Project CRUD, navigation | +| `settingsPage` | Settings navigation, plugin management | +| `dialogPage` | Modal/dialog interactions | +| `plannerPage` | Planner view operations | +| `syncPage` | WebDAV sync setup | +| `tagPage` | Tag management | +| `notePage` | Notes functionality | +| `sideNavPage` | Side navigation | | `testPrefix` | Auto-applied to task/project names for isolation | +## Assertion Helpers + +```typescript +import { + expectTaskCount, + expectTaskVisible, + expectTaskDone, + expectDoneTaskCount, + expectDialogVisible, + expectNoGlobalError, +} from '../../utils/assertions'; + +// Usage: +await expectTaskCount(taskPage, 2); +await expectTaskVisible(taskPage, 'Task Name'); +await expectTaskDone(taskPage, 'Task Name'); +await expectDialogVisible(dialogPage); +await expectNoGlobalError(page); +``` + +## Common Patterns + +### Create project with tasks + +```typescript +await projectPage.createAndGoToTestProject(); +await workViewPage.addTask('Task 1'); +await workViewPage.addTask('Task 2'); +await expectTaskCount(taskPage, 2); +``` + +### Mark task done and verify + +```typescript +await workViewPage.addTask('My Task'); +const task = taskPage.getTaskByText('My Task'); +await taskPage.markTaskAsDone(task); +await expectDoneTaskCount(taskPage, 1); +``` + +### Dialog interaction + +```typescript +// Trigger dialog via some action, then: +await dialogPage.waitForDialog(); +await dialogPage.fillDialogInput('input[name="title"]', 'Value'); +await dialogPage.clickSaveButton(); +await dialogPage.waitForDialogToClose(); +``` + +### Sync tests (serial execution required) + +```typescript +test.describe.configure({ mode: 'serial' }); + +test.describe('Sync Feature', () => { + test('should sync data', async ({ syncPage, workViewPage }) => { + // Sync tests require special setup - see sync-test-helpers.ts + }); +}); +``` + ## Key Methods ### workViewPage @@ -46,10 +120,18 @@ test.describe('Feature', () => { - `getTaskByText(text)` → Locator - `getTask(index)` → Locator (1-based) +- `getAllTasks()` → Locator - `markTaskAsDone(task)` - `getTaskCount()` → number +- `getDoneTasks()` / `getUndoneTasks()` → Locator - `waitForTaskWithText(text)` → Locator +### projectPage + +- `createProject(name)` +- `navigateToProjectByName(name)` +- `createAndGoToTestProject()` - Quick setup + ### settingsPage - `navigateToPluginSettings()` @@ -59,30 +141,16 @@ test.describe('Feature', () => { - `waitForDialog()` → Locator - `clickDialogButton(text)`, `clickSaveButton()` +- `fillDialogInput(selector, value)` - `waitForDialogToClose()` -### projectPage - -- `createProject(name)` -- `navigateToProjectByName(name)` -- `createAndGoToTestProject()` - Quick setup - -### Other page objects (instantiate manually) - -```typescript -import { PlannerPage, SyncPage, TagPage, NotePage, SideNavPage } from '../../pages'; - -const plannerPage = new PlannerPage(page); -const tagPage = new TagPage(page, testPrefix); -``` - For full method list, read the page object file: `e2e/pages/.page.ts` ## Selectors ```typescript import { cssSelectors } from '../../constants/selectors'; -// Available: TASK, FIRST_TASK, TASK_TITLE, TASK_DONE_BTN, ADD_TASK_INPUT, MAT_DIALOG, SIDENAV +// Available: TASK, FIRST_TASK, TASK_TITLE, TASK_DONE_BTN, ADD_TASK_INPUT, MAT_DIALOG, SIDENAV, etc. ``` ## Critical Rules @@ -91,3 +159,4 @@ import { cssSelectors } from '../../constants/selectors'; 2. **Use page objects** - not raw `page.locator()` for common actions 3. **No `waitForTimeout()`** - use `expect().toBeVisible()` instead 4. **Tests are isolated** - each gets fresh browser context + IndexedDB +5. **Use assertion helpers** - for consistent, readable tests diff --git a/e2e/constants/selectors.ts b/e2e/constants/selectors.ts index 24c3ab599..92df38587 100644 --- a/e2e/constants/selectors.ts +++ b/e2e/constants/selectors.ts @@ -133,4 +133,6 @@ export const cssSelectors = { SCHEDULE_TASK_ITEM: 'task-detail-item:has(mat-icon:text("alarm")), task-detail-item:has(mat-icon:text("today")), task-detail-item:has(mat-icon:text("schedule"))', TASK_SCHEDULE_BTN: '.ico-btn.schedule-btn', -}; +} as const; + +export type SelectorKey = keyof typeof cssSelectors; diff --git a/e2e/fixtures/test.fixture.ts b/e2e/fixtures/test.fixture.ts index 1674621f6..5202eb94d 100644 --- a/e2e/fixtures/test.fixture.ts +++ b/e2e/fixtures/test.fixture.ts @@ -4,6 +4,11 @@ import { ProjectPage } from '../pages/project.page'; import { TaskPage } from '../pages/task.page'; import { SettingsPage } from '../pages/settings.page'; import { DialogPage } from '../pages/dialog.page'; +import { PlannerPage } from '../pages/planner.page'; +import { SyncPage } from '../pages/sync.page'; +import { TagPage } from '../pages/tag.page'; +import { NotePage } from '../pages/note.page'; +import { SideNavPage } from '../pages/side-nav.page'; import { waitForAppReady } from '../utils/waits'; import { dismissTourIfVisible } from '../utils/tour-helpers'; @@ -13,6 +18,11 @@ type TestFixtures = { taskPage: TaskPage; settingsPage: SettingsPage; dialogPage: DialogPage; + plannerPage: PlannerPage; + syncPage: SyncPage; + tagPage: TagPage; + notePage: NotePage; + sideNavPage: SideNavPage; isolatedContext: BrowserContext; waitForNav: (selector?: string) => Promise; testPrefix: string; @@ -110,6 +120,26 @@ export const test = base.extend({ await use(new DialogPage(page, testPrefix)); }, + plannerPage: async ({ page }, use) => { + await use(new PlannerPage(page)); + }, + + syncPage: async ({ page }, use) => { + await use(new SyncPage(page)); + }, + + tagPage: async ({ page, testPrefix }, use) => { + await use(new TagPage(page, testPrefix)); + }, + + notePage: async ({ page, testPrefix }, use) => { + await use(new NotePage(page, testPrefix)); + }, + + sideNavPage: async ({ page, testPrefix }, use) => { + await use(new SideNavPage(page, testPrefix)); + }, + waitForNav: async ({ page }, use) => { const waitForNav = async (selector?: string): Promise => { await waitForAppReady(page, { diff --git a/e2e/utils/assertions.ts b/e2e/utils/assertions.ts new file mode 100644 index 000000000..2cb5ee90b --- /dev/null +++ b/e2e/utils/assertions.ts @@ -0,0 +1,59 @@ +import { expect } from '@playwright/test'; +import type { Page } from '@playwright/test'; +import type { TaskPage } from '../pages/task.page'; +import type { DialogPage } from '../pages/dialog.page'; + +/** + * Assert that the task list has the expected number of tasks. + */ +export const expectTaskCount = async ( + taskPage: TaskPage, + count: number, +): Promise => { + await expect(taskPage.getAllTasks()).toHaveCount(count); +}; + +/** + * Assert that a task with the given text is visible. + */ +export const expectTaskVisible = async ( + taskPage: TaskPage, + text: string, +): Promise => { + const task = taskPage.getTaskByText(text); + await expect(task).toBeVisible(); +}; + +/** + * Assert that a dialog is currently visible. + */ +export const expectDialogVisible = async (dialogPage: DialogPage): Promise => { + const dialog = await dialogPage.waitForDialog(); + await expect(dialog).toBeVisible(); +}; + +/** + * Assert that no global error alert is displayed. + */ +export const expectNoGlobalError = async (page: Page): Promise => { + const error = page.locator('.global-error-alert'); + await expect(error).not.toBeVisible(); +}; + +/** + * Assert that a task is marked as done. + */ +export const expectTaskDone = async (taskPage: TaskPage, text: string): Promise => { + const task = taskPage.getTaskByText(text); + await expect(task).toHaveClass(/isDone/); +}; + +/** + * Assert that the done task count matches expected. + */ +export const expectDoneTaskCount = async ( + taskPage: TaskPage, + count: number, +): Promise => { + await expect(taskPage.getDoneTasks()).toHaveCount(count); +}; diff --git a/e2e/utils/waits.ts b/e2e/utils/waits.ts index ba8f1664c..72c33fe9c 100644 --- a/e2e/utils/waits.ts +++ b/e2e/utils/waits.ts @@ -83,8 +83,10 @@ export const waitForAppReady = async ( await page.locator(selector).first().waitFor({ state: 'visible', timeout: 8000 }); } - // Wait for Angular to stabilize - await waitForAngularStability(page); + // Note: We no longer call waitForAngularStability here because: + // 1. We've already confirmed .route-wrapper is visible + // 2. Playwright's auto-waiting handles element actionability + // 3. The readyState check in waitForAngularStability can cause flakiness }; /**