Skip to content

Commit c75a72c

Browse files
baotoqclaude
andcommitted
test(web): restructure e2e suite with shared fixtures and tag taxonomy
Introduce e2e/fixtures/{api,product,test,seller}.ts: API URL resolution, product factory with auto-cleanup, combined test export, shadcn-select helper. Move all 28 specs into route-based folders mirroring /seller URL tree; drop redundant seller- prefix. Apply @smoke / @regression / @seed-dependent tag taxonomy plus per-feature tags. Factory-ize edit/preview/create-duplicate tests so they pass against any catalog state. Isolate seed-dependent assertions (specific SKUs, hardcoded counts, page-1 ordering) under @seed-dependent so the smoke gate stays deterministic. Fix create form Status field to use shadcn combobox interaction (commit c5aaef4 changed the rendering from native select to button[role=combobox]). Harden playwright config: CI workers default to 2, expect.timeout=5000, video retain-on-failure, optional blob reporter via PLAYWRIGHT_BLOB_REPORT. Drop test.setTimeout(90_000) and serial mode from pagination spec. Add e2e:smoke and e2e:seed-dependent npm scripts. Ignore Playwright artifacts. Smoke: 46/46 passing against live Aspire stack. Full suite: 105/112, with 7 known @seed-dependent failures isolated for a separate seeded-DB run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c5aaef4 commit c75a72c

56 files changed

Lines changed: 1824 additions & 1583 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/web/.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,12 @@ yarn-error.log*
3939
# typescript
4040
*.tsbuildinfo
4141
next-env.d.ts
42+
43+
# Playwright
44+
/test-results/
45+
/playwright-report/
46+
/blob-report/
47+
/playwright/.cache/
48+
49+
# OMC subagent state
50+
.omc/

src/web/e2e/fixtures/api.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Resolves the Catalog API base URL from Aspire's injected env var first,
2+
// then a developer override, then a sensible local default. Centralising this
3+
// here keeps the 28 spec files free of duplicated env-resolution logic.
4+
export const API_URL =
5+
process.env.services__catalog_api__http__0 ??
6+
process.env.API_URL ??
7+
"http://localhost:5000";
8+
9+
export const productEndpoint = (sku: string) =>
10+
`${API_URL}/api/products/${encodeURIComponent(sku)}`;

src/web/e2e/fixtures/product.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { APIRequestContext, TestInfo } from "@playwright/test";
2+
import { API_URL, productEndpoint } from "./api";
3+
4+
export type ProductPayload = {
5+
sku: string;
6+
name: string;
7+
category: string;
8+
price: number;
9+
inventory: number;
10+
status: "active" | "draft";
11+
};
12+
13+
const defaults: Omit<ProductPayload, "sku"> = {
14+
name: "Test Product",
15+
category: "Ceramics",
16+
price: 49.99,
17+
inventory: 5,
18+
status: "active",
19+
};
20+
21+
// `testInfo.parallelIndex` + `testInfo.testId` make the SKU unique across both
22+
// workers and tests, so concurrent mutations against a shared Catalog DB never
23+
// collide. `Date.now()` alone breaks at sub-millisecond test starts.
24+
export const uniqueSku = (testInfo: TestInfo, prefix = "TEST") =>
25+
`${prefix}-W${testInfo.parallelIndex}-${testInfo.testId.slice(0, 6).toUpperCase()}`;
26+
27+
export const createProduct = async (
28+
request: APIRequestContext,
29+
payload: Partial<ProductPayload> & { sku: string },
30+
): Promise<ProductPayload> => {
31+
const body: ProductPayload = { ...defaults, ...payload };
32+
const res = await request.post(`${API_URL}/api/products`, { data: body });
33+
if (!res.ok()) {
34+
throw new Error(
35+
`createProduct failed: ${res.status()} ${await res.text()}`,
36+
);
37+
}
38+
return body;
39+
};
40+
41+
export const deleteProduct = async (
42+
request: APIRequestContext,
43+
sku: string,
44+
): Promise<void> => {
45+
// Cleanup is best-effort: a teardown failure shouldn't fail the test, but it
46+
// shouldn't be silently swallowed either — Playwright surfaces it in trace.
47+
const res = await request.delete(productEndpoint(sku));
48+
if (!res.ok() && res.status() !== 404) {
49+
throw new Error(
50+
`deleteProduct failed for ${sku}: ${res.status()} ${await res.text()}`,
51+
);
52+
}
53+
};
54+
55+
export const getProduct = async (request: APIRequestContext, sku: string) =>
56+
request.get(productEndpoint(sku));

src/web/e2e/fixtures/seller.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Page } from "@playwright/test";
2+
3+
export const sellerRoutes = {
4+
dashboard: "/seller",
5+
listings: "/seller/listings",
6+
listingsNew: "/seller/listings/new",
7+
listingsBulk: "/seller/listings/bulk",
8+
listingsPublished: "/seller/listings/published",
9+
listingEdit: (sku: string) =>
10+
`/seller/listings/${encodeURIComponent(sku)}/edit`,
11+
listingPreview: (sku: string) =>
12+
`/seller/listings/${encodeURIComponent(sku)}/preview`,
13+
promos: "/seller/promos",
14+
orders: "/seller/orders",
15+
payouts: "/seller/payouts",
16+
analytics: "/seller/analytics",
17+
marketing: "/seller/marketing",
18+
welcome: "/seller/welcome",
19+
apply: "/seller/apply",
20+
} as const;
21+
22+
// shadcn Select renders as <button role="combobox"> + Radix listbox, so
23+
// Playwright's `selectOption` (which targets native <select>) fails. Open the
24+
// trigger then click the option by visible name.
25+
export const selectShadcnOption = async (
26+
page: Page,
27+
labelText: string,
28+
optionName: string,
29+
) => {
30+
await page.getByLabel(labelText).click();
31+
await page.getByRole("option", { name: optionName }).click();
32+
};

src/web/e2e/fixtures/test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { test as base } from "@playwright/test";
2+
import {
3+
createProduct,
4+
deleteProduct,
5+
type ProductPayload,
6+
uniqueSku,
7+
} from "./product";
8+
9+
type ProductFactory = {
10+
create: (
11+
overrides?: Partial<ProductPayload> & { skuPrefix?: string },
12+
) => Promise<ProductPayload>;
13+
};
14+
15+
type Fixtures = {
16+
productFactory: ProductFactory;
17+
};
18+
19+
// Test-scoped factory: every created SKU is tracked and cleaned up after the
20+
// test, even on failure. Removes the inline POST/DELETE boilerplate that was
21+
// duplicated across the mutation specs.
22+
export const test = base.extend<Fixtures>({
23+
productFactory: async ({ request }, use, testInfo) => {
24+
const created: string[] = [];
25+
26+
const factory: ProductFactory = {
27+
create: async (overrides = {}) => {
28+
const { skuPrefix, ...rest } = overrides;
29+
const sku = rest.sku ?? uniqueSku(testInfo, skuPrefix);
30+
const product = await createProduct(request, { ...rest, sku });
31+
created.push(sku);
32+
return product;
33+
},
34+
};
35+
36+
await use(factory);
37+
38+
await Promise.all(
39+
created.map((sku) =>
40+
deleteProduct(request, sku).catch((err) => {
41+
// Cleanup errors are visible in the trace via this annotation; the
42+
// test result itself is preserved.
43+
testInfo.annotations.push({
44+
type: "cleanup-failed",
45+
description: `${sku}: ${err.message}`,
46+
});
47+
}),
48+
),
49+
);
50+
},
51+
});
52+
53+
export { expect } from "@playwright/test";
54+
export { API_URL, productEndpoint } from "./api";
55+
export { createProduct, deleteProduct, getProduct, uniqueSku } from "./product";

src/web/e2e/seller-analytics.spec.ts

Lines changed: 0 additions & 65 deletions
This file was deleted.

src/web/e2e/seller-first-month.spec.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

src/web/e2e/seller-first-order.spec.ts

Lines changed: 0 additions & 62 deletions
This file was deleted.

0 commit comments

Comments
 (0)