cabloy 5.1.202 → 5.1.203

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +11 -0
  3. package/CONTRIBUTING.md +18 -17
  4. package/README.md +2 -2
  5. package/package.json +2 -3
  6. package/repo-agent-governance/managed-assets.json +12 -12
  7. package/repo-agent-governance/policies/repository.md +1 -0
  8. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
  9. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
  10. package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
  11. package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
  12. package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
  13. package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
  14. package/repo-docs/demo.md +14 -0
  15. package/repo-docs/frontend/scripts.md +10 -18
  16. package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
  17. package/repo-docs/index.md +2 -2
  18. package/repo-docs/reference/repo-scripts.md +15 -31
  19. package/repo-e2e/config/playwright.config.ts +14 -19
  20. package/repo-e2e/scripts/e2e.test.ts +17 -0
  21. package/repo-e2e/scripts/e2e.ts +5 -1
  22. package/repo-e2e/scripts/runE2e.ts +3 -15
  23. package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
  24. package/repo-e2e/scripts/runE2eArgs.ts +16 -29
  25. package/repo-e2e/scripts/startE2eVona.ts +20 -9
  26. package/repo-e2e/specs/a-commerce.spec.ts +232 -31
  27. package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
  28. package/repo-e2e/specs/home-user-account.spec.ts +9 -2
  29. package/scripts/upgrade.test.ts +147 -0
  30. package/scripts/upgrade.ts +49 -27
  31. package/vona/packages-cli/cli/package.json +1 -1
  32. package/vona/packages-cli/cli-set-api/package.json +1 -1
  33. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
  34. package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
  35. package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
  36. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
  37. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
  38. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
  39. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
  40. package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
@@ -20,15 +20,11 @@ function withSpecs(callback: (specsDir: string) => void): void {
20
20
  }
21
21
  }
22
22
 
23
- test('parses mode, multiple specs, and repeatable tags', () => {
23
+ test('parses multiple specs and repeatable tags', () => {
24
24
  withSpecs(specsDir => {
25
25
  assert.deepEqual(
26
- parseE2eArgs(
27
- ['--clean', 'cabloy-basic', 'account', '--tag', '@web', '--tag=@smoke'],
28
- specsDir,
29
- ),
26
+ parseE2eArgs(['cabloy-basic', 'account', '--tag', '@web', '--tag=@smoke'], specsDir),
30
27
  {
31
- mode: 'clean',
32
28
  specNames: ['cabloy-basic', 'account'],
33
29
  playwrightArgs: [],
34
30
  tags: ['@web', '@smoke'],
@@ -39,8 +35,7 @@ test('parses mode, multiple specs, and repeatable tags', () => {
39
35
 
40
36
  test('allows no spec names and forwards Playwright filters', () => {
41
37
  withSpecs(specsDir => {
42
- assert.deepEqual(parseE2eArgs(['--fast', '--grep', '@flow'], specsDir), {
43
- mode: 'fast',
38
+ assert.deepEqual(parseE2eArgs(['--grep', '@flow'], specsDir), {
44
39
  specNames: [],
45
40
  playwrightArgs: ['--grep', '@flow'],
46
41
  tags: [],
@@ -50,19 +45,28 @@ test('allows no spec names and forwards Playwright filters', () => {
50
45
 
51
46
  test('rejects invalid or unknown specs and managed config overrides', () => {
52
47
  withSpecs(specsDir => {
53
- assert.throws(() => parseE2eArgs(['--fast', '../account'], specsDir), /Invalid E2E spec name/);
54
- assert.throws(() => parseE2eArgs(['--fast', 'missing'], specsDir), /Unknown E2E spec/);
55
- assert.throws(
56
- () => parseE2eArgs(['--fast', '--config', 'other.ts'], specsDir),
57
- /manages --config/,
58
- );
48
+ assert.throws(() => parseE2eArgs(['../account'], specsDir), /Invalid E2E spec name/);
49
+ assert.throws(() => parseE2eArgs(['missing'], specsDir), /Unknown E2E spec/);
50
+ for (const args of [
51
+ ['--config', 'other.ts'],
52
+ ['--config=other.ts'],
53
+ ['-c', 'other.ts'],
54
+ ['-c=other.ts'],
55
+ ['-cother.ts'],
56
+ ]) {
57
+ assert.throws(() => parseE2eArgs(args, specsDir), /manages --config/);
58
+ }
59
+ for (const args of [['--workers', '2'], ['--workers=2'], ['-j', '2'], ['-j=2'], ['-j2']]) {
60
+ assert.throws(() => parseE2eArgs(args, specsDir), /manages --workers/);
61
+ }
59
62
  });
60
63
  });
61
64
 
62
- test('requires exactly one run mode', () => {
65
+ test('rejects removed clean and fast options', () => {
63
66
  withSpecs(specsDir => {
64
- assert.throws(() => parseE2eArgs(['account'], specsDir), /Missing E2E mode/);
65
- assert.throws(() => parseE2eArgs(['--clean', '--fast'], specsDir), /exactly one/);
67
+ for (const option of ['--clean', '--fast', '--clean=true', '--fast=false']) {
68
+ assert.throws(() => parseE2eArgs(['account', option], specsDir), /Unsupported E2E option/);
69
+ }
66
70
  });
67
71
  });
68
72
 
@@ -1,10 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
- export type E2eRunMode = 'clean' | 'fast';
5
-
6
4
  export type ParsedE2eArgs = {
7
- mode: E2eRunMode;
8
5
  specNames: string[];
9
6
  playwrightArgs: string[];
10
7
  tags: string[];
@@ -28,7 +25,6 @@ const VALUE_OPTIONS = new Set([
28
25
  '--shard',
29
26
  '--timeout',
30
27
  '--trace',
31
- '--workers',
32
28
  ]);
33
29
 
34
30
  function optionName(arg: string): string {
@@ -46,35 +42,23 @@ export function buildTagGrep(tags: string[]): string | undefined {
46
42
  }
47
43
 
48
44
  export function parseE2eArgs(args: string[], specsDir: string): ParsedE2eArgs {
49
- let mode: E2eRunMode | undefined;
50
- const remainingArgs: string[] = [];
51
-
52
- for (const arg of args) {
53
- if (arg === '--clean' || arg === '--fast') {
54
- if (mode && mode !== arg.slice(2)) {
55
- throw new Error('Choose exactly one E2E mode: --clean or --fast.');
56
- }
57
- if (mode === arg.slice(2)) {
58
- throw new Error(`E2E mode ${arg} was provided more than once.`);
59
- }
60
- mode = arg.slice(2) as E2eRunMode;
61
- } else {
62
- remainingArgs.push(arg);
63
- }
64
- }
65
-
66
- if (!mode) {
67
- throw new Error('Missing E2E mode. Use --clean or --fast.');
68
- }
69
-
70
45
  const specNames: string[] = [];
71
46
  const playwrightArgs: string[] = [];
72
47
  const tags: string[] = [];
73
48
  let optionStarted = false;
74
49
  let pendingValueFor: string | undefined;
75
50
 
76
- for (let index = 0; index < remainingArgs.length; index++) {
77
- const arg = remainingArgs[index];
51
+ for (let index = 0; index < args.length; index++) {
52
+ const arg = args[index];
53
+
54
+ if (
55
+ arg === '--clean' ||
56
+ arg.startsWith('--clean=') ||
57
+ arg === '--fast' ||
58
+ arg.startsWith('--fast=')
59
+ ) {
60
+ throw new Error(`Unsupported E2E option: ${arg}. Use the single managed test:e2e command.`);
61
+ }
78
62
 
79
63
  if (pendingValueFor) {
80
64
  if (!arg || arg.startsWith('--')) {
@@ -100,9 +84,12 @@ export function parseE2eArgs(args: string[], specsDir: string): ParsedE2eArgs {
100
84
  continue;
101
85
  }
102
86
 
103
- if (arg === '--config' || arg.startsWith('--config=')) {
87
+ if (arg === '--config' || arg.startsWith('--config=') || arg.startsWith('-c')) {
104
88
  throw new Error('The E2E runner manages --config; do not provide it.');
105
89
  }
90
+ if (arg === '--workers' || arg.startsWith('--workers=') || arg.startsWith('-j')) {
91
+ throw new Error('The E2E runner manages --workers; do not provide it.');
92
+ }
106
93
 
107
94
  if (!optionStarted && !arg.startsWith('-')) {
108
95
  if (!SPEC_NAME_PATTERN.test(arg)) {
@@ -133,7 +120,7 @@ export function parseE2eArgs(args: string[], specsDir: string): ParsedE2eArgs {
133
120
  throw new Error(`Missing value for ${pendingValueFor}.`);
134
121
  }
135
122
 
136
- return { mode, specNames, playwrightArgs, tags };
123
+ return { specNames, playwrightArgs, tags };
137
124
  }
138
125
 
139
126
  export function combineGreps(args: string[], tags: string[]): string[] {
@@ -1,19 +1,30 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
 
3
3
  import { spawn } from 'node:child_process';
4
+ import { resolve } from 'node:path';
4
5
 
5
6
  import { E2E_LOCAL_BASE_URL, E2E_ROOT_DIR } from './e2e.ts';
6
7
 
7
- const child: ChildProcess = spawn('npm', ['run', 'dev:one'], {
8
- cwd: E2E_ROOT_DIR,
9
- detached: process.platform !== 'win32',
10
- stdio: 'inherit',
11
- env: {
12
- ...process.env,
13
- SERVER_SERVE_PROTOCOL: 'http',
14
- SERVER_SERVE_HOST: E2E_LOCAL_BASE_URL.replace(/^https?:\/\//, ''),
8
+ const child: ChildProcess = spawn(
9
+ process.execPath,
10
+ [
11
+ './packages-cli/cli/src/bin/vona.ts',
12
+ ':bin:dev',
13
+ '--workers=1',
14
+ '--flavor=normal',
15
+ '--mode=test',
16
+ ],
17
+ {
18
+ cwd: resolve(E2E_ROOT_DIR, 'vona'),
19
+ detached: process.platform !== 'win32',
20
+ stdio: 'inherit',
21
+ env: {
22
+ ...process.env,
23
+ SERVER_SERVE_PROTOCOL: 'http',
24
+ SERVER_SERVE_HOST: E2E_LOCAL_BASE_URL.replace(/^https?:\/\//, ''),
25
+ },
15
26
  },
16
- });
27
+ );
17
28
 
18
29
  const gracefulShutdownTimeout = 7000;
19
30
 
@@ -8,6 +8,155 @@ const addressResourceUrl =
8
8
  const addressMinePath = '/api/commerce/member/address/mine';
9
9
  const addressActionPath = '/api/commerce/member/address';
10
10
  const passportTestActivateCurrentPath = '/api/home/user/passportTest/activateCurrent';
11
+ const commerceFixturePath = '/api/commerce/trade/e2eFixture';
12
+
13
+ interface IOwnedCommerceFixture {
14
+ customerToken?: string;
15
+ customerName?: string;
16
+ adminToken?: string;
17
+ categoryId?: number | string;
18
+ orderIds: Array<number | string>;
19
+ }
20
+
21
+ const ownedFixtures = new Map<string, IOwnedCommerceFixture>();
22
+
23
+ function ownedFixture(testInfo: TestInfo): IOwnedCommerceFixture {
24
+ let fixture = ownedFixtures.get(testInfo.testId);
25
+ if (!fixture) {
26
+ fixture = { orderIds: [] };
27
+ ownedFixtures.set(testInfo.testId, fixture);
28
+ }
29
+ return fixture;
30
+ }
31
+
32
+ async function fixtureRequest(
33
+ request: APIRequestContext,
34
+ method: 'post' | 'delete',
35
+ path: string,
36
+ token: string,
37
+ data?: object,
38
+ ) {
39
+ const response = await request[method](path, {
40
+ headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
41
+ ...(data ? { data } : {}),
42
+ });
43
+ if (!response.ok())
44
+ throw new Error(`Commerce fixture ${method} ${path} failed (${response.status()})`);
45
+ if (method === 'delete') return;
46
+ return (await response.json()).data;
47
+ }
48
+
49
+ async function createOwnedCatalogue(browser: Browser, testInfo: TestInfo) {
50
+ const fixture = ownedFixture(testInfo);
51
+ const context = await browser.newContext();
52
+ try {
53
+ const page = await context.newPage();
54
+ await login(page, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
55
+ const token = (await context.cookies()).find(cookie => cookie.name === 'token')?.value;
56
+ if (!token) throw new Error('Commerce Admin fixture token unavailable');
57
+ fixture.adminToken = token;
58
+ // Server-side creation is transactional; record the returned category before
59
+ // validating the response so assertion failures still trigger its cleanup.
60
+ const catalogue = await fixtureRequest(
61
+ context.request,
62
+ 'post',
63
+ `${commerceFixturePath}/catalogue`,
64
+ token,
65
+ );
66
+ fixture.categoryId = catalogue.categoryId;
67
+ expectTableIdentity(catalogue.categoryId);
68
+ expectTableIdentity(catalogue.skuId);
69
+ expect(catalogue.initialStock).toBe(10);
70
+ return catalogue as {
71
+ categoryId: number | string;
72
+ skuId: number | string;
73
+ title: string;
74
+ code: string;
75
+ initialStock: number;
76
+ };
77
+ } finally {
78
+ await context.close();
79
+ }
80
+ }
81
+
82
+ async function dispatchOwnedOutcome(
83
+ request: APIRequestContext,
84
+ testInfo: TestInfo,
85
+ checkout: { orderId: number | string; paymentAttemptId: number | string },
86
+ refundAttemptId?: number | string,
87
+ ) {
88
+ const token = ownedFixture(testInfo).customerToken;
89
+ if (!token) throw new Error('Commerce customer fixture token unavailable');
90
+ return await fixtureRequest(
91
+ request,
92
+ 'post',
93
+ `${commerceFixturePath}/order/${checkout.orderId}/${refundAttemptId ? 'refund' : 'payment'}/${refundAttemptId ?? checkout.paymentAttemptId}/dispatch`,
94
+ token,
95
+ );
96
+ }
97
+
98
+ // Playwright invokes afterEach even for partial setup and failed assertions. Never delete
99
+ // shared seed stock: remove only the IDs created by these tests, in reverse dependency order.
100
+ test.afterEach(async ({ request }, testInfo) => {
101
+ const fixture = ownedFixtures.get(testInfo.testId);
102
+ if (!fixture) return;
103
+ ownedFixtures.delete(testInfo.testId);
104
+ const errors: Error[] = [];
105
+ const clean = async (label: string, action: () => Promise<unknown>) => {
106
+ try {
107
+ await action();
108
+ } catch (error) {
109
+ errors.push(new Error(`${label}: ${String(error)}`));
110
+ }
111
+ };
112
+ if (fixture.customerToken) {
113
+ for (const orderId of fixture.orderIds.toReversed()) {
114
+ await clean(`order ${orderId}`, () =>
115
+ fixtureRequest(
116
+ request,
117
+ 'delete',
118
+ `${commerceFixturePath}/order/${orderId}`,
119
+ fixture.customerToken!,
120
+ ),
121
+ );
122
+ }
123
+ if (!errors.length) {
124
+ await clean('customer Commerce resources', () =>
125
+ fixtureRequest(
126
+ request,
127
+ 'delete',
128
+ `${commerceFixturePath}/customer`,
129
+ fixture.customerToken!,
130
+ ),
131
+ );
132
+ }
133
+ // Keep the identity when Commerce cleanup fails, so its surviving records
134
+ // retain their owner and can be identified and removed without a global sweep.
135
+ if (!errors.length) {
136
+ await clean('customer account', () =>
137
+ fixtureRequest(
138
+ request,
139
+ 'delete',
140
+ '/api/home/user/passportTest/removeCurrentFixture',
141
+ fixture.customerToken!,
142
+ ),
143
+ );
144
+ }
145
+ }
146
+ // Preserve the catalogue if order/cart cleanup failed: deleting its SKU would
147
+ // otherwise obscure the precise ownership of a surviving customer's records.
148
+ if (!errors.length && fixture.categoryId && fixture.adminToken) {
149
+ await clean(`catalogue ${fixture.categoryId}`, () =>
150
+ fixtureRequest(
151
+ request,
152
+ 'delete',
153
+ `${commerceFixturePath}/catalogue/${fixture.categoryId}`,
154
+ fixture.adminToken!,
155
+ ),
156
+ );
157
+ }
158
+ if (errors.length) throw new AggregateError(errors, 'Commerce E2E fixture cleanup failed');
159
+ });
11
160
 
12
161
  interface IAddressFixture {
13
162
  addressLine1: string;
@@ -105,7 +254,7 @@ async function registerCustomer(
105
254
  testInfo: TestInfo,
106
255
  ): Promise<{ password: string; username: string }> {
107
256
  const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
108
- const username = `e2e-address-${id}`;
257
+ const username = `e2e-fixture-address-${id}`;
109
258
  const password = 'address-e2e-password';
110
259
  const captchaResponse = await request.post('/api/captcha/create', {
111
260
  data: { scene: 'captcha-simple:simple' },
@@ -131,6 +280,9 @@ async function registerCustomer(
131
280
  expect(registerResponse.ok()).toBeTruthy();
132
281
  const registration = (await registerResponse.json()).data;
133
282
  expect(registration?.jwt?.accessToken).toEqual(expect.any(String));
283
+ const fixture = ownedFixture(testInfo);
284
+ fixture.customerToken = registration.jwt.accessToken;
285
+ fixture.customerName = username;
134
286
  const activateResponse = await request.post(passportTestActivateCurrentPath, {
135
287
  headers: { Authorization: `Bearer ${registration.jwt.accessToken}` },
136
288
  });
@@ -220,7 +372,7 @@ test(
220
372
  async ({ page, request }) => {
221
373
  const response = await request.get('/commerce');
222
374
  expect(response.ok()).toBeTruthy();
223
- expect(response.headers()['cache-control']).toBe('no-cache, no-store, must-revalidate');
375
+ expect(response.headers()['cache-control']).toBe('public, max-age=600');
224
376
  const html = await response.text();
225
377
  const normalizedHtml = html.toLowerCase();
226
378
  expect(normalizedHtml).not.toContain('data-zova-hydrated');
@@ -382,7 +534,7 @@ test(
382
534
  for (const [index, response] of responses.entries()) {
383
535
  expect(response.status(), `public response ${index}`).toBe(200);
384
536
  expect(response.headers()['cache-control'], `public response ${index}`).toBe(
385
- 'no-cache, no-store, must-revalidate',
537
+ 'public, max-age=600',
386
538
  );
387
539
  expect(html[index].toLowerCase()).not.toContain('data-zova-hydrated');
388
540
  expect(html[index]).toContain('data-ssr-theme-dark-false="light"');
@@ -629,16 +781,22 @@ test(
629
781
  testInfo,
630
782
  );
631
783
  try {
784
+ const catalogue = await createOwnedCatalogue(browser, testInfo);
632
785
  await page.goto('/commerce', { waitUntil: 'load' });
633
786
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
634
787
  await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible();
635
788
 
636
- const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
789
+ const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
637
790
  await expect(productLink).toBeVisible();
638
791
  await productLink.click();
639
792
  await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
640
- await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
641
- await expect(page.getByText('COF-SET-01')).toBeVisible();
793
+ await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
794
+ await expect(
795
+ page
796
+ .getByRole('button', { name: 'Add to cart', exact: true })
797
+ .locator('..')
798
+ .getByText(catalogue.code),
799
+ ).toBeVisible();
642
800
  await expect(page.getByText('$45.99')).toBeVisible();
643
801
 
644
802
  const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
@@ -649,7 +807,7 @@ test(
649
807
  await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
650
808
  await expect(page.getByRole('heading', { name: 'Cart' })).toBeVisible();
651
809
  const cartItem = page.locator('article').filter({ has: page.getByRole('spinbutton') });
652
- await expect(cartItem).toContainText('COF-SET-01');
810
+ await expect(cartItem).toContainText(catalogue.code);
653
811
  await expect(cartItem).toContainText('$45.99');
654
812
  await expect(cartItem.getByRole('spinbutton')).toHaveValue('1');
655
813
 
@@ -669,6 +827,7 @@ test(
669
827
  const checkoutResponseValue = await checkoutResponse;
670
828
  expect(checkoutResponseValue.ok()).toBeTruthy();
671
829
  const checkout = (await checkoutResponseValue.json()).data;
830
+ ownedFixture(testInfo).orderIds.push(checkout.orderId);
672
831
  expectTableIdentity(checkout.orderId);
673
832
  expectTableIdentity(checkout.paymentAttemptId);
674
833
  expectTableIdentity(checkout.paymentSessionId);
@@ -699,6 +858,11 @@ test(
699
858
  );
700
859
  await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
701
860
  expect((await completeResponse).ok()).toBeTruthy();
861
+ expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
862
+ orderState: 'paid',
863
+ attemptState: 'succeeded',
864
+ stock: 9,
865
+ });
702
866
  await expect(page).toHaveURL(
703
867
  new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
704
868
  {
@@ -717,8 +881,8 @@ test(
717
881
  expect(orderHtml).toContain(fixture.recipientName);
718
882
  expect(orderHtml).toContain(fixture.addressLine1);
719
883
  expect(orderHtml).toContain('Discount: $0.00');
720
- expect(orderHtml).toContain('Pour-Over Coffee Set');
721
- expect(orderHtml).toContain('COF-SET-01');
884
+ expect(orderHtml).toContain(catalogue.title);
885
+ expect(orderHtml).toContain(catalogue.code);
722
886
  expect(orderHtml).toContain('1 × $45.99 = $45.99');
723
887
 
724
888
  const orderDocumentResponse = await page.reload({ waitUntil: 'load' });
@@ -729,8 +893,8 @@ test(
729
893
  await expect(page.getByText(fixture.recipientName)).toBeVisible();
730
894
  await expect(page.getByText(fixture.addressLine1)).toBeVisible();
731
895
  await expect(page.getByText('Discount: $0.00')).toBeVisible();
732
- await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
733
- await expect(page.getByText('COF-SET-01')).toBeVisible();
896
+ await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
897
+ await expect(page.getByRole('paragraph').filter({ hasText: catalogue.code })).toBeVisible();
734
898
  await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible();
735
899
 
736
900
  const shipmentCarrier = 'Cabloy Express';
@@ -766,8 +930,9 @@ test(
766
930
  await expect(adminPage.getByText('Purchased lines').first()).toBeVisible();
767
931
  await expect(adminPage.getByText('addressSnapshot').first()).toBeVisible();
768
932
  await expect(adminPage.getByText('couponSnapshot').first()).toBeVisible();
769
- await expect(adminPage.getByText('Pour-Over Coffee Set')).toBeVisible();
770
- await expect(adminPage.getByText('COF-SET-01')).toBeVisible();
933
+ await expect(
934
+ adminPage.getByRole('cell', { name: catalogue.title, exact: true }),
935
+ ).toHaveCount(2);
771
936
  await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
772
937
  await adminPage.getByRole('button', { name: 'Back', exact: true }).click();
773
938
  await expect(orderRow).toBeVisible();
@@ -844,15 +1009,16 @@ test(
844
1009
  testInfo,
845
1010
  );
846
1011
  try {
1012
+ const catalogue = await createOwnedCatalogue(browser, testInfo);
847
1013
  await page.goto('/commerce', { waitUntil: 'load' });
848
- const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
1014
+ const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
849
1015
  await expect(productLink).toBeVisible();
850
1016
  await productLink.click();
851
1017
  await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
852
1018
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
853
- await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
854
- await expect(page.getByText('COF-SET-01')).toBeVisible();
1019
+ await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
855
1020
  const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
1021
+ await expect(addToCartButton.locator('..')).toContainText(catalogue.code);
856
1022
  await expect(addToCartButton).toBeEnabled();
857
1023
  const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
858
1024
  await addToCartButton.click();
@@ -868,6 +1034,7 @@ test(
868
1034
  const checkoutResponseValue = await checkoutResponse;
869
1035
  expect(checkoutResponseValue.ok()).toBeTruthy();
870
1036
  const checkout = (await checkoutResponseValue.json()).data;
1037
+ ownedFixture(testInfo).orderIds.push(checkout.orderId);
871
1038
  expectTableIdentity(checkout.orderId);
872
1039
  expectTableIdentity(checkout.paymentSessionId);
873
1040
  const startResponse = waitForApiResponse(
@@ -885,6 +1052,11 @@ test(
885
1052
  );
886
1053
  await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
887
1054
  expect((await completeResponse).ok()).toBeTruthy();
1055
+ expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
1056
+ orderState: 'paid',
1057
+ attemptState: 'succeeded',
1058
+ stock: 9,
1059
+ });
888
1060
  await expect(page).toHaveURL(
889
1061
  new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
890
1062
  {
@@ -951,15 +1123,16 @@ test(
951
1123
  testInfo,
952
1124
  );
953
1125
  try {
1126
+ const catalogue = await createOwnedCatalogue(browser, testInfo);
954
1127
  await page.goto('/commerce', { waitUntil: 'load' });
955
- const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
1128
+ const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
956
1129
  await expect(productLink).toBeVisible();
957
1130
  await productLink.click();
958
1131
  await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
959
1132
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
960
- await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
961
- await expect(page.getByText('COF-SET-01')).toBeVisible();
1133
+ await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
962
1134
  const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
1135
+ await expect(addToCartButton.locator('..')).toContainText(catalogue.code);
963
1136
  await expect(addToCartButton).toBeEnabled();
964
1137
  const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
965
1138
  await addToCartButton.click();
@@ -975,6 +1148,7 @@ test(
975
1148
  const checkoutResponseValue = await checkoutResponse;
976
1149
  expect(checkoutResponseValue.ok()).toBeTruthy();
977
1150
  const checkout = (await checkoutResponseValue.json()).data;
1151
+ ownedFixture(testInfo).orderIds.push(checkout.orderId);
978
1152
  expectTableIdentity(checkout.orderId);
979
1153
  expectTableIdentity(checkout.paymentSessionId);
980
1154
  expect(checkout.state).toBe('awaiting_payment');
@@ -1077,13 +1251,14 @@ test(
1077
1251
  testInfo,
1078
1252
  );
1079
1253
  try {
1254
+ const catalogue = await createOwnedCatalogue(browser, testInfo);
1080
1255
  await page.goto('/commerce', { waitUntil: 'load' });
1081
1256
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
1082
- const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
1257
+ const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
1083
1258
  await expect(productLink).toBeVisible();
1084
1259
  await productLink.click();
1085
1260
  await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
1086
- await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
1261
+ await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
1087
1262
  const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
1088
1263
  await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
1089
1264
  expect((await addResponse).ok()).toBeTruthy();
@@ -1100,6 +1275,7 @@ test(
1100
1275
  const checkoutResponseValue = await checkoutResponse;
1101
1276
  expect(checkoutResponseValue.ok()).toBeTruthy();
1102
1277
  const checkout = (await checkoutResponseValue.json()).data;
1278
+ ownedFixture(testInfo).orderIds.push(checkout.orderId);
1103
1279
  expectTableIdentity(checkout.orderId);
1104
1280
  expectTableIdentity(checkout.paymentSessionId);
1105
1281
  const startResponse = waitForApiResponse(
@@ -1118,6 +1294,11 @@ test(
1118
1294
  );
1119
1295
  await page.getByRole('button', { name: 'Cancel payment', exact: true }).click();
1120
1296
  expect((await cancelResponse).ok()).toBeTruthy();
1297
+ expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
1298
+ orderState: 'cancelled',
1299
+ attemptState: 'cancelled',
1300
+ stock: 10,
1301
+ });
1121
1302
  await expect(page).toHaveURL(
1122
1303
  new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
1123
1304
  {
@@ -1144,15 +1325,21 @@ test(
1144
1325
  testInfo,
1145
1326
  );
1146
1327
  try {
1328
+ const catalogue = await createOwnedCatalogue(browser, testInfo);
1147
1329
  await page.goto('/commerce', { waitUntil: 'load' });
1148
1330
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
1149
1331
  await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible();
1150
- const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
1332
+ const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
1151
1333
  await expect(productLink).toBeVisible();
1152
1334
  await productLink.click();
1153
1335
  await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
1154
- await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
1155
- await expect(page.getByText('COF-SET-01')).toBeVisible();
1336
+ await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
1337
+ await expect(
1338
+ page
1339
+ .getByRole('button', { name: 'Add to cart', exact: true })
1340
+ .locator('..')
1341
+ .getByText(catalogue.code),
1342
+ ).toBeVisible();
1156
1343
  await expect(page.getByText('$45.99')).toBeVisible();
1157
1344
  const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
1158
1345
  await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
@@ -1169,6 +1356,7 @@ test(
1169
1356
  const checkoutResponseValue = await checkoutResponse;
1170
1357
  expect(checkoutResponseValue.ok()).toBeTruthy();
1171
1358
  const checkout = (await checkoutResponseValue.json()).data;
1359
+ ownedFixture(testInfo).orderIds.push(checkout.orderId);
1172
1360
  expectTableIdentity(checkout.orderId);
1173
1361
  expectTableIdentity(checkout.paymentAttemptId);
1174
1362
  expectTableIdentity(checkout.paymentSessionId);
@@ -1189,6 +1377,11 @@ test(
1189
1377
  );
1190
1378
  await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
1191
1379
  expect((await completeResponse).ok()).toBeTruthy();
1380
+ expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
1381
+ orderState: 'paid',
1382
+ attemptState: 'succeeded',
1383
+ stock: 9,
1384
+ });
1192
1385
  await expect(page.getByText('paid · $45.99')).toBeVisible({ timeout: 40_000 });
1193
1386
 
1194
1387
  const requestResponse = waitForApiResponse(
@@ -1329,15 +1522,23 @@ test(
1329
1522
  exact: true,
1330
1523
  }),
1331
1524
  ).toHaveCount(0);
1332
- const completeRefundResponse = waitForApiResponse(
1333
- adminPage,
1334
- 'POST',
1525
+ // The Admin mock button is intentionally hidden in the production-built SSR UI.
1526
+ // Use the existing dev/test Admin API to submit its signed mock webhook, then
1527
+ // dispatch the linked event under the customer's separate identity.
1528
+ const adminToken = ownedFixture(testInfo).adminToken;
1529
+ if (!adminToken) throw new Error('Commerce Admin fixture token unavailable');
1530
+ const completeRefund = await adminContext.request.post(
1335
1531
  `/api/pay/mock/payment-session/refund-operation/${executeResult.refundOperationId}/complete`,
1532
+ { headers: { Authorization: `Bearer ${adminToken}` }, data: { outcome: 'succeeded' } },
1336
1533
  );
1337
- await refundExecutionDialog
1338
- .getByRole('button', { name: 'Complete mock refund', exact: true })
1339
- .click();
1340
- expect((await completeRefundResponse).ok()).toBeTruthy();
1534
+ expect(completeRefund.ok()).toBeTruthy();
1535
+ expect(
1536
+ await dispatchOwnedOutcome(request, testInfo, checkout, executeResult.refundAttemptId),
1537
+ ).toMatchObject({
1538
+ orderState: 'refunded',
1539
+ refundState: 'refunded',
1540
+ stock: 10,
1541
+ });
1341
1542
  await expect
1342
1543
  .poll(
1343
1544
  async () => {