KwadMarket Docs
Best Practices

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 fileCovers
e2e/auth.spec.tsregister, login, logout, forgot-password
e2e/deal-flow.spec.tscreate deal → admin review → approve/decline
e2e/product-search.spec.tssearch, filter by category, product detail
e2e/admin-products.spec.tsCRUD, 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.

On this page