cabloy 5.1.195 → 5.1.196

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 CHANGED
@@ -1 +1 @@
1
- 5.1.195
1
+ 5.1.196
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.196
4
+
5
+ ### Improvements
6
+
7
+ - Clean up home-user account test fixtures.
8
+
3
9
  ## 5.1.195
4
10
 
5
11
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.195",
3
+ "version": "5.1.196",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -13,6 +13,7 @@ const passportCurrentApiPath = '/api/home/user/passport/current';
13
13
  const passportLoginApiPath = '/api/home/user/passport/login';
14
14
  const passportRegisterApiPath = '/api/home/user/passport/register';
15
15
  const passportActivateCurrentApiPath = '/api/home/user/passportTest/activateCurrent';
16
+ const passportRemoveCurrentFixtureApiPath = '/api/home/user/passportTest/removeCurrentFixture';
16
17
  const accountProfileApiPath = '/api/home/user/account/profile';
17
18
  const imageUploadApiPath = '/api/image/upload';
18
19
  const avatarFixturePath = path.resolve(
@@ -31,18 +32,59 @@ function waitForApiResponse(page: Page, method: string, path: string) {
31
32
  });
32
33
  }
33
34
 
34
- async function registerAccountUser(request: APIRequestContext, testInfo: TestInfo) {
35
- const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
36
- const username = `e2e-account-${suffix}`;
37
- const password = 'account-e2e-password';
38
- const captchaResponse = await request.post('/api/captcha/create', {
35
+ interface RegisteredAccount {
36
+ id: number | string;
37
+ username: string;
38
+ password: string;
39
+ accessToken: string;
40
+ roles: Array<{ id: number | string; name: string }>;
41
+ }
42
+
43
+ interface PassportJwt {
44
+ passport: { user: { id: RegisteredAccount['id'] }; roles: RegisteredAccount['roles'] };
45
+ jwt: { accessToken: string };
46
+ }
47
+
48
+ async function createCaptcha(request: APIRequestContext): Promise<{ id: string; token: string }> {
49
+ const response = await request.post('/api/captcha/create', {
39
50
  data: { scene: 'captcha-simple:simple' },
40
51
  });
41
- expect(captchaResponse.ok()).toBeTruthy();
42
- const captcha = (await captchaResponse.json()).data;
52
+ expect(response.ok()).toBeTruthy();
53
+ const captcha = (await response.json()).data;
43
54
  expect(captcha?.id).toEqual(expect.any(String));
44
55
  expect(captcha?.token).toEqual(expect.any(String));
56
+ return { id: captcha.id, token: captcha.token };
57
+ }
45
58
 
59
+ async function loginAsAccount(
60
+ request: APIRequestContext,
61
+ username: string,
62
+ password: string,
63
+ ): Promise<RegisteredAccount> {
64
+ const captcha = await createCaptcha(request);
65
+ const response = await request.post(passportLoginApiPath, {
66
+ data: { username, password, captcha },
67
+ });
68
+ expect(response.ok()).toBeTruthy();
69
+ const login = (await response.json()).data as PassportJwt;
70
+ return {
71
+ id: login.passport.user.id,
72
+ username,
73
+ password,
74
+ accessToken: login.jwt.accessToken,
75
+ roles: login.passport.roles,
76
+ };
77
+ }
78
+
79
+ async function registerAccountUser(
80
+ request: APIRequestContext,
81
+ testInfo: TestInfo,
82
+ onRegistered: (account: RegisteredAccount) => void,
83
+ ): Promise<RegisteredAccount> {
84
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${crypto.randomUUID()}`;
85
+ const username = `e2e-fixture-home-account-${suffix}`;
86
+ const password = 'account-e2e-password';
87
+ const captcha = await createCaptcha(request);
46
88
  const baseURL = testInfo.project.use.baseURL;
47
89
  if (!baseURL) throw new Error('account E2E base URL is unavailable');
48
90
  const consumerUrl = new URL('/home/user/activation', baseURL).toString();
@@ -53,17 +95,39 @@ async function registerAccountUser(request: APIRequestContext, testInfo: TestInf
53
95
  password,
54
96
  passwordConfirm: password,
55
97
  consumerUrl,
56
- captcha: { id: captcha.id, token: captcha.token },
98
+ captcha,
57
99
  },
58
100
  });
59
101
  expect(registerResponse.ok()).toBeTruthy();
60
- const registration = (await registerResponse.json()).data;
61
- expect(registration?.jwt?.accessToken).toEqual(expect.any(String));
102
+ const registration = (await registerResponse.json()).data as PassportJwt;
103
+ const account: RegisteredAccount = {
104
+ id: registration.passport.user.id,
105
+ username,
106
+ password,
107
+ accessToken: registration.jwt.accessToken,
108
+ roles: registration.passport.roles,
109
+ };
110
+ onRegistered(account);
111
+ expect(account.id).toEqual(expect.anything());
112
+ expect(account.accessToken).toEqual(expect.any(String));
62
113
  const activateResponse = await request.post(passportActivateCurrentApiPath, {
63
- headers: { Authorization: `Bearer ${registration.jwt.accessToken}` },
114
+ headers: { Authorization: `Bearer ${account.accessToken}` },
64
115
  });
65
116
  expect(activateResponse.ok()).toBeTruthy();
66
- return { password, username };
117
+ const activatedAccount = await loginAsAccount(request, username, password);
118
+ expect(String(activatedAccount.id)).toBe(String(account.id));
119
+ return activatedAccount;
120
+ }
121
+
122
+ async function removeAccountFixture(request: APIRequestContext, account: RegisteredAccount) {
123
+ const currentAccount = await loginAsAccount(request, account.username, account.password);
124
+ if (String(currentAccount.id) !== String(account.id)) {
125
+ throw new Error(`Fixture login resolved a different account: ${account.username}`);
126
+ }
127
+ const response = await request.delete(passportRemoveCurrentFixtureApiPath, {
128
+ headers: { Authorization: `Bearer ${currentAccount.accessToken}` },
129
+ });
130
+ expect(response.ok()).toBeTruthy();
67
131
  }
68
132
 
69
133
  function collectPageErrors(page: Page) {
@@ -115,43 +179,58 @@ test(
115
179
  test(
116
180
  'ATP-HUA-REG-01: Login registration uses the Passport contract and defers site admission until activation',
117
181
  { tag: ['@account', '@web', '@flow'] },
118
- async ({ page }, testInfo) => {
119
- const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${Date.now()}`;
120
- const username = `e2e-register-${suffix}`;
182
+ async ({ page, request }, testInfo) => {
183
+ const suffix = `${testInfo.workerIndex}-${testInfo.parallelIndex ?? testInfo.retry}-${crypto.randomUUID()}`;
184
+ const username = `e2e-fixture-home-account-register-${suffix}`;
121
185
  const password = 'account-e2e-password';
122
-
123
- await page.goto(accountPath, { waitUntil: 'load' });
124
- await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
125
- await expect(page).toHaveURL(/\/login(?:\?|$)/);
126
- expect(new URL(page.url()).searchParams.get('returnTo')).toBe(accountPath);
127
- await page.getByRole('button', { name: 'Create account', exact: true }).click();
128
- await expect(page).toHaveURL(
129
- new RegExp(`/home/login/register\\?returnTo=${encodeURIComponent(accountPath)}`),
130
- );
131
- await expect(page.getByRole('heading', { name: 'Create account' })).toBeVisible();
132
-
133
- await page.getByRole('group', { name: 'User Name *' }).getByRole('textbox').fill(username);
134
- await page
135
- .getByRole('group', { name: 'Email *' })
136
- .getByRole('textbox')
137
- .fill(`${username}@example.test`);
138
- await page.locator('input[name="password"]').fill(password);
139
- await page.locator('input[name="passwordConfirm"]').fill(password);
140
- await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
141
- const registerResponse = waitForApiResponse(page, 'POST', passportRegisterApiPath);
142
- await page.getByRole('button', { name: 'Create account', exact: true }).click();
143
- expect((await registerResponse).ok()).toBeTruthy();
144
- await expect(page).toHaveURL(
145
- new RegExp(`/home/login/register\\?returnTo=${encodeURIComponent(accountPath)}`),
146
- );
147
- await expect(
148
- page.getByText('Check your email to activate your account before signing in.', {
149
- exact: true,
150
- }),
151
- ).toBeVisible();
152
- await page.getByRole('button', { name: 'Back to login', exact: true }).click();
153
- await expect(page).toHaveURL(/\/login(?:\?|$)/);
154
- expect(new URL(page.url()).searchParams.get('returnTo')).toBe(accountPath);
186
+ let account: RegisteredAccount | undefined;
187
+ try {
188
+ await page.goto(accountPath, { waitUntil: 'load' });
189
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
190
+ await expect(page).toHaveURL(/\/login(?:\?|$)/);
191
+ expect(new URL(page.url()).searchParams.get('returnTo')).toBe(accountPath);
192
+ await page.getByRole('button', { name: 'Create account', exact: true }).click();
193
+ await expect(page).toHaveURL(
194
+ new RegExp(`/home/login/register\\?returnTo=${encodeURIComponent(accountPath)}`),
195
+ );
196
+ await expect(page.getByRole('heading', { name: 'Create account' })).toBeVisible();
197
+
198
+ await page.getByRole('group', { name: 'User Name *' }).getByRole('textbox').fill(username);
199
+ await page
200
+ .getByRole('group', { name: 'Email *' })
201
+ .getByRole('textbox')
202
+ .fill(`${username}@example.test`);
203
+ await page.locator('input[name="password"]').fill(password);
204
+ await page.locator('input[name="passwordConfirm"]').fill(password);
205
+ await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
206
+ const registerResponse = waitForApiResponse(page, 'POST', passportRegisterApiPath);
207
+ await page.getByRole('button', { name: 'Create account', exact: true }).click();
208
+ const registrationResponse = await registerResponse;
209
+ expect(registrationResponse.ok()).toBeTruthy();
210
+ const registration = (await registrationResponse.json()).data as PassportJwt;
211
+ account = {
212
+ id: registration.passport.user.id,
213
+ username,
214
+ password,
215
+ accessToken: registration.jwt.accessToken,
216
+ roles: registration.passport.roles,
217
+ };
218
+ expect(account.id).toEqual(expect.anything());
219
+ expect(account.accessToken).toEqual(expect.any(String));
220
+ await expect(page).toHaveURL(
221
+ new RegExp(`/home/login/register\\?returnTo=${encodeURIComponent(accountPath)}`),
222
+ );
223
+ await expect(
224
+ page.getByText('Check your email to activate your account before signing in.', {
225
+ exact: true,
226
+ }),
227
+ ).toBeVisible();
228
+ await page.getByRole('button', { name: 'Back to login', exact: true }).click();
229
+ await expect(page).toHaveURL(/\/login(?:\?|$)/);
230
+ expect(new URL(page.url()).searchParams.get('returnTo')).toBe(accountPath);
231
+ } finally {
232
+ if (account) await removeAccountFixture(request, account);
233
+ }
155
234
  },
156
235
  );
157
236
 
@@ -306,89 +385,98 @@ test(
306
385
  'ATP-ACCOUNT-WEB-01: signed-in Web menu refreshes after profile save and keeps drafts isolated',
307
386
  { tag: ['@account', '@web', '@flow'] },
308
387
  async ({ page, request }, testInfo) => {
309
- const account = await registerAccountUser(request, testInfo);
310
- const pageErrors = collectPageErrors(page);
311
- const consoleErrors = collectConsoleErrors(page);
312
-
313
- await page.goto('/login?returnTo=%2Fhome%2Fuser%2Faccount', { waitUntil: 'load' });
314
- await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
315
- await page.getByPlaceholder('Your Username').fill(account.username);
316
- await page.getByPlaceholder('Your Password').fill(account.password);
317
- await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
318
- const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
319
- const accountResponse = waitForApiResponse(page, 'GET', accountApiPath);
320
- await page.getByRole('button', { name: 'Login', exact: true }).click();
321
- expect((await loginResponse).ok()).toBeTruthy();
322
- const accountResponseBody = await accountResponse;
323
- expect(accountResponseBody.ok()).toBeTruthy();
324
- expect((await accountResponseBody.json()).data.avatar).toBeNull();
325
- await expect(page).toHaveURL(accountPath);
326
- await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
327
-
328
- const avatarPreview = page.getByAltText('Choose avatar', { exact: true });
329
- await expect(avatarPreview).toBeVisible();
330
- await expect
331
- .poll(() =>
332
- avatarPreview.evaluate(image => ({
333
- complete: (image as HTMLImageElement).complete,
334
- naturalWidth: (image as HTMLImageElement).naturalWidth,
335
- })),
336
- )
337
- .toEqual({ complete: true, naturalWidth: expect.any(Number) });
338
- await expect
339
- .poll(() => avatarPreview.evaluate(image => (image as HTMLImageElement).naturalWidth))
340
- .toBeGreaterThan(0);
341
-
342
- const profileName = `E2E Account ${testInfo.workerIndex}-${Date.now()}`;
343
- await page
344
- .getByRole('group', { name: 'Display name', exact: true })
345
- .getByRole('textbox')
346
- .fill(profileName);
347
- const timezone = page
348
- .getByRole('group', { name: 'Time zone', exact: true })
349
- .getByRole('textbox');
350
- const browserTimezone = await page.evaluate(
351
- () => Intl.DateTimeFormat().resolvedOptions().timeZone,
352
- );
353
- await expect(timezone).toHaveAttribute('placeholder', browserTimezone);
354
- await expect(timezone).toHaveValue('');
355
- await timezone.fill('UTC');
356
-
357
- // The password form is independently schema-rendered; profile submission must not mutate it.
358
- const passwordDraftSnapshot = await page
359
- .locator('input[type="password"]')
360
- .evaluateAll(inputs => inputs.map(input => (input as HTMLInputElement).value));
361
-
362
- const profileResponse = waitForApiResponse(page, 'PATCH', accountProfileApiPath);
363
- const refreshedPassportResponse = waitForApiResponse(page, 'GET', passportCurrentApiPath);
364
- await page.getByRole('button', { name: 'Save profile', exact: true }).click();
365
- expect((await profileResponse).ok()).toBeTruthy();
366
- expect((await refreshedPassportResponse).ok()).toBeTruthy();
367
- await expect(page.getByText('Profile saved.', { exact: true })).toBeVisible();
368
- await expect
369
- .poll(() =>
370
- page
371
- .locator('input[type="password"]')
372
- .evaluateAll(inputs => inputs.map(input => (input as HTMLInputElement).value)),
373
- )
374
- .toEqual(passwordDraftSnapshot);
375
-
376
- await page.goto('/', { waitUntil: 'load' });
377
- await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
378
- const userMenu = page
379
- .locator('details')
380
- .filter({ has: page.getByText('Logout', { exact: true }) });
381
- await expect(userMenu).toHaveCount(1);
382
- await expect(userMenu.locator('summary')).toContainText(profileName);
383
- await userMenu.locator('summary').click();
384
- const links = userMenu.locator('ul > li > a');
385
- await expect(links).toHaveText(['Account Settings', 'Logout']);
386
- await userMenu.getByText('Account Settings', { exact: true }).click();
387
- await expect(page).toHaveURL(accountPath);
388
- await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
389
- await expect(userMenu).not.toHaveAttribute('open', '');
390
- expect(pageErrors).toEqual([]);
391
- expect(consoleErrors).toEqual([]);
388
+ let account: RegisteredAccount | undefined;
389
+ try {
390
+ account = await registerAccountUser(request, testInfo, registered => {
391
+ account = registered;
392
+ });
393
+ expect(account.roles.map(role => role.name)).toContain('registeredUser');
394
+ const pageErrors = collectPageErrors(page);
395
+ const consoleErrors = collectConsoleErrors(page);
396
+
397
+ await page.goto('/login?returnTo=%2Fhome%2Fuser%2Faccount', { waitUntil: 'load' });
398
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
399
+ await page.getByPlaceholder('Your Username').fill(account.username);
400
+ await page.getByPlaceholder('Your Password').fill(account.password);
401
+ await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
402
+ const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
403
+ const accountResponse = waitForApiResponse(page, 'GET', accountApiPath);
404
+ await page.getByRole('button', { name: 'Login', exact: true }).click();
405
+ expect((await loginResponse).ok()).toBeTruthy();
406
+ const accountResponseBody = await accountResponse;
407
+ expect(accountResponseBody.ok()).toBeTruthy();
408
+ expect((await accountResponseBody.json()).data.avatar).toBeNull();
409
+ await expect(page).toHaveURL(accountPath);
410
+ await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
411
+
412
+ const avatarPreview = page.getByAltText('Choose avatar', { exact: true });
413
+ await expect(avatarPreview).toBeVisible();
414
+ await expect
415
+ .poll(() =>
416
+ avatarPreview.evaluate(image => ({
417
+ complete: (image as HTMLImageElement).complete,
418
+ naturalWidth: (image as HTMLImageElement).naturalWidth,
419
+ })),
420
+ )
421
+ .toEqual({ complete: true, naturalWidth: expect.any(Number) });
422
+ await expect
423
+ .poll(() => avatarPreview.evaluate(image => (image as HTMLImageElement).naturalWidth))
424
+ .toBeGreaterThan(0);
425
+
426
+ const profileName = `e2e-fixture-home-account-profile-${testInfo.workerIndex}-${crypto.randomUUID()}`;
427
+ await page
428
+ .getByRole('group', { name: 'Display name', exact: true })
429
+ .getByRole('textbox')
430
+ .fill(profileName);
431
+ const timezone = page
432
+ .getByRole('group', { name: 'Time zone', exact: true })
433
+ .getByRole('textbox');
434
+ const browserTimezone = await page.evaluate(
435
+ () => Intl.DateTimeFormat().resolvedOptions().timeZone,
436
+ );
437
+ await expect(timezone).toHaveAttribute('placeholder', browserTimezone);
438
+ await expect(timezone).toHaveValue('');
439
+ await timezone.fill('UTC');
440
+
441
+ // The password form is independently schema-rendered; profile submission must not mutate it.
442
+ const passwordDraftSnapshot = await page
443
+ .locator('input[type="password"]')
444
+ .evaluateAll(inputs => inputs.map(input => (input as HTMLInputElement).value));
445
+
446
+ const profileResponse = waitForApiResponse(page, 'PATCH', accountProfileApiPath);
447
+ const refreshedPassportResponse = waitForApiResponse(page, 'GET', passportCurrentApiPath);
448
+ await page.getByRole('button', { name: 'Save profile', exact: true }).click();
449
+ expect((await profileResponse).ok()).toBeTruthy();
450
+ account.username = profileName;
451
+ expect((await refreshedPassportResponse).ok()).toBeTruthy();
452
+ await expect(page.getByText('Profile saved.', { exact: true })).toBeVisible();
453
+ await expect
454
+ .poll(() =>
455
+ page
456
+ .locator('input[type="password"]')
457
+ .evaluateAll(inputs => inputs.map(input => (input as HTMLInputElement).value)),
458
+ )
459
+ .toEqual(passwordDraftSnapshot);
460
+
461
+ await page.goto('/', { waitUntil: 'load' });
462
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
463
+ const userMenu = page
464
+ .locator('details')
465
+ .filter({ has: page.getByText('Logout', { exact: true }) });
466
+ await expect(userMenu).toHaveCount(1);
467
+ await expect(userMenu.locator('summary')).toContainText(profileName);
468
+ await userMenu.locator('summary').click();
469
+ const links = userMenu.locator('ul > li > a');
470
+ await expect(links).toHaveText(['Account Settings', 'Logout']);
471
+ await userMenu.getByText('Account Settings', { exact: true }).click();
472
+ await expect(page).toHaveURL(accountPath);
473
+ await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
474
+ await expect(userMenu).not.toHaveAttribute('open', '');
475
+ expect(pageErrors).toEqual([]);
476
+ expect(consoleErrors).toEqual([]);
477
+ } finally {
478
+ if (account) await removeAccountFixture(request, account);
479
+ }
392
480
  },
393
481
  );
394
482
 
@@ -396,54 +484,62 @@ test(
396
484
  'ATP-ACCOUNT-AVATAR-01: avatar crop defers file upload until crop approval',
397
485
  { tag: ['@account', '@web', '@flow'] },
398
486
  async ({ page, request }, testInfo) => {
399
- const account = await registerAccountUser(request, testInfo);
400
- const pageErrors = collectPageErrors(page);
401
- const consoleErrors = collectConsoleErrors(page);
402
-
403
- await page.goto('/login?returnTo=%2Fhome%2Fuser%2Faccount', { waitUntil: 'load' });
404
- await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
405
- await page.getByPlaceholder('Your Username').fill(account.username);
406
- await page.getByPlaceholder('Your Password').fill(account.password);
407
- await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
408
- const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
409
- const accountResponse = waitForApiResponse(page, 'GET', accountApiPath);
410
- await page.getByRole('button', { name: 'Login', exact: true }).click();
411
- expect((await loginResponse).ok()).toBeTruthy();
412
- expect((await accountResponse).ok()).toBeTruthy();
413
- await expect(page).toHaveURL(accountPath);
414
-
415
- const fileInput = page.locator('input[type="file"]').first();
416
- let uploadCount = 0;
417
- const uploadListener = (response: import('@playwright/test').Response) => {
418
- if (new URL(response.url()).pathname === imageUploadApiPath) uploadCount++;
419
- };
420
- page.on('response', uploadListener);
421
- await fileInput.setInputFiles(avatarFixturePath);
422
- await expect(page.getByRole('button', { name: 'Apply crop', exact: true })).toBeVisible();
423
- expect(uploadCount).toBe(0);
424
- await page.getByRole('button', { name: 'Cancel', exact: true }).click();
425
- await expect(page.getByRole('button', { name: 'Apply crop', exact: true })).toHaveCount(0);
426
- expect(uploadCount).toBe(0);
427
-
428
- await fileInput.setInputFiles(avatarFixturePath);
429
- await expect(page.getByRole('button', { name: 'Apply crop', exact: true })).toBeVisible();
430
- await page.waitForTimeout(1_000);
431
- await page.getByRole('button', { name: 'Apply crop', exact: true }).click();
432
- await expect.poll(() => uploadCount, { timeout: 10_000 }).toBeGreaterThan(0);
433
- await expect(page.getByText('Avatar is ready to save.', { exact: true })).toBeVisible();
434
- page.off('response', uploadListener);
435
-
436
- const profileResponse = page.waitForResponse(response => {
437
- const url = new URL(response.url());
438
- return response.request().method() === 'PATCH' && url.pathname === accountProfileApiPath;
439
- });
440
- await page.getByRole('button', { name: 'Save profile', exact: true }).click();
441
- const profile = await profileResponse;
442
- expect(profile.ok()).toBeTruthy();
443
- const profileBody = (await profile.json()).data;
444
- expect(profileBody.avatar).toEqual(expect.any(String));
445
- expect(pageErrors).toEqual([]);
446
- expect(consoleErrors).toEqual([]);
487
+ let account: RegisteredAccount | undefined;
488
+ try {
489
+ account = await registerAccountUser(request, testInfo, registered => {
490
+ account = registered;
491
+ });
492
+ expect(account.roles.map(role => role.name)).toContain('registeredUser');
493
+ const pageErrors = collectPageErrors(page);
494
+ const consoleErrors = collectConsoleErrors(page);
495
+
496
+ await page.goto('/login?returnTo=%2Fhome%2Fuser%2Faccount', { waitUntil: 'load' });
497
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
498
+ await page.getByPlaceholder('Your Username').fill(account.username);
499
+ await page.getByPlaceholder('Your Password').fill(account.password);
500
+ await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
501
+ const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
502
+ const accountResponse = waitForApiResponse(page, 'GET', accountApiPath);
503
+ await page.getByRole('button', { name: 'Login', exact: true }).click();
504
+ expect((await loginResponse).ok()).toBeTruthy();
505
+ expect((await accountResponse).ok()).toBeTruthy();
506
+ await expect(page).toHaveURL(accountPath);
507
+
508
+ const fileInput = page.locator('input[type="file"]').first();
509
+ let uploadCount = 0;
510
+ const uploadListener = (response: import('@playwright/test').Response) => {
511
+ if (new URL(response.url()).pathname === imageUploadApiPath) uploadCount++;
512
+ };
513
+ page.on('response', uploadListener);
514
+ await fileInput.setInputFiles(avatarFixturePath);
515
+ await expect(page.getByRole('button', { name: 'Apply crop', exact: true })).toBeVisible();
516
+ expect(uploadCount).toBe(0);
517
+ await page.getByRole('button', { name: 'Cancel', exact: true }).click();
518
+ await expect(page.getByRole('button', { name: 'Apply crop', exact: true })).toHaveCount(0);
519
+ expect(uploadCount).toBe(0);
520
+
521
+ await fileInput.setInputFiles(avatarFixturePath);
522
+ await expect(page.getByRole('button', { name: 'Apply crop', exact: true })).toBeVisible();
523
+ await page.waitForTimeout(1_000);
524
+ await page.getByRole('button', { name: 'Apply crop', exact: true }).click();
525
+ await expect.poll(() => uploadCount, { timeout: 10_000 }).toBeGreaterThan(0);
526
+ await expect(page.getByText('Avatar is ready to save.', { exact: true })).toBeVisible();
527
+ page.off('response', uploadListener);
528
+
529
+ const profileResponse = page.waitForResponse(response => {
530
+ const url = new URL(response.url());
531
+ return response.request().method() === 'PATCH' && url.pathname === accountProfileApiPath;
532
+ });
533
+ await page.getByRole('button', { name: 'Save profile', exact: true }).click();
534
+ const profile = await profileResponse;
535
+ expect(profile.ok()).toBeTruthy();
536
+ const profileBody = (await profile.json()).data;
537
+ expect(profileBody.avatar).toEqual(expect.any(String));
538
+ expect(pageErrors).toEqual([]);
539
+ expect(consoleErrors).toEqual([]);
540
+ } finally {
541
+ if (account) await removeAccountFixture(request, account);
542
+ }
447
543
  },
448
544
  );
449
545
 
@@ -451,34 +547,42 @@ test(
451
547
  'ATP-ACCOUNT-SSR-02: signed-in Account session SSR hydrates without mismatch',
452
548
  { tag: ['@account', '@web', '@ssr'] },
453
549
  async ({ page, request }, testInfo) => {
454
- const account = await registerAccountUser(request, testInfo);
455
- const pageErrors = collectPageErrors(page);
456
- const consoleErrors = collectConsoleErrors(page);
457
-
458
- await page.goto('/login?returnTo=%2Fhome%2Fuser%2Faccount', { waitUntil: 'load' });
459
- await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
460
- await page.getByPlaceholder('Your Username').fill(account.username);
461
- await page.getByPlaceholder('Your Password').fill(account.password);
462
- await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
463
- const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
464
- const initialAccountResponse = waitForApiResponse(page, 'GET', accountApiPath);
465
- await page.getByRole('button', { name: 'Login', exact: true }).click();
466
- expect((await loginResponse).ok()).toBeTruthy();
467
- expect((await initialAccountResponse).ok()).toBeTruthy();
468
- await expect(page).toHaveURL(accountPath);
469
- await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
470
-
471
- pageErrors.length = 0;
472
- consoleErrors.length = 0;
473
- const documentResponse = await page.reload({ waitUntil: 'load' });
474
- expect(documentResponse?.ok()).toBeTruthy();
475
- expect(documentResponse?.url()).toContain(accountPath);
476
- const html = await documentResponse!.text();
477
- expect(html).toContain('Account Settings');
478
- await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
479
- await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
480
- expect(pageErrors).toEqual([]);
481
- expect(consoleErrors).toEqual([]);
550
+ let account: RegisteredAccount | undefined;
551
+ try {
552
+ account = await registerAccountUser(request, testInfo, registered => {
553
+ account = registered;
554
+ });
555
+ expect(account.roles.map(role => role.name)).toContain('registeredUser');
556
+ const pageErrors = collectPageErrors(page);
557
+ const consoleErrors = collectConsoleErrors(page);
558
+
559
+ await page.goto('/login?returnTo=%2Fhome%2Fuser%2Faccount', { waitUntil: 'load' });
560
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
561
+ await page.getByPlaceholder('Your Username').fill(account.username);
562
+ await page.getByPlaceholder('Your Password').fill(account.password);
563
+ await expect(page.getByPlaceholder('Please input captcha')).not.toHaveValue('');
564
+ const loginResponse = waitForApiResponse(page, 'POST', passportLoginApiPath);
565
+ const initialAccountResponse = waitForApiResponse(page, 'GET', accountApiPath);
566
+ await page.getByRole('button', { name: 'Login', exact: true }).click();
567
+ expect((await loginResponse).ok()).toBeTruthy();
568
+ expect((await initialAccountResponse).ok()).toBeTruthy();
569
+ await expect(page).toHaveURL(accountPath);
570
+ await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
571
+
572
+ pageErrors.length = 0;
573
+ consoleErrors.length = 0;
574
+ const documentResponse = await page.reload({ waitUntil: 'load' });
575
+ expect(documentResponse?.ok()).toBeTruthy();
576
+ expect(documentResponse?.url()).toContain(accountPath);
577
+ const html = await documentResponse!.text();
578
+ expect(html).toContain('Account Settings');
579
+ await expect(page.locator('html')).toHaveAttribute('data-zova-hydrated', 'web');
580
+ await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
581
+ expect(pageErrors).toEqual([]);
582
+ expect(consoleErrors).toEqual([]);
583
+ } finally {
584
+ if (account) await removeAccountFixture(request, account);
585
+ }
482
586
  },
483
587
  );
484
588