@jskit-ai/users-web 0.2.8 → 0.2.10
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/fixtures/account-settings/index.html +12 -0
- package/fixtures/account-settings/main.js +140 -0
- package/fixtures/account-settings/vite.config.mjs +9 -0
- package/package.json +8 -8
- package/patterns/account-settings/example/src/components/account/settings/AccountSettingsPreferencesSection.vue +7 -7
- package/patterns/account-settings/example/src/components/account/settings/AccountSettingsProfileSection.vue +1 -1
- package/src/client/account-settings/sections.js +8 -18
- package/src/client/bootstrap/user-bootstrap-handler.js +31 -33
- package/src/client/components/ProfileClientElement.vue +17 -76
- package/src/client/composables/account-settings/accountSettingsAvatarUploadRuntime.js +11 -1
- package/src/client/composables/useAccountSettingsRuntime.js +18 -14
- package/src/client/lib/accountLocale.js +2 -2
- package/src/client/lib/profileElementPresentation.js +85 -0
- package/src/client/lib/theme.js +20 -46
- package/src/client/providers/UsersWebClientProvider.js +6 -0
- package/src/client/types.js +65 -0
- package/test/accountLocale.test.js +12 -5
- package/test/accountSettings.browser.test.js +395 -0
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { after, before, test } from "node:test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { chromium, expect } from "@playwright/test";
|
|
6
|
+
import { createChromiumLaunchOptions, startViteFixture, stopProcess } from "../../../tooling/testUtils/browserFixture.mjs";
|
|
7
|
+
|
|
8
|
+
const RUN = process.env.JSKIT_USERS_WEB_BROWSER_INTEGRATION === "1";
|
|
9
|
+
const options = { skip: !RUN, timeout: 120_000 };
|
|
10
|
+
|
|
11
|
+
let runtime = null;
|
|
12
|
+
let browser = null;
|
|
13
|
+
|
|
14
|
+
before(async () => {
|
|
15
|
+
if (!RUN) return;
|
|
16
|
+
runtime = await startViteFixture({ fixtureRoot: fileURLToPath(new URL("../fixtures/account-settings/", import.meta.url)) });
|
|
17
|
+
browser = await chromium.launch(createChromiumLaunchOptions());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
after(async () => {
|
|
21
|
+
await browser?.close();
|
|
22
|
+
await stopProcess(runtime);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function settings(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
profile: {
|
|
28
|
+
displayName: "Ada",
|
|
29
|
+
email: "ada@example.com",
|
|
30
|
+
avatar: { uploadedUrl: "/a.png", gravatarUrl: "", effectiveUrl: "", hasUploadedAvatar: true, size: 64, version: "1" }
|
|
31
|
+
},
|
|
32
|
+
preferences: {
|
|
33
|
+
theme: "dark",
|
|
34
|
+
locale: "it-IT",
|
|
35
|
+
timeZone: "UTC",
|
|
36
|
+
dateFormat: "system",
|
|
37
|
+
numberFormat: "system",
|
|
38
|
+
currencyCode: "USD",
|
|
39
|
+
avatarSize: 64
|
|
40
|
+
},
|
|
41
|
+
notifications: { productUpdates: true, accountActivity: false, securityAlerts: true },
|
|
42
|
+
security: {
|
|
43
|
+
authMethods: [
|
|
44
|
+
{ kind: "password", provider: "local", configured: true },
|
|
45
|
+
{ kind: "oauth", provider: "google", label: "Google", configured: true, canDisable: true },
|
|
46
|
+
{ kind: "oauth", provider: "github", label: "GitHub", configured: false, canEnable: true }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
...overrides
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function document(attributes, type = "user-settings") {
|
|
54
|
+
return { contentType: "application/vnd.api+json", json: { data: { type, id: "7", attributes } } };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One page with the settings API behind it; `routes` answers "METHOD /path", anything else is a test failure. */
|
|
58
|
+
async function openApp({ routes = {} } = {}) {
|
|
59
|
+
const page = await browser.newPage({ baseURL: runtime.baseURL, viewport: { width: 1280, height: 900 } });
|
|
60
|
+
page.setDefaultTimeout(15_000);
|
|
61
|
+
page.setDefaultNavigationTimeout(60_000);
|
|
62
|
+
const api = { requests: [], unexpected: [], errors: [], page };
|
|
63
|
+
page.on("pageerror", (error) => api.errors.push(error.message));
|
|
64
|
+
await page.route("**/api/**", async (route) => {
|
|
65
|
+
const request = route.request();
|
|
66
|
+
const { pathname, search } = new URL(request.url());
|
|
67
|
+
const key = `${request.method()} ${pathname}`;
|
|
68
|
+
if (key === "GET /api/session") {
|
|
69
|
+
await route.fulfill({ json: { authenticated: true, csrfToken: "fixture-csrf" } });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
api.requests.push({ key, search, body: request.postDataJSON() });
|
|
73
|
+
const answer = routes[key] || (key === "GET /api/settings" ? document(settings()) : null);
|
|
74
|
+
if (!answer) {
|
|
75
|
+
api.unexpected.push(key);
|
|
76
|
+
await route.fulfill({ status: 500, json: { error: `unexpected ${key}` } });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await route.fulfill(typeof answer === "function" ? await answer(request.postDataJSON()) : answer);
|
|
80
|
+
});
|
|
81
|
+
return api;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function finish(api) {
|
|
85
|
+
assert.deepEqual(api.unexpected, []);
|
|
86
|
+
assert.deepEqual(api.errors, []);
|
|
87
|
+
await api.page.close();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Seconds the page's main thread works during one idle second: an update loop keeps it near 1. */
|
|
91
|
+
async function busySeconds(page) {
|
|
92
|
+
const session = await page.context().newCDPSession(page);
|
|
93
|
+
await session.send("Performance.enable");
|
|
94
|
+
async function taskSeconds() {
|
|
95
|
+
const { metrics } = await session.send("Performance.getMetrics");
|
|
96
|
+
return metrics.find((metric) => metric.name === "TaskDuration").value;
|
|
97
|
+
}
|
|
98
|
+
const before = await taskSeconds();
|
|
99
|
+
await page.waitForTimeout(1000);
|
|
100
|
+
return (await taskSeconds()) - before;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function lastReport(page) {
|
|
104
|
+
return page.evaluate(() => window.accountFixture.reports.at(-1)?.message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test("the profile section loads, saves, shows a refused field and removes the avatar", options, async () => {
|
|
108
|
+
let saves = 0;
|
|
109
|
+
const api = await openApp({
|
|
110
|
+
routes: {
|
|
111
|
+
"PATCH /api/settings/profile": () => {
|
|
112
|
+
saves += 1;
|
|
113
|
+
return saves === 1
|
|
114
|
+
? document(settings({ profile: { ...settings().profile, displayName: "Ada Lovelace" } }))
|
|
115
|
+
: {
|
|
116
|
+
status: 422,
|
|
117
|
+
json: { error: "Validation failed.", details: { fieldErrors: { displayName: "Name is taken." } } }
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
"DELETE /api/settings/profile/avatar": document(settings({
|
|
121
|
+
profile: { ...settings().profile, avatar: { ...settings().profile.avatar, hasUploadedAvatar: false } }
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const { page } = api;
|
|
126
|
+
await page.goto("/account?returnTo=/home");
|
|
127
|
+
const name = page.getByLabel("Display name");
|
|
128
|
+
await expect(name).toHaveValue("Ada");
|
|
129
|
+
await expect(page.getByLabel("Email", { exact: true })).toHaveValue("ada@example.com");
|
|
130
|
+
await expect(page.getByRole("link", { name: "Back" })).toHaveAttribute("href", "/home");
|
|
131
|
+
|
|
132
|
+
await name.fill(" Ada Lovelace ");
|
|
133
|
+
await page.getByRole("button", { name: "Save profile" }).click();
|
|
134
|
+
await expect.poll(() => lastReport(page)).toBe("Profile updated.");
|
|
135
|
+
await expect(name).toHaveValue("Ada Lovelace");
|
|
136
|
+
const saved = api.requests.find((request) => request.key === "PATCH /api/settings/profile");
|
|
137
|
+
assert.deepEqual(saved.body.data.attributes, { displayName: "Ada Lovelace" });
|
|
138
|
+
assert.equal(saved.body.data.type, "user-profiles");
|
|
139
|
+
|
|
140
|
+
await name.fill("Taken");
|
|
141
|
+
await page.getByRole("button", { name: "Save profile" }).click();
|
|
142
|
+
await expect(page.getByText("Name is taken.")).toBeVisible();
|
|
143
|
+
// A new error array on every render keeps Vuetify's form updating past Vue's limit, which shows here as a page error.
|
|
144
|
+
assert.deepEqual(api.errors, []);
|
|
145
|
+
|
|
146
|
+
await page.getByRole("button", { name: "Remove avatar" }).click();
|
|
147
|
+
await expect(page.getByRole("button", { name: "Remove avatar" })).toHaveCount(0);
|
|
148
|
+
await expect.poll(() => lastReport(page)).toBe("Avatar removed.");
|
|
149
|
+
|
|
150
|
+
await page.getByRole("link", { name: "Back" }).click();
|
|
151
|
+
await expect(page.getByTestId("home")).toBeVisible();
|
|
152
|
+
await finish(api);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("saved preferences follow into the theme, the stored preference and the account locale", options, async () => {
|
|
156
|
+
const api = await openApp({
|
|
157
|
+
routes: {
|
|
158
|
+
"PATCH /api/settings/preferences": document(settings({
|
|
159
|
+
preferences: { ...settings().preferences, theme: "light", locale: "en-GB" }
|
|
160
|
+
}))
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const { page } = api;
|
|
164
|
+
await page.goto("/account");
|
|
165
|
+
await expect(page.locator(".v-application.v-theme--dark")).toHaveCount(1);
|
|
166
|
+
await expect(page.getByTestId("account-locale")).toHaveText("it-IT");
|
|
167
|
+
assert.equal(await page.evaluate(() => localStorage.getItem("jskit.themePreference")), "dark");
|
|
168
|
+
|
|
169
|
+
await page.getByRole("listitem").filter({ hasText: "Preferences" }).click();
|
|
170
|
+
await page.waitForURL("**/account?section=preferences");
|
|
171
|
+
await page.getByRole("button", { name: /Save preferences/u }).click();
|
|
172
|
+
await expect.poll(() => lastReport(page)).toBe("Preferences updated.");
|
|
173
|
+
const saved = api.requests.find((request) => request.key === "PATCH /api/settings/preferences");
|
|
174
|
+
assert.equal(saved.body.data.type, "user-preferences");
|
|
175
|
+
assert.equal(saved.body.data.attributes.avatarSize, 64);
|
|
176
|
+
await expect(page.locator(".v-application.v-theme--light")).toHaveCount(1);
|
|
177
|
+
await expect(page.getByTestId("account-locale")).toHaveText("en-GB");
|
|
178
|
+
assert.equal(await page.evaluate(() => localStorage.getItem("jskit.themePreference")), "light");
|
|
179
|
+
await finish(api);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("a refused preference shows its message", options, async () => {
|
|
183
|
+
const api = await openApp({
|
|
184
|
+
routes: {
|
|
185
|
+
"PATCH /api/settings/preferences": {
|
|
186
|
+
status: 422,
|
|
187
|
+
json: { error: "Validation failed.", details: { fieldErrors: { theme: "Theme is not allowed." } } }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
const { page } = api;
|
|
192
|
+
await page.goto("/account?section=preferences");
|
|
193
|
+
await page.getByRole("button", { name: /Save preferences/u }).click();
|
|
194
|
+
await expect(page.getByText("Theme is not allowed.")).toBeVisible();
|
|
195
|
+
// The old error array looped here for seconds before Vue gave up, so the page must also stay idle.
|
|
196
|
+
const busy = await busySeconds(page);
|
|
197
|
+
assert.ok(busy < 0.5, `the page worked for ${busy} s of an idle second`);
|
|
198
|
+
await finish(api);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("notifications save, and sign-in methods link through the provider and unlink in place", options, async () => {
|
|
202
|
+
const api = await openApp({
|
|
203
|
+
routes: {
|
|
204
|
+
"PATCH /api/settings/notifications": document(settings()),
|
|
205
|
+
"DELETE /api/settings/security/oauth/google": document({
|
|
206
|
+
securityStatus: { authMethods: [{ kind: "oauth", provider: "google", label: "Google", configured: false, canEnable: true }] }
|
|
207
|
+
}, "user-security-settings"),
|
|
208
|
+
"GET /api/settings/security/oauth/google/start": { contentType: "text/html", body: "<p>Provider</p>" }
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
const { page } = api;
|
|
212
|
+
await page.goto("/account?section=notifications");
|
|
213
|
+
await page.getByRole("button", { name: /Save notification settings/u }).click();
|
|
214
|
+
await expect.poll(() => lastReport(page)).toBe("Notification settings updated.");
|
|
215
|
+
const saved = api.requests.find((request) => request.key === "PATCH /api/settings/notifications");
|
|
216
|
+
assert.deepEqual(saved.body.data.attributes, { productUpdates: true, accountActivity: false, securityAlerts: true });
|
|
217
|
+
|
|
218
|
+
await page.getByRole("listitem").filter({ hasText: "Sign-in methods" }).click();
|
|
219
|
+
await page.getByRole("button", { name: "Unlink" }).click();
|
|
220
|
+
await expect.poll(() => lastReport(page)).toBe("Sign-in method removed.");
|
|
221
|
+
await expect(page.getByRole("button", { name: "Unlink" })).toHaveCount(0);
|
|
222
|
+
await page.getByRole("button", { name: "Link" }).click();
|
|
223
|
+
await page.getByText("Provider").waitFor();
|
|
224
|
+
assert.equal(api.requests.at(-1).search, "?returnTo=%2Faccount%3Fsection%3Dsign-in-methods");
|
|
225
|
+
await finish(api);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("each section stays busy until its save or removal answers", options, async () => {
|
|
229
|
+
const answers = {
|
|
230
|
+
profile: Promise.withResolvers(),
|
|
231
|
+
avatar: Promise.withResolvers(),
|
|
232
|
+
preferences: Promise.withResolvers(),
|
|
233
|
+
notifications: Promise.withResolvers()
|
|
234
|
+
};
|
|
235
|
+
const api = await openApp({
|
|
236
|
+
routes: {
|
|
237
|
+
"PATCH /api/settings/profile": () => answers.profile.promise,
|
|
238
|
+
"DELETE /api/settings/profile/avatar": () => answers.avatar.promise,
|
|
239
|
+
"PATCH /api/settings/preferences": () => answers.preferences.promise,
|
|
240
|
+
"PATCH /api/settings/notifications": () => answers.notifications.promise
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
const { page } = api;
|
|
244
|
+
const loading = /v-btn--loading/u;
|
|
245
|
+
|
|
246
|
+
await page.goto("/account");
|
|
247
|
+
const name = page.getByLabel("Display name");
|
|
248
|
+
const saveProfile = page.getByRole("button", { name: "Save profile" });
|
|
249
|
+
const removeAvatar = page.getByRole("button", { name: "Remove avatar" });
|
|
250
|
+
await expect(name).toHaveValue("Ada");
|
|
251
|
+
await saveProfile.click();
|
|
252
|
+
await expect(saveProfile).toHaveClass(loading);
|
|
253
|
+
await expect(name).not.toBeEditable();
|
|
254
|
+
await expect(removeAvatar).toBeDisabled();
|
|
255
|
+
answers.profile.resolve(document(settings()));
|
|
256
|
+
await expect(saveProfile).not.toHaveClass(loading);
|
|
257
|
+
await expect(name).toBeEditable();
|
|
258
|
+
|
|
259
|
+
await removeAvatar.click();
|
|
260
|
+
await expect(removeAvatar).toHaveClass(loading);
|
|
261
|
+
await expect(saveProfile).toBeDisabled();
|
|
262
|
+
answers.avatar.resolve(document(settings()));
|
|
263
|
+
await expect(removeAvatar).not.toHaveClass(loading);
|
|
264
|
+
await expect(saveProfile).toBeEnabled();
|
|
265
|
+
|
|
266
|
+
await page.getByRole("listitem").filter({ hasText: "Preferences" }).click();
|
|
267
|
+
const savePreferences = page.getByRole("button", { name: /Save preferences/u });
|
|
268
|
+
const disabledFields = page.locator(".v-input--disabled");
|
|
269
|
+
await savePreferences.click();
|
|
270
|
+
await expect(savePreferences).toHaveClass(loading);
|
|
271
|
+
await expect(disabledFields).not.toHaveCount(0);
|
|
272
|
+
answers.preferences.resolve(document(settings()));
|
|
273
|
+
await expect(savePreferences).not.toHaveClass(loading);
|
|
274
|
+
await expect(disabledFields).toHaveCount(0);
|
|
275
|
+
|
|
276
|
+
await page.getByRole("listitem").filter({ hasText: "Notifications" }).click();
|
|
277
|
+
const saveNotifications = page.getByRole("button", { name: /Save notification settings/u });
|
|
278
|
+
const productUpdates = page.getByLabel("Product updates");
|
|
279
|
+
await saveNotifications.click();
|
|
280
|
+
await expect(saveNotifications).toHaveClass(loading);
|
|
281
|
+
await expect(productUpdates).toBeDisabled();
|
|
282
|
+
answers.notifications.resolve(document(settings()));
|
|
283
|
+
await expect(saveNotifications).not.toHaveClass(loading);
|
|
284
|
+
await expect(productUpdates).toBeEnabled();
|
|
285
|
+
await finish(api);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("the section list follows the address and the placements, and says when there is nothing to show", options, async () => {
|
|
289
|
+
const api = await openApp();
|
|
290
|
+
const { page } = api;
|
|
291
|
+
await page.goto("/account?section=unknown");
|
|
292
|
+
await expect(page.getByLabel("Display name")).toBeVisible();
|
|
293
|
+
|
|
294
|
+
await page.evaluate(() => window.accountFixture.placeSection());
|
|
295
|
+
await page.getByRole("listitem").filter({ hasText: "Extra" }).click();
|
|
296
|
+
await page.getByText("Extra section").waitFor();
|
|
297
|
+
await page.waitForURL("**/account?section=extra");
|
|
298
|
+
await page.getByRole("listitem").filter({ hasText: "Profile" }).click();
|
|
299
|
+
await expect(page.getByLabel("Display name")).toBeVisible();
|
|
300
|
+
await page.waitForURL((url) => url.search === "");
|
|
301
|
+
await finish(api);
|
|
302
|
+
|
|
303
|
+
const empty = await openApp();
|
|
304
|
+
await empty.page.goto("/account?empty");
|
|
305
|
+
await expect(empty.page.getByText("No account settings sections are registered.")).toBeVisible();
|
|
306
|
+
await finish(empty);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("settings that fail to load offer a retry", options, async () => {
|
|
310
|
+
let reads = 0;
|
|
311
|
+
const api = await openApp({
|
|
312
|
+
routes: {
|
|
313
|
+
"GET /api/settings": () => {
|
|
314
|
+
reads += 1;
|
|
315
|
+
return reads === 1 ? { status: 400, json: { error: "Settings refused." } } : document(settings());
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
const { page } = api;
|
|
320
|
+
await page.goto("/account");
|
|
321
|
+
await expect(page.getByText("Unable to load account settings")).toBeVisible();
|
|
322
|
+
await page.getByRole("button", { name: "Retry" }).click();
|
|
323
|
+
await expect(page.getByLabel("Display name")).toHaveValue("Ada");
|
|
324
|
+
await finish(api);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("ProfileClientElement presents its options, calls its actions and reports each outcome", options, async () => {
|
|
328
|
+
const api = await openApp();
|
|
329
|
+
const { page } = api;
|
|
330
|
+
await page.goto("/profile-element");
|
|
331
|
+
const root = page.getByTestId("profile-client-element");
|
|
332
|
+
await expect(root).toHaveClass(
|
|
333
|
+
"profile-client-element profile-client-element--layout-comfortable profile-client-element--surface-carded profile-client-element--density-comfortable profile-client-element--tone-neutral"
|
|
334
|
+
);
|
|
335
|
+
await expect(page.getByRole("heading", { name: "Profile" })).toBeVisible();
|
|
336
|
+
await expect(page.getByText("Preview size: 64 px")).toBeVisible();
|
|
337
|
+
await expect(page.getByText("AD", { exact: true })).toBeVisible();
|
|
338
|
+
await expect(page.getByTestId("form-after")).toHaveText("ada@example.com");
|
|
339
|
+
|
|
340
|
+
await page.getByLabel("Display name").fill(" Ada L ");
|
|
341
|
+
await page.getByTestId("profile-submit-button").click();
|
|
342
|
+
const submit = { displayName: "Ada L" };
|
|
343
|
+
await expect.poll(() => page.evaluate(() => window.accountFixture.events.length)).toBe(4);
|
|
344
|
+
assert.deepEqual(await page.evaluate(() => window.accountFixture.events.splice(0)), [
|
|
345
|
+
{ name: "profile:submit", payload: submit },
|
|
346
|
+
{ name: "interaction", payload: { type: "profile:submit", ...submit } },
|
|
347
|
+
{ name: "action:started", payload: { action: "submitProfile", payload: submit } },
|
|
348
|
+
{ name: "action:succeeded", payload: { action: "submitProfile", payload: submit } }
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
await page.getByTestId("profile-avatar-replace-button").click();
|
|
352
|
+
await expect(page.getByText("Selected file: ada.png")).toBeVisible();
|
|
353
|
+
assert.deepEqual(await page.evaluate(() => window.accountFixture.events.splice(0)), [
|
|
354
|
+
{ name: "avatar:replace", payload: {} },
|
|
355
|
+
{ name: "interaction", payload: { type: "avatar:replace" } },
|
|
356
|
+
{ name: "action:started", payload: { action: "openAvatarEditor", payload: {} } },
|
|
357
|
+
{ name: "action:succeeded", payload: { action: "openAvatarEditor", payload: {} } }
|
|
358
|
+
]);
|
|
359
|
+
|
|
360
|
+
await page.getByTestId("profile-avatar-remove-button").click();
|
|
361
|
+
await expect.poll(() => page.evaluate(() => window.accountFixture.events.length)).toBe(4);
|
|
362
|
+
await expect.poll(() => api.errors.splice(0)).toEqual(["Avatar service down."]);
|
|
363
|
+
assert.deepEqual((await page.evaluate(() => window.accountFixture.events.splice(0))).at(-1), {
|
|
364
|
+
name: "action:failed",
|
|
365
|
+
payload: { action: "submitAvatarDelete", payload: {}, message: "Avatar service down." }
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
await page.evaluate(() => Object.assign(window.accountFixture.profileProps, {
|
|
369
|
+
copy: { saveProfile: "Keep it" },
|
|
370
|
+
variant: { layout: "compact", surface: "plain", density: "compact", tone: "emphasized" },
|
|
371
|
+
features: { header: false, removeAvatar: false },
|
|
372
|
+
ui: { classes: { root: "custom-root", card: "custom-card" }, testIds: { root: "custom-id", submitButton: "custom-submit" } }
|
|
373
|
+
}));
|
|
374
|
+
const custom = page.getByTestId("custom-id");
|
|
375
|
+
await expect(custom).toHaveClass(
|
|
376
|
+
"profile-client-element profile-client-element--layout-compact profile-client-element--surface-plain profile-client-element--density-compact profile-client-element--tone-emphasized custom-root"
|
|
377
|
+
);
|
|
378
|
+
await expect(page.getByTestId("profile-client-card")).toHaveClass(/custom-card/u);
|
|
379
|
+
await expect(page.getByTestId("custom-submit")).toHaveText(/Keep it/iu);
|
|
380
|
+
await expect(page.getByRole("heading", { name: "Profile" })).toHaveCount(0);
|
|
381
|
+
await expect(page.getByTestId("profile-avatar-remove-button")).toHaveCount(0);
|
|
382
|
+
await finish(api);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("ProfileClientElement shows a refused field", options, async () => {
|
|
386
|
+
const api = await openApp();
|
|
387
|
+
const { page } = api;
|
|
388
|
+
await page.goto("/profile-element");
|
|
389
|
+
await expect(page.getByLabel("Display name")).toHaveValue("Ada");
|
|
390
|
+
await page.evaluate(() => {
|
|
391
|
+
window.accountFixture.profileState.profileFieldErrors.displayName = "Name is taken.";
|
|
392
|
+
});
|
|
393
|
+
await expect(page.getByText("Name is taken.")).toBeVisible();
|
|
394
|
+
await finish(api);
|
|
395
|
+
});
|