refactor(e2e): improve test infrastructure for easier expansion

- Add all page objects to fixtures (plannerPage, syncPage, tagPage, notePage, sideNavPage)
- Create assertion helpers (expectTaskCount, expectTaskVisible, etc.)
- Enhance CLAUDE.md with patterns, import paths, and all fixtures
- Add const assertion to selectors for better TypeScript support
This commit is contained in:
Johannes Millan 2026-01-03 17:51:18 +01:00
parent 9faf80c53e
commit eca5fc930f
5 changed files with 189 additions and 27 deletions

View file

@ -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/<name>.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

View file

@ -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;

View file

@ -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<void>;
testPrefix: string;
@ -110,6 +120,26 @@ export const test = base.extend<TestFixtures>({
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<void> => {
await waitForAppReady(page, {

59
e2e/utils/assertions.ts Normal file
View file

@ -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<void> => {
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<void> => {
const task = taskPage.getTaskByText(text);
await expect(task).toBeVisible();
};
/**
* Assert that a dialog is currently visible.
*/
export const expectDialogVisible = async (dialogPage: DialogPage): Promise<void> => {
const dialog = await dialogPage.waitForDialog();
await expect(dialog).toBeVisible();
};
/**
* Assert that no global error alert is displayed.
*/
export const expectNoGlobalError = async (page: Page): Promise<void> => {
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<void> => {
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<void> => {
await expect(taskPage.getDoneTasks()).toHaveCount(count);
};

View file

@ -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
};
/**