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 +1 -1
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/repo-e2e/specs/home-user-account.spec.ts +311 -207
- package/vona/pnpm-lock.yaml +6 -6
- package/vona/src/suite/a-home/modules/home-user/src/.metadata/index.ts +379 -376
- package/vona/src/suite/a-home/modules/home-user/src/controller/passportTest.ts +60 -0
- package/vona/src/suite/a-home/modules/home-user/test/passportTest.test.ts +164 -1
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.196
|
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
42
|
-
const captcha = (await
|
|
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
|
|
98
|
+
captcha,
|
|
57
99
|
},
|
|
58
100
|
});
|
|
59
101
|
expect(registerResponse.ok()).toBeTruthy();
|
|
60
|
-
const registration = (await registerResponse.json()).data;
|
|
61
|
-
|
|
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 ${
|
|
114
|
+
headers: { Authorization: `Bearer ${account.accessToken}` },
|
|
64
115
|
});
|
|
65
116
|
expect(activateResponse.ok()).toBeTruthy();
|
|
66
|
-
|
|
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}-${
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|