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.
- package/.cabloy-version +1 -1
- package/.claude/skills/cabloy-backend-scaffold/SKILL.md +9 -0
- package/.claude/skills/cabloy-zova-source-reading/SKILL.md +24 -1
- package/.claude/skills/cabloy-zova-source-reading/references/analysis-modes.md +12 -0
- package/.claude/skills/cabloy-zova-source-reading/references/core-reading-paths.md +28 -0
- package/.github/workflows/playwright-e2e.yml +2 -2
- package/.github/workflows/vona-test-pg.yml +1 -1
- package/.github/workflows/vona-test-sqlite3.yml +1 -1
- package/CHANGELOG.md +38 -0
- package/CLAUDE.md +1 -0
- package/cabloy-docs/.vitepress/config.mjs +7 -0
- package/cabloy-docs/backend/markdown-guide.md +189 -0
- package/cabloy-docs/frontend/component-guide.md +6 -0
- package/cabloy-docs/frontend/css-in-js-guide.md +6 -0
- package/cabloy-docs/frontend/markdown-guide.md +258 -0
- package/cabloy-docs/frontend/page-guide.md +48 -0
- package/cabloy-docs/frontend/page-params-guide.md +4 -2
- package/cabloy-docs/frontend/page-route-guide.md +2 -2
- package/cabloy-docs/frontend/zova-router-under-the-hood.md +2 -0
- package/cabloy-docs/frontend/zova-source-reading-map.md +53 -9
- package/cabloy-docs/fullstack/a-pay-payment-suite.md +333 -0
- package/cabloy-docs/fullstack/payment-sandbox-configuration.md +285 -0
- package/e2e/specs/a-commerce/commerce.spec.ts +503 -47
- package/e2e/specs/cabloy-basic/basic.spec.ts +30 -0
- package/package.json +1 -1
- package/vona/env/.env +2 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/cli/templates/init/error/boilerplate/config/errors.ts +6 -1
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudBasic/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
- package/vona/packages-cli/cli-set-api/cli/templates/tools/crudStart/boilerplate/src/entity/{{resourceName}}.tsx_ +2 -0
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-utils/localeutil/package.json +1 -1
- package/vona/packages-utils/localeutil/src/util.ts +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/patches/{zova-core@5.1.82.patch → zova-core@5.1.84.patch} +2 -2
- package/vona/pnpm-lock.yaml +826 -270
- package/vona/pnpm-workspace.yaml +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/package.json +1 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/.metadata/index.ts +99 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/bean/meta.version.ts +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/controller/product.ts +3 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentUpdate.tsx +14 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productContentView.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productCreate.tsx +48 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productPublicDetail.tsx +15 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productUpdate.tsx +48 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/productView.tsx +52 -3
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/productContent.tsx +36 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/product.ts +6 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/model/productContent.ts +22 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/service/product.ts +82 -6
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productContent.test.ts +271 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/productPresentation.test.ts +81 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/package.json +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/bean/payScene.commerceOrder.ts +32 -15
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +8 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/index.ts +8 -2
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/.metadata/locales.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/bean/ssrMenu.home.ts +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/en-us.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/config/locale/zh-cn.ts +3 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteweb/src/index.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/checkout.ts +13 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/providerReference.ts +18 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +43 -8
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +74 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/providerReference.test.ts +33 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +73 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/reservationExpiry.test.ts +2 -0
- package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +30 -0
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +7 -2
- package/vona/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +4 -1
- package/vona/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +4 -1
- package/vona/src/suite/a-home/modules/home-user/src/config/roles.ts +4 -4
- package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +2 -2
- package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +8 -1
- package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +6 -2
- package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/role.test.ts +4 -4
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/package.json +64 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/index.ts +54 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/.metadata/this.ts +2 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/bean/bean.markdown.ts +137 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.build.json +11 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-markdown/tsconfig.json +7 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/bean.ssr.ts +17 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +11 -3
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -1
- package/vona/src/suite-vendor/a-cabloy/tsconfig.json +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +8 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/lib/providerOperationFailure.ts +8 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +187 -29
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +4 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/bean/payProvider.paypal.ts +0 -6
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +2 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/bean/payProvider.stripe.ts +458 -22
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/index.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/lib/stripeGateway.ts +85 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/src/types/index.ts +2 -0
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +14 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/guard.passport.ts +4 -0
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +3 -2
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +4 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/packages-utils/logger/package.json +2 -2
- package/zova/packages-utils/zova-jsx/package.json +2 -2
- package/zova/packages-zova/zova/package.json +3 -3
- package/zova/packages-zova/zova-core/package.json +3 -3
- package/zova/packages-zova/zova-core/src/core/sys/resource.ts +1 -1
- package/zova/packages-zova/zova-core/src/core/sys/util.ts +8 -0
- package/zova/pnpm-lock.yaml +570 -11
- package/zova/src/front/config/config/config.cabloyCommerce.ts +0 -6
- package/zova/src/suite/a-commerce/modules/commerce-catalog/package.json +3 -1
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/schemas.ts +236 -36
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/api/openapi/types.ts +4962 -2402
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/page/product/controller.tsx +8 -3
- package/zova/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +6 -7
- package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +1 -3
- package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +2 -3
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +13 -20
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +3 -9
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +9 -11
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +0 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +6 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +78 -17
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +10 -13
- package/zova/src/suite/a-home/modules/home-api/mock/passport.fake.ts +3 -3
- package/zova/src/suite/a-home/modules/home-api/src/.metadata/index.ts +31 -1
- package/zova/src/suite/a-home/modules/home-api/src/api/homeBaseSiteCatalog.ts +24 -0
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/schemas.ts +329 -17
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +5866 -1829
- package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeBaseSiteCatalog.ts +13 -0
- package/zova/src/suite/a-home/modules/home-base/src/component/page/controller.tsx +1 -0
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +0 -1
- package/zova/src/suite/cabloy-basic/modules/basic-details/src/types/detail.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/package.json +61 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/formFieldMarkdown.ts +36 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/component/markdownHtml.ts +36 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/index.ts +147 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/.metadata/this.ts +2 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/controller.tsx +111 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/render.tsx +40 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/formFieldMarkdown/style.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/controller.tsx +17 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/render.tsx +9 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/component/markdownHtml/style.ts +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/index.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/src/lib/richTextContentStyle.ts +142 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.build.json +13 -0
- package/zova/src/suite/cabloy-basic/modules/basic-markdown/tsconfig.json +5 -0
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx +25 -3
- package/zova/src/suite/cabloy-basic/modules/basic-page/src/types/page.ts +1 -0
- package/zova/src/suite/cabloy-basic/modules/basic-pageentry/src/types/pageEntry.ts +1 -0
- package/zova/src/suite/cabloy-basic/package.json +1 -0
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/package.json +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/model/resource.ts +7 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-logger/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/bean/sys.router.ts +3 -2
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrProfile.test.ts +88 -0
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +5 -5
- 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(
|
|
165
|
-
|
|
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(
|
|
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/
|
|
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', {
|
|
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(
|
|
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(
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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', {
|
|
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(
|
|
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(
|
|
688
|
-
|
|
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(
|
|
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(
|
|
854
|
-
|
|
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(
|
|
915
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
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(
|
|
1621
|
-
expect(response.headers()
|
|
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');
|