diff --git a/e2e/.gitignore b/e2e/.gitignore index cbc6ce8..1fcd284 100644 --- a/e2e/.gitignore +++ b/e2e/.gitignore @@ -1,4 +1,5 @@ node_modules/ .backend.log +.objectstore/ test-results/ playwright-report/ diff --git a/e2e/fixtures/images/good.png b/e2e/fixtures/images/good.png new file mode 100644 index 0000000..0a50ae4 Binary files /dev/null and b/e2e/fixtures/images/good.png differ diff --git a/e2e/fixtures/images/tiny.png b/e2e/fixtures/images/tiny.png new file mode 100644 index 0000000..293a859 Binary files /dev/null and b/e2e/fixtures/images/tiny.png differ diff --git a/e2e/fixtures/with-images.csv b/e2e/fixtures/with-images.csv new file mode 100644 index 0000000..4677cac --- /dev/null +++ b/e2e/fixtures/with-images.csv @@ -0,0 +1,4 @@ +Handle,Title,Image Src +good-lamp,Good Lamp,http://127.0.0.1:8799/good.png +tiny-lamp,Tiny Lamp,http://127.0.0.1:8799/tiny.png +gone-lamp,Gone Lamp,http://127.0.0.1:8799/missing.png diff --git a/e2e/helpers.ts b/e2e/helpers.ts index c911b97..fec4f28 100644 --- a/e2e/helpers.ts +++ b/e2e/helpers.ts @@ -83,6 +83,16 @@ export async function importGoodCsv(page: Page) { 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. diff --git a/e2e/image-host.mjs b/e2e/image-host.mjs new file mode 100644 index 0000000..c8ef1ac --- /dev/null +++ b/e2e/image-host.mjs @@ -0,0 +1,19 @@ +import { createServer } from "node:http"; +import { readFile } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const dir = join(dirname(fileURLToPath(import.meta.url)), "fixtures", "images"); +createServer(async (req, res) => { + const name = (req.url || "").replace(/^\//, "").split("?")[0]; + if (name === "good.png" || name === "tiny.png") { + try { + const buf = await readFile(join(dir, name)); + res.writeHead(200, { "Content-Type": "image/png" }); + res.end(buf); + return; + } catch { /* fallthrough */ } + } + res.writeHead(404); + res.end(); +}).listen(8799, "127.0.0.1"); diff --git a/e2e/serve.sh b/e2e/serve.sh index a6db7eb..c2c1cee 100755 --- a/e2e/serve.sh +++ b/e2e/serve.sh @@ -18,6 +18,17 @@ PY export ECOMM_DATABASE_URL="postgresql://ecomm:ecomm@localhost:5432/ecomm_e2e" export ECOMM_MAILER=log + +# Image pipeline (SLICE-7): local objectstore + a fixture image host on :8799. +# ECOMM_IMAGE_FETCH_ALLOW_PRIVATE lets the SSRF guard reach 127.0.0.1 — TEST-ONLY, +# never set in the deployed overlay. The &-backgrounded node host is a child of +# this serve process; Playwright tears the whole webServer group down between runs. +export ECOMM_OBJECTSTORE_KIND=local +export ECOMM_OBJECTSTORE_DIR="$repo_root/e2e/.objectstore" +export ECOMM_IMAGE_FETCH_ALLOW_PRIVATE=1 +rm -rf "$repo_root/e2e/.objectstore" +node "$repo_root/e2e/image-host.mjs" & + cd "$repo_root/backend" exec "$repo_root/.venv/bin/python" -m uvicorn app.main:app --port 8765 \ > "$repo_root/e2e/.backend.log" 2>&1 diff --git a/e2e/tests/image-outcomes.spec.ts b/e2e/tests/image-outcomes.spec.ts new file mode 100644 index 0000000..ce8736b --- /dev/null +++ b/e2e/tests/image-outcomes.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from "@playwright/test"; +import { gotoProducts, importWithImages, signUpWithStorefront } from "../helpers"; + +test("e2e_image_outcomes: per-image outcomes, problem rows, notice band", async ({ page }) => { + await signUpWithStorefront(page); + await gotoProducts(page); + await importWithImages(page); + // Run detail polls fetching_images → terminal; wait for the outcome summary. + await expect(page.getByText("1 fetched · 1 rejected · 1 failed")).toBeVisible({ timeout: 30_000 }); + // Problem images listed in the outcomes table. + await expect(page.getByText("tiny-lamp")).toBeVisible(); + await expect(page.getByText("gone-lamp")).toBeVisible(); + // Products page surfaces the aggregate notice. + await gotoProducts(page); + await expect(page.getByText(/products have image problems/)).toBeVisible(); +});