cabloy 5.1.134 → 5.1.136

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 (185) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/SKILL.md +9 -0
  3. package/.claude/skills/cabloy-zova-source-reading/SKILL.md +24 -1
  4. package/.claude/skills/cabloy-zova-source-reading/references/analysis-modes.md +12 -0
  5. package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +28 -0
  6. package/.github/workflows/playwright-e2e.yml +2 -2
  7. package/.github/workflows/vona-test-pg.yml +1 -1
  8. package/.github/workflows/vona-test-sqlite3.yml +1 -1
  9. package/CHANGELOG.md +38 -0
  10. package/CLAUDE.md +1 -0
  11. package/cabloy-docs/.vitepress/config.mjs +7 -0
  12. package/cabloy-docs/backend/markdown-guide.md +189 -0
  13. package/cabloy-docs/frontend/component-guide.md +6 -0
  14. package/cabloy-docs/frontend/css-in-js-guide.md +6 -0
  15. package/cabloy-docs/frontend/markdown-guide.md +258 -0
  16. package/cabloy-docs/frontend/page-guide.md +48 -0
  17. package/cabloy-docs/frontend/page-params-guide.md +4 -2
  18. package/cabloy-docs/frontend/page-route-guide.md +2 -2
  19. package/cabloy-docs/frontend/zova-router-under-the-hood.md +2 -0
  20. package/cabloy-docs/frontend/zova-source-reading-map.md +53 -9
  21. package/cabloy-docs/fullstack/a-pay-payment-suite.md +333 -0
  22. package/cabloy-docs/fullstack/payment-sandbox-configuration.md +285 -0
  23. package/e2e/specs/a-commerce/commerce.spec.ts +503 -47
  24. package/e2e/specs/cabloy-basic/basic.spec.ts +30 -0
  25. package/package.json +1 -1
  26. package/vona/env/.env +2 -0
  27. package/vona/packages-cli/cli/package.json +1 -1
  28. package/vona/packages-cli/cli-set-api/cli/templates/init/error/boilerplate/config/errors.ts +6 -1
  29. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
  30. package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
  31. package/vona/packages-cli/cli-set-api/package.json +1 -1
  32. package/vona/packages-utils/localeutil/package.json +1 -1
  33. package/vona/packages-utils/localeutil/src/util.ts +1 -1
  34. package/vona/packages-vona/vona/package.json +1 -1
  35. package/vona/packages-vona/vona-core/package.json +1 -1
  36. package/vona/packages-vona/vona-mock/package.json +1 -1
  37. package/vona/patches/{zova-core@5.1.82.patch → zova-core@5.1.84.patch} +2 -2
  38. package/vona/pnpm-lock.yaml +826 -270
  39. package/vona/pnpm-workspace.yaml +1 -1
  40. package/vona/src/suite/a-commerce/modules/commerce-catalog/package.json +1 -1
  41. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +99 -1
  42. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.index.ts +1 -0
  43. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +19 -0
  44. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
  45. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
  46. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/product.ts +3 -3
  47. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentUpdate.tsx +14 -0
  48. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentView.tsx +19 -0
  49. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +48 -3
  50. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicDetail.tsx +15 -0
  51. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +48 -3
  52. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +52 -3
  53. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/productContent.tsx +36 -0
  54. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +6 -0
  55. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/productContent.ts +22 -0
  56. package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +82 -6
  57. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productContent.test.ts +271 -0
  58. package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productPresentation.test.ts +81 -2
  59. package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +4 -1
  60. package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +32 -15
  61. package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +8 -2
  62. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/index.ts +8 -2
  63. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/locales.ts +18 -0
  64. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +3 -1
  65. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/en-us.ts +3 -0
  66. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/zh-cn.ts +3 -0
  67. package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/index.ts +1 -0
  68. package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +13 -2
  69. package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/providerReference.ts +18 -0
  70. package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +43 -8
  71. package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +74 -0
  72. package/vona/src/suite/a-commerce/modules/commerce-trade/test/providerReference.test.ts +33 -0
  73. package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +73 -0
  74. package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +2 -0
  75. package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +30 -0
  76. package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +7 -2
  77. package/vona/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +4 -1
  78. package/vona/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +4 -1
  79. package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +4 -4
  80. package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +2 -2
  81. package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +8 -1
  82. package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +6 -2
  83. package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +1 -0
  84. package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +4 -4
  85. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +64 -0
  86. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/index.ts +54 -0
  87. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/this.ts +2 -0
  88. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/bean.markdown.ts +137 -0
  89. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/index.ts +1 -0
  90. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.build.json +11 -0
  91. package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.json +7 -0
  92. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  93. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/bean.ssr.ts +17 -0
  94. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +11 -3
  95. package/vona/src/suite-vendor/a-cabloy/package.json +2 -1
  96. package/vona/src/suite-vendor/a-cabloy/tsconfig.json +3 -0
  97. package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
  98. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +1 -0
  99. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +8 -1
  100. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +1 -0
  101. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/providerOperationFailure.ts +8 -0
  102. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +187 -29
  103. package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +4 -0
  104. package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
  105. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
  106. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +0 -6
  107. package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +1 -0
  108. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +2 -1
  109. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +458 -22
  110. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +1 -0
  111. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/lib/stripeGateway.ts +85 -0
  112. package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/types/index.ts +2 -0
  113. package/vona/src/suite-vendor/a-pay/package.json +5 -5
  114. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  115. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  116. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +1 -0
  117. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  118. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +14 -0
  119. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.passport.ts +4 -0
  120. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +3 -2
  121. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +4 -0
  122. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  123. package/zova/packages-utils/logger/package.json +2 -2
  124. package/zova/packages-utils/zova-jsx/package.json +2 -2
  125. package/zova/packages-zova/zova/package.json +3 -3
  126. package/zova/packages-zova/zova-core/package.json +3 -3
  127. package/zova/packages-zova/zova-core/src/core/sys/resource.ts +1 -1
  128. package/zova/packages-zova/zova-core/src/core/sys/util.ts +8 -0
  129. package/zova/pnpm-lock.yaml +570 -11
  130. package/zova/src/front/config/config/config.cabloyCommerce.ts +0 -6
  131. package/zova/src/suite/a-commerce/modules/commerce-catalog/package.json +3 -1
  132. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/schemas.ts +236 -36
  133. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/types.ts +4962 -2402
  134. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
  135. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
  136. package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +8 -3
  137. package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +6 -7
  138. package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +1 -3
  139. package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +2 -3
  140. package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +13 -20
  141. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +3 -9
  142. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +9 -11
  143. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +0 -1
  144. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +6 -4
  145. package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +78 -17
  146. package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +10 -13
  147. package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +3 -3
  148. package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +31 -1
  149. package/zova/src/suite/a-home/modules/home-api/src/api/homeBaseSiteCatalog.ts +24 -0
  150. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +329 -17
  151. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +5866 -1829
  152. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeBaseSiteCatalog.ts +13 -0
  153. package/zova/src/suite/a-home/modules/home-base/src/component/page/controller.tsx +1 -0
  154. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +0 -1
  155. package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/detail.ts +1 -0
  156. package/zova/src/suite/cabloy-basic/modules/basic-markdown/package.json +61 -0
  157. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/formFieldMarkdown.ts +36 -0
  158. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/markdownHtml.ts +36 -0
  159. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/index.ts +147 -0
  160. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/this.ts +2 -0
  161. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/controller.tsx +111 -0
  162. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/render.tsx +40 -0
  163. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/style.ts +13 -0
  164. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/controller.tsx +17 -0
  165. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/render.tsx +9 -0
  166. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/style.ts +13 -0
  167. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/index.ts +1 -0
  168. package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/richTextContentStyle.ts +142 -0
  169. package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.build.json +13 -0
  170. package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.json +5 -0
  171. package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +25 -3
  172. package/zova/src/suite/cabloy-basic/modules/basic-page/src/types/page.ts +1 -0
  173. package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/pageEntry.ts +1 -0
  174. package/zova/src/suite/cabloy-basic/package.json +1 -0
  175. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
  176. package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +7 -0
  177. package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
  178. package/zova/src/suite-vendor/a-zova/modules/a-logger/package.json +2 -2
  179. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  180. package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +3 -2
  181. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  182. package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrProfile.test.ts +88 -0
  183. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  184. package/zova/src/suite-vendor/a-zova/package.json +5 -5
  185. package/.claude/scheduled_tasks.lock +0 -1
@@ -29,6 +29,16 @@ function collectPageErrors(page: Page) {
29
29
  return errors;
30
30
  }
31
31
 
32
+ function collectConsoleErrors(page: Page) {
33
+ const errors: string[] = [];
34
+ page.on('console', message => {
35
+ if (message.type() === 'error' || /hydration mismatch/i.test(message.text())) {
36
+ errors.push(message.text());
37
+ }
38
+ });
39
+ return errors;
40
+ }
41
+
32
42
  function waitForApiResponse(page: Page, method: string, path: string | RegExp) {
33
43
  return page.waitForResponse(response => {
34
44
  const url = new URL(response.url());
@@ -161,8 +171,14 @@ async function createAddressThroughCustomerPage(
161
171
  const page = await context.newPage();
162
172
  const pageErrors = collectPageErrors(page);
163
173
 
164
- await login(page, '/commerce/address', customer.username, customer.password, 'commerce');
165
- await expect(page).toHaveURL(/\/commerce\/address(?:\?|$)/);
174
+ await login(
175
+ page,
176
+ '/commerce/commerce/member/address',
177
+ customer.username,
178
+ customer.password,
179
+ 'commerce',
180
+ );
181
+ await expect(page).toHaveURL(/\/commerce\/commerce\/member\/address(?:\?|$)/);
166
182
  await expect(page.getByRole('heading', { name: 'Addresses' })).toBeVisible();
167
183
  await expect(page.getByText('No addresses yet.')).toBeVisible();
168
184
 
@@ -243,7 +259,13 @@ test(
243
259
  'true',
244
260
  );
245
261
 
246
- await login(page, '/commerce/address', customer.username, customer.password, 'commerce');
262
+ await login(
263
+ page,
264
+ '/commerce/commerce/member/address',
265
+ customer.username,
266
+ customer.password,
267
+ 'commerce',
268
+ );
247
269
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
248
270
  await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
249
271
  expect(pageErrors).toEqual([]);
@@ -254,6 +276,165 @@ test(
254
276
  },
255
277
  );
256
278
 
279
+ test(
280
+ 'Commerce theme: session cookie mirrors public local preference without hydration mismatch',
281
+ { tag: ['@web', '@ssr', '@theme'] },
282
+ async ({ browser, request }, testInfo) => {
283
+ const customer = await registerCustomer(request, testInfo);
284
+ const context = await browser.newContext();
285
+ const page = await context.newPage();
286
+ const pageErrors = collectPageErrors(page);
287
+ const consoleErrors = collectConsoleErrors(page);
288
+ try {
289
+ await login(
290
+ page,
291
+ '/commerce/commerce/member/address',
292
+ customer.username,
293
+ customer.password,
294
+ 'commerce',
295
+ );
296
+ await context.addCookies([
297
+ {
298
+ name: 'themedark',
299
+ value: 'false',
300
+ url: new URL(page.url()).origin,
301
+ },
302
+ ]);
303
+ await page.reload({ waitUntil: 'load' });
304
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
305
+ await expect(page.locator('body')).toHaveAttribute('data-theme', 'light');
306
+ await expect.poll(() => page.evaluate(() => localStorage.getItem('themedark'))).toBe('false');
307
+ expect(pageErrors).toEqual([]);
308
+ expect(consoleErrors).toEqual([]);
309
+
310
+ const publicContext = await browser.newContext();
311
+ await publicContext.addInitScript(() => localStorage.setItem('themedark', 'false'));
312
+ const publicPage = await publicContext.newPage();
313
+ const publicPageErrors = collectPageErrors(publicPage);
314
+ const publicConsoleErrors = collectConsoleErrors(publicPage);
315
+ try {
316
+ await publicPage.goto('/commerce', { waitUntil: 'load' });
317
+ await expect(publicPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
318
+ await expect(publicPage.locator('body')).toHaveAttribute('data-theme', 'light');
319
+ await expect
320
+ .poll(() => publicPage.evaluate(() => localStorage.getItem('themedark')))
321
+ .toBe('false');
322
+ expect(
323
+ (await publicContext.cookies()).find(cookie => cookie.name === 'themedark')?.value,
324
+ ).toBe('false');
325
+ expect(publicPageErrors).toEqual([]);
326
+ expect(publicConsoleErrors).toEqual([]);
327
+ } finally {
328
+ await publicContext.close();
329
+ }
330
+ } finally {
331
+ await context.close();
332
+ }
333
+ },
334
+ );
335
+
336
+ test(
337
+ 'Commerce theme: public auto preference mirrors resolved media value without hydration mismatch',
338
+ { tag: ['@web', '@ssr', '@theme'] },
339
+ async ({ browser }) => {
340
+ const context = await browser.newContext();
341
+ await context.addInitScript(() => localStorage.setItem('themedark', JSON.stringify('auto')));
342
+ const page = await context.newPage();
343
+ await page.emulateMedia({ colorScheme: 'dark' });
344
+ const pageErrors = collectPageErrors(page);
345
+ const consoleErrors = collectConsoleErrors(page);
346
+ try {
347
+ await page.goto('/commerce', { waitUntil: 'load' });
348
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
349
+ await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
350
+ await expect
351
+ .poll(() => page.evaluate(() => localStorage.getItem('themedark')))
352
+ .toBe('"auto"');
353
+ expect((await context.cookies()).find(cookie => cookie.name === 'themedark')?.value).toBe(
354
+ 'true',
355
+ );
356
+ expect(pageErrors).toEqual([]);
357
+ expect(consoleErrors).toEqual([]);
358
+ } finally {
359
+ await context.close();
360
+ }
361
+ },
362
+ );
363
+
364
+ test(
365
+ 'Commerce public SSR: theme cookie does not change cacheable HTML',
366
+ { tag: ['@web', '@ssr', '@theme'] },
367
+ async ({ request }) => {
368
+ const responses = await Promise.all([
369
+ request.get('/commerce'),
370
+ request.get('/commerce', { headers: { Cookie: 'themedark=true' } }),
371
+ request.get('/commerce', { headers: { Cookie: 'themedark=false' } }),
372
+ ]);
373
+ const html = await Promise.all(responses.map(response => response.text()));
374
+ for (const [index, response] of responses.entries()) {
375
+ expect(response.status(), `public response ${index}`).toBe(200);
376
+ expect(response.headers()['cache-control'], `public response ${index}`).toBe(
377
+ 'public, max-age=600',
378
+ );
379
+ expect(html[index].toLowerCase()).not.toContain('data-zova-hydrated');
380
+ expect(html[index]).toContain('data-ssr-theme-dark-false="light"');
381
+ expect(html[index]).toContain('data-ssr-theme-dark-true="dark"');
382
+ expect(html[index]).not.toContain('ssr_cookie_themedark');
383
+ expect(html[index]).toContain("window.matchMedia('(prefers-color-scheme: dark)')");
384
+ expect(html[index]).not.toMatch(/<body[^>]*data-theme="(?:dark|light)"/);
385
+ }
386
+ },
387
+ );
388
+
389
+ test(
390
+ 'Commerce session SSR: theme cookie selects raw server theme',
391
+ { tag: ['@web', '@ssr', '@theme'] },
392
+ async ({ browser, request }, testInfo) => {
393
+ const customer = await registerCustomer(request, testInfo);
394
+ const context = await browser.newContext();
395
+ const page = await context.newPage();
396
+ try {
397
+ await login(
398
+ page,
399
+ '/commerce/commerce/member/address',
400
+ customer.username,
401
+ customer.password,
402
+ 'commerce',
403
+ );
404
+ const origin = new URL(page.url()).origin;
405
+ const authCookies = (await context.cookies()).filter(cookie => cookie.name !== 'themedark');
406
+ const getSessionHtml = async (value: string) => {
407
+ const sessionContext = await browser.newContext();
408
+ try {
409
+ await sessionContext.addCookies([
410
+ ...authCookies.map(({ domain, path, ...cookie }) => ({ ...cookie, url: origin })),
411
+ { name: 'themedark', value, url: origin },
412
+ ]);
413
+ const response = await sessionContext.request.get('/commerce/commerce/member/address');
414
+ return { response, html: await response.text() };
415
+ } finally {
416
+ await sessionContext.close();
417
+ }
418
+ };
419
+ for (const [value, theme] of [
420
+ ['true', 'dark'],
421
+ ['false', 'light'],
422
+ ] as const) {
423
+ const { response, html } = await getSessionHtml(value);
424
+ expect(response.status()).toBe(200);
425
+ expect(response.headers()['cache-control']).toBe('private, no-store');
426
+ expect(html).toContain(`data-theme="${theme}"`);
427
+ expect(html).not.toContain('data-ssr-theme-dark-false');
428
+ expect(html).not.toContain('data-ssr-theme-dark-true');
429
+ expect(html).not.toContain("window.matchMedia('(prefers-color-scheme: dark)')");
430
+ expect(html).toContain('Addresses');
431
+ }
432
+ } finally {
433
+ await context.close();
434
+ }
435
+ },
436
+ );
437
+
257
438
  test(
258
439
  'Commerce catalogue: public sellable inventory renders after hydration',
259
440
  { tag: ['@web', '@flow'] },
@@ -307,7 +488,7 @@ test(
307
488
  await expect(page.getByText('库存 24 件')).toBeVisible();
308
489
  await expect(page.getByRole('link', { name: '购物车' })).toHaveAttribute(
309
490
  'href',
310
- '/commerce/zh-cn/cart',
491
+ '/commerce/commerce/trade/cart',
311
492
  );
312
493
  expect(pageErrors).toEqual([]);
313
494
  },
@@ -331,7 +512,7 @@ test(
331
512
  await expect(page).toHaveURL(productPath);
332
513
  await expect(page.getByRole('heading', { name: 'Wireless Headphones' })).toBeVisible();
333
514
 
334
- const cartLink = page.locator('a[href="/commerce/cart"]');
515
+ const cartLink = page.locator('a[href="/commerce/commerce/trade/cart"]');
335
516
  await expect(cartLink).toHaveCount(1);
336
517
  await expect(cartLink.locator('.badge')).toHaveCount(0);
337
518
  expect(pageErrors).toEqual([]);
@@ -342,8 +523,8 @@ test(
342
523
  'Commerce Address: session SSR protects an anonymous document request',
343
524
  { tag: ['@web', '@cart'] },
344
525
  async ({ page, request }) => {
345
- const path = '/commerce/address';
346
- const routePath = '/address';
526
+ const path = '/commerce/commerce/member/address';
527
+ const routePath = '/commerce/member/address';
347
528
  const response = await request.get(path, { maxRedirects: 0 });
348
529
  expect(response.status()).toBe(302);
349
530
  expect(response.headers()['cache-control']).toBe('private, no-store');
@@ -374,7 +555,7 @@ test(
374
555
  testInfo,
375
556
  );
376
557
  try {
377
- const addressResponse = await context.request.get('/commerce/address');
558
+ const addressResponse = await context.request.get('/commerce/commerce/member/address');
378
559
  expect(addressResponse.ok()).toBeTruthy();
379
560
  expect(addressResponse.headers()['cache-control']).toBe('private, no-store');
380
561
  const addressHtml = await addressResponse.text();
@@ -387,7 +568,9 @@ test(
387
568
  const coldPage = await context.newPage();
388
569
  try {
389
570
  const coldPageErrors = collectPageErrors(coldPage);
390
- const coldResponse = await coldPage.goto('/commerce/address', { waitUntil: 'load' });
571
+ const coldResponse = await coldPage.goto('/commerce/commerce/member/address', {
572
+ waitUntil: 'load',
573
+ });
391
574
  expect(coldResponse?.ok()).toBeTruthy();
392
575
  await expect(coldPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
393
576
  await expect(coldPage.getByRole('heading', { name: 'Addresses' })).toBeVisible();
@@ -454,7 +637,7 @@ test(
454
637
  await addResponse;
455
638
 
456
639
  await page.getByRole('link', { name: /^Cart/ }).click();
457
- await expect(page).toHaveURL(/\/commerce\/cart(?:\/|$)/);
640
+ await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
458
641
  await expect(page.getByRole('heading', { name: 'Cart' })).toBeVisible();
459
642
  const cartItem = page.locator('article').filter({ has: page.getByRole('spinbutton') });
460
643
  await expect(cartItem).toContainText('COF-SET-01');
@@ -462,7 +645,7 @@ test(
462
645
  await expect(cartItem.getByRole('spinbutton')).toHaveValue('1');
463
646
 
464
647
  await page.getByRole('link', { name: 'Checkout', exact: true }).click();
465
- await expect(page).toHaveURL(/\/commerce\/checkout(?:\/|$)/);
648
+ await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/checkout(?:\/|$)/);
466
649
  await expect(page.getByRole('heading', { name: 'Checkout' })).toBeVisible();
467
650
  const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
468
651
  await expect(addressChoice).toBeVisible();
@@ -485,7 +668,9 @@ test(
485
668
  expect(checkout.currency).toBe('USD');
486
669
  expect(checkout.payableTotalCents).toBe(4599);
487
670
  await expect(page).toHaveURL(
488
- new RegExp(`/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}(?:/|$)`),
671
+ new RegExp(
672
+ `/commerce/commerce/trade/payment/${checkout.paymentSessionId}/${checkout.orderId}(?:/|$)`,
673
+ ),
489
674
  );
490
675
  await expect(page.getByRole('heading', { name: 'Payment', exact: true })).toBeVisible();
491
676
 
@@ -505,10 +690,13 @@ test(
505
690
  );
506
691
  await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
507
692
  expect((await completeResponse).ok()).toBeTruthy();
508
- await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
509
- timeout: 40_000,
510
- });
511
- const orderPath = `/commerce/order/${checkout.orderId}`;
693
+ await expect(page).toHaveURL(
694
+ new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
695
+ {
696
+ timeout: 40_000,
697
+ },
698
+ );
699
+ const orderPath = `/commerce/commerce/trade/order/${checkout.orderId}`;
512
700
  const orderResponse = await context.request.get(orderPath);
513
701
  expect(orderResponse.ok()).toBeTruthy();
514
702
  expect(orderResponse.headers()['cache-control']).toBe('private, no-store');
@@ -604,9 +792,11 @@ test(
604
792
  await expect(page.getByText(shipmentCarrier)).toBeVisible();
605
793
  await expect(page.getByText(shipmentTrackingNumber)).toBeVisible();
606
794
 
607
- const ordersDocumentResponse = await page.goto('/commerce/orders', { waitUntil: 'load' });
795
+ const ordersDocumentResponse = await page.goto('/commerce/commerce/trade/orders', {
796
+ waitUntil: 'load',
797
+ });
608
798
  expect(ordersDocumentResponse?.ok()).toBeTruthy();
609
- await expect(page).toHaveURL(/\/commerce\/orders(?:\/|$)/);
799
+ await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/orders(?:\/|$)/);
610
800
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
611
801
  await expect(page.getByRole('heading', { name: 'My orders' })).toBeVisible();
612
802
  const orderCard = page
@@ -620,7 +810,9 @@ test(
620
810
  new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`),
621
811
  );
622
812
  await orderCard.getByRole('button', { name: 'View', exact: true }).click();
623
- await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
813
+ await expect(page).toHaveURL(
814
+ new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
815
+ );
624
816
  await historyDetailResponse;
625
817
  await expect(page.getByRole('heading', { name: `Order #${checkout.orderId}` })).toBeVisible();
626
818
  await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible();
@@ -684,11 +876,14 @@ test(
684
876
  );
685
877
  await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
686
878
  expect((await completeResponse).ok()).toBeTruthy();
687
- await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
688
- timeout: 40_000,
689
- });
879
+ await expect(page).toHaveURL(
880
+ new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
881
+ {
882
+ timeout: 40_000,
883
+ },
884
+ );
690
885
 
691
- const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=return`;
886
+ const callbackPath = `/commerce/commerce/trade/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=return`;
692
887
  await page.evaluate(() => {
693
888
  localStorage.clear();
694
889
  sessionStorage.clear();
@@ -721,7 +916,9 @@ test(
721
916
  expect(documentResponse?.ok()).toBeTruthy();
722
917
  expect((await reconcileResponse).ok()).toBeTruthy();
723
918
  await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
724
- await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
919
+ await expect(page).toHaveURL(
920
+ new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
921
+ );
725
922
  expect(reconcileCount).toBe(1);
726
923
  expect(paymentViewCount).toBeLessThanOrEqual(10);
727
924
  expect(orderViewCount).toBeLessThanOrEqual(5);
@@ -773,7 +970,7 @@ test(
773
970
  expectTableIdentity(checkout.paymentSessionId);
774
971
  expect(checkout.state).toBe('awaiting_payment');
775
972
 
776
- const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=cancel`;
973
+ const callbackPath = `/commerce/commerce/trade/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=cancel`;
777
974
  const paymentSessionPath = new RegExp(
778
975
  `/api/pay/payment-session/${checkout.paymentSessionId}$`,
779
976
  );
@@ -850,8 +1047,9 @@ test(
850
1047
  expect(reconcileCount).toBe(1);
851
1048
 
852
1049
  await page.getByRole('button', { name: 'Open order', exact: true }).click();
853
- await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
854
- await expect(page.getByText('awaiting_payment · $45.99')).toBeVisible();
1050
+ await expect(page).toHaveURL(
1051
+ new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
1052
+ );
855
1053
  expect(pageErrors).toEqual([]);
856
1054
  } finally {
857
1055
  await context.close().catch(() => {});
@@ -881,9 +1079,9 @@ test(
881
1079
  await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
882
1080
  expect((await addResponse).ok()).toBeTruthy();
883
1081
  await page.getByRole('link', { name: /^Cart/ }).click();
884
- await expect(page).toHaveURL(/\/commerce\/cart(?:\/|$)/);
1082
+ await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
885
1083
  await page.getByRole('link', { name: 'Checkout', exact: true }).click();
886
- await expect(page).toHaveURL(/\/commerce\/checkout(?:\/|$)/);
1084
+ await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/checkout(?:\/|$)/);
887
1085
  const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
888
1086
  await expect(addressChoice).toBeVisible();
889
1087
  await addressChoice.check();
@@ -911,9 +1109,12 @@ test(
911
1109
  );
912
1110
  await page.getByRole('button', { name: 'Cancel payment', exact: true }).click();
913
1111
  expect((await cancelResponse).ok()).toBeTruthy();
914
- await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
915
- timeout: 40_000,
916
- });
1112
+ await expect(page).toHaveURL(
1113
+ new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
1114
+ {
1115
+ timeout: 40_000,
1116
+ },
1117
+ );
917
1118
  await expect(page.getByText('cancelled · $45.99')).toBeVisible();
918
1119
  await expect(page.getByRole('alert')).toHaveCount(0);
919
1120
  expect(pageErrors).toEqual([]);
@@ -948,9 +1149,9 @@ test(
948
1149
  await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
949
1150
  await addResponse;
950
1151
  await page.getByRole('link', { name: /^Cart/ }).click();
951
- await expect(page).toHaveURL(/\/commerce\/cart(?:\/|$)/);
1152
+ await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
952
1153
  await page.getByRole('link', { name: 'Checkout', exact: true }).click();
953
- await expect(page).toHaveURL(/\/commerce\/checkout(?:\/|$)/);
1154
+ await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/checkout(?:\/|$)/);
954
1155
  const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
955
1156
  await addressChoice.check();
956
1157
  await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
@@ -1056,13 +1257,9 @@ test(
1056
1257
  await adminContext.close().catch(() => {});
1057
1258
  }
1058
1259
 
1059
- const refundedDetailResponse = waitForApiResponse(
1060
- page,
1061
- 'GET',
1062
- new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`),
1063
- );
1260
+ // The session SSR reload serializes the refreshed order detail, so no browser API request is
1261
+ // required before the page renders the refunded state.
1064
1262
  await page.reload({ waitUntil: 'load' });
1065
- await refundedDetailResponse;
1066
1263
  await expect(page.getByText('refunded · $45.99')).toBeVisible();
1067
1264
  await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible();
1068
1265
  await expect(page.getByRole('heading', { name: 'Shipment' })).toHaveCount(0);
@@ -1424,6 +1621,264 @@ test(
1424
1621
  },
1425
1622
  );
1426
1623
 
1624
+ test(
1625
+ 'Commerce Product Content: SSR renders stored safe HTML and hydrates without errors',
1626
+ { tag: ['@web', '@flow', '@product'] },
1627
+ async ({ browser }, testInfo) => {
1628
+ test.setTimeout(60_000);
1629
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
1630
+ const categoryName = `E2E Product Content Web Category ${suffix}`;
1631
+ const productTitle = `E2E Product Content Web ${suffix}`;
1632
+ const skuCode = `E2E-PC-WEB-${suffix}`;
1633
+ const markdown = `# Product details ${suffix}\n\nA **durable** product.\n\n- [ ] Open task\n- [x] Completed task`;
1634
+ const unsafeHtml = '<a href="javascript:alert(\'unsafe\')">unsafe</a>';
1635
+ const adminContext = await browser.newContext();
1636
+ const adminPage = await adminContext.newPage();
1637
+ const webContext = await browser.newContext();
1638
+ const webPage = await webContext.newPage();
1639
+ const webPageErrors = collectPageErrors(webPage);
1640
+ const webConsoleErrors = collectConsoleErrors(webPage);
1641
+ const categoryActionPath = '/api/commerce/catalog/category';
1642
+ const productActionPath = '/api/commerce/catalog/product';
1643
+ const skuActionPath = '/api/commerce/catalog/sku';
1644
+ const stockAdjustPath = '/api/commerce/trade/stockBalance/adjustStock';
1645
+ let categoryId: number | string | undefined;
1646
+ let productId: number | string | undefined;
1647
+ let skuId: number | string | undefined;
1648
+ let headers: { Authorization: string } | undefined;
1649
+ try {
1650
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
1651
+ const accessToken = (await adminContext.cookies()).find(
1652
+ cookie => cookie.name === 'token',
1653
+ )?.value;
1654
+ expect(accessToken).toBeTruthy();
1655
+ headers = { Authorization: `Bearer ${accessToken}` };
1656
+
1657
+ const categoryResponse = await adminPage.request.post(categoryActionPath, {
1658
+ data: { name: categoryName, published: true },
1659
+ headers,
1660
+ });
1661
+ expect(categoryResponse.ok()).toBeTruthy();
1662
+ categoryId = (await categoryResponse.json()).data;
1663
+ expectTableIdentity(categoryId);
1664
+
1665
+ const productResponse = await adminPage.request.post(productActionPath, {
1666
+ data: {
1667
+ categoryId,
1668
+ title: productTitle,
1669
+ published: true,
1670
+ productContentForm: {
1671
+ descriptionMarkdown: `${markdown}\n\n${unsafeHtml}`,
1672
+ },
1673
+ },
1674
+ headers,
1675
+ });
1676
+ expect(productResponse.ok()).toBeTruthy();
1677
+ productId = (await productResponse.json()).data;
1678
+ expectTableIdentity(productId);
1679
+
1680
+ const skuResponse = await adminPage.request.post(skuActionPath, {
1681
+ data: { code: skuCode, productId, priceCents: 1999, lifecycle: 'active' },
1682
+ headers,
1683
+ });
1684
+ expect(skuResponse.ok()).toBeTruthy();
1685
+ skuId = (await skuResponse.json()).data;
1686
+ expectTableIdentity(skuId);
1687
+
1688
+ const stockResponse = await adminPage.request.post(stockAdjustPath, {
1689
+ data: {
1690
+ skuId,
1691
+ delta: 1,
1692
+ reason: 'Product content SSR E2E fixture',
1693
+ correlationId: `product-content-web-${suffix}`,
1694
+ },
1695
+ headers,
1696
+ });
1697
+ expect(stockResponse.ok()).toBeTruthy();
1698
+
1699
+ const productPath = `/commerce/product/${productId}`;
1700
+ const ssrResponse = await webPage.request.get(productPath);
1701
+ expect(ssrResponse.ok()).toBeTruthy();
1702
+ const ssrHtml = await ssrResponse.text();
1703
+ expect(ssrHtml.toLowerCase()).not.toContain('data-zova-hydrated');
1704
+ expect(ssrHtml).toContain(`<h1>Product details ${suffix}</h1>`);
1705
+ expect(ssrHtml).toContain('<strong>durable</strong>');
1706
+ expect(ssrHtml).toContain('<ul data-type="taskList">');
1707
+ expect(ssrHtml).toContain('<input type="checkbox" disabled="disabled" />');
1708
+ expect(ssrHtml).toContain('<input type="checkbox" checked="checked" disabled="disabled" />');
1709
+ expect(ssrHtml).not.toMatch(/<a[^>]+javascript:/i);
1710
+
1711
+ const documentResponse = await webPage.goto(productPath, { waitUntil: 'load' });
1712
+ expect(documentResponse?.ok()).toBeTruthy();
1713
+ await expect(webPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
1714
+ await expect(webPage.getByRole('heading', { name: productTitle })).toBeVisible();
1715
+ await expect(webPage.locator('.product-description h1')).toHaveText(
1716
+ `Product details ${suffix}`,
1717
+ );
1718
+ await expect(webPage.locator('.product-description strong')).toHaveText('durable');
1719
+ const description = webPage.locator('.product-description');
1720
+ await expect(description).not.toHaveClass(/\bprose\b/);
1721
+ await expect(description.locator('ul[data-type="taskList"]')).toHaveCount(1);
1722
+ await expect(description.locator('input[type="checkbox"]')).toHaveCount(2);
1723
+ await expect(description.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
1724
+ await expect(description.locator('input[type="checkbox"]').nth(0)).toBeDisabled();
1725
+ await expect(description.locator('input[type="checkbox"]').nth(1)).toBeChecked();
1726
+ await expect(description.locator('input[type="checkbox"]').nth(1)).toBeDisabled();
1727
+ expect(webPageErrors).toEqual([]);
1728
+ expect(webConsoleErrors).toEqual([]);
1729
+ } finally {
1730
+ if (skuId && headers) {
1731
+ const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
1732
+ expect(response.ok()).toBeTruthy();
1733
+ }
1734
+ if (productId && headers) {
1735
+ const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
1736
+ headers,
1737
+ });
1738
+ expect(response.ok()).toBeTruthy();
1739
+ }
1740
+ if (categoryId && headers) {
1741
+ const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
1742
+ headers,
1743
+ });
1744
+ expect(response.ok()).toBeTruthy();
1745
+ }
1746
+ await webContext.close().catch(() => {});
1747
+ await adminContext.close().catch(() => {});
1748
+ }
1749
+ },
1750
+ );
1751
+
1752
+ test(
1753
+ 'ATP-SPC-02: Product Content opens the rich-text editor and saves Markdown',
1754
+ { tag: ['@admin', '@flow', '@product'] },
1755
+ async ({ browser }, testInfo) => {
1756
+ test.setTimeout(60_000);
1757
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
1758
+ const categoryName = `E2E Product Content Category ${suffix}`;
1759
+ const productTitle = `E2E Product Content ${suffix}`;
1760
+ const markdown = `# Product content ${suffix}
1761
+
1762
+ - First benefit
1763
+ - Second benefit
1764
+
1765
+ - [ ] Open task
1766
+ - [x] Completed task
1767
+
1768
+ > Product quote
1769
+
1770
+ \`\`\`ts
1771
+ const product = '${suffix}';
1772
+ \`\`\`
1773
+
1774
+ ==highlighted text==
1775
+
1776
+ | Feature | Value |
1777
+ | --- | --- |
1778
+ | Material | Durable |`;
1779
+ const adminContext = await browser.newContext();
1780
+ const adminPage = await adminContext.newPage();
1781
+ const adminPageErrors = collectPageErrors(adminPage);
1782
+ const adminConsoleErrors = collectConsoleErrors(adminPage);
1783
+ const productListPath = '/commerce-admin/rest/resource/commerce-catalog%3Aproduct';
1784
+ const categoryActionPath = '/api/commerce/catalog/category';
1785
+ const productActionPath = '/api/commerce/catalog/product';
1786
+ let categoryId: number | string | undefined;
1787
+ let productId: number | string | undefined;
1788
+ let headers: { Authorization: string } | undefined;
1789
+ try {
1790
+ await adminPage.setViewportSize({ width: 1440, height: 900 });
1791
+ await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
1792
+ const accessToken = (await adminContext.cookies()).find(
1793
+ cookie => cookie.name === 'token',
1794
+ )?.value;
1795
+ expect(accessToken).toBeTruthy();
1796
+ headers = { Authorization: `Bearer ${accessToken}` };
1797
+
1798
+ const categoryResponse = await adminPage.request.post(categoryActionPath, {
1799
+ data: { name: categoryName, published: false },
1800
+ headers,
1801
+ });
1802
+ expect(categoryResponse.ok()).toBeTruthy();
1803
+ categoryId = (await categoryResponse.json()).data;
1804
+ expectTableIdentity(categoryId);
1805
+
1806
+ const productResponse = await adminPage.request.post(productActionPath, {
1807
+ data: {
1808
+ categoryId,
1809
+ title: productTitle,
1810
+ published: false,
1811
+ productContentForm: { descriptionMarkdown: markdown },
1812
+ },
1813
+ headers,
1814
+ });
1815
+ expect(productResponse.ok()).toBeTruthy();
1816
+ productId = (await productResponse.json()).data;
1817
+ expectTableIdentity(productId);
1818
+
1819
+ await adminPage.goto(`${productListPath}/${productId}/edit`, { waitUntil: 'load' });
1820
+ await expect(adminPage.locator('html')).toHaveAttribute(
1821
+ 'data-zova-hydrated',
1822
+ 'commerceAdmin',
1823
+ );
1824
+ expect(adminPageErrors).toEqual([]);
1825
+ expect(adminConsoleErrors).toEqual([]);
1826
+ const editor = adminPage.locator('[contenteditable="true"]');
1827
+ await expect(editor).toBeVisible();
1828
+ const editorSurface = editor.locator('..');
1829
+ await expect(editorSurface).not.toHaveClass(/\bprose\b/);
1830
+ const editorSurfaceClass = await editorSurface.getAttribute('class');
1831
+ expect(editorSurfaceClass?.trim()).toBeTruthy();
1832
+ await editorSurface.click({ position: { x: 20, y: 280 } });
1833
+ await expect(editor).toBeFocused();
1834
+ await expect(editor.locator('h1')).toHaveText(`Product content ${suffix}`);
1835
+ await expect(editor.locator('ul:not([data-type="taskList"])')).toHaveCount(1);
1836
+ await expect(editor.locator('blockquote')).toHaveText('Product quote');
1837
+ await expect(editor.locator('pre code')).toHaveText(`const product = '${suffix}';`);
1838
+ await expect(editor.locator('mark')).toHaveText('highlighted text');
1839
+ await expect(editor.locator('table th')).toHaveCount(2);
1840
+ await expect(editor.locator('table td')).toHaveCount(2);
1841
+ await expect(editor.locator('h1')).toHaveCSS('font-weight', '700');
1842
+ await expect(editor.locator('ul:not([data-type="taskList"])')).toHaveCSS(
1843
+ 'list-style-type',
1844
+ 'disc',
1845
+ );
1846
+ await expect(editor.locator('ul[data-type="taskList"]')).toHaveCount(1);
1847
+ await expect(editor.locator('input[type="checkbox"]')).toHaveCount(2);
1848
+ await expect(editor.locator('input[type="checkbox"]').nth(0)).not.toBeChecked();
1849
+ await expect(editor.locator('input[type="checkbox"]').nth(1)).toBeChecked();
1850
+ await expect(editor.locator('blockquote')).toHaveCSS('border-left-width', '4px');
1851
+ await expect(editor.locator('pre')).toHaveCSS('overflow-x', 'auto');
1852
+ await expect(editor.locator('table th').first()).toHaveCSS('font-weight', '600');
1853
+ await adminPage.getByRole('button', { name: 'Submit', exact: true }).click();
1854
+
1855
+ const productResponseAfterUpdate = await adminPage.request.get(
1856
+ `${productActionPath}/${productId}`,
1857
+ { headers },
1858
+ );
1859
+ expect(productResponseAfterUpdate.ok()).toBeTruthy();
1860
+ expect((await productResponseAfterUpdate.json()).productContentForm.descriptionMarkdown).toBe(
1861
+ markdown,
1862
+ );
1863
+ expect(adminPageErrors).toEqual([]);
1864
+ } finally {
1865
+ if (productId && headers) {
1866
+ const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
1867
+ headers,
1868
+ });
1869
+ expect(response.ok()).toBeTruthy();
1870
+ }
1871
+ if (categoryId && headers) {
1872
+ const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
1873
+ headers,
1874
+ });
1875
+ expect(response.ok()).toBeTruthy();
1876
+ }
1877
+ await adminContext.close().catch(() => {});
1878
+ }
1879
+ },
1880
+ );
1881
+
1427
1882
  test(
1428
1883
  'ATP-SPC-02: SKU renders semantic Admin currency and lifecycle controls',
1429
1884
  { tag: ['@admin', '@flow', '@sku'] },
@@ -1592,7 +2047,7 @@ test(
1592
2047
  'Commerce Cart: anonymous browser is redirected to login',
1593
2048
  { tag: ['@web', '@cart'] },
1594
2049
  async ({ page, request }) => {
1595
- const path = '/commerce/cart';
2050
+ const path = '/commerce/commerce/trade/cart';
1596
2051
  const response = await request.get(path);
1597
2052
  expect(response.ok()).toBeTruthy();
1598
2053
  expect((await response.text()).toLowerCase()).not.toContain('data-zova-hydrated');
@@ -1610,15 +2065,16 @@ test(
1610
2065
  { tag: ['@web', '@flow'] },
1611
2066
  async ({ page, request }) => {
1612
2067
  const routes = [
1613
- ['/commerce/checkout', '/checkout'],
1614
- ['/commerce/payment/1/1', '/payment/1/1'],
1615
- ['/commerce/orders', '/orders'],
1616
- ['/commerce/order/1', '/order/1'],
2068
+ ['/commerce/commerce/trade/checkout', '/commerce/trade/checkout'],
2069
+ ['/commerce/commerce/trade/payment/1/1', '/commerce/trade/payment/1/1'],
2070
+ ['/commerce/commerce/trade/orders', '/commerce/trade/orders'],
2071
+ ['/commerce/commerce/trade/order/1', '/commerce/trade/order/1'],
1617
2072
  ] as const;
1618
2073
  for (const [path, routePath] of routes) {
1619
2074
  const response = await request.get(path, { maxRedirects: 0 });
1620
- expect(response.status(), path).toBe(200);
1621
- expect(response.headers().location, path).toBeUndefined();
2075
+ expect(response.status(), path).toBe(302);
2076
+ expect(response.headers()['cache-control'], path).toBe('private, no-store');
2077
+ expect(response.headers().location, path).toMatch(/^\/commerce\/login\?(?:.*&)?returnTo=/);
1622
2078
  const html = await response.text();
1623
2079
  expect(html.toLowerCase(), path).not.toContain('data-zova-hydrated');
1624
2080
  expect(html, path).not.toContain('Payment Customer');