cabloy 5.1.201 → 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 (55) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +17 -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 +33 -33
  7. package/repo-agent-governance/policies/repository.md +1 -0
  8. package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +3 -2
  9. package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
  10. package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +11 -10
  11. package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
  12. package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +4 -2
  13. package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +16 -13
  14. package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +18 -11
  15. package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
  16. package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
  17. package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
  18. package/repo-agent-governance/tests/spec-audit.test.mjs +323 -0
  19. package/repo-agent-governance/tools/spec-audit/audit.mjs +129 -3
  20. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +26 -7
  21. package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +82 -0
  22. package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +40 -1
  23. package/repo-docs/ai/ai-spec-driven-development.md +1 -0
  24. package/repo-docs/ai/playbook-spec-execution.md +3 -2
  25. package/repo-docs/ai/playbook-spec-generation.md +1 -1
  26. package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
  27. package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
  28. package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
  29. package/repo-docs/demo.md +14 -0
  30. package/repo-docs/frontend/scripts.md +10 -18
  31. package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
  32. package/repo-docs/index.md +2 -2
  33. package/repo-docs/reference/repo-scripts.md +15 -31
  34. package/repo-e2e/config/playwright.config.ts +14 -19
  35. package/repo-e2e/scripts/e2e.test.ts +17 -0
  36. package/repo-e2e/scripts/e2e.ts +5 -1
  37. package/repo-e2e/scripts/runE2e.ts +3 -15
  38. package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
  39. package/repo-e2e/scripts/runE2eArgs.ts +16 -29
  40. package/repo-e2e/scripts/startE2eVona.ts +20 -9
  41. package/repo-e2e/specs/a-commerce.spec.ts +232 -31
  42. package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
  43. package/repo-e2e/specs/home-user-account.spec.ts +9 -2
  44. package/scripts/upgrade.test.ts +147 -0
  45. package/scripts/upgrade.ts +49 -27
  46. package/vona/packages-cli/cli/package.json +1 -1
  47. package/vona/packages-cli/cli-set-api/package.json +1 -1
  48. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
  49. package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
  50. package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
  52. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
  53. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
@@ -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 () => {
@@ -40,11 +40,23 @@ function collectConsoleErrors(page: Page) {
40
40
 
41
41
  async function loginAsAdmin(page: Page) {
42
42
  await page.goto('/admin/', { waitUntil: 'load' });
43
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
43
44
  if (page.url().includes('/admin/login')) {
44
- await page.getByPlaceholder('Your Username').fill('admin');
45
- await page.getByPlaceholder('Your Password').fill('123456');
45
+ const usernameInput = page.getByPlaceholder('Your Username');
46
+ const passwordInput = page.getByPlaceholder('Your Password');
47
+ await usernameInput.fill('admin');
48
+ await passwordInput.fill('123456');
49
+ await expect(usernameInput).toHaveValue('admin');
50
+ await expect(passwordInput).toHaveValue('123456');
46
51
  await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
52
+ const loginResponse = page.waitForResponse(response => {
53
+ const url = new URL(response.url());
54
+ return (
55
+ response.request().method() === 'POST' && url.pathname === '/api/home/user/passport/login'
56
+ );
57
+ });
47
58
  await page.getByRole('button', { name: 'Login', exact: true }).click();
59
+ expect((await loginResponse).ok()).toBeTruthy();
48
60
  await expect(page).not.toHaveURL(/\/admin\/login(?:\?|$)/);
49
61
  }
50
62
 
@@ -158,7 +170,7 @@ test(
158
170
  async ({ page, request }) => {
159
171
  const response = await request.get('/');
160
172
  expect(response.ok()).toBeTruthy();
161
- expect(response.headers()['cache-control']).toBe('no-cache, no-store, must-revalidate');
173
+ expect(response.headers()['cache-control']).toBe('public, max-age=600');
162
174
  const html = await response.text();
163
175
  expect(html.toLowerCase()).not.toContain('data-zova-hydrated');
164
176
  expect(html).not.toContain('ssr-body-ready-observer');
@@ -166,9 +178,7 @@ test(
166
178
 
167
179
  const routeOverrideResponse = await request.get('/demo/basic/routeQueryB');
168
180
  expect(routeOverrideResponse.ok()).toBeTruthy();
169
- expect(routeOverrideResponse.headers()['cache-control']).toBe(
170
- 'no-cache, no-store, must-revalidate',
171
- );
181
+ expect(routeOverrideResponse.headers()['cache-control']).toBe('public, max-age=300');
172
182
 
173
183
  const unlocalizedResponse = await request.get('/demo/basic/state');
174
184
  expect(unlocalizedResponse.ok()).toBeTruthy();
@@ -192,7 +202,7 @@ test(
192
202
  );
193
203
 
194
204
  test(
195
- 'ATP-BASIC-SSR-03: concurrent public routes are non-cacheable in development',
205
+ 'ATP-BASIC-SSR-03: concurrent public routes preserve their cache policies in managed test mode',
196
206
  { tag: ['@web', '@smoke'] },
197
207
  async ({ request }) => {
198
208
  const responses = await Promise.all(
@@ -205,9 +215,13 @@ test(
205
215
 
206
216
  for (const [index, response] of responses.entries()) {
207
217
  expect(response.ok(), `response ${index}`).toBeTruthy();
208
- expect(response.headers()['cache-control'], `response ${index}`).toBe(
209
- 'no-cache, no-store, must-revalidate',
210
- );
218
+ const cacheControl =
219
+ index % 3 === 0
220
+ ? 'public, max-age=600'
221
+ : index % 3 === 1
222
+ ? 'public, max-age=300'
223
+ : 'no-cache, no-store, must-revalidate';
224
+ expect(response.headers()['cache-control'], `response ${index}`).toBe(cacheControl);
211
225
  }
212
226
  },
213
227
  );
@@ -338,6 +352,7 @@ test(
338
352
  const consoleErrors = collectConsoleErrors(page);
339
353
  try {
340
354
  await page.goto('/admin/login', { waitUntil: 'load' });
355
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
341
356
  await page.evaluate(() => {
342
357
  indexedDB.deleteDatabase('localforage');
343
358
  localStorage.clear();
@@ -346,13 +361,25 @@ test(
346
361
  await page.goto('/admin/login?returnTo=/rest/resource/training-student%253Astudent', {
347
362
  waitUntil: 'load',
348
363
  });
349
- await page.getByPlaceholder('Your Username').fill('admin');
350
- await page.getByPlaceholder('Your Password').fill('123456');
364
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
365
+ const usernameInput = page.getByPlaceholder('Your Username');
366
+ const passwordInput = page.getByPlaceholder('Your Password');
367
+ await usernameInput.fill('admin');
368
+ await passwordInput.fill('123456');
369
+ await expect(usernameInput).toHaveValue('admin');
370
+ await expect(passwordInput).toHaveValue('123456');
351
371
  await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
352
372
 
353
373
  const initialSelect = waitForStudentSelect(page);
374
+ const loginResponse = page.waitForResponse(response => {
375
+ const url = new URL(response.url());
376
+ return (
377
+ response.request().method() === 'POST' && url.pathname === '/api/home/user/passport/login'
378
+ );
379
+ });
354
380
  await page.getByRole('button', { name: 'Login', exact: true }).click();
355
381
  try {
382
+ expect((await loginResponse).ok()).toBeTruthy();
356
383
  await expect(page).toHaveURL(studentResourceUrl);
357
384
  await initialSelect;
358
385
  } finally {
@@ -591,11 +591,18 @@ test(
591
591
  { tag: ['@account', '@admin'] },
592
592
  async ({ page }) => {
593
593
  await page.goto('/admin/', { waitUntil: 'load' });
594
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
594
595
  if (page.url().includes('/admin/login')) {
595
- await page.getByPlaceholder('Your Username').fill('admin');
596
- await page.getByPlaceholder('Your Password').fill('123456');
596
+ const usernameInput = page.getByPlaceholder('Your Username');
597
+ const passwordInput = page.getByPlaceholder('Your Password');
598
+ await usernameInput.fill('admin');
599
+ await passwordInput.fill('123456');
600
+ await expect(usernameInput).toHaveValue('admin');
601
+ await expect(passwordInput).toHaveValue('123456');
597
602
  await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
603
+ const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
598
604
  await page.getByRole('button', { name: 'Login', exact: true }).click();
605
+ expect((await loginResponse).ok()).toBeTruthy();
599
606
  await expect(page).not.toHaveURL(/\/admin\/login(?:\?|$)/);
600
607
  }
601
608