cabloy 5.1.201 → 5.1.203
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cabloy-version +1 -1
- package/CHANGELOG.md +17 -0
- package/CONTRIBUTING.md +18 -17
- package/README.md +2 -2
- package/package.json +2 -3
- package/repo-agent-governance/managed-assets.json +33 -33
- package/repo-agent-governance/policies/repository.md +1 -0
- package/repo-agent-governance/skills/cabloy-spec-execution/SKILL.md +3 -2
- package/repo-agent-governance/skills/cabloy-spec-execution/references/execution-protocol.md +1 -1
- package/repo-agent-governance/skills/cabloy-spec-execution/references/status-and-evidence.md +11 -10
- package/repo-agent-governance/skills/cabloy-spec-generation/SKILL.md +1 -1
- package/repo-agent-governance/skills/cabloy-spec-generation/references/canonical-spec-input.md +4 -2
- package/repo-agent-governance/skills/cabloy-spec-generation/references/repo-specs-document-set.md +16 -13
- package/repo-agent-governance/skills/cabloy-spec-generation/references/traceability-and-status-rules.md +18 -11
- package/repo-agent-governance/skills/cabloy-workflow/SKILL.md +2 -2
- package/repo-agent-governance/skills/cabloy-workflow/evals/evals.json +1 -1
- package/repo-agent-governance/skills/cabloy-worktree-environment/SKILL.md +5 -5
- package/repo-agent-governance/tests/spec-audit.test.mjs +323 -0
- package/repo-agent-governance/tools/spec-audit/audit.mjs +129 -3
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.mjs +26 -7
- package/repo-agent-governance/tools/spec-charts/generate-implementation-charts.test.mjs +82 -0
- package/repo-agent-governance/tools/spec-charts/spec-parser.mjs +40 -1
- package/repo-docs/ai/ai-spec-driven-development.md +1 -0
- package/repo-docs/ai/playbook-spec-execution.md +3 -2
- package/repo-docs/ai/playbook-spec-generation.md +1 -1
- package/repo-docs/assets/img/cabloy-admin-ssr-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-admin-tabs-cover-en.png +0 -0
- package/repo-docs/assets/img/cabloy-ai-crud-cover-en.png +0 -0
- package/repo-docs/demo.md +14 -0
- package/repo-docs/frontend/scripts.md +10 -18
- package/repo-docs/fullstack/parallel-worktree-environment.md +2 -2
- package/repo-docs/index.md +2 -2
- package/repo-docs/reference/repo-scripts.md +15 -31
- package/repo-e2e/config/playwright.config.ts +14 -19
- package/repo-e2e/scripts/e2e.test.ts +17 -0
- package/repo-e2e/scripts/e2e.ts +5 -1
- package/repo-e2e/scripts/runE2e.ts +3 -15
- package/repo-e2e/scripts/runE2eArgs.test.ts +21 -17
- package/repo-e2e/scripts/runE2eArgs.ts +16 -29
- package/repo-e2e/scripts/startE2eVona.ts +20 -9
- package/repo-e2e/specs/a-commerce.spec.ts +232 -31
- package/repo-e2e/specs/cabloy-basic.spec.ts +39 -12
- package/repo-e2e/specs/home-user-account.spec.ts +9 -2
- package/scripts/upgrade.test.ts +147 -0
- package/scripts/upgrade.ts +49 -27
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +51 -5
- package/vona/packages-cli/cli-set-api/src/lib/command/bin.dev.ts +6 -1
- package/vona/packages-cli/cli-set-api/test/binDevMode.test.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/.metadata/index.ts +41 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/controller/e2eFixture.ts +52 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/lib/e2eFixture.ts +224 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/src/service/e2eFixture.ts +694 -0
- package/vona/src/suite/a-commerce/modules/commerce-trade/test/e2eFixture.test.ts +434 -0
|
@@ -8,6 +8,155 @@ const addressResourceUrl =
|
|
|
8
8
|
const addressMinePath = '/api/commerce/member/address/mine';
|
|
9
9
|
const addressActionPath = '/api/commerce/member/address';
|
|
10
10
|
const passportTestActivateCurrentPath = '/api/home/user/passportTest/activateCurrent';
|
|
11
|
+
const commerceFixturePath = '/api/commerce/trade/e2eFixture';
|
|
12
|
+
|
|
13
|
+
interface IOwnedCommerceFixture {
|
|
14
|
+
customerToken?: string;
|
|
15
|
+
customerName?: string;
|
|
16
|
+
adminToken?: string;
|
|
17
|
+
categoryId?: number | string;
|
|
18
|
+
orderIds: Array<number | string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ownedFixtures = new Map<string, IOwnedCommerceFixture>();
|
|
22
|
+
|
|
23
|
+
function ownedFixture(testInfo: TestInfo): IOwnedCommerceFixture {
|
|
24
|
+
let fixture = ownedFixtures.get(testInfo.testId);
|
|
25
|
+
if (!fixture) {
|
|
26
|
+
fixture = { orderIds: [] };
|
|
27
|
+
ownedFixtures.set(testInfo.testId, fixture);
|
|
28
|
+
}
|
|
29
|
+
return fixture;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function fixtureRequest(
|
|
33
|
+
request: APIRequestContext,
|
|
34
|
+
method: 'post' | 'delete',
|
|
35
|
+
path: string,
|
|
36
|
+
token: string,
|
|
37
|
+
data?: object,
|
|
38
|
+
) {
|
|
39
|
+
const response = await request[method](path, {
|
|
40
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' },
|
|
41
|
+
...(data ? { data } : {}),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok())
|
|
44
|
+
throw new Error(`Commerce fixture ${method} ${path} failed (${response.status()})`);
|
|
45
|
+
if (method === 'delete') return;
|
|
46
|
+
return (await response.json()).data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function createOwnedCatalogue(browser: Browser, testInfo: TestInfo) {
|
|
50
|
+
const fixture = ownedFixture(testInfo);
|
|
51
|
+
const context = await browser.newContext();
|
|
52
|
+
try {
|
|
53
|
+
const page = await context.newPage();
|
|
54
|
+
await login(page, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
|
|
55
|
+
const token = (await context.cookies()).find(cookie => cookie.name === 'token')?.value;
|
|
56
|
+
if (!token) throw new Error('Commerce Admin fixture token unavailable');
|
|
57
|
+
fixture.adminToken = token;
|
|
58
|
+
// Server-side creation is transactional; record the returned category before
|
|
59
|
+
// validating the response so assertion failures still trigger its cleanup.
|
|
60
|
+
const catalogue = await fixtureRequest(
|
|
61
|
+
context.request,
|
|
62
|
+
'post',
|
|
63
|
+
`${commerceFixturePath}/catalogue`,
|
|
64
|
+
token,
|
|
65
|
+
);
|
|
66
|
+
fixture.categoryId = catalogue.categoryId;
|
|
67
|
+
expectTableIdentity(catalogue.categoryId);
|
|
68
|
+
expectTableIdentity(catalogue.skuId);
|
|
69
|
+
expect(catalogue.initialStock).toBe(10);
|
|
70
|
+
return catalogue as {
|
|
71
|
+
categoryId: number | string;
|
|
72
|
+
skuId: number | string;
|
|
73
|
+
title: string;
|
|
74
|
+
code: string;
|
|
75
|
+
initialStock: number;
|
|
76
|
+
};
|
|
77
|
+
} finally {
|
|
78
|
+
await context.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function dispatchOwnedOutcome(
|
|
83
|
+
request: APIRequestContext,
|
|
84
|
+
testInfo: TestInfo,
|
|
85
|
+
checkout: { orderId: number | string; paymentAttemptId: number | string },
|
|
86
|
+
refundAttemptId?: number | string,
|
|
87
|
+
) {
|
|
88
|
+
const token = ownedFixture(testInfo).customerToken;
|
|
89
|
+
if (!token) throw new Error('Commerce customer fixture token unavailable');
|
|
90
|
+
return await fixtureRequest(
|
|
91
|
+
request,
|
|
92
|
+
'post',
|
|
93
|
+
`${commerceFixturePath}/order/${checkout.orderId}/${refundAttemptId ? 'refund' : 'payment'}/${refundAttemptId ?? checkout.paymentAttemptId}/dispatch`,
|
|
94
|
+
token,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Playwright invokes afterEach even for partial setup and failed assertions. Never delete
|
|
99
|
+
// shared seed stock: remove only the IDs created by these tests, in reverse dependency order.
|
|
100
|
+
test.afterEach(async ({ request }, testInfo) => {
|
|
101
|
+
const fixture = ownedFixtures.get(testInfo.testId);
|
|
102
|
+
if (!fixture) return;
|
|
103
|
+
ownedFixtures.delete(testInfo.testId);
|
|
104
|
+
const errors: Error[] = [];
|
|
105
|
+
const clean = async (label: string, action: () => Promise<unknown>) => {
|
|
106
|
+
try {
|
|
107
|
+
await action();
|
|
108
|
+
} catch (error) {
|
|
109
|
+
errors.push(new Error(`${label}: ${String(error)}`));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
if (fixture.customerToken) {
|
|
113
|
+
for (const orderId of fixture.orderIds.toReversed()) {
|
|
114
|
+
await clean(`order ${orderId}`, () =>
|
|
115
|
+
fixtureRequest(
|
|
116
|
+
request,
|
|
117
|
+
'delete',
|
|
118
|
+
`${commerceFixturePath}/order/${orderId}`,
|
|
119
|
+
fixture.customerToken!,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (!errors.length) {
|
|
124
|
+
await clean('customer Commerce resources', () =>
|
|
125
|
+
fixtureRequest(
|
|
126
|
+
request,
|
|
127
|
+
'delete',
|
|
128
|
+
`${commerceFixturePath}/customer`,
|
|
129
|
+
fixture.customerToken!,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
// Keep the identity when Commerce cleanup fails, so its surviving records
|
|
134
|
+
// retain their owner and can be identified and removed without a global sweep.
|
|
135
|
+
if (!errors.length) {
|
|
136
|
+
await clean('customer account', () =>
|
|
137
|
+
fixtureRequest(
|
|
138
|
+
request,
|
|
139
|
+
'delete',
|
|
140
|
+
'/api/home/user/passportTest/removeCurrentFixture',
|
|
141
|
+
fixture.customerToken!,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Preserve the catalogue if order/cart cleanup failed: deleting its SKU would
|
|
147
|
+
// otherwise obscure the precise ownership of a surviving customer's records.
|
|
148
|
+
if (!errors.length && fixture.categoryId && fixture.adminToken) {
|
|
149
|
+
await clean(`catalogue ${fixture.categoryId}`, () =>
|
|
150
|
+
fixtureRequest(
|
|
151
|
+
request,
|
|
152
|
+
'delete',
|
|
153
|
+
`${commerceFixturePath}/catalogue/${fixture.categoryId}`,
|
|
154
|
+
fixture.adminToken!,
|
|
155
|
+
),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (errors.length) throw new AggregateError(errors, 'Commerce E2E fixture cleanup failed');
|
|
159
|
+
});
|
|
11
160
|
|
|
12
161
|
interface IAddressFixture {
|
|
13
162
|
addressLine1: string;
|
|
@@ -105,7 +254,7 @@ async function registerCustomer(
|
|
|
105
254
|
testInfo: TestInfo,
|
|
106
255
|
): Promise<{ password: string; username: string }> {
|
|
107
256
|
const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
|
|
108
|
-
const username = `e2e-address-${id}`;
|
|
257
|
+
const username = `e2e-fixture-address-${id}`;
|
|
109
258
|
const password = 'address-e2e-password';
|
|
110
259
|
const captchaResponse = await request.post('/api/captcha/create', {
|
|
111
260
|
data: { scene: 'captcha-simple:simple' },
|
|
@@ -131,6 +280,9 @@ async function registerCustomer(
|
|
|
131
280
|
expect(registerResponse.ok()).toBeTruthy();
|
|
132
281
|
const registration = (await registerResponse.json()).data;
|
|
133
282
|
expect(registration?.jwt?.accessToken).toEqual(expect.any(String));
|
|
283
|
+
const fixture = ownedFixture(testInfo);
|
|
284
|
+
fixture.customerToken = registration.jwt.accessToken;
|
|
285
|
+
fixture.customerName = username;
|
|
134
286
|
const activateResponse = await request.post(passportTestActivateCurrentPath, {
|
|
135
287
|
headers: { Authorization: `Bearer ${registration.jwt.accessToken}` },
|
|
136
288
|
});
|
|
@@ -220,7 +372,7 @@ test(
|
|
|
220
372
|
async ({ page, request }) => {
|
|
221
373
|
const response = await request.get('/commerce');
|
|
222
374
|
expect(response.ok()).toBeTruthy();
|
|
223
|
-
expect(response.headers()['cache-control']).toBe('
|
|
375
|
+
expect(response.headers()['cache-control']).toBe('public, max-age=600');
|
|
224
376
|
const html = await response.text();
|
|
225
377
|
const normalizedHtml = html.toLowerCase();
|
|
226
378
|
expect(normalizedHtml).not.toContain('data-zova-hydrated');
|
|
@@ -382,7 +534,7 @@ test(
|
|
|
382
534
|
for (const [index, response] of responses.entries()) {
|
|
383
535
|
expect(response.status(), `public response ${index}`).toBe(200);
|
|
384
536
|
expect(response.headers()['cache-control'], `public response ${index}`).toBe(
|
|
385
|
-
'
|
|
537
|
+
'public, max-age=600',
|
|
386
538
|
);
|
|
387
539
|
expect(html[index].toLowerCase()).not.toContain('data-zova-hydrated');
|
|
388
540
|
expect(html[index]).toContain('data-ssr-theme-dark-false="light"');
|
|
@@ -629,16 +781,22 @@ test(
|
|
|
629
781
|
testInfo,
|
|
630
782
|
);
|
|
631
783
|
try {
|
|
784
|
+
const catalogue = await createOwnedCatalogue(browser, testInfo);
|
|
632
785
|
await page.goto('/commerce', { waitUntil: 'load' });
|
|
633
786
|
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
634
787
|
await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible();
|
|
635
788
|
|
|
636
|
-
const productLink = page.getByRole('link', { name:
|
|
789
|
+
const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
|
|
637
790
|
await expect(productLink).toBeVisible();
|
|
638
791
|
await productLink.click();
|
|
639
792
|
await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
|
|
640
|
-
await expect(page.getByRole('heading', { name:
|
|
641
|
-
await expect(
|
|
793
|
+
await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
|
|
794
|
+
await expect(
|
|
795
|
+
page
|
|
796
|
+
.getByRole('button', { name: 'Add to cart', exact: true })
|
|
797
|
+
.locator('..')
|
|
798
|
+
.getByText(catalogue.code),
|
|
799
|
+
).toBeVisible();
|
|
642
800
|
await expect(page.getByText('$45.99')).toBeVisible();
|
|
643
801
|
|
|
644
802
|
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
@@ -649,7 +807,7 @@ test(
|
|
|
649
807
|
await expect(page).toHaveURL(/\/commerce\/commerce\/trade\/cart(?:\/|$)/);
|
|
650
808
|
await expect(page.getByRole('heading', { name: 'Cart' })).toBeVisible();
|
|
651
809
|
const cartItem = page.locator('article').filter({ has: page.getByRole('spinbutton') });
|
|
652
|
-
await expect(cartItem).toContainText(
|
|
810
|
+
await expect(cartItem).toContainText(catalogue.code);
|
|
653
811
|
await expect(cartItem).toContainText('$45.99');
|
|
654
812
|
await expect(cartItem.getByRole('spinbutton')).toHaveValue('1');
|
|
655
813
|
|
|
@@ -669,6 +827,7 @@ test(
|
|
|
669
827
|
const checkoutResponseValue = await checkoutResponse;
|
|
670
828
|
expect(checkoutResponseValue.ok()).toBeTruthy();
|
|
671
829
|
const checkout = (await checkoutResponseValue.json()).data;
|
|
830
|
+
ownedFixture(testInfo).orderIds.push(checkout.orderId);
|
|
672
831
|
expectTableIdentity(checkout.orderId);
|
|
673
832
|
expectTableIdentity(checkout.paymentAttemptId);
|
|
674
833
|
expectTableIdentity(checkout.paymentSessionId);
|
|
@@ -699,6 +858,11 @@ test(
|
|
|
699
858
|
);
|
|
700
859
|
await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
|
|
701
860
|
expect((await completeResponse).ok()).toBeTruthy();
|
|
861
|
+
expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
|
|
862
|
+
orderState: 'paid',
|
|
863
|
+
attemptState: 'succeeded',
|
|
864
|
+
stock: 9,
|
|
865
|
+
});
|
|
702
866
|
await expect(page).toHaveURL(
|
|
703
867
|
new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
|
|
704
868
|
{
|
|
@@ -717,8 +881,8 @@ test(
|
|
|
717
881
|
expect(orderHtml).toContain(fixture.recipientName);
|
|
718
882
|
expect(orderHtml).toContain(fixture.addressLine1);
|
|
719
883
|
expect(orderHtml).toContain('Discount: $0.00');
|
|
720
|
-
expect(orderHtml).toContain(
|
|
721
|
-
expect(orderHtml).toContain(
|
|
884
|
+
expect(orderHtml).toContain(catalogue.title);
|
|
885
|
+
expect(orderHtml).toContain(catalogue.code);
|
|
722
886
|
expect(orderHtml).toContain('1 × $45.99 = $45.99');
|
|
723
887
|
|
|
724
888
|
const orderDocumentResponse = await page.reload({ waitUntil: 'load' });
|
|
@@ -729,8 +893,8 @@ test(
|
|
|
729
893
|
await expect(page.getByText(fixture.recipientName)).toBeVisible();
|
|
730
894
|
await expect(page.getByText(fixture.addressLine1)).toBeVisible();
|
|
731
895
|
await expect(page.getByText('Discount: $0.00')).toBeVisible();
|
|
732
|
-
await expect(page.getByRole('heading', { name:
|
|
733
|
-
await expect(page.
|
|
896
|
+
await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
|
|
897
|
+
await expect(page.getByRole('paragraph').filter({ hasText: catalogue.code })).toBeVisible();
|
|
734
898
|
await expect(page.getByText('1 × $45.99 = $45.99')).toBeVisible();
|
|
735
899
|
|
|
736
900
|
const shipmentCarrier = 'Cabloy Express';
|
|
@@ -766,8 +930,9 @@ test(
|
|
|
766
930
|
await expect(adminPage.getByText('Purchased lines').first()).toBeVisible();
|
|
767
931
|
await expect(adminPage.getByText('addressSnapshot').first()).toBeVisible();
|
|
768
932
|
await expect(adminPage.getByText('couponSnapshot').first()).toBeVisible();
|
|
769
|
-
await expect(
|
|
770
|
-
|
|
933
|
+
await expect(
|
|
934
|
+
adminPage.getByRole('cell', { name: catalogue.title, exact: true }),
|
|
935
|
+
).toHaveCount(2);
|
|
771
936
|
await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
|
|
772
937
|
await adminPage.getByRole('button', { name: 'Back', exact: true }).click();
|
|
773
938
|
await expect(orderRow).toBeVisible();
|
|
@@ -844,15 +1009,16 @@ test(
|
|
|
844
1009
|
testInfo,
|
|
845
1010
|
);
|
|
846
1011
|
try {
|
|
1012
|
+
const catalogue = await createOwnedCatalogue(browser, testInfo);
|
|
847
1013
|
await page.goto('/commerce', { waitUntil: 'load' });
|
|
848
|
-
const productLink = page.getByRole('link', { name:
|
|
1014
|
+
const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
|
|
849
1015
|
await expect(productLink).toBeVisible();
|
|
850
1016
|
await productLink.click();
|
|
851
1017
|
await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
|
|
852
1018
|
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
853
|
-
await expect(page.getByRole('heading', { name:
|
|
854
|
-
await expect(page.getByText('COF-SET-01')).toBeVisible();
|
|
1019
|
+
await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
|
|
855
1020
|
const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
|
|
1021
|
+
await expect(addToCartButton.locator('..')).toContainText(catalogue.code);
|
|
856
1022
|
await expect(addToCartButton).toBeEnabled();
|
|
857
1023
|
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
858
1024
|
await addToCartButton.click();
|
|
@@ -868,6 +1034,7 @@ test(
|
|
|
868
1034
|
const checkoutResponseValue = await checkoutResponse;
|
|
869
1035
|
expect(checkoutResponseValue.ok()).toBeTruthy();
|
|
870
1036
|
const checkout = (await checkoutResponseValue.json()).data;
|
|
1037
|
+
ownedFixture(testInfo).orderIds.push(checkout.orderId);
|
|
871
1038
|
expectTableIdentity(checkout.orderId);
|
|
872
1039
|
expectTableIdentity(checkout.paymentSessionId);
|
|
873
1040
|
const startResponse = waitForApiResponse(
|
|
@@ -885,6 +1052,11 @@ test(
|
|
|
885
1052
|
);
|
|
886
1053
|
await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
|
|
887
1054
|
expect((await completeResponse).ok()).toBeTruthy();
|
|
1055
|
+
expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
|
|
1056
|
+
orderState: 'paid',
|
|
1057
|
+
attemptState: 'succeeded',
|
|
1058
|
+
stock: 9,
|
|
1059
|
+
});
|
|
888
1060
|
await expect(page).toHaveURL(
|
|
889
1061
|
new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
|
|
890
1062
|
{
|
|
@@ -951,15 +1123,16 @@ test(
|
|
|
951
1123
|
testInfo,
|
|
952
1124
|
);
|
|
953
1125
|
try {
|
|
1126
|
+
const catalogue = await createOwnedCatalogue(browser, testInfo);
|
|
954
1127
|
await page.goto('/commerce', { waitUntil: 'load' });
|
|
955
|
-
const productLink = page.getByRole('link', { name:
|
|
1128
|
+
const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
|
|
956
1129
|
await expect(productLink).toBeVisible();
|
|
957
1130
|
await productLink.click();
|
|
958
1131
|
await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
|
|
959
1132
|
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
960
|
-
await expect(page.getByRole('heading', { name:
|
|
961
|
-
await expect(page.getByText('COF-SET-01')).toBeVisible();
|
|
1133
|
+
await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
|
|
962
1134
|
const addToCartButton = page.getByRole('button', { name: 'Add to cart', exact: true });
|
|
1135
|
+
await expect(addToCartButton.locator('..')).toContainText(catalogue.code);
|
|
963
1136
|
await expect(addToCartButton).toBeEnabled();
|
|
964
1137
|
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
965
1138
|
await addToCartButton.click();
|
|
@@ -975,6 +1148,7 @@ test(
|
|
|
975
1148
|
const checkoutResponseValue = await checkoutResponse;
|
|
976
1149
|
expect(checkoutResponseValue.ok()).toBeTruthy();
|
|
977
1150
|
const checkout = (await checkoutResponseValue.json()).data;
|
|
1151
|
+
ownedFixture(testInfo).orderIds.push(checkout.orderId);
|
|
978
1152
|
expectTableIdentity(checkout.orderId);
|
|
979
1153
|
expectTableIdentity(checkout.paymentSessionId);
|
|
980
1154
|
expect(checkout.state).toBe('awaiting_payment');
|
|
@@ -1077,13 +1251,14 @@ test(
|
|
|
1077
1251
|
testInfo,
|
|
1078
1252
|
);
|
|
1079
1253
|
try {
|
|
1254
|
+
const catalogue = await createOwnedCatalogue(browser, testInfo);
|
|
1080
1255
|
await page.goto('/commerce', { waitUntil: 'load' });
|
|
1081
1256
|
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
1082
|
-
const productLink = page.getByRole('link', { name:
|
|
1257
|
+
const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
|
|
1083
1258
|
await expect(productLink).toBeVisible();
|
|
1084
1259
|
await productLink.click();
|
|
1085
1260
|
await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
|
|
1086
|
-
await expect(page.getByRole('heading', { name:
|
|
1261
|
+
await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
|
|
1087
1262
|
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
1088
1263
|
await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
|
|
1089
1264
|
expect((await addResponse).ok()).toBeTruthy();
|
|
@@ -1100,6 +1275,7 @@ test(
|
|
|
1100
1275
|
const checkoutResponseValue = await checkoutResponse;
|
|
1101
1276
|
expect(checkoutResponseValue.ok()).toBeTruthy();
|
|
1102
1277
|
const checkout = (await checkoutResponseValue.json()).data;
|
|
1278
|
+
ownedFixture(testInfo).orderIds.push(checkout.orderId);
|
|
1103
1279
|
expectTableIdentity(checkout.orderId);
|
|
1104
1280
|
expectTableIdentity(checkout.paymentSessionId);
|
|
1105
1281
|
const startResponse = waitForApiResponse(
|
|
@@ -1118,6 +1294,11 @@ test(
|
|
|
1118
1294
|
);
|
|
1119
1295
|
await page.getByRole('button', { name: 'Cancel payment', exact: true }).click();
|
|
1120
1296
|
expect((await cancelResponse).ok()).toBeTruthy();
|
|
1297
|
+
expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
|
|
1298
|
+
orderState: 'cancelled',
|
|
1299
|
+
attemptState: 'cancelled',
|
|
1300
|
+
stock: 10,
|
|
1301
|
+
});
|
|
1121
1302
|
await expect(page).toHaveURL(
|
|
1122
1303
|
new RegExp(`/commerce/commerce/trade/order/${checkout.orderId}(?:/|$)`),
|
|
1123
1304
|
{
|
|
@@ -1144,15 +1325,21 @@ test(
|
|
|
1144
1325
|
testInfo,
|
|
1145
1326
|
);
|
|
1146
1327
|
try {
|
|
1328
|
+
const catalogue = await createOwnedCatalogue(browser, testInfo);
|
|
1147
1329
|
await page.goto('/commerce', { waitUntil: 'load' });
|
|
1148
1330
|
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
|
|
1149
1331
|
await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible();
|
|
1150
|
-
const productLink = page.getByRole('link', { name:
|
|
1332
|
+
const productLink = page.getByRole('link', { name: catalogue.title, exact: true });
|
|
1151
1333
|
await expect(productLink).toBeVisible();
|
|
1152
1334
|
await productLink.click();
|
|
1153
1335
|
await expect(page).toHaveURL(/\/commerce\/product\/\d+(?:\/|$)/);
|
|
1154
|
-
await expect(page.getByRole('heading', { name:
|
|
1155
|
-
await expect(
|
|
1336
|
+
await expect(page.getByRole('heading', { name: catalogue.title })).toBeVisible();
|
|
1337
|
+
await expect(
|
|
1338
|
+
page
|
|
1339
|
+
.getByRole('button', { name: 'Add to cart', exact: true })
|
|
1340
|
+
.locator('..')
|
|
1341
|
+
.getByText(catalogue.code),
|
|
1342
|
+
).toBeVisible();
|
|
1156
1343
|
await expect(page.getByText('$45.99')).toBeVisible();
|
|
1157
1344
|
const addResponse = waitForApiResponse(page, 'POST', '/api/commerce/trade/cart/items');
|
|
1158
1345
|
await page.getByRole('button', { name: 'Add to cart', exact: true }).click();
|
|
@@ -1169,6 +1356,7 @@ test(
|
|
|
1169
1356
|
const checkoutResponseValue = await checkoutResponse;
|
|
1170
1357
|
expect(checkoutResponseValue.ok()).toBeTruthy();
|
|
1171
1358
|
const checkout = (await checkoutResponseValue.json()).data;
|
|
1359
|
+
ownedFixture(testInfo).orderIds.push(checkout.orderId);
|
|
1172
1360
|
expectTableIdentity(checkout.orderId);
|
|
1173
1361
|
expectTableIdentity(checkout.paymentAttemptId);
|
|
1174
1362
|
expectTableIdentity(checkout.paymentSessionId);
|
|
@@ -1189,6 +1377,11 @@ test(
|
|
|
1189
1377
|
);
|
|
1190
1378
|
await page.getByRole('button', { name: 'Payment succeeded', exact: true }).click();
|
|
1191
1379
|
expect((await completeResponse).ok()).toBeTruthy();
|
|
1380
|
+
expect(await dispatchOwnedOutcome(request, testInfo, checkout)).toMatchObject({
|
|
1381
|
+
orderState: 'paid',
|
|
1382
|
+
attemptState: 'succeeded',
|
|
1383
|
+
stock: 9,
|
|
1384
|
+
});
|
|
1192
1385
|
await expect(page.getByText('paid · $45.99')).toBeVisible({ timeout: 40_000 });
|
|
1193
1386
|
|
|
1194
1387
|
const requestResponse = waitForApiResponse(
|
|
@@ -1329,15 +1522,23 @@ test(
|
|
|
1329
1522
|
exact: true,
|
|
1330
1523
|
}),
|
|
1331
1524
|
).toHaveCount(0);
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1525
|
+
// The Admin mock button is intentionally hidden in the production-built SSR UI.
|
|
1526
|
+
// Use the existing dev/test Admin API to submit its signed mock webhook, then
|
|
1527
|
+
// dispatch the linked event under the customer's separate identity.
|
|
1528
|
+
const adminToken = ownedFixture(testInfo).adminToken;
|
|
1529
|
+
if (!adminToken) throw new Error('Commerce Admin fixture token unavailable');
|
|
1530
|
+
const completeRefund = await adminContext.request.post(
|
|
1335
1531
|
`/api/pay/mock/payment-session/refund-operation/${executeResult.refundOperationId}/complete`,
|
|
1532
|
+
{ headers: { Authorization: `Bearer ${adminToken}` }, data: { outcome: 'succeeded' } },
|
|
1336
1533
|
);
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
.
|
|
1340
|
-
|
|
1534
|
+
expect(completeRefund.ok()).toBeTruthy();
|
|
1535
|
+
expect(
|
|
1536
|
+
await dispatchOwnedOutcome(request, testInfo, checkout, executeResult.refundAttemptId),
|
|
1537
|
+
).toMatchObject({
|
|
1538
|
+
orderState: 'refunded',
|
|
1539
|
+
refundState: 'refunded',
|
|
1540
|
+
stock: 10,
|
|
1541
|
+
});
|
|
1341
1542
|
await expect
|
|
1342
1543
|
.poll(
|
|
1343
1544
|
async () => {
|
|
@@ -40,11 +40,23 @@ function collectConsoleErrors(page: Page) {
|
|
|
40
40
|
|
|
41
41
|
async function loginAsAdmin(page: Page) {
|
|
42
42
|
await page.goto('/admin/', { waitUntil: 'load' });
|
|
43
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
43
44
|
if (page.url().includes('/admin/login')) {
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const usernameInput = page.getByPlaceholder('Your Username');
|
|
46
|
+
const passwordInput = page.getByPlaceholder('Your Password');
|
|
47
|
+
await usernameInput.fill('admin');
|
|
48
|
+
await passwordInput.fill('123456');
|
|
49
|
+
await expect(usernameInput).toHaveValue('admin');
|
|
50
|
+
await expect(passwordInput).toHaveValue('123456');
|
|
46
51
|
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
52
|
+
const loginResponse = page.waitForResponse(response => {
|
|
53
|
+
const url = new URL(response.url());
|
|
54
|
+
return (
|
|
55
|
+
response.request().method() === 'POST' && url.pathname === '/api/home/user/passport/login'
|
|
56
|
+
);
|
|
57
|
+
});
|
|
47
58
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
59
|
+
expect((await loginResponse).ok()).toBeTruthy();
|
|
48
60
|
await expect(page).not.toHaveURL(/\/admin\/login(?:\?|$)/);
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -158,7 +170,7 @@ test(
|
|
|
158
170
|
async ({ page, request }) => {
|
|
159
171
|
const response = await request.get('/');
|
|
160
172
|
expect(response.ok()).toBeTruthy();
|
|
161
|
-
expect(response.headers()['cache-control']).toBe('
|
|
173
|
+
expect(response.headers()['cache-control']).toBe('public, max-age=600');
|
|
162
174
|
const html = await response.text();
|
|
163
175
|
expect(html.toLowerCase()).not.toContain('data-zova-hydrated');
|
|
164
176
|
expect(html).not.toContain('ssr-body-ready-observer');
|
|
@@ -166,9 +178,7 @@ test(
|
|
|
166
178
|
|
|
167
179
|
const routeOverrideResponse = await request.get('/demo/basic/routeQueryB');
|
|
168
180
|
expect(routeOverrideResponse.ok()).toBeTruthy();
|
|
169
|
-
expect(routeOverrideResponse.headers()['cache-control']).toBe(
|
|
170
|
-
'no-cache, no-store, must-revalidate',
|
|
171
|
-
);
|
|
181
|
+
expect(routeOverrideResponse.headers()['cache-control']).toBe('public, max-age=300');
|
|
172
182
|
|
|
173
183
|
const unlocalizedResponse = await request.get('/demo/basic/state');
|
|
174
184
|
expect(unlocalizedResponse.ok()).toBeTruthy();
|
|
@@ -192,7 +202,7 @@ test(
|
|
|
192
202
|
);
|
|
193
203
|
|
|
194
204
|
test(
|
|
195
|
-
'ATP-BASIC-SSR-03: concurrent public routes
|
|
205
|
+
'ATP-BASIC-SSR-03: concurrent public routes preserve their cache policies in managed test mode',
|
|
196
206
|
{ tag: ['@web', '@smoke'] },
|
|
197
207
|
async ({ request }) => {
|
|
198
208
|
const responses = await Promise.all(
|
|
@@ -205,9 +215,13 @@ test(
|
|
|
205
215
|
|
|
206
216
|
for (const [index, response] of responses.entries()) {
|
|
207
217
|
expect(response.ok(), `response ${index}`).toBeTruthy();
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
218
|
+
const cacheControl =
|
|
219
|
+
index % 3 === 0
|
|
220
|
+
? 'public, max-age=600'
|
|
221
|
+
: index % 3 === 1
|
|
222
|
+
? 'public, max-age=300'
|
|
223
|
+
: 'no-cache, no-store, must-revalidate';
|
|
224
|
+
expect(response.headers()['cache-control'], `response ${index}`).toBe(cacheControl);
|
|
211
225
|
}
|
|
212
226
|
},
|
|
213
227
|
);
|
|
@@ -338,6 +352,7 @@ test(
|
|
|
338
352
|
const consoleErrors = collectConsoleErrors(page);
|
|
339
353
|
try {
|
|
340
354
|
await page.goto('/admin/login', { waitUntil: 'load' });
|
|
355
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
341
356
|
await page.evaluate(() => {
|
|
342
357
|
indexedDB.deleteDatabase('localforage');
|
|
343
358
|
localStorage.clear();
|
|
@@ -346,13 +361,25 @@ test(
|
|
|
346
361
|
await page.goto('/admin/login?returnTo=/rest/resource/training-student%253Astudent', {
|
|
347
362
|
waitUntil: 'load',
|
|
348
363
|
});
|
|
349
|
-
await page.
|
|
350
|
-
|
|
364
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
365
|
+
const usernameInput = page.getByPlaceholder('Your Username');
|
|
366
|
+
const passwordInput = page.getByPlaceholder('Your Password');
|
|
367
|
+
await usernameInput.fill('admin');
|
|
368
|
+
await passwordInput.fill('123456');
|
|
369
|
+
await expect(usernameInput).toHaveValue('admin');
|
|
370
|
+
await expect(passwordInput).toHaveValue('123456');
|
|
351
371
|
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
352
372
|
|
|
353
373
|
const initialSelect = waitForStudentSelect(page);
|
|
374
|
+
const loginResponse = page.waitForResponse(response => {
|
|
375
|
+
const url = new URL(response.url());
|
|
376
|
+
return (
|
|
377
|
+
response.request().method() === 'POST' && url.pathname === '/api/home/user/passport/login'
|
|
378
|
+
);
|
|
379
|
+
});
|
|
354
380
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
355
381
|
try {
|
|
382
|
+
expect((await loginResponse).ok()).toBeTruthy();
|
|
356
383
|
await expect(page).toHaveURL(studentResourceUrl);
|
|
357
384
|
await initialSelect;
|
|
358
385
|
} finally {
|
|
@@ -591,11 +591,18 @@ test(
|
|
|
591
591
|
{ tag: ['@account', '@admin'] },
|
|
592
592
|
async ({ page }) => {
|
|
593
593
|
await page.goto('/admin/', { waitUntil: 'load' });
|
|
594
|
+
await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'admin');
|
|
594
595
|
if (page.url().includes('/admin/login')) {
|
|
595
|
-
|
|
596
|
-
|
|
596
|
+
const usernameInput = page.getByPlaceholder('Your Username');
|
|
597
|
+
const passwordInput = page.getByPlaceholder('Your Password');
|
|
598
|
+
await usernameInput.fill('admin');
|
|
599
|
+
await passwordInput.fill('123456');
|
|
600
|
+
await expect(usernameInput).toHaveValue('admin');
|
|
601
|
+
await expect(passwordInput).toHaveValue('123456');
|
|
597
602
|
await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
|
|
603
|
+
const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
|
|
598
604
|
await page.getByRole('button', { name: 'Login', exact: true }).click();
|
|
605
|
+
expect((await loginResponse).ok()).toBeTruthy();
|
|
599
606
|
await expect(page).not.toHaveURL(/\/admin\/login(?:\?|$)/);
|
|
600
607
|
}
|
|
601
608
|
|