SLICE-5: products import spine — canonical CSV → preview → confirm (SD-0002 §7.2) #26
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
.backend.log
|
||||
test-results/
|
||||
playwright-report/
|
||||
@@ -0,0 +1,4 @@
|
||||
Handle,Title,Vendor,Tags,Status,Published,Option1 Name,Option1 Value,Variant SKU,Variant Price,Variant Inventory Qty
|
||||
moon-mug,Moon Mug,Wiggle Goods,"kitchen, mugs",active,TRUE,,,WG-MUG-001,18.00,40
|
||||
star-tee,Star Tee,Wiggle Goods,apparel,active,TRUE,Size,S,WG-TEE-S,24.00,12
|
||||
star-tee,,,,,,,M,WG-TEE-M,24.00,18
|
||||
|
@@ -0,0 +1,2 @@
|
||||
Handle,Vendor
|
||||
mug,Acme
|
||||
|
@@ -0,0 +1,4 @@
|
||||
Handle,Title,Variant Price
|
||||
good-mug,Good Mug,10.00
|
||||
bad-tee,Bad Tee,not-a-price
|
||||
also-good,Also Good,5.00
|
||||
|
@@ -0,0 +1,74 @@
|
||||
// Shared E2E helpers — the sign-up journey (SD-0001 §5.1–§5.4) and products-page
|
||||
// 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 { join } from "node:path";
|
||||
|
||||
const LOG = join(__dirname, ".backend.log");
|
||||
let seq = 0;
|
||||
|
||||
// The storefront name every helper-created merchant uses; tests assert against it.
|
||||
export const STOREFRONT_NAME = "E2E Test Goods";
|
||||
|
||||
export function freshEmail(): string {
|
||||
return `merchant${Date.now()}-${seq++}@example.com`;
|
||||
}
|
||||
|
||||
// LogMailer's exact line (backend/app/platform/mailer.py):
|
||||
// INFO:ecomm.mailer: LogMailer -> <to> | Your ecomm code: <6 digits>\n<body>
|
||||
// The first 6-digit group after the marker is the code.
|
||||
async function codeFor(email: string): Promise<string> {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const log = await readFile(LOG, "utf8").catch(() => "");
|
||||
const at = log.lastIndexOf(`LogMailer -> ${email}`);
|
||||
if (at >= 0) {
|
||||
const m = log.slice(at).match(/\b(\d{6})\b/);
|
||||
if (m) return m[1];
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
throw new Error(`no code logged for ${email}`);
|
||||
}
|
||||
|
||||
export async function signUpWithStorefront(page: Page, email = freshEmail()): Promise<string> {
|
||||
// Landing → the sign-up door.
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "Create your storefront →" }).click();
|
||||
|
||||
// Sign-in step 1: request the one-time code.
|
||||
await page.getByLabel("Email").fill(email);
|
||||
await page.getByRole("button", { name: "Send code" }).click();
|
||||
|
||||
// Sign-in step 2: read the code from the backend log and verify it.
|
||||
await expect(page.getByRole("heading", { name: "Check your email" })).toBeVisible();
|
||||
const code = await codeFor(email);
|
||||
await page.getByLabel("One-time code").fill(code);
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
|
||||
// Create-storefront screen (a new account has none yet).
|
||||
await expect(page.getByRole("heading", { name: "Create your storefront" })).toBeVisible();
|
||||
await page.getByLabel("Storefront name").fill(STOREFRONT_NAME);
|
||||
await page.getByRole("button", { name: "Create storefront", exact: true }).click();
|
||||
|
||||
// Admin shell: nav strip + the storefront identity in the topbar.
|
||||
await expect(page.getByRole("navigation", { name: "Admin sections" })).toBeVisible();
|
||||
await expect(page.locator(".storeid__name")).toHaveText(STOREFRONT_NAME);
|
||||
return email;
|
||||
}
|
||||
|
||||
export async function gotoProducts(page: Page) {
|
||||
await page
|
||||
.getByRole("navigation", { name: "Admin sections" })
|
||||
.getByRole("link", { name: "Products" })
|
||||
.click();
|
||||
await expect(page.getByRole("heading", { level: 1, name: "Products" })).toBeVisible();
|
||||
}
|
||||
|
||||
export async function uploadFixture(page: Page, fixture: string) {
|
||||
// Two "Import products" links render on the empty products page (header + empty
|
||||
// state) — same destination, so take the first.
|
||||
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(join(__dirname, "fixtures", fixture));
|
||||
}
|
||||
Generated
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "wiggleverse-ecomm-e2e",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wiggleverse-ecomm-e2e",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
|
||||
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
|
||||
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.60.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.60.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
|
||||
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "wiggleverse-ecomm-e2e",
|
||||
"private": true,
|
||||
"scripts": { "test": "playwright test" },
|
||||
"devDependencies": { "@playwright/test": "^1.48.0" }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests",
|
||||
timeout: 60_000,
|
||||
retries: 0,
|
||||
// One shared backend + log file; parallel sign-ins would interleave codes.
|
||||
workers: 1,
|
||||
use: { baseURL: "http://localhost:8765" },
|
||||
webServer: {
|
||||
command: "bash ./serve.sh",
|
||||
url: "http://localhost:8765/healthz",
|
||||
reuseExistingServer: false,
|
||||
timeout: 120_000,
|
||||
},
|
||||
});
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# E2E server: fresh ecomm_e2e database, LogMailer (codes land in .backend.log),
|
||||
# backend on :8765 serving the built SPA (the deployed topology, SD-0001 §6.2).
|
||||
set -euo pipefail
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
if [ ! -f "$repo_root/frontend/dist/index.html" ]; then
|
||||
( cd "$repo_root/frontend" && npm run build )
|
||||
fi
|
||||
|
||||
"$repo_root/.venv/bin/python" - <<'PY'
|
||||
import psycopg
|
||||
admin = psycopg.connect("postgresql://ecomm:ecomm@localhost:5432/postgres", autocommit=True)
|
||||
admin.execute("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='ecomm_e2e' AND pid <> pg_backend_pid()")
|
||||
admin.execute("DROP DATABASE IF EXISTS ecomm_e2e")
|
||||
admin.execute("CREATE DATABASE ecomm_e2e")
|
||||
PY
|
||||
|
||||
export ECOMM_DATABASE_URL="postgresql://ecomm:ecomm@localhost:5432/ecomm_e2e"
|
||||
export ECOMM_MAILER=log
|
||||
cd "$repo_root/backend"
|
||||
exec "$repo_root/.venv/bin/python" -m uvicorn app.main:app --port 8765 \
|
||||
> "$repo_root/e2e/.backend.log" 2>&1
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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();
|
||||
});
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# E2E browser gate (SD-0002 §6.8) — Playwright against a fresh local stack.
|
||||
# Not yet part of check.sh/CI: the CI runner has no browsers (§10.6 gap).
|
||||
set -euo pipefail
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$repo_root/e2e"
|
||||
if [ ! -d node_modules ]; then
|
||||
npm install
|
||||
npx playwright install chromium
|
||||
fi
|
||||
npx playwright test "$@"
|
||||
Reference in New Issue
Block a user