cabloy 5.1.160 → 5.1.162

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +23 -0
  3. package/CLAUDE.md +1 -1
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +34 -58
  6. package/package.json +1 -1
  7. package/repo-docs/.vitepress/config.mjs +58 -32
  8. package/repo-docs/ai/ai-spec-driven-development.md +99 -0
  9. package/repo-docs/ai/docs-skills-rules-mapping.md +25 -8
  10. package/repo-docs/ai/edition-consistency-checklist.md +2 -3
  11. package/repo-docs/ai/introduction.md +7 -4
  12. package/repo-docs/ai/playbook-spec-execution.md +19 -5
  13. package/repo-docs/ai/playbook-spec-generation.md +69 -101
  14. package/repo-docs/ai/repo-guidance.md +1 -1
  15. package/repo-docs/ai/skills.md +31 -10
  16. package/repo-docs/backend/dto-guide.md +1 -0
  17. package/repo-docs/backend/openapi-guide.md +1 -1
  18. package/repo-docs/editions/overview.md +51 -120
  19. package/repo-docs/frontend/environment-config-guide.md +1 -1
  20. package/repo-docs/frontend/form-layout-guide.md +4 -2
  21. package/repo-docs/frontend/introduction.md +2 -1
  22. package/repo-docs/frontend/quickstart.md +2 -8
  23. package/repo-docs/frontend/scripts.md +7 -3
  24. package/repo-docs/frontend/ssr-architecture-overview.md +1 -1
  25. package/repo-docs/frontend/ssr-build-deploy-guide.md +4 -2
  26. package/repo-docs/fullstack/comparison-with-other-frameworks.md +6 -2
  27. package/repo-docs/fullstack/contract-loop-playbook.md +4 -0
  28. package/repo-docs/fullstack/introduction.md +13 -4
  29. package/repo-docs/fullstack/parallel-worktree-environment.md +2 -0
  30. package/repo-docs/fullstack/quickstart.md +65 -74
  31. package/repo-docs/fullstack/semantic-presentation-contract.md +121 -0
  32. package/repo-docs/fullstack/ssr-site-and-flavor-setup.md +2 -2
  33. package/repo-docs/index.md +16 -11
  34. package/repo-docs/reference/glossary.md +30 -0
  35. package/repo-docs/reference/package-map.md +1 -1
  36. package/repo-docs/reference/repo-scripts.md +6 -4
  37. package/repo-e2e/specs/cabloy-basic.spec.ts +16 -16
  38. package/test-results/.last-run.json +2 -20
  39. package/vona/pnpm-lock.yaml +38 -214
  40. package/zova/pnpm-lock.yaml +8 -0
  41. package/zova/src/suite/a-home/modules/home-base/src/.metadata/component/siteEntryTables.ts +12 -0
  42. package/zova/src/suite/a-home/modules/home-base/src/.metadata/index.ts +13 -0
  43. package/zova/src/suite/a-home/modules/home-base/src/component/siteEntryTables/controller.tsx +71 -0
  44. package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +8 -0
  45. package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +8 -0
  46. package/zova/src/suite/a-home/modules/home-indexadmin/package.json +3 -0
  47. package/zova/src/suite/a-home/modules/home-indexadmin/src/page/dashboard/controller.tsx +6 -13
  48. package/zova/src/suite/a-home/modules/home-indexweb/package.json +3 -1
  49. package/zova/src/suite/a-home/modules/home-indexweb/src/page/home/controller.tsx +6 -14
  50. package/repo-docs/ai/edition-detection.md +0 -30
  51. package/repo-docs/editions/cabloy-basic.md +0 -26
  52. package/repo-docs/editions/cabloy-start.md +0 -53
  53. package/repo-docs/editions/choosing-between-basic-and-start.md +0 -83
  54. package/repo-docs/editions/detection.md +0 -31
  55. package/test-results/a-commerce-ATP-ADDR-01-aut-11dea-ss-through-Web-self-service/error-context.md +0 -238
  56. package/test-results/a-commerce-ATP-SPC-01-Coup-ef380-mantic-Admin-field-controls/error-context.md +0 -240
  57. package/test-results/a-commerce-ATP-SPC-02-Cate-9f120-on-and-publication-controls/error-context.md +0 -238
  58. package/test-results/a-commerce-ATP-SPC-02-Prod-5f92a-on-and-publication-controls/error-context.md +0 -238
  59. package/test-results/a-commerce-ATP-SPC-02-SKU--2e61e-ency-and-lifecycle-controls/error-context.md +0 -238
  60. package/test-results/a-commerce-ATP-SPC-04-Stoc-fa895--readonly-and-mutation-free/error-context.md +0 -235
  61. package/test-results/a-commerce-ATP-SPC-05-syst-fb406-e-without-mutation-controls/error-context.md +0 -238
  62. package/test-results/a-commerce-Commerce-sessio-4e963-ie-selects-raw-server-theme/error-context.md +0 -226
  63. package/test-results/a-commerce-Commerce-theme--062fe--without-hydration-mismatch/error-context.md +0 -229
  64. package/test-results/a-commerce-PayPal-browser--8672c-t-or-open-an-awaiting-order/error-context.md +0 -238
  65. package/test-results/a-commerce-Payment-callbac-f56c2--reconciles-after-hydration/error-context.md +0 -238
  66. package/test-results/a-commerce-Payment-cancell-3fc74-ified-provider-confirmation/error-context.md +0 -238
  67. package/test-results/a-commerce-Phase-50-60-aut-71d61--observes-operator-shipment/error-context.md +0 -238
  68. package/test-results/a-commerce-Phase-60-custom-407cd-ecutes-a-whole-order-refund/error-context.md +0 -238
  69. package/test-results/cabloy-basic-ATP-BASIC-TAB-e7253-and-pins-configured-columns/error-context.md +0 -240
  70. package/test-results/markdown-ATP-SPC-02-Produc-9d5c2-link-toolbar-edits-Markdown/error-context.md +0 -211
  71. package/test-results/markdown-ATP-SPC-02-Produc-9d6ea-t-editor-and-saves-Markdown/error-context.md +0 -238
@@ -1,235 +0,0 @@
1
- # Instructions
2
-
3
- - Following Playwright test failed.
4
- - Explain why, be concise, respect Playwright best practices.
5
- - Provide a snippet of code with the fix, if possible.
6
-
7
- # Test info
8
-
9
- - Name: a-commerce.spec.ts >> ATP-SPC-04: Stock Admin resources remain readonly and mutation-free
10
- - Location: repo-e2e/specs/a-commerce.spec.ts:1885:1
11
-
12
- # Error details
13
-
14
- ```
15
- Error: expect(page).toHaveURL(expected) failed
16
-
17
- Expected pattern: /\/commerce-admin\/rest\/resource\/commerce-trade(?:%3A|:|%253A)stockBalance(?:[/?#]|$)/
18
- Received string: "http://127.0.0.1:7102/commerce-admin/home/base/errorAccessDenied"
19
- Timeout: 5000ms
20
-
21
- Call log:
22
- - Expect "toHaveURL" with timeout 5000ms
23
- 2 × locator resolved to <html lang="en-us">…</html>
24
- - unexpected value "http://127.0.0.1:7102/commerce-admin/home/base/errorAccessDenied"
25
- 12 × locator resolved to <html lang="en-us" data-zova-hydrated="commerceAdmin">…</html>
26
- - unexpected value "http://127.0.0.1:7102/commerce-admin/home/base/errorAccessDenied"
27
-
28
- ```
29
-
30
- ```yaml
31
- - text: 403 Access denied.
32
- - button "Go Home"
33
- ```
34
-
35
- # Test source
36
-
37
- ```ts
38
- 1815 | 'data-zova-hydrated',
39
- 1816 | 'commerceAdmin',
40
- 1817 | );
41
- 1818 | await adjustStockButton.click();
42
- 1819 | await expect(adminPage.getByRole('dialog')).toBeVisible();
43
- 1820 | await expect(adminPage.getByRole('dialog').getByText(skuCode, { exact: true })).toBeVisible();
44
- 1821 | await expect(adminPage.getByRole('dialog').getByText('0', { exact: true })).toBeVisible();
45
- 1822 | const deltaInput = adminPage.getByRole('group', { name: 'Delta' }).getByRole('spinbutton');
46
- 1823 | const reasonInput = adminPage.getByRole('group', { name: 'Reason' }).getByRole('textbox');
47
- 1824 | await deltaInput.fill('7');
48
- 1825 | await reasonInput.fill('Initial E2E stock');
49
- 1826 | const stockRequest = adminPage.waitForRequest(request => {
50
- 1827 | return request.method() === 'POST' && new URL(request.url()).pathname === stockActionPath;
51
- 1828 | });
52
- 1829 | const stockResponse = adminPage.waitForResponse(response => {
53
- 1830 | return (
54
- 1831 | response.request().method() === 'POST' &&
55
- 1832 | new URL(response.url()).pathname === stockActionPath
56
- 1833 | );
57
- 1834 | });
58
- 1835 | await adminPage
59
- 1836 | .getByRole('dialog')
60
- 1837 | .getByRole('button', { name: 'Adjust stock', exact: true })
61
- 1838 | .click();
62
- 1839 | const stockRequestValue = await stockRequest;
63
- 1840 | expect(stockRequestValue.postDataJSON()).toMatchObject({
64
- 1841 | skuId,
65
- 1842 | delta: 7,
66
- 1843 | reason: 'Initial E2E stock',
67
- 1844 | correlationId: expect.any(String),
68
- 1845 | });
69
- 1846 | expect((await stockResponse).ok()).toBeTruthy();
70
- 1847 | await expect(adminPage.getByRole('dialog')).toHaveCount(0);
71
- 1848 | await expect(skuRow.getByText('7', { exact: true })).toBeVisible();
72
- 1849 | await expect(
73
- 1850 | adminPage.locator('section').getByText('SKU code', { exact: true }),
74
- 1851 | ).toBeVisible();
75
- 1852 | await expect(adminPage.getByText('Created At', { exact: true })).toHaveCount(1);
76
- 1853 | await expect(adminPage.getByRole('group', { name: 'Attributes' })).toHaveCount(0);
77
- 1854 |
78
- 1855 | await adminPage.goto(`${skuListPath}/${skuId}`, { waitUntil: 'load' });
79
- 1856 | await expect(
80
- 1857 | adminPage.getByRole('group', { name: 'SKU lifecycle' }).getByRole('textbox'),
81
- 1858 | ).toHaveValue('Draft');
82
- 1859 | await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
83
- 1860 | await expect(adminPage.getByRole('button', { name: 'Back', exact: true })).toBeVisible();
84
- 1861 | expect(adminPageErrors).toEqual([]);
85
- 1862 | } finally {
86
- 1863 | if (skuId && headers) {
87
- 1864 | await adminPage.request.delete(`${stockActionPath}/${skuId}`, { headers }).catch(() => {});
88
- 1865 | const response = await adminPage.request.delete(`${skuActionPath}/${skuId}`, { headers });
89
- 1866 | expect(response.ok()).toBeTruthy();
90
- 1867 | }
91
- 1868 | if (productId && headers) {
92
- 1869 | const response = await adminPage.request.delete(`${productActionPath}/${productId}`, {
93
- 1870 | headers,
94
- 1871 | });
95
- 1872 | expect(response.ok()).toBeTruthy();
96
- 1873 | }
97
- 1874 | if (categoryId && headers) {
98
- 1875 | const response = await adminPage.request.delete(`${categoryActionPath}/${categoryId}`, {
99
- 1876 | headers,
100
- 1877 | });
101
- 1878 | expect(response.ok()).toBeTruthy();
102
- 1879 | }
103
- 1880 | await adminContext.close().catch(() => {});
104
- 1881 | }
105
- 1882 | },
106
- 1883 | );
107
- 1884 |
108
- 1885 | test(
109
- 1886 | 'ATP-SPC-04: Stock Admin resources remain readonly and mutation-free',
110
- 1887 | { tag: ['@admin', '@flow', '@stock'] },
111
- 1888 | async ({ browser }) => {
112
- 1889 | test.setTimeout(60_000);
113
- 1890 | const adminContext = await browser.newContext();
114
- 1891 | const adminPage = await adminContext.newPage();
115
- 1892 | const adminPageErrors = collectPageErrors(adminPage);
116
- 1893 | const mutationMethods: string[] = [];
117
- 1894 | adminPage.on('request', request => {
118
- 1895 | const url = new URL(request.url());
119
- 1896 | if (
120
- 1897 | url.pathname.includes('/api/commerce/trade/stock') &&
121
- 1898 | ['POST', 'PATCH', 'PUT', 'DELETE'].includes(request.method())
122
- 1899 | ) {
123
- 1900 | mutationMethods.push(request.method());
124
- 1901 | }
125
- 1902 | });
126
- 1903 |
127
- 1904 | try {
128
- 1905 | await adminPage.setViewportSize({ width: 1440, height: 900 });
129
- 1906 | await login(adminPage, '/commerce-admin/', 'admin', '123456', 'commerceAdmin');
130
- 1907 |
131
- 1908 | for (const [resourcePath, title] of [
132
- 1909 | ['commerce-trade%3AstockBalance', 'Stock Balance'],
133
- 1910 | ['commerce-trade%3AstockAudit', 'Stock Audit'],
134
- 1911 | ] as const) {
135
- 1912 | await adminPage.goto(`/commerce-admin/rest/resource/${resourcePath}`, {
136
- 1913 | waitUntil: 'load',
137
- 1914 | });
138
- > 1915 | await expect(adminPage).toHaveURL(
139
- | ^ Error: expect(page).toHaveURL(expected) failed
140
- 1916 | new RegExp(
141
- 1917 | `/commerce-admin/rest/resource/commerce-trade(?:%3A|:|%253A)${title === 'Stock Balance' ? 'stockBalance' : 'stockAudit'}(?:[/?#]|$)`,
142
- 1918 | ),
143
- 1919 | );
144
- 1920 | await expect(adminPage.getByText(title, { exact: true }).first()).toBeVisible();
145
- 1921 | await expect(adminPage.getByRole('button', { name: 'Create', exact: true })).toHaveCount(0);
146
- 1922 | await expect(
147
- 1923 | adminPage.locator('a[href*="/rest/resource/"]').filter({ hasText: 'Create' }),
148
- 1924 | ).toHaveCount(0);
149
- 1925 | await expect(adminPage.getByRole('button', { name: 'Submit', exact: true })).toHaveCount(0);
150
- 1926 | }
151
- 1927 |
152
- 1928 | expect(mutationMethods).toEqual([]);
153
- 1929 | expect(adminPageErrors).toEqual([]);
154
- 1930 | } finally {
155
- 1931 | await adminContext.close().catch(() => {});
156
- 1932 | }
157
- 1933 | },
158
- 1934 | );
159
- 1935 |
160
- 1936 | test(
161
- 1937 | 'Commerce Cart: anonymous browser is redirected to login',
162
- 1938 | { tag: ['@web', '@cart'] },
163
- 1939 | async ({ page, request }) => {
164
- 1940 | const path = '/commerce/commerce/trade/cart';
165
- 1941 | const response = await request.get(path);
166
- 1942 | expect(response.ok()).toBeTruthy();
167
- 1943 | expect((await response.text()).toLowerCase()).not.toContain('data-zova-hydrated');
168
- 1944 |
169
- 1945 | const pageErrors = collectPageErrors(page);
170
- 1946 | const documentResponse = await page.goto(path, { waitUntil: 'load' });
171
- 1947 | expect(documentResponse?.ok()).toBeTruthy();
172
- 1948 | await expect(page).toHaveURL(/\/commerce\/login\?(?:.*&)?returnTo=/);
173
- 1949 | expect(pageErrors).toEqual([]);
174
- 1950 | },
175
- 1951 | );
176
- 1952 |
177
- 1953 | test(
178
- 1954 | 'Phase 50: anonymous payment and order routes remain SSR-neutral and protected',
179
- 1955 | { tag: ['@web', '@flow'] },
180
- 1956 | async ({ page, request }) => {
181
- 1957 | const routes = [
182
- 1958 | ['/commerce/commerce/trade/checkout', '/commerce/trade/checkout'],
183
- 1959 | ['/commerce/commerce/trade/payment/1/1', '/commerce/trade/payment/1/1'],
184
- 1960 | ['/commerce/commerce/trade/orders', '/commerce/trade/orders'],
185
- 1961 | ['/commerce/commerce/trade/order/1', '/commerce/trade/order/1'],
186
- 1962 | ] as const;
187
- 1963 | for (const [path, routePath] of routes) {
188
- 1964 | const response = await request.get(path, { maxRedirects: 0 });
189
- 1965 | expect(response.status(), path).toBe(302);
190
- 1966 | expect(response.headers()['cache-control'], path).toBe('private, no-store');
191
- 1967 | expect(response.headers().location, path).toMatch(/^\/commerce\/login\?(?:.*&)?returnTo=/);
192
- 1968 | const html = await response.text();
193
- 1969 | expect(html.toLowerCase(), path).not.toContain('data-zova-hydrated');
194
- 1970 | expect(html, path).not.toContain('Payment Customer');
195
- 1971 | expect(html, path).not.toContain('Payment succeeded');
196
- 1972 | expect(html, path).not.toContain('Order #');
197
- 1973 | expect(html, path).not.toContain('Checkout');
198
- 1974 | expect(html, path).not.toContain('Mock payment simulator');
199
- 1975 | expect(html, path).not.toContain('My orders');
200
- 1976 |
201
- 1977 | const pageErrors = collectPageErrors(page);
202
- 1978 | const documentResponse = await page.goto(path, { waitUntil: 'load' });
203
- 1979 | expect(documentResponse?.ok(), path).toBeTruthy();
204
- 1980 | await expect(page, path).toHaveURL(/\/commerce\/login\?(?:.*&)?returnTo=/);
205
- 1981 | const loginUrl = new URL(page.url());
206
- 1982 | expect(loginUrl.searchParams.getAll('returnTo'), path).toEqual([routePath]);
207
- 1983 | expect(pageErrors, path).toEqual([]);
208
- 1984 | }
209
- 1985 | },
210
- 1986 | );
211
- 1987 |
212
- 1988 | test(
213
- 1989 | 'ATP-SSR-02: Commerce Admin is an independent SSR site',
214
- 1990 | { tag: ['@admin', '@smoke'] },
215
- 1991 | async ({ page, request }) => {
216
- 1992 | const response = await request.get('/commerce-admin/', { maxRedirects: 0 });
217
- 1993 | expect(response.status()).toBe(302);
218
- 1994 | const loginPath = response.headers().location;
219
- 1995 | expect(loginPath).toMatch(/^\/commerce-admin\/login(?:\?|$)/);
220
- 1996 |
221
- 1997 | const loginResponse = await request.get(loginPath!);
222
- 1998 | expect(loginResponse.ok()).toBeTruthy();
223
- 1999 | expect((await loginResponse.text()).toLowerCase()).not.toContain('data-zova-hydrated');
224
- 2000 |
225
- 2001 | const pageErrors = collectPageErrors(page);
226
- 2002 | const documentResponse = await page.goto('/commerce-admin/', { waitUntil: 'load' });
227
- 2003 | expect(documentResponse?.ok()).toBeTruthy();
228
- 2004 | await expect(page).toHaveURL(/\/commerce-admin\/login(?:\?|$)/);
229
- 2005 | await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerceAdmin');
230
- 2006 | await expect(page.locator('body')).toBeVisible();
231
- 2007 | expect(pageErrors).toEqual([]);
232
- 2008 | },
233
- 2009 | );
234
- 2010 |
235
- ```
@@ -1,238 +0,0 @@
1
- # Instructions
2
-
3
- - Following Playwright test failed.
4
- - Explain why, be concise, respect Playwright best practices.
5
- - Provide a snippet of code with the fix, if possible.
6
-
7
- # Test info
8
-
9
- - Name: a-commerce.spec.ts >> ATP-SPC-05: systemAdmin inspects readonly Address Resource without mutation controls
10
- - Location: repo-e2e/specs/a-commerce.spec.ts:1371:1
11
-
12
- # Error details
13
-
14
- ```
15
- Error: expect(page).toHaveURL(expected) failed
16
-
17
- Expected pattern: /\/commerce\/commerce\/member\/address(?:\?|$)/
18
- Received string: "http://127.0.0.1:7102/commerce/home/base/errorAccessDenied"
19
- Timeout: 5000ms
20
-
21
- Call log:
22
- - Expect "toHaveURL" with timeout 5000ms
23
- 14 × locator resolved to <html lang="en-us" data-zova-hydrated="commerce">…</html>
24
- - unexpected value "http://127.0.0.1:7102/commerce/home/base/errorAccessDenied"
25
-
26
- ```
27
-
28
- ```yaml
29
- - text: 403 Access denied.
30
- - button "Go Home"
31
- ```
32
-
33
- # Test source
34
-
35
- ```ts
36
- 89 | await expect(passwordInput).toHaveValue(password);
37
- 90 | await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
38
- 91 | const loginResponse = waitForApiResponse(page, 'POST', '/api/home/user/passport/login');
39
- 92 | await page.getByRole('button', { name: 'Login', exact: true }).click();
40
- 93 | const loginResponseValue = await loginResponse;
41
- 94 | if (!loginResponseValue.ok()) {
42
- 95 | throw new Error(
43
- 96 | `login failed: ${loginResponseValue.status()} ${await loginResponseValue.text()}`,
44
- 97 | );
45
- 98 | }
46
- 99 | await expect(page).not.toHaveURL(/\/login(?:\?|$)/);
47
- 100 | }
48
- 101 | }
49
- 102 |
50
- 103 | async function registerCustomer(
51
- 104 | request: APIRequestContext,
52
- 105 | testInfo: TestInfo,
53
- 106 | ): Promise<{ password: string; username: string }> {
54
- 107 | const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
55
- 108 | const username = `e2e-address-${id}`;
56
- 109 | const password = 'address-e2e-password';
57
- 110 | const captchaResponse = await request.post('/api/captcha/create', {
58
- 111 | data: { scene: 'captcha-simple:simple' },
59
- 112 | });
60
- 113 | expect(captchaResponse.ok()).toBeTruthy();
61
- 114 | const captcha = (await captchaResponse.json()).data;
62
- 115 | expect(captcha?.id).toEqual(expect.any(String));
63
- 116 | expect(captcha?.token).toEqual(expect.any(String));
64
- 117 |
65
- 118 | const baseURL = testInfo.project.use.baseURL;
66
- 119 | if (!baseURL) throw new Error('commerce E2E base URL is unavailable');
67
- 120 | const consumerUrl = new URL('/home/user/activation', baseURL).toString();
68
- 121 | const registerResponse = await request.post('/api/home/user/passport/register', {
69
- 122 | data: {
70
- 123 | username,
71
- 124 | email: `${username}@example.test`,
72
- 125 | password,
73
- 126 | passwordConfirm: password,
74
- 127 | consumerUrl,
75
- 128 | captcha: { id: captcha.id, token: captcha.token },
76
- 129 | },
77
- 130 | });
78
- 131 | expect(registerResponse.ok()).toBeTruthy();
79
- 132 | const registration = (await registerResponse.json()).data;
80
- 133 | expect(registration?.jwt?.accessToken).toEqual(expect.any(String));
81
- 134 | const activateResponse = await request.post(passportTestActivateCurrentPath, {
82
- 135 | headers: { Authorization: `Bearer ${registration.jwt.accessToken}` },
83
- 136 | });
84
- 137 | expect(activateResponse.ok()).toBeTruthy();
85
- 138 | return { username, password };
86
- 139 | }
87
- 140 |
88
- 141 | function createAddressFixture(testInfo: TestInfo): IAddressFixture {
89
- 142 | const id = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
90
- 143 | return {
91
- 144 | recipientName: `E2E Address ${id}`,
92
- 145 | phone: '15555550123',
93
- 146 | countryCode: 'US',
94
- 147 | region: 'California',
95
- 148 | city: `Original City ${id}`,
96
- 149 | postalCode: '94105',
97
- 150 | addressLine1: `1 Test Street ${id}`,
98
- 151 | addressLine2: 'Suite E2E',
99
- 152 | updatedCity: `Updated City ${id}`,
100
- 153 | };
101
- 154 | }
102
- 155 |
103
- 156 | function getAddressTextbox(page: Page, name: string): Locator {
104
- 157 | return page.getByRole('group', { name, exact: true }).getByRole('textbox');
105
- 158 | }
106
- 159 |
107
- 160 | async function fillAddressForm(page: Page, fixture: IAddressFixture) {
108
- 161 | await getAddressTextbox(page, 'Recipient Name *').fill(fixture.recipientName);
109
- 162 | await getAddressTextbox(page, 'Phone *').fill(fixture.phone);
110
- 163 | await getAddressTextbox(page, 'Country Code *').fill(fixture.countryCode);
111
- 164 | await getAddressTextbox(page, 'Region *').fill(fixture.region);
112
- 165 | await getAddressTextbox(page, 'City *').fill(fixture.city);
113
- 166 | await getAddressTextbox(page, 'Postal Code *').fill(fixture.postalCode);
114
- 167 | await getAddressTextbox(page, 'Address Line 1 *').fill(fixture.addressLine1);
115
- 168 | await getAddressTextbox(page, 'Address Line 2').fill(fixture.addressLine2);
116
- 169 | }
117
- 170 |
118
- 171 | async function createAddressThroughCustomerPage(
119
- 172 | browser: Browser,
120
- 173 | request: APIRequestContext,
121
- 174 | testInfo: TestInfo,
122
- 175 | ) {
123
- 176 | const customer = await registerCustomer(request, testInfo);
124
- 177 | const fixture = createAddressFixture(testInfo);
125
- 178 | const context = await browser.newContext();
126
- 179 | const page = await context.newPage();
127
- 180 | const pageErrors = collectPageErrors(page);
128
- 181 |
129
- 182 | await login(
130
- 183 | page,
131
- 184 | '/commerce/commerce/member/address',
132
- 185 | customer.username,
133
- 186 | customer.password,
134
- 187 | 'commerce',
135
- 188 | );
136
- > 189 | await expect(page).toHaveURL(/\/commerce\/commerce\/member\/address(?:\?|$)/);
137
- | ^ Error: expect(page).toHaveURL(expected) failed
138
- 190 | await expect(page.getByRole('heading', { name: 'Addresses' })).toBeVisible();
139
- 191 | await expect(page.getByText('No addresses yet.')).toBeVisible();
140
- 192 |
141
- 193 | await fillAddressForm(page, fixture);
142
- 194 | const createResponse = waitForAddressResponse(page, 'POST', `${addressActionPath}/createMine`);
143
- 195 | const mineResponse = waitForAddressMine(page);
144
- 196 | await page.getByRole('button', { name: 'Save address', exact: true }).click();
145
- 197 | await createResponse;
146
- 198 | await mineResponse;
147
- 199 | await expect(page.getByRole('heading', { name: fixture.recipientName })).toBeVisible();
148
- 200 |
149
- 201 | return { context, fixture, page, pageErrors };
150
- 202 | }
151
- 203 |
152
- 204 | async function deleteAddressThroughCustomerPage(page: Page, fixture: IAddressFixture) {
153
- 205 | const card = page
154
- 206 | .locator('article')
155
- 207 | .filter({ has: page.getByRole('heading', { name: fixture.recipientName }) });
156
- 208 | const deleteResponse = waitForAddressResponse(page, 'DELETE', /\/deleteMine\/[^/]+$/);
157
- 209 | const mineResponse = waitForAddressMine(page);
158
- 210 | await card.getByRole('button', { name: 'Delete', exact: true }).click();
159
- 211 | await deleteResponse;
160
- 212 | await mineResponse;
161
- 213 | await expect(page.getByRole('heading', { name: fixture.recipientName })).toHaveCount(0);
162
- 214 | await expect(page.getByText('No addresses yet.')).toBeVisible();
163
- 215 | }
164
- 216 |
165
- 217 | test(
166
- 218 | 'ATP-SSR-01: anonymous Commerce HTML is public and hydration completes',
167
- 219 | { tag: ['@web', '@smoke'] },
168
- 220 | async ({ page, request }) => {
169
- 221 | const response = await request.get('/commerce');
170
- 222 | expect(response.ok()).toBeTruthy();
171
- 223 | expect(response.headers()['cache-control']).toBe('no-cache, no-store, must-revalidate');
172
- 224 | const html = await response.text();
173
- 225 | const normalizedHtml = html.toLowerCase();
174
- 226 | expect(normalizedHtml).not.toContain('data-zova-hydrated');
175
- 227 |
176
- 228 | for (const marker of privateMarkers) {
177
- 229 | expect(normalizedHtml).not.toContain(`"${marker}"`);
178
- 230 | }
179
- 231 |
180
- 232 | const pageErrors = collectPageErrors(page);
181
- 233 | const documentResponse = await page.goto('/commerce', { waitUntil: 'load' });
182
- 234 | expect(documentResponse?.ok()).toBeTruthy();
183
- 235 | await expect(page).toHaveURL(/\/commerce(?:\/|$)/);
184
- 236 | await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
185
- 237 | await expect(page.getByRole('heading', { name: 'Commerce catalogue' })).toBeVisible();
186
- 238 | await expect(page.getByText('Dashboard')).toHaveCount(0);
187
- 239 | await expect(page.locator('body')).toBeVisible();
188
- 240 | await expect(page).not.toHaveTitle(/error/i);
189
- 241 | expect(pageErrors).toEqual([]);
190
- 242 | },
191
- 243 | );
192
- 244 |
193
- 245 | test(
194
- 246 | 'Commerce theme: public local preference mirrors session cookie without hydration mismatch',
195
- 247 | { tag: ['@web', '@ssr', '@theme'] },
196
- 248 | async ({ browser, request }, testInfo) => {
197
- 249 | const customer = await registerCustomer(request, testInfo);
198
- 250 | const context = await browser.newContext();
199
- 251 | await context.addInitScript(() => localStorage.setItem('themedark', 'true'));
200
- 252 | const page = await context.newPage();
201
- 253 | const pageErrors = collectPageErrors(page);
202
- 254 | const consoleErrors: string[] = [];
203
- 255 | page.on('console', message => {
204
- 256 | if (message.type() === 'error' || /hydration mismatch/i.test(message.text())) {
205
- 257 | consoleErrors.push(message.text());
206
- 258 | }
207
- 259 | });
208
- 260 | try {
209
- 261 | await page.goto('/commerce', { waitUntil: 'load' });
210
- 262 | await page.evaluate(() => localStorage.setItem('themedark', 'true'));
211
- 263 | await page.reload({ waitUntil: 'load' });
212
- 264 | await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
213
- 265 | await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
214
- 266 | expect((await context.cookies()).find(cookie => cookie.name === 'themedark')?.value).toBe(
215
- 267 | 'true',
216
- 268 | );
217
- 269 |
218
- 270 | await login(
219
- 271 | page,
220
- 272 | '/commerce/commerce/member/address',
221
- 273 | customer.username,
222
- 274 | customer.password,
223
- 275 | 'commerce',
224
- 276 | );
225
- 277 | await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'commerce');
226
- 278 | await expect(page.locator('body')).toHaveAttribute('data-theme', 'dark');
227
- 279 | expect(pageErrors).toEqual([]);
228
- 280 | expect(consoleErrors).toEqual([]);
229
- 281 | } finally {
230
- 282 | await context.close();
231
- 283 | }
232
- 284 | },
233
- 285 | );
234
- 286 |
235
- 287 | test(
236
- 288 | 'Commerce theme: session cookie mirrors public local preference without hydration mismatch',
237
- 289 | { tag: ['@web', '@ssr', '@theme'] },
238
- ```