// Shared E2E helpers — the sign-up journey (SD-0001 §5.1–§5.4) and products-page // navigation (SD-0002 §5.2). Selectors are role/label-based against the real screens // (Landing.tsx, SignIn.tsx, CreateStorefront.tsx, Admin.tsx, ProductsPage.tsx). import { expect, type Page } from "@playwright/test"; import { readFile, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; const LOG = join(__dirname, ".backend.log"); let seq = 0; // The storefront name every helper-created merchant uses; tests assert against it. export const STOREFRONT_NAME = "E2E Test Goods"; export function freshEmail(): string { return `merchant${Date.now()}-${seq++}@example.com`; } // LogMailer's exact line (backend/app/platform/mailer.py): // INFO:ecomm.mailer: LogMailer -> | Your ecomm code: <6 digits>\n // The first 6-digit group after the marker is the code. async function codeFor(email: string): Promise { for (let i = 0; i < 50; i++) { const log = await readFile(LOG, "utf8").catch(() => ""); const at = log.lastIndexOf(`LogMailer -> ${email}`); if (at >= 0) { const m = log.slice(at).match(/\b(\d{6})\b/); if (m) return m[1]; } await new Promise((r) => setTimeout(r, 200)); } throw new Error(`no code logged for ${email}`); } export async function signUpWithStorefront(page: Page, email = freshEmail()): Promise { // Landing → the sign-up door. await page.goto("/"); await page.getByRole("button", { name: "Create your storefront →" }).click(); // Sign-in step 1: request the one-time code. await page.getByLabel("Email").fill(email); await page.getByRole("button", { name: "Send code" }).click(); // Sign-in step 2: read the code from the backend log and verify it. await expect(page.getByRole("heading", { name: "Check your email" })).toBeVisible(); const code = await codeFor(email); await page.getByLabel("One-time code").fill(code); await page.getByRole("button", { name: "Continue" }).click(); // Create-storefront screen (a new account has none yet). await expect(page.getByRole("heading", { name: "Create your storefront" })).toBeVisible(); await page.getByLabel("Storefront name").fill(STOREFRONT_NAME); await page.getByRole("button", { name: "Create storefront", exact: true }).click(); // Admin shell: nav strip + the storefront identity in the topbar. await expect(page.getByRole("navigation", { name: "Admin sections" })).toBeVisible(); await expect(page.locator(".storeid__name")).toHaveText(STOREFRONT_NAME); return email; } export async function gotoProducts(page: Page) { await page .getByRole("navigation", { name: "Admin sections" }) .getByRole("link", { name: "Products" }) .click(); await expect(page.getByRole("heading", { level: 1, name: "Products" })).toBeVisible(); } export async function uploadFixture(page: Page, fixture: string) { // Two "Import products" links render on the empty products page (header + empty // state) — same destination, so take the first. await page.getByRole("link", { name: "Import products" }).first().click(); await expect(page.getByRole("heading", { name: "Import products" })).toBeVisible(); await page.locator('input[type="file"]').setInputFiles(join(__dirname, "fixtures", fixture)); } // Import good.csv and confirm it, leaving a 2-product catalog. Returns nothing; // callers continue from the run-detail screen. export async function importGoodCsv(page: Page) { await uploadFixture(page, "good.csv"); await expect(page.getByRole("heading", { name: "Import preview — good.csv" })).toBeVisible(); await page.getByRole("button", { name: "Import 2 products" }).click(); await expect(page.getByRole("heading", { level: 1, name: "good.csv" })).toBeVisible(); } // Import with-images.csv (3 products, each with one Image Src) and confirm it. // Mirrors importGoodCsv; callers continue from the run-detail screen, where the // background image fetch progresses fetching_images → terminal. export async function importWithImages(page: Page) { await uploadFixture(page, "with-images.csv"); await expect(page.getByRole("heading", { name: "Import preview — with-images.csv" })).toBeVisible(); await page.getByRole("button", { name: "Import 3 products" }).click(); await expect(page.getByRole("heading", { level: 1, name: "with-images.csv" })).toBeVisible(); } // Click an Export status option and capture the downloaded CSV's text + path. export async function exportCatalog(page: Page, label: string): Promise<{ text: string; path: string }> { // Open the
menu, then click the status option, capturing the download. // The menu is a native
that *toggles* on each summary click and is // NOT closed by clicking a download (a download doesn't navigate). So on a // second export the menu may already be open — deterministically open it // rather than blind-toggling, and wait for the status link to be visible. const details = page.locator("details.products__export"); if (!(await details.evaluate((el: HTMLDetailsElement) => el.open))) { await details.locator("> summary").click(); } const link = page.getByRole("link", { name: label, exact: true }); await expect(link).toBeVisible(); const [download] = await Promise.all([ page.waitForEvent("download"), link.click(), ]); const stream = await download.createReadStream(); const chunks: Buffer[] = []; for await (const c of stream) chunks.push(c as Buffer); const text = Buffer.concat(chunks).toString("utf8"); const path = join(tmpdir(), `export-${Date.now()}.csv`); await writeFile(path, text, "utf8"); return { text, path }; }