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:
+37
-1
@@ -2,7 +2,8 @@
|
|||||||
// navigation (SD-0002 §5.2). Selectors are role/label-based against the real screens
|
// navigation (SD-0002 §5.2). Selectors are role/label-based against the real screens
|
||||||
// (Landing.tsx, SignIn.tsx, CreateStorefront.tsx, Admin.tsx, ProductsPage.tsx).
|
// (Landing.tsx, SignIn.tsx, CreateStorefront.tsx, Admin.tsx, ProductsPage.tsx).
|
||||||
import { expect, type Page } from "@playwright/test";
|
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";
|
import { join } from "node:path";
|
||||||
|
|
||||||
const LOG = join(__dirname, ".backend.log");
|
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 expect(page.getByRole("heading", { name: "Import products" })).toBeVisible();
|
||||||
await page.locator('input[type="file"]').setInputFiles(join(__dirname, "fixtures", fixture));
|
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 };
|
||||||
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user