mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
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:
parent
9faf80c53e
commit
eca5fc930f
5 changed files with 189 additions and 27 deletions
117
e2e/CLAUDE.md
117
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/<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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
59
e2e/utils/assertions.ts
Normal 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);
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue