diff --git a/e2e/tests/import-cancel.spec.ts b/e2e/tests/import-cancel.spec.ts new file mode 100644 index 0000000..2264bb7 --- /dev/null +++ b/e2e/tests/import-cancel.spec.ts @@ -0,0 +1,23 @@ +// DoD scenario e2e_import_cancel_no_trace (SD-0002 §6.8): cancelling at the +// preview gate (PUC-3a) leaves no trace — no products, no run in the history. +import { expect, test } from "@playwright/test"; +import { gotoProducts, signUpWithStorefront, uploadFixture } from "../helpers"; + +test("e2e_import_cancel_no_trace", async ({ page }) => { + await signUpWithStorefront(page); + await gotoProducts(page); + await uploadFixture(page, "good.csv"); + + // The preview gate is up. + await expect(page.getByRole("heading", { name: "Import preview — good.csv" })).toBeVisible(); + await expect(page.getByRole("button", { name: "2 to add" })).toBeVisible(); + + await page.getByRole("button", { name: "Cancel" }).click(); + + // Lands back on Products — still the empty state, no run recorded. + await expect(page.getByRole("heading", { level: 1, name: "Products" })).toBeVisible(); + await expect( + page.getByText("No products yet. Bulk import is how product data gets in."), + ).toBeVisible(); + await expect(page.getByText("No imports yet.")).toBeVisible(); +}); diff --git a/e2e/tests/import-errors.spec.ts b/e2e/tests/import-errors.spec.ts new file mode 100644 index 0000000..9c6a2f1 --- /dev/null +++ b/e2e/tests/import-errors.spec.ts @@ -0,0 +1,41 @@ +// DoD scenario e2e_import_errors_actionable (SD-0002 §6.8): row-level errors are +// actionable — line, column, message — at the preview gate AND on the run report +// card (PUC-5), while the good rows still import. +import { expect, test } from "@playwright/test"; +import { gotoProducts, signUpWithStorefront, uploadFixture } from "../helpers"; + +test("e2e_import_errors_actionable", async ({ page }) => { + await signUpWithStorefront(page); + await gotoProducts(page); + await uploadFixture(page, "mixed-errors.csv"); + + // Preview tiles: two good products, one errored row. + await expect( + page.getByRole("heading", { name: "Import preview — mixed-errors.csv" }), + ).toBeVisible(); + await expect(page.getByRole("button", { name: "2 to add" })).toBeVisible(); + await page.getByRole("button", { name: "1 errors" }).click(); + + // The error table names the line, the column, and the problem (actionable, PUC-5). + const previewRow = page.locator(".errortable tbody tr"); + await expect(previewRow).toHaveCount(1); + await expect(previewRow.locator("td").nth(0)).toHaveText("3"); + await expect(previewRow.locator("td").nth(1)).toHaveText("Variant Price"); + await expect(previewRow.locator("td").nth(2)).toContainText("is not a price"); + + // Good rows still import. + await page.getByRole("button", { name: "Import 2 products" }).click(); + + // Run detail: counts include the errored row, and the same error row persists. + await expect(page.getByRole("heading", { level: 1, name: "mixed-errors.csv" })).toBeVisible(); + await expect(page.getByText("2 added · 0 updated · 1 rows in error")).toBeVisible(); + const runRow = page.locator(".errortable tbody tr"); + await expect(runRow).toHaveCount(1); + await expect(runRow.locator("td").nth(0)).toHaveText("3"); + await expect(runRow.locator("td").nth(1)).toHaveText("Variant Price"); + await expect(runRow.locator("td").nth(2)).toContainText("is not a price"); + + // The catalog gained the two good products. + await gotoProducts(page); + await expect(page.getByRole("heading", { level: 1, name: "Products · 2" })).toBeVisible(); +}); diff --git a/e2e/tests/import-file-rejected.spec.ts b/e2e/tests/import-file-rejected.spec.ts new file mode 100644 index 0000000..3872296 --- /dev/null +++ b/e2e/tests/import-file-rejected.spec.ts @@ -0,0 +1,25 @@ +// DoD scenario e2e_import_file_rejected (SD-0002 §6.8): a file-level rejection +// (PUC-5a) renders in place on the upload screen, the picker stays live for a +// retry, and nothing is recorded — no products, no run. +import { expect, test } from "@playwright/test"; +import { gotoProducts, signUpWithStorefront, uploadFixture } from "../helpers"; + +test("e2e_import_file_rejected", async ({ page }) => { + await signUpWithStorefront(page); + await gotoProducts(page); + await uploadFixture(page, "missing-title.csv"); + + // The rejection renders in place, naming the missing column. + await expect(page.getByText("That file can't be imported")).toBeVisible(); + await expect(page.getByText("missing the required column 'Title'")).toBeVisible(); + + // The picker is live again for a retry. + await expect(page.locator('input[type="file"]')).toBeEnabled(); + + // No trace: still the empty catalog, and no run recorded. + await gotoProducts(page); + await expect( + page.getByText("No products yet. Bulk import is how product data gets in."), + ).toBeVisible(); + await expect(page.getByText("No imports yet.")).toBeVisible(); +}); diff --git a/e2e/tests/import-preview-confirm.spec.ts b/e2e/tests/import-preview-confirm.spec.ts new file mode 100644 index 0000000..7b8091b --- /dev/null +++ b/e2e/tests/import-preview-confirm.spec.ts @@ -0,0 +1,49 @@ +// DoD scenario e2e_import_preview_confirm (SD-0002 §6.8): the happy path end to +// end — sign up, empty catalog, upload good.csv, preview gate (PUC-2/3), confirm, +// run report card, and the catalog + history reflecting the import. Subsumes the +// Task-15 harness smoke (sign-up journey + products empty state). +import { expect, test } from "@playwright/test"; +import { gotoProducts, signUpWithStorefront, STOREFRONT_NAME, uploadFixture } from "../helpers"; + +test("e2e_import_preview_confirm", async ({ page }) => { + await signUpWithStorefront(page); + + // Admin topbar: storefront identity + the signed-in account chip (ex-smoke). + await expect(page.locator(".storeid__name")).toHaveText(STOREFRONT_NAME); + await expect(page.getByRole("button", { name: "Sign out" })).toBeVisible(); + + await gotoProducts(page); + + // Empty state + the import affordances (SD-0002 §5.2, ex-smoke). + await expect( + page.getByText("No products yet. Bulk import is how product data gets in."), + ).toBeVisible(); + await expect(page.getByRole("link", { name: "Download sample CSV" })).toBeVisible(); + + await uploadFixture(page, "good.csv"); + + // Preview (§5.4): summary tiles + the file's name in the heading. + await expect(page.getByRole("heading", { name: "Import preview — good.csv" })).toBeVisible(); + await expect(page.getByRole("button", { name: "2 to add" })).toBeVisible(); + await expect(page.getByRole("button", { name: "0 errors" })).toBeVisible(); + + // Drill in: open star-tee's diff row and see field-level detail (the S-variant SKU). + const starTee = page.locator(".difflist__item", { hasText: "star-tee" }); + await starTee.locator("summary").click(); + await expect(starTee.getByText("WG-TEE-S")).toBeVisible(); + + // Consent gate (PUC-3): confirm the import. + await page.getByRole("button", { name: "Import 2 products" }).click(); + + // Run detail (§5.5): report card with the file name, counts, and status. + await expect(page.getByRole("heading", { level: 1, name: "good.csv" })).toBeVisible(); + await expect(page.getByText("2 added · 0 updated · 0 rows in error")).toBeVisible(); + await expect(page.getByText("Complete", { exact: true })).toBeVisible(); + + // Back on Products: the catalog count and exactly one history row for this run. + await gotoProducts(page); + await expect(page.getByRole("heading", { level: 1, name: "Products · 2" })).toBeVisible(); + const rows = page.locator(".datatable tbody tr"); + await expect(rows).toHaveCount(1); + await expect(rows.first().getByRole("link", { name: "good.csv" })).toBeVisible(); +}); diff --git a/e2e/tests/smoke.spec.ts b/e2e/tests/smoke.spec.ts deleted file mode 100644 index 4915427..0000000 --- a/e2e/tests/smoke.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Harness smoke (Task 15): the full sign-up journey lands in the admin, and the -// products section's empty state renders with its import affordances. The four -// SD-0002 scenario specs build on these helpers. -import { expect, test } from "@playwright/test"; -import { gotoProducts, signUpWithStorefront, STOREFRONT_NAME } from "../helpers"; - -test("smoke: sign up, storefront, products nav", async ({ page }) => { - await signUpWithStorefront(page); - - // Admin topbar: storefront identity + the signed-in account chip. - await expect(page.locator(".storeid__name")).toHaveText(STOREFRONT_NAME); - await expect(page.getByRole("button", { name: "Sign out" })).toBeVisible(); - - await gotoProducts(page); - - // Empty state + the import affordances (SD-0002 §5.2). - await expect(page.getByText("No products yet. Bulk import is how product data gets in.")).toBeVisible(); - await expect(page.getByRole("link", { name: "Import products" }).first()).toBeVisible(); - await expect(page.getByRole("link", { name: "Download sample CSV" })).toBeVisible(); -});