super-productivity/e2e/fixtures/test.fixture.ts
Johannes Millan 85fa50974b Merge branch 'master' into feat/operation-logs
* master:
  refactor(e2e): improve test infrastructure for easier expansion
  chore(e2e): remove broken/empty skipped tests
  test(e2e): fix flaky plugin and WebDAV sync tests
  refactor(e2e): replace waitForTimeout with condition-based waits
  perf(e2e): remove ineffective waits to speed up test runs
  docs(e2e): add CLAUDE.md reference and barrel export for easier test creation
  build: update dep
  refactor(e2e): simplify waits and fix flaky tests
  feat(e2e): streamline e2e test development with improved infrastructure
  perf(e2e): optimize wait utilities and addTask method for faster test execution
  16.8.1

# Conflicts:
#	e2e/pages/base.page.ts
#	e2e/pages/project.page.ts
#	e2e/tests/reminders/reminders-schedule-page.spec.ts
#	e2e/tests/sync/webdav-sync-advanced.spec.ts
#	e2e/tests/sync/webdav-sync-expansion.spec.ts
#	e2e/tests/sync/webdav-sync-full.spec.ts
#	e2e/utils/waits.ts
2026-01-03 18:51:51 +01:00

156 lines
4.6 KiB
TypeScript

import { BrowserContext, test as base } from '@playwright/test';
import { WorkViewPage } from '../pages/work-view.page';
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';
type TestFixtures = {
workViewPage: WorkViewPage;
projectPage: ProjectPage;
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;
};
export const test = base.extend<TestFixtures>({
// Create isolated context for each test
isolatedContext: async ({ browser, baseURL }, use, testInfo) => {
const url = baseURL || testInfo.project.use.baseURL || 'http://localhost:4242';
// Create a new context with isolated storage
const context = await browser.newContext({
// Each test gets its own storage state
storageState: undefined,
// Preserve the base userAgent and add worker index for debugging
userAgent: `PLAYWRIGHT PLAYWRIGHT-WORKER-${testInfo.workerIndex}`,
baseURL: url,
});
await use(context);
// Cleanup
await context.close();
},
// Override page to use isolated context
page: async ({ isolatedContext }, use) => {
const page = await isolatedContext.newPage();
try {
// Set up error handling
page.on('pageerror', (error) => {
console.error('Page error:', error.message);
});
page.on('console', (msg) => {
if (msg.type() === 'error') {
console.error('Console error:', msg.text());
} else if (process.env.E2E_VERBOSE) {
console.log(`Console ${msg.type()}:`, msg.text());
}
});
// Navigate to the app with retry logic
let navigationSuccess = false;
for (let attempt = 0; attempt < 3 && !navigationSuccess; attempt++) {
try {
await page.goto('/', {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
navigationSuccess = true;
} catch (error) {
if (attempt === 2) throw error;
console.log(`Navigation attempt ${attempt + 1} failed, retrying...`);
await page.waitForTimeout(1000);
}
}
await waitForAppReady(page);
// Dismiss Shepherd tour if it appears
await dismissTourIfVisible(page);
await use(page);
} finally {
// Cleanup - make sure context is still available
if (!page.isClosed()) {
await page.close();
}
}
},
// Provide test prefix for data namespacing
testPrefix: async ({}, use, testInfo) => {
// Use worker index and parallel index for unique prefixes
const prefix = `W${testInfo.workerIndex}-P${testInfo.parallelIndex}`;
await use(prefix);
},
workViewPage: async ({ page, testPrefix }, use) => {
await use(new WorkViewPage(page, testPrefix));
},
projectPage: async ({ page, testPrefix }, use) => {
await use(new ProjectPage(page, testPrefix));
},
taskPage: async ({ page, testPrefix }, use) => {
await use(new TaskPage(page, testPrefix));
},
settingsPage: async ({ page, testPrefix }, use) => {
await use(new SettingsPage(page, testPrefix));
},
dialogPage: async ({ page, testPrefix }, use) => {
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, {
ensureRoute: false,
selector,
});
};
await use(waitForNav);
},
});
export { expect } from '@playwright/test';