diff --git a/e2e/helpers.ts b/e2e/helpers.ts index 2a1b0a3..c911b97 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -2,7 +2,8 @@ // 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 } from "node:fs/promises"; +import { readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; const LOG = join(__dirname, ".backend.log"); @@ -72,3 +73,38 @@ export async function uploadFixture(page: Page, fixture: string) { 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(); +} + +// 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 }; +} diff --git a/e2e/tests/export-download.spec.ts b/e2e/tests/export-download.spec.ts new file mode 100644 index 0000000..78ba518 --- /dev/null +++ b/e2e/tests/export-download.spec.ts @@ -0,0 +1,21 @@ +// DoD scenario e2e_export_download (SD-0002 §6.8): export downloads a canonical +// CSV and the status filter is respected. +import { expect, test } from "@playwright/test"; +import { exportCatalog, gotoProducts, importGoodCsv, signUpWithStorefront } from "../helpers"; + +test("e2e_export_download", async ({ page }) => { + await signUpWithStorefront(page); + await gotoProducts(page); + await importGoodCsv(page); + await gotoProducts(page); + + // Export "All products" → a canonical CSV with both handles. + const all = await exportCatalog(page, "All products"); + expect(all.text.split("\n")[0]).toContain("Handle,"); + expect(all.text).toContain("moon-mug"); + expect(all.text).toContain("star-tee"); + + // good.csv's products are active → "Active" exports both, "Draft" is empty/disabled-path. + const active = await exportCatalog(page, "Active"); + expect(active.text).toContain("moon-mug"); +}); diff --git a/e2e/tests/roundtrip-noop.spec.ts b/e2e/tests/roundtrip-noop.spec.ts new file mode 100644 index 0000000..bef0c82 --- /dev/null +++ b/e2e/tests/roundtrip-noop.spec.ts @@ -0,0 +1,30 @@ +// DoD scenario e2e_roundtrip_noop (SD-0002 §6.8, PUC-10): exporting then +// re-importing the unmodified file previews as all-unchanged with the import +// action disabled and the "Nothing to change" note. +import { expect, test } from "@playwright/test"; +import { exportCatalog, gotoProducts, importGoodCsv, signUpWithStorefront } from "../helpers"; + +test("e2e_roundtrip_noop", async ({ page }) => { + await signUpWithStorefront(page); + await gotoProducts(page); + await importGoodCsv(page); + await gotoProducts(page); + + // Export the catalog, then re-import the unmodified file. + const { path } = await exportCatalog(page, "All products"); + 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(path); + + // Preview: everything unchanged, nothing to add/update (PUC-10). + await expect(page.getByRole("button", { name: "2 unchanged" })).toBeVisible(); + await expect(page.getByRole("button", { name: "0 to add" })).toBeVisible(); + await expect(page.getByRole("button", { name: "0 to update" })).toBeVisible(); + + // The import action is disabled, with the no-op note. + const importBtn = page.getByRole("button", { name: /^Import 0 products$/ }); + await expect(importBtn).toBeDisabled(); + await expect( + page.getByText("Nothing to change — your catalog already matches this file"), + ).toBeVisible(); +});