import { APIRequestContext, expect } from '@playwright/test'; /** * Where the app under test is served. * * Matches playwright.config.ts's baseURL. It exists as a constant because * `beforeAll` runs with worker-scoped fixtures only and cannot read the * test-scoped `baseURL` option, so a seeding hook has to name the host itself. * One spec used to write it inline, which meant changing the port in the config * moved every test except that one. */ export const BASE_URL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:5173'; /** * Seeding through the admin API. * * Every spec that needs stock used to write its own version of this, and the * copies had drifted: some checked `res.ok()`, some checked `res.status()`, * some checked nothing, and one built its own request context against a * hardcoded `http://localhost:5173` rather than the configured baseURL. * * Seeding through the API rather than the UI on purpose. Creating an item by * driving the admin form makes every storefront test depend on the admin form * working, so a broken form fails a hundred tests that are not about it. */ /** An admin API context for `beforeAll`, where the `adminApi` fixture is out of reach. */ export async function createAdminContext( playwright: { request: { newContext: (o: { baseURL: string }) => Promise } } ): Promise { return playwright.request.newContext({ baseURL: BASE_URL }); } export interface SeededItem { id: number; name: string; } /** Unique enough to survive a parallel run and a database nobody truncated. */ export function uniqueSuffix(): string { return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`; } export function uniqueEmail(prefix = 'pw'): string { return `${prefix}-${uniqueSuffix()}@example.com`; } interface CreateItemOptions { name: string; price?: string; description?: string; categoryId?: number | null; tags?: string[]; /** * New items are created pending, and the storefront lists only published * ones. Most fixtures stand in for ordinary stock rather than staged drafts, * so publishing is the default — a spec that wants a draft asks for one. */ publish?: boolean; } export async function createItem( api: APIRequestContext, options: CreateItemOptions ): Promise { const res = await api.post('/api/admin/items', { // multipart rather than JSON: the route accepts image uploads, so it reads // its fields from a multipart body even when no image is attached. multipart: { name: options.name, description: options.description ?? '', price: options.price ?? '50', category_id: options.categoryId == null ? '' : String(options.categoryId), tags: JSON.stringify(options.tags ?? []) } }); expect(res.ok(), `creating item ${options.name}`).toBeTruthy(); const item = { id: (await res.json()).id as number, name: options.name }; if (options.publish ?? true) { await publishItem(api, item.id); } return item; } export async function publishItem(api: APIRequestContext, id: number): Promise { const res = await api.post(`/api/admin/items/${id}/mark-available`); expect(res.ok(), `publishing item ${id}`).toBeTruthy(); } export async function sellItem(api: APIRequestContext, id: number): Promise { const res = await api.post(`/api/admin/items/${id}/mark-sold`); expect(res.ok(), `selling item ${id}`).toBeTruthy(); } export async function createCategory( api: APIRequestContext, name: string, parentId: number | null = null ): Promise { const res = await api.post('/api/admin/categories', { data: { name, parent_id: parentId } }); expect(res.status(), `creating category ${name}`).toBe(201); return (await res.json()).id as number; } export async function createTag(api: APIRequestContext, name: string): Promise { const res = await api.post('/api/admin/tags', { data: { name } }); expect(res.ok(), `creating tag ${name}`).toBeTruthy(); return (await res.json()).id as number; }