cabloy 5.1.160 → 5.1.161

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 (23) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/package.json +1 -1
  4. package/repo-e2e/specs/cabloy-basic.spec.ts +16 -16
  5. package/test-results/.last-run.json +2 -20
  6. package/vona/pnpm-lock.yaml +31 -121
  7. package/test-results/a-commerce-ATP-ADDR-01-aut-11dea-ss-through-Web-self-service/error-context.md +0 -238
  8. package/test-results/a-commerce-ATP-SPC-01-Coup-ef380-mantic-Admin-field-controls/error-context.md +0 -240
  9. package/test-results/a-commerce-ATP-SPC-02-Cate-9f120-on-and-publication-controls/error-context.md +0 -238
  10. package/test-results/a-commerce-ATP-SPC-02-Prod-5f92a-on-and-publication-controls/error-context.md +0 -238
  11. package/test-results/a-commerce-ATP-SPC-02-SKU--2e61e-ency-and-lifecycle-controls/error-context.md +0 -238
  12. package/test-results/a-commerce-ATP-SPC-04-Stoc-fa895--readonly-and-mutation-free/error-context.md +0 -235
  13. package/test-results/a-commerce-ATP-SPC-05-syst-fb406-e-without-mutation-controls/error-context.md +0 -238
  14. package/test-results/a-commerce-Commerce-sessio-4e963-ie-selects-raw-server-theme/error-context.md +0 -226
  15. package/test-results/a-commerce-Commerce-theme--062fe--without-hydration-mismatch/error-context.md +0 -229
  16. package/test-results/a-commerce-PayPal-browser--8672c-t-or-open-an-awaiting-order/error-context.md +0 -238
  17. package/test-results/a-commerce-Payment-callbac-f56c2--reconciles-after-hydration/error-context.md +0 -238
  18. package/test-results/a-commerce-Payment-cancell-3fc74-ified-provider-confirmation/error-context.md +0 -238
  19. package/test-results/a-commerce-Phase-50-60-aut-71d61--observes-operator-shipment/error-context.md +0 -238
  20. package/test-results/a-commerce-Phase-60-custom-407cd-ecutes-a-whole-order-refund/error-context.md +0 -238
  21. package/test-results/cabloy-basic-ATP-BASIC-TAB-e7253-and-pins-configured-columns/error-context.md +0 -240
  22. package/test-results/markdown-ATP-SPC-02-Produc-9d5c2-link-toolbar-edits-Markdown/error-context.md +0 -211
  23. package/test-results/markdown-ATP-SPC-02-Produc-9d6ea-t-editor-and-saves-Markdown/error-context.md +0 -238
@@ -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 >> Phase 60: customer requests and operator executes a whole-order refund
10
- - Location: repo-e2e/specs/a-commerce.spec.ts:1136: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
- ```
@@ -1,240 +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: cabloy-basic.spec.ts >> ATP-BASIC-TABLE-01: Training Student table submits server sorting and pins configured columns
10
- - Location: repo-e2e/specs/cabloy-basic.spec.ts:385:1
11
-
12
- # Error details
13
-
14
- ```
15
- Error: expect(locator).toHaveCSS(expected) failed
16
-
17
- Locator: getByRole('button', { name: 'Sort by name', exact: true }).locator('xpath=ancestor::th')
18
- Expected: "0px"
19
- Received: "50px"
20
- Timeout: 5000ms
21
-
22
- Call log:
23
- - Expect "toHaveCSS" with timeout 5000ms
24
- - waiting for getByRole('button', { name: 'Sort by name', exact: true }).locator('xpath=ancestor::th')
25
- 14 × locator resolved to <th aria-sort="none" class="bg-base-100">…</th>
26
- - unexpected value "50px"
27
-
28
- ```
29
-
30
- ```yaml
31
- - columnheader "Sort by name":
32
- - button "Sort by name": Student Name
33
- ```
34
-
35
- # Test source
36
-
37
- ```ts
38
- 321 | }
39
- 322 | const actionFlowMatchesFieldFlow = await search.evaluate(element => {
40
- 323 | const actionFlow = element.parentElement?.parentElement?.parentElement;
41
- 324 | const fieldFlow = element.closest('section')?.querySelector('label')
42
- 325 | ?.parentElement?.parentElement;
43
- 326 | return actionFlow === fieldFlow;
44
- 327 | });
45
- 328 | expect(actionFlowMatchesFieldFlow).toBeTruthy();
46
- 329 |
47
- 330 | const drawer = page.locator('.drawer').first();
48
- 331 | await expect(drawer).toHaveClass(/\bdrawer-open\b/);
49
- 332 |
50
- 333 | await page.setViewportSize({ width: 1023, height: 900 });
51
- 334 | await expect(drawer).not.toHaveClass(/\bdrawer-open\b/);
52
- 335 | await expect
53
- 336 | .poll(
54
- 337 | async () =>
55
- 338 | new Set(
56
- 339 | (await getFlowGeometry(name, level, createdAtStart)).map(item =>
57
- 340 | Math.round(item.field.top),
58
- 341 | ),
59
- 342 | ).size,
60
- 343 | )
61
- 344 | .toBeGreaterThan(1);
62
- 345 |
63
- 346 | const narrowGeometry = await getFlowGeometry(name, level, createdAtStart);
64
- 347 | assertVisualOrder(narrowGeometry);
65
- 348 | const viewport = page.viewportSize()!;
66
- 349 | for (const geometry of narrowGeometry) {
67
- 350 | expect(geometry.field.left).toBeGreaterThanOrEqual(-1);
68
- 351 | expect(geometry.field.right).toBeLessThanOrEqual(viewport.width + 1);
69
- 352 | }
70
- 353 | const createdAtEndBox = await createdAtEnd.boundingBox();
71
- 354 | expect(createdAtEndBox).not.toBeNull();
72
- 355 | expect(createdAtEndBox!.x + createdAtEndBox!.width).toBeLessThanOrEqual(viewport.width + 1);
73
- 356 | await expect.poll(() => getDocumentHorizontalOverflow(page)).toBeLessThanOrEqual(1);
74
- 357 |
75
- 358 | await page.setViewportSize({ width: 1024, height: 900 });
76
- 359 | await expect(drawer).toHaveClass(/\bdrawer-open\b/);
77
- 360 |
78
- 361 | await page.setViewportSize({ width: 1440, height: 900 });
79
- 362 | await name.fill('Flow E2E');
80
- 363 | await level.selectOption('2');
81
- 364 | await createdAtStart.fill('2026-01-10');
82
- 365 | await createdAtEnd.fill('2026-01-20');
83
- 366 |
84
- 367 | const searchResponse = waitForStudentSelect(page);
85
- 368 | await search.click();
86
- 369 | const searchUrl = new URL((await searchResponse).url());
87
- 370 | expect(searchUrl.searchParams.get('name')).toBe('Flow E2E');
88
- 371 | expect(searchUrl.searchParams.get('level')).toBe('2');
89
- 372 | expect(searchUrl.searchParams.get('createdAt')).toBe('2026-01-10~2026-01-20');
90
- 373 | await expect(name).toHaveValue('Flow E2E');
91
- 374 | await expect(level).toHaveValue('2');
92
- 375 |
93
- 376 | await reset.click();
94
- 377 | await expect(name).toHaveValue('');
95
- 378 | await expect(level).toHaveValue('');
96
- 379 | await expect(createdAtStart).toHaveValue('');
97
- 380 | await expect(createdAtEnd).toHaveValue('');
98
- 381 | expect(pageErrors).toEqual([]);
99
- 382 | },
100
- 383 | );
101
- 384 |
102
- 385 | test(
103
- 386 | 'ATP-BASIC-TABLE-01: Training Student table submits server sorting and pins configured columns',
104
- 387 | { tag: ['@admin', '@flow'] },
105
- 388 | async ({ page }) => {
106
- 389 | await page.setViewportSize({ width: 1440, height: 900 });
107
- 390 | const pageErrors = collectPageErrors(page);
108
- 391 | await loginAsAdmin(page);
109
- 392 |
110
- 393 | const studentName = `Table E2E ${Date.now()}`;
111
- 394 | const accessToken = (await page.context().cookies()).find(
112
- 395 | cookie => cookie.name === 'token',
113
- 396 | )?.value;
114
- 397 | expect(accessToken).toBeTruthy();
115
- 398 | const headers = { Authorization: `Bearer ${accessToken}` };
116
- 399 | let studentId: string | number | undefined;
117
- 400 | try {
118
- 401 | const createResponse = await page.request.post('/api/training/student', {
119
- 402 | data: { name: studentName, mobile: '13812345678', level: 1 },
120
- 403 | headers,
121
- 404 | });
122
- 405 | expect(createResponse.ok()).toBeTruthy();
123
- 406 | studentId = (await createResponse.json()).data;
124
- 407 | expect(['string', 'number']).toContain(typeof studentId);
125
- 408 |
126
- 409 | const initialSelect = waitForStudentSelect(page);
127
- 410 | await page.getByRole('link', { name: 'Student', exact: true }).click();
128
- 411 | await expect(page).toHaveURL(studentResourceUrl);
129
- 412 | await initialSelect;
130
- 413 | await page.setViewportSize({ width: 800, height: 900 });
131
- 414 |
132
- 415 | const nameSort = page.getByRole('button', { name: 'Sort by name', exact: true });
133
- 416 | const nameHeader = nameSort.locator('xpath=ancestor::th');
134
- 417 | const operationsHeader = page.getByRole('columnheader', { name: 'Operations', exact: true });
135
- 418 | await expect(nameSort).toBeVisible();
136
- 419 | await expect(nameHeader).toHaveAttribute('aria-sort', 'none');
137
- 420 | await expect(nameHeader).toHaveCSS('position', 'sticky');
138
- > 421 | await expect(nameHeader).toHaveCSS('left', '0px');
139
- | ^ Error: expect(locator).toHaveCSS(expected) failed
140
- 422 | await expect(nameHeader).toHaveCSS('min-width', '240px');
141
- 423 | await expect(nameHeader).toHaveCSS('text-align', 'left');
142
- 424 | await expect(operationsHeader).toHaveCSS('position', 'sticky');
143
- 425 | await expect(operationsHeader).toHaveCSS('right', '0px');
144
- 426 | await expect(operationsHeader).toHaveCSS('min-width', '360px');
145
- 427 | await expect(operationsHeader).toHaveCSS('text-align', 'center');
146
- 428 |
147
- 429 | const ascendingResponse = waitForStudentSelect(page, false);
148
- 430 | await nameSort.click();
149
- 431 | await expect(nameHeader).toHaveAttribute('aria-sort', 'ascending');
150
- 432 | const ascending = await ascendingResponse;
151
- 433 | expect(ascending.status()).toBe(200);
152
- 434 | const ascendingUrl = new URL(ascending.url());
153
- 435 | expect(JSON.parse(ascendingUrl.searchParams.get('orders')!)).toEqual([['name', 'asc']]);
154
- 436 |
155
- 437 | const descendingResponse = waitForStudentSelect(page, false);
156
- 438 | await nameSort.click();
157
- 439 | await expect(nameHeader).toHaveAttribute('aria-sort', 'descending');
158
- 440 | const descending = await descendingResponse;
159
- 441 | expect(descending.status()).toBe(200);
160
- 442 | const descendingUrl = new URL(descending.url());
161
- 443 | expect(JSON.parse(descendingUrl.searchParams.get('orders')!)).toEqual([['name', 'desc']]);
162
- 444 |
163
- 445 | const tableWrapper = page
164
- 446 | .locator('div.overflow-x-auto')
165
- 447 | .filter({ has: page.locator('table') })
166
- 448 | .first();
167
- 449 | await expect(tableWrapper).toBeVisible();
168
- 450 | const overflow = await tableWrapper.evaluate(element => {
169
- 451 | return element.scrollWidth > element.clientWidth;
170
- 452 | });
171
- 453 | expect(overflow).toBeTruthy();
172
- 454 | await tableWrapper.evaluate(element => {
173
- 455 | element.scrollLeft = element.scrollWidth;
174
- 456 | });
175
- 457 | await expect(operationsHeader).toHaveCSS('right', '0px');
176
- 458 |
177
- 459 | const operationsRow = page
178
- 460 | .locator('tbody tr')
179
- 461 | .filter({
180
- 462 | has: page.getByRole('button', { name: 'Summary', exact: true }),
181
- 463 | })
182
- 464 | .first();
183
- 465 | const operationActions = operationsRow.getByRole('button');
184
- 466 | await expect(operationActions).toHaveCount(4);
185
- 467 | for (const action of await operationActions.all()) {
186
- 468 | await expect(action).toBeVisible();
187
- 469 | }
188
- 470 | const operationsGeometry = await operationActions.evaluateAll(actions => {
189
- 471 | const cell = actions[0]?.closest('td');
190
- 472 | const join = actions[0]?.parentElement;
191
- 473 | const wrapper = cell?.closest('table')?.parentElement;
192
- 474 | if (!cell || !join || !wrapper)
193
- 475 | throw new Error('Could not resolve the Operations action cell');
194
- 476 | const cellRect = cell.getBoundingClientRect();
195
- 477 | const joinRect = join.getBoundingClientRect();
196
- 478 | const wrapperRect = wrapper.getBoundingClientRect();
197
- 479 | const tolerance = 2;
198
- 480 | return {
199
- 481 | fitsCell: join.scrollWidth <= cell.clientWidth,
200
- 482 | actionsFitCell: actions.every(action => {
201
- 483 | const rect = action.getBoundingClientRect();
202
- 484 | return (
203
- 485 | rect.left >= cellRect.left - tolerance && rect.right <= cellRect.right + tolerance
204
- 486 | );
205
- 487 | }),
206
- 488 | joinFitsCell:
207
- 489 | joinRect.left >= cellRect.left - tolerance &&
208
- 490 | joinRect.right <= cellRect.right + tolerance,
209
- 491 | actionsFitWrapper: actions.every(action => {
210
- 492 | const rect = action.getBoundingClientRect();
211
- 493 | return (
212
- 494 | rect.left >= wrapperRect.left - tolerance &&
213
- 495 | rect.right <= wrapperRect.right + tolerance
214
- 496 | );
215
- 497 | }),
216
- 498 | };
217
- 499 | });
218
- 500 | expect(operationsGeometry).toEqual({
219
- 501 | fitsCell: true,
220
- 502 | actionsFitCell: true,
221
- 503 | joinFitsCell: true,
222
- 504 | actionsFitWrapper: true,
223
- 505 | });
224
- 506 | expect(pageErrors).toEqual([]);
225
- 507 | } finally {
226
- 508 | if (studentId !== undefined) {
227
- 509 | const deleteResponse = await page.request.delete(
228
- 510 | `/api/training/student/deleteForce/${studentId}`,
229
- 511 | { headers },
230
- 512 | );
231
- 513 | expect(deleteResponse.ok()).toBeTruthy();
232
- 514 | }
233
- 515 | }
234
- 516 | },
235
- 517 | );
236
- 518 |
237
- 519 | test(
238
- 520 | 'ATP-BASIC-FORM-01: Training Student content remains in the Basic Information tab',
239
- 521 | { tag: ['@admin', '@flow'] },
240
- ```