E2E Testing
Playwright coverage, DB-backed fixture factories and test utilities
Status (reconciled 2026-07-02)
The 3 existing Playwright specs (auth, browse-products, create-deal) now run in CI — currently failing on under-scoped selectors (plan phase 0). The fixture-factory and coverage expansion below is still open.
The cal.diy pattern
50+ Playwright e2e files under apps/web/playwright/ covering auth (login, 2FA, OAuth, password reset, delete account), booking flows, payments, webhooks, i18n routing, settings, onboarding…
Fixture system (playwright/lib/fixtures.ts):
// base.extend with typed fixtures backed by real Prisma factories
export const test = base.extend<Fixtures>({
users: async ({ browser }, use) => {
const usersFixture = createUsersFixture(browser, page);
await use(usersFixture);
await usersFixture.deleteAll(); // auto-cleanup in afterEach
},
bookings: async ({ page }, use) => {
const bookingsFixture = createBookingsFixture(page);
await use(bookingsFixture);
await bookingsFixture.deleteAll();
},
});Each fixture creates real data via Prisma, runs the test, and deletes everything on teardown — no test-data pollution.
playwright/lib/testUtils.ts avoids brittle waitForTimeout() calls:
async function submitAndWaitForResponse(page, locator, responseUrl) {
const [response] = await Promise.all([
page.waitForResponse(responseUrl),
locator.click(),
]);
return response;
}What to implement
DB-backed fixture factories
apps/web/e2e/fixtures/users.ts:
import { PrismaClient } from "@marketplace/database";
const prisma = new PrismaClient();
export function createUsersFixture() {
const users: User[] = [];
return {
async create(data: Partial<User> = {}) {
const user = await prisma.user.create({
data: {
email: `test-${Date.now()}@example.com`,
password: await bcrypt.hash("password123", 10),
...data,
},
});
users.push(user);
return user;
},
async deleteAll() {
await prisma.user.deleteMany({ where: { id: { in: users.map((u) => u.id) } } });
},
};
}Create apps/web/e2e/fixtures/deals.ts similarly; wire both into apps/web/e2e/fixtures/index.ts using base.extend<Fixtures>().
Test utilities
apps/web/e2e/lib/testUtils.ts with submitAndWaitForResponse(page, trigger, urlPattern) — replaces every waitForTimeout with a deterministic response wait.
Expand spec files
| Spec file | Covers |
|---|---|
e2e/auth.spec.ts | register, login, logout, forgot-password |
e2e/deal-flow.spec.ts | create deal → admin review → approve/decline |
e2e/product-search.spec.ts | search, filter by category, product detail |
e2e/admin-products.spec.ts | CRUD, import, scraper review |
Selector discipline
Selectors must be scoped enough to survive unrelated UI additions — a global getByPlaceholder(/search/i) broke when header search shipped. See the frontend conventions.