cabloy 5.1.132 → 5.1.134
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/scheduled_tasks.lock +1 -0
- package/.claude/skills/cabloy-worktree-environment/SKILL.md +6 -6
- package/.claude/skills/cabloy-worktree-environment/evals/evals.json +2 -2
- package/.github/workflows/vona-test-pg.yml +1 -1
- package/.github/workflows/vona-test-sqlite3.yml +1 -1
- package/CHANGELOG.md +40 -0
- package/cabloy-docs/backend/dto-guide.md +39 -4
- package/cabloy-docs/backend/dto-infer-generation.md +29 -0
- package/cabloy-docs/backend/openapi-guide.md +16 -0
- package/cabloy-docs/backend/validation-guide.md +45 -0
- package/cabloy-docs/frontend/environment-config-guide.md +8 -15
- package/cabloy-docs/frontend/ioc-and-beans.md +20 -0
- package/cabloy-docs/frontend/navigation-guards-guide.md +3 -3
- package/cabloy-docs/frontend/page-route-guide.md +4 -4
- package/cabloy-docs/frontend/ssr-env.md +22 -29
- package/cabloy-docs/frontend/theme-guide.md +4 -4
- package/cabloy-docs/frontend/zova-router-under-the-hood.md +1 -1
- package/cabloy-docs/fullstack/deploy-cloudflare-docker.md +6 -6
- package/cabloy-docs/fullstack/parallel-worktree-environment.md +2 -2
- package/e2e/specs/a-commerce/commerce.spec.ts +540 -53
- package/e2e/specs/cabloy-basic/basic.spec.ts +30 -5
- package/package.json +4 -4
- package/vona/.claude/skills/detect-ssr-leak/SKILL.md +1 -1
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +336 -62
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuCreate.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuSelectResItem.tsx +13 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuUpdate.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/dto/skuView.tsx +13 -1
- package/vona/src/suite/a-commerce/modules/commerce-catalog/src/entity/sku.tsx +6 -0
- package/vona/src/suite/a-commerce/modules/commerce-catalog/test/skuPresentation.test.ts +227 -0
- package/vona/src/suite/a-commerce/modules/commerce-member/src/.metadata/index.ts +46 -36
- package/vona/src/suite/a-commerce/modules/commerce-member/src/dto/addressSelectResItem.tsx +5 -13
- package/vona/src/suite/a-commerce/modules/commerce-member/src/service/address.ts +16 -1
- package/vona/src/suite/a-commerce/modules/commerce-member/test/addressPresentation.test.ts +210 -0
- package/vona/src/suite/a-commerce/modules/commerce-payment/src/service/commercePayScene.ts +13 -2
- package/vona/src/suite/a-commerce/modules/commerce-payment/test/paymentAttempt.test.ts +12 -0
- package/vona/src/suite/a-commerce/modules/commerce-promotion/src/service/coupon.ts +7 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +45 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +23 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +23 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/order.ts +31 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineBase.tsx +56 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineResItem.tsx +17 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderAdminLineView.tsx +13 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderSelectResItem.tsx +26 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/orderView.tsx +197 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryAction.tsx +19 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/refundRecoveryView.tsx +70 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditSelectResItem.tsx +7 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockAuditView.tsx +21 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceSelectResItem.tsx +7 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/dto/stockBalanceView.tsx +3 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/entity/order.tsx +12 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +15 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/order.ts +215 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockAudit.ts +25 -2
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/stockBalance.ts +14 -3
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/checkoutReservation.test.ts +4 -4
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderPresentation.test.ts +358 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/orderSnapshot.test.ts +8 -8
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/paymentOutcome.test.ts +5 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/refundLifecycle.test.ts +50 -1
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/stockPresentation.test.ts +324 -0
- package/vona/src/suite/a-home/modules/home-base/package.json +3 -2
- package/vona/src/suite/a-home/modules/home-base/src/.metadata/index.ts +93 -16
- package/vona/src/suite/a-home/modules/home-base/src/controller/siteCatalog.ts +21 -0
- package/vona/src/suite/a-home/modules/home-base/src/dto/siteCatalogSelectRes.ts +11 -0
- package/vona/src/suite/a-home/modules/home-base/src/dto/siteCatalogSelectResItem.ts +15 -0
- package/vona/src/suite/a-home/modules/home-base/src/service/siteCatalog.ts +29 -0
- package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +3 -0
- package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +3 -0
- package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/config/locale/en-us.ts +1 -0
- package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/config/locale/zh-cn.ts +1 -0
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +1 -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/types/env.ts +1 -3
- package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +2 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
- 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/.metadata/index.ts +67 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payProvider.ts +1 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/bean.payScene.ts +5 -4
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.index.ts +8 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/meta.version.ts +22 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/bean/schedule.providerOperationDispatch.ts +1 -1
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/paymentSession.tsx +6 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperation.tsx +3 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/providerOperationRecoveryAudit.tsx +53 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/entity/refundOperation.tsx +6 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/model/providerOperationRecoveryAudit.ts +12 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/paymentSession.ts +6 -2
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/providerOperation.ts +236 -30
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/refundOperation.ts +8 -5
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/service/webhook.ts +34 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payProvider.ts +4 -0
- package/vona/src/suite-vendor/a-pay/modules/a-pay/src/types/payment.ts +6 -0
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/bean/payProvider.mock.ts +15 -7
- package/vona/src/suite-vendor/a-pay/modules/pay-mock/src/service/payMock.ts +9 -5
- 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 +289 -86
- package/vona/src/suite-vendor/a-pay/modules/pay-paypal/src/lib/paypalGateway.ts +9 -1
- package/vona/src/suite-vendor/a-pay/modules/pay-stripe/package.json +1 -1
- package/vona/src/suite-vendor/a-pay/package.json +5 -5
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/rest.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/utils.ts +12 -0
- package/vona/src/suite-vendor/a-vona/modules/a-orm/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoCreate.ts +5 -1
- package/vona/src/suite-vendor/a-vona/modules/a-orm/src/lib/dto/dtoUpdate.ts +5 -1
- package/vona/src/suite-vendor/a-vona/modules/a-validation/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-validation/src/bean/bean.validator.ts +60 -1
- package/vona/src/suite-vendor/a-vona/modules/a-web/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-web/src/lib/decorator/bean.ts +7 -4
- package/vona/src/suite-vendor/a-vona/modules/a-web/src/types/dto.ts +2 -1
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/env/.env +3 -5
- package/zova/env/.env.cabloyBasicAdmin +1 -4
- package/zova/env/.env.cabloyBasicWeb +1 -5
- package/zova/env/.env.cabloyCommerce +1 -5
- package/zova/env/.env.cabloyCommerceAdmin +1 -4
- package/zova/env/.env.ssr.admin +1 -4
- package/zova/env/.env.ssr.web +1 -5
- 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 -1
- package/zova/packages-zova/zova-core/src/core/component/locale.ts +10 -4
- package/zova/packages-zova/zova-core/src/core/sys/config.ts +14 -6
- package/zova/packages-zova/zova-core/src/types/utils/env.ts +4 -2
- package/zova/pnpm-lock.yaml +50 -44
- package/zova/src/front/config/config/config.ts +22 -7
- package/zova/src/suite/a-commerce/modules/commerce-catalog/src/routes.ts +2 -2
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/schemas.ts +16 -8
- package/zova/src/suite/a-commerce/modules/commerce-member/src/api/openapi/types.ts +150 -216
- package/zova/src/suite/a-commerce/modules/commerce-member/src/model/addressMine.ts +1 -2
- package/zova/src/suite/a-commerce/modules/commerce-member/src/page/address/controller.tsx +5 -8
- package/zova/src/suite/a-commerce/modules/commerce-member/src/routes.ts +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-promotion/src/model/coupon.ts +1 -2
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/routes.ts +3 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/cli/openapi.config.ts +3 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/commerceTradeOrder.ts +72 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/schemas.ts +8 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/api/openapi/types.ts +226 -16
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/apiSchema/commerceTradeOrder.ts +15 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/component/tableCellActionRefund/controller.tsx +155 -36
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/en-us.ts +7 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/config/locale/zh-cn.ts +7 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/cart.ts +3 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/order.ts +42 -0
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/model/orderMine.ts +2 -4
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/cart/controller.tsx +5 -1
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/checkout/controller.tsx +14 -10
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/order/controller.tsx +5 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/orders/controller.tsx +5 -8
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/page/payment/controller.tsx +151 -24
- package/zova/src/suite/a-commerce/modules/commerce-trade/src/routes.ts +5 -5
- package/zova/src/suite/a-demo/modules/demo-basic/src/routes.ts +26 -7
- package/zova/src/suite/a-demo/modules/demo-todo/src/routes.ts +2 -2
- package/zova/src/suite/a-devui/modules/devui-adapter/src/bean/meta.themeHandler.ts +1 -1
- package/zova/src/suite/a-home/modules/home-base/package.json +1 -0
- package/zova/src/suite/a-home/modules/home-base/src/routes.ts +5 -0
- package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +1 -1
- package/zova/src/suite/a-home/modules/home-base/src/service/ssrLayout.ts +18 -14
- package/zova/src/suite/a-home/modules/home-indexadmin/src/routes.ts +1 -1
- package/zova/src/suite/a-home/modules/home-indexweb/src/routes.ts +1 -0
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/controller.tsx +12 -4
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/config/config.ts +8 -0
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/model/layout.ts +5 -8
- package/zova/src/suite/a-home/modules/home-layoutempty/src/.metadata/index.ts +10 -3
- package/zova/src/suite/a-home/modules/home-layoutempty/src/component/layoutEmpty/controller.tsx +9 -3
- package/zova/src/suite/a-home/modules/home-layoutempty/src/config/config.ts +12 -0
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/controller.tsx +13 -8
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.header.tsx +1 -1
- package/zova/src/suite/a-home/modules/home-layoutweb/src/config/config.ts +4 -0
- package/zova/src/suite/a-home/modules/home-layoutweb/src/model/layout.ts +5 -8
- package/zova/src/suite/a-home/modules/home-login/src/routes.ts +1 -0
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +2 -3
- package/zova/src/suite/cabloy-basic/modules/basic-metrics/src/routes.ts +3 -1
- 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 +1 -1
- package/zova/src/suite-vendor/a-cabloy/modules/rest-resource/src/routes.ts +3 -0
- package/zova/src/suite-vendor/a-cabloy/package.json +2 -2
- package/zova/src/suite-vendor/a-pay/modules/a-pay/package.json +1 -1
- package/zova/src/suite-vendor/a-pay/modules/a-pay/src/model/paymentSession.ts +2 -2
- package/zova/src/suite-vendor/a-pay/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/modules/a-interceptor/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-interceptor/src/bean/interceptor.jwt.ts +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.persister.ts +28 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/model/sdk.ts +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-openapi/src/types/rest.ts +1 -0
- 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/monkey.ts +23 -0
- package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkeySys.ts +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/config/config.ts +1 -17
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/bodyReadyObserver.ts +27 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/index.ts +2 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +33 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrMetaStore.ts +3 -35
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrProfile.ts +38 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +14 -0
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/config.ts +3 -10
- package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/types/ssr.ts +17 -8
- package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-ssrserver/src/service/ssrHandler.ts +33 -18
- package/zova/src/suite-vendor/a-zova/modules/a-style/package.json +1 -1
- package/zova/src/suite-vendor/a-zova/modules/a-style/src/bean/bean.theme.ts +22 -3
- package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
- package/zova/src/suite-vendor/a-zova/package.json +9 -9
|
@@ -11,8 +11,13 @@ const passportTestActivateCurrentPath = '/api/home/user/passportTest/activateCur
|
|
|
11
11
|
|
|
12
12
|
interface IAddressFixture {
|
|
13
13
|
addressLine1: string;
|
|
14
|
+
addressLine2: string;
|
|
14
15
|
city: string;
|
|
16
|
+
countryCode: string;
|
|
17
|
+
phone: string;
|
|
18
|
+
postalCode: string;
|
|
15
19
|
recipientName: string;
|
|
20
|
+
region: string;
|
|
16
21
|
updatedCity: string;
|
|
17
22
|
}
|
|
18
23
|
|
|
@@ -48,7 +53,7 @@ function waitForAddressMine(page: Page) {
|
|
|
48
53
|
}
|
|
49
54
|
|
|
50
55
|
async function getAdminOrderRow(page: Page, orderId: number): Promise<Locator> {
|
|
51
|
-
const row = page.locator(`tr:has(td:
|
|
56
|
+
const row = page.locator(`tr:has(td a:text-is("${orderId}"))`);
|
|
52
57
|
await expect(row).toHaveCount(1);
|
|
53
58
|
return row;
|
|
54
59
|
}
|
|
@@ -123,21 +128,26 @@ function createAddressFixture(testInfo: TestInfo): IAddressFixture {
|
|
|
123
128
|
const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
124
129
|
return {
|
|
125
130
|
recipientName: `E2E Address ${id}`,
|
|
126
|
-
|
|
131
|
+
phone: '15555550123',
|
|
132
|
+
countryCode: 'US',
|
|
133
|
+
region: 'California',
|
|
127
134
|
city: `Original City ${id}`,
|
|
135
|
+
postalCode: '94105',
|
|
136
|
+
addressLine1: `1 Test Street ${id}`,
|
|
137
|
+
addressLine2: 'Suite E2E',
|
|
128
138
|
updatedCity: `Updated City ${id}`,
|
|
129
139
|
};
|
|
130
140
|
}
|
|
131
141
|
|
|
132
142
|
async function fillAddressForm(page: Page, fixture: IAddressFixture) {
|
|
133
143
|
await page.getByPlaceholder('Recipient name').fill(fixture.recipientName);
|
|
134
|
-
await page.getByPlaceholder('Phone').fill(
|
|
135
|
-
await page.getByPlaceholder('Country code').fill(
|
|
136
|
-
await page.getByPlaceholder('Region').fill(
|
|
144
|
+
await page.getByPlaceholder('Phone').fill(fixture.phone);
|
|
145
|
+
await page.getByPlaceholder('Country code').fill(fixture.countryCode);
|
|
146
|
+
await page.getByPlaceholder('Region').fill(fixture.region);
|
|
137
147
|
await page.getByPlaceholder('City').fill(fixture.city);
|
|
138
|
-
await page.getByPlaceholder('Postal code').fill(
|
|
148
|
+
await page.getByPlaceholder('Postal code').fill(fixture.postalCode);
|
|
139
149
|
await page.getByPlaceholder('Address line 1').fill(fixture.addressLine1);
|
|
140
|
-
await page.getByPlaceholder('Address line 2').fill(
|
|
150
|
+
await page.getByPlaceholder('Address line 2').fill(fixture.addressLine2);
|
|
141
151
|
}
|
|
142
152
|
|
|
143
153
|
async function createAddressThroughCustomerPage(
|
|
@@ -186,6 +196,7 @@ test(
|
|
|
186
196
|
async ({ page, request }) => {
|
|
187
197
|
const response = await request.get('/commerce');
|
|
188
198
|
expect(response.ok()).toBeTruthy();
|
|
199
|
+
expect(response.headers()['cache-control']).toBe('public, max-age=600');
|
|
189
200
|
const html = await response.text();
|
|
190
201
|
const normalizedHtml = html.toLowerCase();
|
|
191
202
|
expect(normalizedHtml).not.toContain('data-zova-hydrated');
|
|
@@ -207,6 +218,42 @@ test(
|
|
|
207
218
|
},
|
|
208
219
|
);
|
|
209
220
|
|
|
221
|
+
test(
|
|
222
|
+
'Commerce theme: public local preference mirrors session cookie without hydration mismatch',
|
|
223
|
+
{ tag: ['@web', '@ssr', '@theme'] },
|
|
224
|
+
async ({ browser, request }, testInfo) => {
|
|
225
|
+
const customer = await registerCustomer(request, testInfo);
|
|
226
|
+
const context = await browser.newContext();
|
|
227
|
+
await context.addInitScript(() => localStorage.setItem('themedark', 'true'));
|
|
228
|
+
const page = await context.newPage();
|
|
229
|
+
const pageErrors = collectPageErrors(page);
|
|
230
|
+
const consoleErrors: string[] = [];
|
|
231
|
+
page.on('console', message => {
|
|
232
|
+
if (message.type() === 'error' || /hydration mismatch/i.test(message.text())) {
|
|
233
|
+
consoleErrors.push(message.text());
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
await page.goto('/commerce', { waitUntil: 'load' });
|
|
238
|
+
await page.evaluate(() => localStorage.setItem('themedark', 'true'));
|
|
239
|
+
await page.reload({ waitUntil: 'load' });
|
|
240
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
241
|
+
await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
|
|
242
|
+
expect((await context.cookies()).find(cookie => cookie.name === 'themedark')?.value).toBe(
|
|
243
|
+
'true',
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await login(page, '/commerce/address', customer.username, customer.password, 'commerce');
|
|
247
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
248
|
+
await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
|
|
249
|
+
expect(pageErrors).toEqual([]);
|
|
250
|
+
expect(consoleErrors).toEqual([]);
|
|
251
|
+
} finally {
|
|
252
|
+
await context.close();
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
);
|
|
256
|
+
|
|
210
257
|
test(
|
|
211
258
|
'Commerce catalogue: public sellable inventory renders after hydration',
|
|
212
259
|
{ tag: ['@web', '@flow'] },
|
|
@@ -292,15 +339,18 @@ test(
|
|
|
292
339
|
);
|
|
293
340
|
|
|
294
341
|
test(
|
|
295
|
-
'Commerce Address:
|
|
342
|
+
'Commerce Address: session SSR protects an anonymous document request',
|
|
296
343
|
{ tag: ['@web', '@cart'] },
|
|
297
344
|
async ({ page, request }) => {
|
|
298
345
|
const path = '/commerce/address';
|
|
299
346
|
const routePath = '/address';
|
|
300
347
|
const response = await request.get(path, { maxRedirects: 0 });
|
|
301
|
-
expect(response.status()).toBe(
|
|
302
|
-
expect(response.headers()
|
|
303
|
-
|
|
348
|
+
expect(response.status()).toBe(302);
|
|
349
|
+
expect(response.headers()['cache-control']).toBe('private, no-store');
|
|
350
|
+
const html = await response.text();
|
|
351
|
+
expect(html).not.toContain('ssr-body-ready-observer');
|
|
352
|
+
expect(html).not.toContain('__leftDrawerOpenJS');
|
|
353
|
+
expect(response.headers().location).toMatch(/^\/commerce\/login\?(?:.*&)?returnTo=/);
|
|
304
354
|
|
|
305
355
|
const pageErrors = collectPageErrors(page);
|
|
306
356
|
const documentResponse = await page.goto(path, { waitUntil: 'load' });
|
|
@@ -324,6 +374,33 @@ test(
|
|
|
324
374
|
testInfo,
|
|
325
375
|
);
|
|
326
376
|
try {
|
|
377
|
+
const addressResponse = await context.request.get('/commerce/address');
|
|
378
|
+
expect(addressResponse.ok()).toBeTruthy();
|
|
379
|
+
expect(addressResponse.headers()['cache-control']).toBe('private, no-store');
|
|
380
|
+
const addressHtml = await addressResponse.text();
|
|
381
|
+
expect(addressHtml.toLowerCase()).not.toContain('data-zova-hydrated');
|
|
382
|
+
expect(addressHtml).not.toContain('ssr-body-ready-observer');
|
|
383
|
+
expect(addressHtml).not.toContain('__leftDrawerOpenJS');
|
|
384
|
+
expect(addressHtml).toContain(fixture.recipientName);
|
|
385
|
+
expect(addressHtml).toContain(fixture.addressLine1);
|
|
386
|
+
|
|
387
|
+
const coldPage = await context.newPage();
|
|
388
|
+
try {
|
|
389
|
+
const coldPageErrors = collectPageErrors(coldPage);
|
|
390
|
+
const coldResponse = await coldPage.goto('/commerce/address', { waitUntil: 'load' });
|
|
391
|
+
expect(coldResponse?.ok()).toBeTruthy();
|
|
392
|
+
await expect(coldPage.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
393
|
+
await expect(coldPage.getByRole('heading', { name: 'Addresses' })).toBeVisible();
|
|
394
|
+
const coldCard = coldPage
|
|
395
|
+
.locator('article')
|
|
396
|
+
.filter({ has: coldPage.getByRole('heading', { name: fixture.recipientName }) });
|
|
397
|
+
await expect(coldCard).toContainText(fixture.addressLine1);
|
|
398
|
+
await expect(coldPage.getByRole('alert')).toHaveCount(0);
|
|
399
|
+
expect(coldPageErrors).toEqual([]);
|
|
400
|
+
} finally {
|
|
401
|
+
await coldPage.close();
|
|
402
|
+
}
|
|
403
|
+
|
|
327
404
|
const card = page
|
|
328
405
|
.locator('article')
|
|
329
406
|
.filter({ has: page.getByRole('heading', { name: fixture.recipientName }) });
|
|
@@ -431,32 +508,25 @@ test(
|
|
|
431
508
|
await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
|
|
432
509
|
timeout: 40_000,
|
|
433
510
|
});
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
expect(
|
|
442
|
-
|
|
443
|
-
expect(
|
|
444
|
-
expect(
|
|
445
|
-
expect(
|
|
446
|
-
expect(
|
|
447
|
-
expect(
|
|
448
|
-
expect(
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
skuCodeSnapshot: 'COF-SET-01',
|
|
454
|
-
unitPriceCents: 4599,
|
|
455
|
-
quantity: 1,
|
|
456
|
-
lineTotalCents: 4599,
|
|
457
|
-
}),
|
|
458
|
-
]);
|
|
459
|
-
|
|
511
|
+
const orderPath = `/commerce/order/${checkout.orderId}`;
|
|
512
|
+
const orderResponse = await context.request.get(orderPath);
|
|
513
|
+
expect(orderResponse.ok()).toBeTruthy();
|
|
514
|
+
expect(orderResponse.headers()['cache-control']).toBe('private, no-store');
|
|
515
|
+
const orderHtml = await orderResponse.text();
|
|
516
|
+
expect(orderHtml.toLowerCase()).not.toContain('data-zova-hydrated');
|
|
517
|
+
expect(orderHtml).toContain(`Order #${checkout.orderId}`);
|
|
518
|
+
expect(orderHtml).toContain('paid · $45.99');
|
|
519
|
+
expect(orderHtml).toContain('USD');
|
|
520
|
+
expect(orderHtml).toContain(fixture.recipientName);
|
|
521
|
+
expect(orderHtml).toContain(fixture.addressLine1);
|
|
522
|
+
expect(orderHtml).toContain('Discount: $0.00');
|
|
523
|
+
expect(orderHtml).toContain('Pour-Over Coffee Set');
|
|
524
|
+
expect(orderHtml).toContain('COF-SET-01');
|
|
525
|
+
expect(orderHtml).toContain('1 × $45.99 = $45.99');
|
|
526
|
+
|
|
527
|
+
const orderDocumentResponse = await page.reload({ waitUntil: 'load' });
|
|
528
|
+
expect(orderDocumentResponse?.ok()).toBeTruthy();
|
|
529
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
460
530
|
await expect(page.getByRole('heading', { name: `Order #${checkout.orderId}` })).toBeVisible();
|
|
461
531
|
await expect(page.getByText('paid · $45.99')).toBeVisible();
|
|
462
532
|
await expect(page.getByText(fixture.recipientName)).toBeVisible();
|
|
@@ -483,6 +553,27 @@ test(
|
|
|
483
553
|
);
|
|
484
554
|
const orderRow = await getAdminOrderRow(adminPage, checkout.orderId);
|
|
485
555
|
await expect(orderRow).toBeVisible();
|
|
556
|
+
await expect(orderRow.locator('td')).toHaveCount(6);
|
|
557
|
+
await expect(orderRow).toContainText('Paid');
|
|
558
|
+
await expect(orderRow).toContainText('45.99');
|
|
559
|
+
await expect(orderRow).not.toContainText(fixture.recipientName);
|
|
560
|
+
const orderViewResponse = waitForApiResponse(
|
|
561
|
+
adminPage,
|
|
562
|
+
'GET',
|
|
563
|
+
`/api/commerce/trade/order/${checkout.orderId}`,
|
|
564
|
+
);
|
|
565
|
+
await orderRow.getByRole('link', { name: String(checkout.orderId), exact: true }).click();
|
|
566
|
+
expect((await orderViewResponse).ok()).toBeTruthy();
|
|
567
|
+
await expect(adminPage.getByText('Money summary').first()).toBeVisible();
|
|
568
|
+
await expect(adminPage.getByText('Delivery address').first()).toBeVisible();
|
|
569
|
+
await expect(adminPage.getByText('Purchased lines').first()).toBeVisible();
|
|
570
|
+
await expect(adminPage.getByText('addressSnapshot').first()).toBeVisible();
|
|
571
|
+
await expect(adminPage.getByText('couponSnapshot').first()).toBeVisible();
|
|
572
|
+
await expect(adminPage.getByText('Pour-Over Coffee Set')).toBeVisible();
|
|
573
|
+
await expect(adminPage.getByText('COF-SET-01')).toBeVisible();
|
|
574
|
+
await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
|
|
575
|
+
await adminPage.getByRole('button', { name: 'Back', exact: true }).click();
|
|
576
|
+
await expect(orderRow).toBeVisible();
|
|
486
577
|
const carrierInput = orderRow.getByPlaceholder('Carrier');
|
|
487
578
|
const trackingNumberInput = orderRow.getByPlaceholder('Tracking number');
|
|
488
579
|
const confirmationCheckbox = orderRow.getByRole('checkbox');
|
|
@@ -499,28 +590,24 @@ test(
|
|
|
499
590
|
);
|
|
500
591
|
await orderRow.getByRole('button', { name: 'Ship order', exact: true }).click();
|
|
501
592
|
expect((await shipmentResponse).ok()).toBeTruthy();
|
|
502
|
-
await expect(orderRow).toContainText('
|
|
593
|
+
await expect(orderRow).toContainText('Shipped');
|
|
503
594
|
expect(adminPageErrors).toEqual([]);
|
|
504
595
|
} finally {
|
|
505
596
|
await adminContext.close().catch(() => {});
|
|
506
597
|
}
|
|
507
598
|
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`),
|
|
512
|
-
);
|
|
513
|
-
await page.reload({ waitUntil: 'load' });
|
|
514
|
-
await shippedDetailResponse;
|
|
599
|
+
const shippedDocumentResponse = await page.reload({ waitUntil: 'load' });
|
|
600
|
+
expect(shippedDocumentResponse?.ok()).toBeTruthy();
|
|
601
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
515
602
|
await expect(page.getByText('shipped · $45.99')).toBeVisible();
|
|
516
603
|
await expect(page.getByRole('heading', { name: 'Shipment' })).toBeVisible();
|
|
517
604
|
await expect(page.getByText(shipmentCarrier)).toBeVisible();
|
|
518
605
|
await expect(page.getByText(shipmentTrackingNumber)).toBeVisible();
|
|
519
606
|
|
|
520
|
-
const
|
|
521
|
-
|
|
607
|
+
const ordersDocumentResponse = await page.goto('/commerce/orders', { waitUntil: 'load' });
|
|
608
|
+
expect(ordersDocumentResponse?.ok()).toBeTruthy();
|
|
522
609
|
await expect(page).toHaveURL(/\/commerce\/orders(?:\/|$)/);
|
|
523
|
-
await
|
|
610
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
524
611
|
await expect(page.getByRole('heading', { name: 'My orders' })).toBeVisible();
|
|
525
612
|
const orderCard = page
|
|
526
613
|
.locator('article')
|
|
@@ -545,6 +632,233 @@ test(
|
|
|
545
632
|
},
|
|
546
633
|
);
|
|
547
634
|
|
|
635
|
+
test(
|
|
636
|
+
'Payment callback: authenticated direct route reconciles after hydration',
|
|
637
|
+
{ tag: ['@web', '@flow', '@payment'] },
|
|
638
|
+
async ({ browser, request }, testInfo) => {
|
|
639
|
+
test.setTimeout(60_000);
|
|
640
|
+
const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage(
|
|
641
|
+
browser,
|
|
642
|
+
request,
|
|
643
|
+
testInfo,
|
|
644
|
+
);
|
|
645
|
+
try {
|
|
646
|
+
await page.goto('/commerce', { waitUntil: 'load' });
|
|
647
|
+
const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
|
|
648
|
+
await expect(productLink).toBeVisible();
|
|
649
|
+
await productLink.click();
|
|
650
|
+
await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
|
|
651
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
652
|
+
await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
|
|
653
|
+
await expect(page.getByText('COF-SET-01')).toBeVisible();
|
|
654
|
+
const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
|
|
655
|
+
await expect(addToCartButton).toBeEnabled();
|
|
656
|
+
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
657
|
+
await addToCartButton.click();
|
|
658
|
+
expect((await addResponse).ok()).toBeTruthy();
|
|
659
|
+
await page.getByRole('link', { name: /^Cart/ }).click();
|
|
660
|
+
await page.getByRole('link', { name: 'Checkout', exact: true }).click();
|
|
661
|
+
const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
|
|
662
|
+
await expect(addressChoice).toBeVisible();
|
|
663
|
+
await addressChoice.check();
|
|
664
|
+
await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
|
|
665
|
+
const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout');
|
|
666
|
+
await page.getByRole('button', { name: 'Create order', exact: true }).click();
|
|
667
|
+
const checkoutResponseValue = await checkoutResponse;
|
|
668
|
+
expect(checkoutResponseValue.ok()).toBeTruthy();
|
|
669
|
+
const checkout = (await checkoutResponseValue.json()).data;
|
|
670
|
+
expectTableIdentity(checkout.orderId);
|
|
671
|
+
expectTableIdentity(checkout.paymentSessionId);
|
|
672
|
+
const startResponse = waitForApiResponse(
|
|
673
|
+
page,
|
|
674
|
+
'POST',
|
|
675
|
+
new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}/start$`),
|
|
676
|
+
);
|
|
677
|
+
await page.getByRole('button', { name: 'Start payment', exact: true }).click();
|
|
678
|
+
expect((await startResponse).ok()).toBeTruthy();
|
|
679
|
+
await expect(page.getByRole('heading', { name: 'Mock payment simulator' })).toBeVisible();
|
|
680
|
+
const completeResponse = waitForApiResponse(
|
|
681
|
+
page,
|
|
682
|
+
'POST',
|
|
683
|
+
new RegExp(`/api/pay/mock/payment-session/${checkout.paymentSessionId}/complete$`),
|
|
684
|
+
);
|
|
685
|
+
await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
|
|
686
|
+
expect((await completeResponse).ok()).toBeTruthy();
|
|
687
|
+
await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`), {
|
|
688
|
+
timeout: 40_000,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=return`;
|
|
692
|
+
await page.evaluate(() => {
|
|
693
|
+
localStorage.clear();
|
|
694
|
+
sessionStorage.clear();
|
|
695
|
+
});
|
|
696
|
+
const callbackResponse = await context.request.get(callbackPath);
|
|
697
|
+
expect(callbackResponse.ok()).toBeTruthy();
|
|
698
|
+
const callbackHtml = (await callbackResponse.text()).toLowerCase();
|
|
699
|
+
expect(callbackHtml).not.toContain('data-zova-hydrated');
|
|
700
|
+
expect(callbackHtml).not.toContain('mock payment simulator');
|
|
701
|
+
|
|
702
|
+
const reconcilePath = new RegExp(
|
|
703
|
+
`/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`,
|
|
704
|
+
);
|
|
705
|
+
const paymentViewPath = new RegExp(`/api/pay/payment-session/${checkout.paymentSessionId}$`);
|
|
706
|
+
const orderViewPath = new RegExp(`/api/commerce/trade/order/viewMine/${checkout.orderId}$`);
|
|
707
|
+
let reconcileCount = 0;
|
|
708
|
+
let paymentViewCount = 0;
|
|
709
|
+
let orderViewCount = 0;
|
|
710
|
+
page.on('request', request => {
|
|
711
|
+
const url = new URL(request.url());
|
|
712
|
+
if (request.method() !== 'GET') {
|
|
713
|
+
if (request.method() === 'POST' && reconcilePath.test(url.pathname)) reconcileCount++;
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
if (paymentViewPath.test(url.pathname)) paymentViewCount++;
|
|
717
|
+
if (orderViewPath.test(url.pathname)) orderViewCount++;
|
|
718
|
+
});
|
|
719
|
+
const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath);
|
|
720
|
+
const documentResponse = await page.goto(callbackPath, { waitUntil: 'load' });
|
|
721
|
+
expect(documentResponse?.ok()).toBeTruthy();
|
|
722
|
+
expect((await reconcileResponse).ok()).toBeTruthy();
|
|
723
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
724
|
+
await expect(page).toHaveURL(new RegExp(`/commerce/order/${checkout.orderId}(?:/|$)`));
|
|
725
|
+
expect(reconcileCount).toBe(1);
|
|
726
|
+
expect(paymentViewCount).toBeLessThanOrEqual(10);
|
|
727
|
+
expect(orderViewCount).toBeLessThanOrEqual(5);
|
|
728
|
+
await expect(page.getByText('paid · $45.99')).toBeVisible();
|
|
729
|
+
await expect(page.getByRole('alert')).toHaveCount(0);
|
|
730
|
+
expect(pageErrors).toEqual([]);
|
|
731
|
+
} finally {
|
|
732
|
+
await context.close().catch(() => {});
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
);
|
|
736
|
+
|
|
737
|
+
test(
|
|
738
|
+
'PayPal browser cancel: customer can continue payment or open an awaiting order',
|
|
739
|
+
{ tag: ['@web', '@flow', '@payment'] },
|
|
740
|
+
async ({ browser, request }, testInfo) => {
|
|
741
|
+
test.setTimeout(60_000);
|
|
742
|
+
const { context, fixture, page, pageErrors } = await createAddressThroughCustomerPage(
|
|
743
|
+
browser,
|
|
744
|
+
request,
|
|
745
|
+
testInfo,
|
|
746
|
+
);
|
|
747
|
+
try {
|
|
748
|
+
await page.goto('/commerce', { waitUntil: 'load' });
|
|
749
|
+
const productLink = page.getByRole('link', { name: 'Pour-Over Coffee Set', exact: true });
|
|
750
|
+
await expect(productLink).toBeVisible();
|
|
751
|
+
await productLink.click();
|
|
752
|
+
await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
|
|
753
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
754
|
+
await expect(page.getByRole('heading', { name: 'Pour-Over Coffee Set' })).toBeVisible();
|
|
755
|
+
await expect(page.getByText('COF-SET-01')).toBeVisible();
|
|
756
|
+
const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
|
|
757
|
+
await expect(addToCartButton).toBeEnabled();
|
|
758
|
+
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
759
|
+
await addToCartButton.click();
|
|
760
|
+
expect((await addResponse).ok()).toBeTruthy();
|
|
761
|
+
await page.getByRole('link', { name: /^Cart/ }).click();
|
|
762
|
+
await page.getByRole('link', { name: 'Checkout', exact: true }).click();
|
|
763
|
+
const addressChoice = page.getByRole('radio', { name: new RegExp(fixture.recipientName) });
|
|
764
|
+
await expect(addressChoice).toBeVisible();
|
|
765
|
+
await addressChoice.check();
|
|
766
|
+
await page.getByRole('radio', { name: 'No coupon', exact: true }).check();
|
|
767
|
+
const checkoutResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/checkout');
|
|
768
|
+
await page.getByRole('button', { name: 'Create order', exact: true }).click();
|
|
769
|
+
const checkoutResponseValue = await checkoutResponse;
|
|
770
|
+
expect(checkoutResponseValue.ok()).toBeTruthy();
|
|
771
|
+
const checkout = (await checkoutResponseValue.json()).data;
|
|
772
|
+
expectTableIdentity(checkout.orderId);
|
|
773
|
+
expectTableIdentity(checkout.paymentSessionId);
|
|
774
|
+
expect(checkout.state).toBe('awaiting_payment');
|
|
775
|
+
|
|
776
|
+
const callbackPath = `/commerce/payment/${checkout.paymentSessionId}/${checkout.orderId}?providerResult=cancel`;
|
|
777
|
+
const paymentSessionPath = new RegExp(
|
|
778
|
+
`/api/pay/payment-session/${checkout.paymentSessionId}$`,
|
|
779
|
+
);
|
|
780
|
+
const reconcilePath = new RegExp(
|
|
781
|
+
`/api/pay/payment-session/${checkout.paymentSessionId}/reconcile$`,
|
|
782
|
+
);
|
|
783
|
+
const requiresActionSession = {
|
|
784
|
+
id: checkout.paymentSessionId,
|
|
785
|
+
state: 'requires_action',
|
|
786
|
+
providerName: 'pay-paypal:paypal',
|
|
787
|
+
nextAction: { kind: 'redirect', url: 'https://example.test/paypal-approval' },
|
|
788
|
+
amountMinor: 4599,
|
|
789
|
+
currency: 'USD',
|
|
790
|
+
};
|
|
791
|
+
const callbackResponse = await context.request.get(callbackPath);
|
|
792
|
+
expect(callbackResponse.ok()).toBeTruthy();
|
|
793
|
+
const callbackHtml = await callbackResponse.text();
|
|
794
|
+
expect(callbackHtml).toContain('Start payment');
|
|
795
|
+
expect(callbackHtml).not.toContain(
|
|
796
|
+
'You returned from the payment provider without completing payment.',
|
|
797
|
+
);
|
|
798
|
+
|
|
799
|
+
let reconcileCount = 0;
|
|
800
|
+
let reconcileSeen = false;
|
|
801
|
+
let resolvePaymentSessionRefresh: () => void;
|
|
802
|
+
const paymentSessionRefresh = new Promise<void>(resolve => {
|
|
803
|
+
resolvePaymentSessionRefresh = resolve;
|
|
804
|
+
});
|
|
805
|
+
await page.route(
|
|
806
|
+
url => paymentSessionPath.test(url.pathname) || reconcilePath.test(url.pathname),
|
|
807
|
+
async route => {
|
|
808
|
+
const request = route.request();
|
|
809
|
+
const pathname = new URL(request.url()).pathname;
|
|
810
|
+
if (request.method() === 'POST' && reconcilePath.test(pathname)) {
|
|
811
|
+
reconcileCount++;
|
|
812
|
+
reconcileSeen = true;
|
|
813
|
+
} else if (
|
|
814
|
+
reconcileSeen &&
|
|
815
|
+
request.method() === 'GET' &&
|
|
816
|
+
paymentSessionPath.test(pathname)
|
|
817
|
+
) {
|
|
818
|
+
resolvePaymentSessionRefresh();
|
|
819
|
+
}
|
|
820
|
+
await route.fulfill({
|
|
821
|
+
contentType: 'application/json',
|
|
822
|
+
body: JSON.stringify({
|
|
823
|
+
code: 0,
|
|
824
|
+
message: '',
|
|
825
|
+
data: requiresActionSession,
|
|
826
|
+
}),
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
const reconcileResponse = waitForApiResponse(page, 'POST', reconcilePath);
|
|
832
|
+
await page.goto(callbackPath, { waitUntil: 'load' });
|
|
833
|
+
expect((await reconcileResponse).ok()).toBeTruthy();
|
|
834
|
+
await paymentSessionRefresh;
|
|
835
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
836
|
+
await expect(
|
|
837
|
+
page.getByText(
|
|
838
|
+
'You returned from the payment provider without completing payment. Your payment status has been refreshed. You can continue to payment or open your order.',
|
|
839
|
+
),
|
|
840
|
+
).toBeVisible();
|
|
841
|
+
await expect(page.getByRole('button', { name: 'Start payment', exact: true })).toHaveCount(0);
|
|
842
|
+
await expect(
|
|
843
|
+
page.getByRole('button', { name: 'Continue to payment', exact: true }),
|
|
844
|
+
).toBeVisible();
|
|
845
|
+
await expect(page.getByRole('button', { name: 'Open order', exact: true })).toBeVisible();
|
|
846
|
+
await expect(
|
|
847
|
+
page.getByText('Waiting for verified provider confirmation and order settlement…'),
|
|
848
|
+
).toHaveCount(0);
|
|
849
|
+
await expect(page.getByRole('alert')).toHaveCount(0);
|
|
850
|
+
expect(reconcileCount).toBe(1);
|
|
851
|
+
|
|
852
|
+
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();
|
|
855
|
+
expect(pageErrors).toEqual([]);
|
|
856
|
+
} finally {
|
|
857
|
+
await context.close().catch(() => {});
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
);
|
|
861
|
+
|
|
548
862
|
test(
|
|
549
863
|
'Payment cancellation: customer observes the cancelled order after verified provider confirmation',
|
|
550
864
|
{ tag: ['@web', '@flow', '@payment'] },
|
|
@@ -701,7 +1015,7 @@ test(
|
|
|
701
1015
|
);
|
|
702
1016
|
await orderRow.getByRole('button', { name: 'Approve refund', exact: true }).click();
|
|
703
1017
|
expect((await approveResponse).ok()).toBeTruthy();
|
|
704
|
-
await expect(orderRow).toContainText('
|
|
1018
|
+
await expect(orderRow).toContainText('Refund approved');
|
|
705
1019
|
const executeResponse = waitForApiResponse(
|
|
706
1020
|
adminPage,
|
|
707
1021
|
'POST',
|
|
@@ -735,8 +1049,8 @@ test(
|
|
|
735
1049
|
},
|
|
736
1050
|
{ timeout: 40_000 },
|
|
737
1051
|
)
|
|
738
|
-
.toContain('
|
|
739
|
-
await expect(await getAdminOrderRow(adminPage, checkout.orderId)).toContainText('
|
|
1052
|
+
.toContain('Refunded');
|
|
1053
|
+
await expect(await getAdminOrderRow(adminPage, checkout.orderId)).toContainText('Refunded');
|
|
740
1054
|
expect(adminPageErrors).toEqual([]);
|
|
741
1055
|
} finally {
|
|
742
1056
|
await adminContext.close().catch(() => {});
|
|
@@ -761,7 +1075,7 @@ test(
|
|
|
761
1075
|
);
|
|
762
1076
|
|
|
763
1077
|
test(
|
|
764
|
-
'ATP-
|
|
1078
|
+
'ATP-SPC-05: systemAdmin inspects readonly Address Resource without mutation controls',
|
|
765
1079
|
{ tag: ['@admin', '@flow', '@address'] },
|
|
766
1080
|
async ({ browser, request }, testInfo) => {
|
|
767
1081
|
test.setTimeout(60_000);
|
|
@@ -774,7 +1088,7 @@ test(
|
|
|
774
1088
|
const url = new URL(request.url());
|
|
775
1089
|
if (
|
|
776
1090
|
url.pathname.startsWith(addressActionPath) &&
|
|
777
|
-
['POST', 'PATCH', 'DELETE'].includes(request.method())
|
|
1091
|
+
['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method())
|
|
778
1092
|
) {
|
|
779
1093
|
addressMutationMethods.push(request.method());
|
|
780
1094
|
}
|
|
@@ -789,6 +1103,14 @@ test(
|
|
|
789
1103
|
await expect(adminPage).toHaveURL(addressResourceUrl);
|
|
790
1104
|
const fixtureRow = adminPage.getByRole('row', { name: customer.fixture.recipientName });
|
|
791
1105
|
await expect(fixtureRow).toBeVisible();
|
|
1106
|
+
await expect(fixtureRow).toContainText(customer.fixture.recipientName);
|
|
1107
|
+
await expect(fixtureRow).toContainText(customer.fixture.phone);
|
|
1108
|
+
await expect(fixtureRow).toContainText(customer.fixture.countryCode);
|
|
1109
|
+
await expect(fixtureRow).toContainText(customer.fixture.city);
|
|
1110
|
+
await expect(fixtureRow).not.toContainText(customer.fixture.region);
|
|
1111
|
+
await expect(fixtureRow).not.toContainText(customer.fixture.postalCode);
|
|
1112
|
+
await expect(fixtureRow).not.toContainText(customer.fixture.addressLine1);
|
|
1113
|
+
await expect(fixtureRow).not.toContainText(customer.fixture.addressLine2);
|
|
792
1114
|
await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0);
|
|
793
1115
|
await expect(
|
|
794
1116
|
adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }),
|
|
@@ -796,6 +1118,7 @@ test(
|
|
|
796
1118
|
|
|
797
1119
|
const addressId = await fixtureRow.getByRole('cell').first().textContent();
|
|
798
1120
|
expect(addressId).toMatch(/^\d+$/);
|
|
1121
|
+
await expect(fixtureRow.getByRole('link', { name: addressId! })).toHaveCount(1);
|
|
799
1122
|
await adminPage.goto(`/commerce-admin/rest/resource/commerce-member%3Aaddress/${addressId}`, {
|
|
800
1123
|
waitUntil: 'load',
|
|
801
1124
|
});
|
|
@@ -1101,6 +1424,170 @@ test(
|
|
|
1101
1424
|
},
|
|
1102
1425
|
);
|
|
1103
1426
|
|
|
1427
|
+
test(
|
|
1428
|
+
'ATP-SPC-02: SKU renders semantic Admin currency and lifecycle controls',
|
|
1429
|
+
{ tag: ['@admin', '@flow', '@sku'] },
|
|
1430
|
+
async ({ browser }, testInfo) => {
|
|
1431
|
+
test.setTimeout(60_000);
|
|
1432
|
+
const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
1433
|
+
const categoryName = `E2E SKU Category ${suffix}`;
|
|
1434
|
+
const productTitle = `E2E SKU Product ${suffix}`;
|
|
1435
|
+
const skuCode = `E2E-SKU-${suffix}`;
|
|
1436
|
+
const adminContext = await browser.newContext();
|
|
1437
|
+
const adminPage = await adminContext.newPage();
|
|
1438
|
+
const adminPageErrors = collectPageErrors(adminPage);
|
|
1439
|
+
const skuCreatePath = '/commerce-admin/rest/resource/commerce-catalog%3Asku/create';
|
|
1440
|
+
const skuListPath = '/commerce-admin/rest/resource/commerce-catalog%3Asku';
|
|
1441
|
+
const categoryActionPath = '/api/commerce/catalog/category';
|
|
1442
|
+
const productActionPath = '/api/commerce/catalog/product';
|
|
1443
|
+
const skuActionPath = '/api/commerce/catalog/sku';
|
|
1444
|
+
let categoryId: number | string | undefined;
|
|
1445
|
+
let productId: number | string | undefined;
|
|
1446
|
+
let skuId: number | string | undefined;
|
|
1447
|
+
let headers: { Authorization: string } | undefined;
|
|
1448
|
+
try {
|
|
1449
|
+
await adminPage.setViewportSize({ width: 1440, height: 900 });
|
|
1450
|
+
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
1451
|
+
const accessToken = (await adminContext.cookies()).find(
|
|
1452
|
+
cookie => cookie.name === 'token',
|
|
1453
|
+
)?.value;
|
|
1454
|
+
expect(accessToken).toBeTruthy();
|
|
1455
|
+
headers = { Authorization: `Bearer ${accessToken}` };
|
|
1456
|
+
|
|
1457
|
+
const categoryResponse = await adminPage.request.post(categoryActionPath, {
|
|
1458
|
+
data: { name: categoryName, published: true },
|
|
1459
|
+
headers,
|
|
1460
|
+
});
|
|
1461
|
+
expect(categoryResponse.ok()).toBeTruthy();
|
|
1462
|
+
categoryId = (await categoryResponse.json()).data;
|
|
1463
|
+
expectTableIdentity(categoryId);
|
|
1464
|
+
|
|
1465
|
+
const productResponse = await adminPage.request.post(productActionPath, {
|
|
1466
|
+
data: { categoryId, title: productTitle, published: true },
|
|
1467
|
+
headers,
|
|
1468
|
+
});
|
|
1469
|
+
expect(productResponse.ok()).toBeTruthy();
|
|
1470
|
+
productId = (await productResponse.json()).data;
|
|
1471
|
+
expectTableIdentity(productId);
|
|
1472
|
+
|
|
1473
|
+
await adminPage.goto(skuCreatePath, { waitUntil: 'load' });
|
|
1474
|
+
const productField = adminPage.getByRole('group', { name: 'Product' });
|
|
1475
|
+
await expect(productField).toBeVisible();
|
|
1476
|
+
await expect(productField.getByRole('combobox')).toHaveCount(0);
|
|
1477
|
+
await expect(productField.getByRole('textbox')).toBeVisible();
|
|
1478
|
+
const lifecycle = adminPage
|
|
1479
|
+
.getByRole('group', { name: 'SKU lifecycle' })
|
|
1480
|
+
.getByRole('combobox');
|
|
1481
|
+
await expect(lifecycle).toBeVisible();
|
|
1482
|
+
await expect(lifecycle.locator('option')).toHaveText([
|
|
1483
|
+
'',
|
|
1484
|
+
'Draft',
|
|
1485
|
+
'Active',
|
|
1486
|
+
'Inactive',
|
|
1487
|
+
'Archived',
|
|
1488
|
+
]);
|
|
1489
|
+
await expect(adminPage.getByRole('group', { name: 'Price (cents)' })).toBeVisible();
|
|
1490
|
+
|
|
1491
|
+
const skuResponse = await adminPage.request.post(skuActionPath, {
|
|
1492
|
+
data: { code: skuCode, productId, priceCents: 1234, lifecycle: 'draft' },
|
|
1493
|
+
headers,
|
|
1494
|
+
});
|
|
1495
|
+
expect(skuResponse.ok()).toBeTruthy();
|
|
1496
|
+
skuId = (await skuResponse.json()).data;
|
|
1497
|
+
expectTableIdentity(skuId);
|
|
1498
|
+
|
|
1499
|
+
await adminPage.goto(skuListPath, { waitUntil: 'load' });
|
|
1500
|
+
const skuRow = adminPage.getByRole('row', { name: new RegExp(skuCode) });
|
|
1501
|
+
await expect(skuRow).toBeVisible();
|
|
1502
|
+
await expect(skuRow.getByText('12.34', { exact: true })).toBeVisible();
|
|
1503
|
+
await expect(skuRow.getByText('Draft', { exact: true })).toBeVisible();
|
|
1504
|
+
await expect(skuRow.getByRole('link', { name: skuCode, exact: true })).toBeVisible();
|
|
1505
|
+
await expect(
|
|
1506
|
+
adminPage.locator('section').getByText('SKU code', { exact: true }),
|
|
1507
|
+
).toBeVisible();
|
|
1508
|
+
await expect(adminPage.getByText('Created At', { exact: true })).toHaveCount(1);
|
|
1509
|
+
await expect(adminPage.getByRole('group', { name: 'Attributes' })).toHaveCount(0);
|
|
1510
|
+
|
|
1511
|
+
await adminPage.goto(`${skuListPath}/${skuId}`, { waitUntil: 'load' });
|
|
1512
|
+
await expect(
|
|
1513
|
+
adminPage.getByRole('group', { name: 'SKU lifecycle' }).getByRole('textbox'),
|
|
1514
|
+
).toHaveValue('Draft');
|
|
1515
|
+
await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
|
|
1516
|
+
await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
|
|
1517
|
+
expect(adminPageErrors).toEqual([]);
|
|
1518
|
+
} finally {
|
|
1519
|
+
if (skuId && headers) {
|
|
1520
|
+
const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
|
|
1521
|
+
expect(response.ok()).toBeTruthy();
|
|
1522
|
+
}
|
|
1523
|
+
if (productId && headers) {
|
|
1524
|
+
const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
|
|
1525
|
+
headers,
|
|
1526
|
+
});
|
|
1527
|
+
expect(response.ok()).toBeTruthy();
|
|
1528
|
+
}
|
|
1529
|
+
if (categoryId && headers) {
|
|
1530
|
+
const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
|
|
1531
|
+
headers,
|
|
1532
|
+
});
|
|
1533
|
+
expect(response.ok()).toBeTruthy();
|
|
1534
|
+
}
|
|
1535
|
+
await adminContext.close().catch(() => {});
|
|
1536
|
+
}
|
|
1537
|
+
},
|
|
1538
|
+
);
|
|
1539
|
+
|
|
1540
|
+
test(
|
|
1541
|
+
'ATP-SPC-04: Stock Admin resources remain readonly and mutation-free',
|
|
1542
|
+
{ tag: ['@admin', '@flow', '@stock'] },
|
|
1543
|
+
async ({ browser }) => {
|
|
1544
|
+
test.setTimeout(60_000);
|
|
1545
|
+
const adminContext = await browser.newContext();
|
|
1546
|
+
const adminPage = await adminContext.newPage();
|
|
1547
|
+
const adminPageErrors = collectPageErrors(adminPage);
|
|
1548
|
+
const mutationMethods: string[] = [];
|
|
1549
|
+
adminPage.on('request', request => {
|
|
1550
|
+
const url = new URL(request.url());
|
|
1551
|
+
if (
|
|
1552
|
+
url.pathname.includes('/api/commerce/trade/stock') &&
|
|
1553
|
+
['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method())
|
|
1554
|
+
) {
|
|
1555
|
+
mutationMethods.push(request.method());
|
|
1556
|
+
}
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
try {
|
|
1560
|
+
await adminPage.setViewportSize({ width: 1440, height: 900 });
|
|
1561
|
+
await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
1562
|
+
|
|
1563
|
+
for (const [resourcePath, title] of [
|
|
1564
|
+
['commerce-trade%3AstockBalance', 'Stock Balance'],
|
|
1565
|
+
['commerce-trade%3AstockAudit', 'Stock Audit'],
|
|
1566
|
+
] as const) {
|
|
1567
|
+
await adminPage.goto(`/commerce-admin/rest/resource/${resourcePath}`, {
|
|
1568
|
+
waitUntil: 'load',
|
|
1569
|
+
});
|
|
1570
|
+
await expect(adminPage).toHaveURL(
|
|
1571
|
+
new RegExp(
|
|
1572
|
+
`/commerce-admin/rest/resource/commerce-trade(?:%3A|:|%253A)${title === 'Stock Balance' ? 'stockBalance' : 'stockAudit'}(?:[/?#]|$)`,
|
|
1573
|
+
),
|
|
1574
|
+
);
|
|
1575
|
+
await expect(adminPage.getByText(title, { exact: true }).first()).toBeVisible();
|
|
1576
|
+
await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0);
|
|
1577
|
+
await expect(
|
|
1578
|
+
adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }),
|
|
1579
|
+
).toHaveCount(0);
|
|
1580
|
+
await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
expect(mutationMethods).toEqual([]);
|
|
1584
|
+
expect(adminPageErrors).toEqual([]);
|
|
1585
|
+
} finally {
|
|
1586
|
+
await adminContext.close().catch(() => {});
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
);
|
|
1590
|
+
|
|
1104
1591
|
test(
|
|
1105
1592
|
'Commerce Cart: anonymous browser is redirected to login',
|
|
1106
1593
|
{ tag: ['@web', '@cart'] },
|