test(e2e): e2e_export_download + e2e_roundtrip_noop (SD-0002 §6.8, PUC-9/10)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 22:12:53 -07:00
parent 0a85c4fef8
commit 8077f2e07b
3 changed files with 88 additions and 1 deletions
+37 -1
View File
@@ -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 <details> menu, then click the status option, capturing the download.
// The menu is a native <details> that *toggles* on each summary click and is
// NOT closed by clicking a download <a> (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 };
}
+21
View File
@@ -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");
});
+30
View File
@@ -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();
});