1import type { Locator, Page } from "@playwright/test";
2import { Api } from "../../helpers/api";
3import { expect, skipGettingStarted, test, uid, usePersona } from "../../helpers/fixtures";
4import { PASSWORD, personas } from "../../helpers/personas";
5import { createThrowawayAgent, openSettings, snapshotSettings } from "../../helpers/settings";
6
7const PIXEL = "e2e/fixtures/pixel.png";
8
9usePersona("admin");
10
11test.describe("my settings", () => {
12 test.use({ storageState: { cookies: [], origins: [] } });
13
14 test("an agent renames themselves, sets and removes a photo, and changes their password", async ({
15 page,
16 api,
17 baseURL,
18 }) => {
19 const email = await loginAsNewAgent(page, api);
20 const dialog = await openSettings(page, / Profile$/);
21
22 await dialog.getByText(email.split("@")[0], { exact: true }).locator("xpath=following-sibling::button").click();
23 await dialog.getByRole("textbox").fill("Renamed Person");
24 await page.keyboard.press("Enter");
25 await expect.poll(async () => (await api.get("User", email)).last_name).toBe("Person");
26 expect((await api.get("User", email)).first_name).toBe("Renamed");
27
28 const chooser = page.waitForEvent("filechooser");
29 await dialog.getByRole("button", { name: "Upload Photo" }).click();
30 await (await chooser).setFiles(PIXEL);
31 await expect.poll(async () => (await api.get("User", email)).user_image).toContain("/files/");
32 await dialog.getByRole("button", { name: "Remove Photo" }).click();
33 await expect.poll(async () => (await api.get("User", email)).user_image).toBeFalsy();
34
35 const newPassword = `E2e-Changed-${uid()}!`;
36 await dialog.getByRole("button", { name: "Change Password" }).click();
37 const change = page.getByRole("dialog", { name: "Change Password" });
38 const update = change.getByRole("button", { name: "Update" });
39 await change.getByPlaceholder("New Password").fill("short");
40 await expect(change.getByText("Password must be at least 8 characters")).toBeVisible();
41 await change.getByPlaceholder("New Password").fill(newPassword);
42 await change.getByPlaceholder("Confirm Password").fill(`${newPassword}x`);
43 await expect(change.getByText("Passwords do not match")).toBeVisible();
44 await expect(update).toBeDisabled();
45 await change.getByPlaceholder("Confirm Password").fill(newPassword);
46 await expect(change.getByText("Passwords match")).toBeVisible();
47 await update.click();
48 await expect(change).toBeHidden();
49 await Api.login(baseURL!, email, newPassword);
50 });
51
52 test("an agent sets a signature, and leaving unsaved changes asks first", async ({ page, api }) => {
53 const email = await loginAsNewAgent(page, api);
54 const dialog = await openSettings(page, / Profile$/);
55 await dialog.getByRole("button", { name: "Configure" }).click();
56
57 await dialog.locator(".ProseMirror").fill("Regards from E2E");
58 await dialog.getByRole("button", { name: "Update" }).click();
59 await expect.poll(async () => (await api.get("User", email)).email_signature).toContain("Regards from E2E");
60
61 await dialog.locator(".ProseMirror").fill("Unsaved signature");
62 await dialog.getByRole("button", { name: "Email Settings" }).click();
63 await page.getByRole("dialog", { name: "Unsaved changes" }).getByRole("button", { name: "Confirm" }).click();
64 await expect(dialog.getByRole("button", { name: "Change Password" })).toBeVisible();
65 expect((await api.get("User", email)).email_signature).not.toContain("Unsaved signature");
66 });
67});
68
69test("the theme preference applies at once and survives a reload", async ({ page }) => {
70 await page.goto("/opendesk/tickets");
71 const dialog = await openSettings(page, "Preferences");
72 const themes = dialog.getByRole("radiogroup");
73 await themes.getByText("Dark", { exact: true }).click();
74 await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
75 await page.reload();
76 await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
77
78 const reopened = await openSettings(page, "Preferences");
79 await reopened.getByRole("radiogroup").getByText("Light", { exact: true }).click();
80 await expect(page.locator("html")).toHaveAttribute("data-theme", "light");
81});
82
83test("restricting tickets by team needs a restriction option before saving", async ({ page, api }) => {
84 const before = await api.get("OD Settings", "OD Settings");
85 await page.goto("/opendesk/tickets");
86 const dialog = await openSettings(page, "General");
87 const restrict = settingRow(dialog, "Restrict tickets by team").getByRole("switch");
88 await restrict.click();
89 await dialog.getByRole("checkbox", { name: "Do not restrict tickets without a team" }).uncheck();
90 await dialog.getByRole("checkbox", { name: "Restrict agent assignment to selected team" }).uncheck();
91 await expect(dialog.getByText("Disable global saved replies", { exact: true })).toBeVisible();
92
93 await dialog.getByRole("button", { name: "Save", exact: true }).click();
94 await expect(page.getByText("Please select at least one restriction option for teams in the settings.")).toBeVisible();
95 const after = await api.get("OD Settings", "OD Settings");
96 expect(after.restrict_tickets_by_agent_group).toBe(before.restrict_tickets_by_agent_group);
97
98 await restrict.click();
99 await dialog.getByRole("checkbox", { name: "Do not restrict tickets without a team" }).waitFor({ state: "detached" });
100});
101
102test("ticket automation settings save status updates, auto close and the banner", async ({ page, api }) => {
103 const restore = await snapshotSettings(api, [
104 "auto_update_status",
105 "update_status_to",
106 "default_ticket_type",
107 "auto_close_tickets",
108 "auto_close_status",
109 "auto_close_after_days",
110 "enable_outside_hours_banner",
111 "outside_working_hours_message",
112 ]);
113 try {
114 // The switch turns the banner on, so start from off.
115 await api.update("OD Settings", "OD Settings", { enable_outside_hours_banner: 0 });
116 await page.goto("/opendesk/tickets");
117 const dialog = await openSettings(page, "General");
118 await pickFromDropdown(page, settingRow(dialog, "Auto update status"), "Replied");
119 await pickFromDropdown(page, settingRow(dialog, "Default ticket type"), "Question");
120 await pickFromDropdown(page, settingRow(dialog, "Ticket status"), "Resolved");
121 const days = dialog.getByRole("spinbutton", { name: "Auto-close after (Days)" });
122 await days.fill("0");
123 const dayError = dialog.getByText("The number of days must be 1 or more");
124 await expect(dayError).toBeVisible();
125 await days.fill("5");
126 // The field is debounced; the error clearing shows 5 reached the form.
127 await expect(dayError).toBeHidden();
128
129 await settingRow(dialog, "Outside working hours notice").getByRole("switch").click();
130 const banner = dialog.getByPlaceholder("Enter Notification Message");
131 const defaultMessage = await banner.inputValue();
132 expect(defaultMessage).not.toBe("");
133 await banner.fill("E2E custom banner");
134 await dialog.getByRole("button", { name: "Reset Content" }).click();
135 await expect(banner).toHaveValue(defaultMessage);
136
137 await dialog.getByRole("button", { name: "Save", exact: true }).click();
138 await expect
139 .poll(() => api.get("OD Settings", "OD Settings"))
140 .toMatchObject({
141 auto_update_status: 1,
142 update_status_to: "Replied",
143 default_ticket_type: "Question",
144 auto_close_tickets: 1,
145 auto_close_status: "Resolved",
146 auto_close_after_days: 5,
147 enable_outside_hours_banner: 1,
148 });
149 } finally {
150 await restore();
151 }
152});
153
154test("the brand logo uploads and removes, and signup can be disabled", async ({ page, api }) => {
155 const restore = await snapshotSettings(api, ["brand_logo"]);
156 const signup = (await api.get("Website Settings", "Website Settings")).disable_signup;
157 try {
158 await page.goto("/opendesk/tickets");
159 const dialog = await openSettings(page, "General");
160 const logo = settingRow(dialog, "Logo");
161 const chooser = page.waitForEvent("filechooser");
162 await logo.getByRole("button", { name: "Upload" }).click();
163 await (await chooser).setFiles(PIXEL);
164 await expect.poll(async () => (await api.get("OD Settings", "OD Settings")).brand_logo).toContain("/files/");
165
166 await logo.getByRole("button", { name: "Remove" }).click();
167 await page.getByRole("dialog", { name: "Remove Logo" }).getByRole("button", { name: "Confirm" }).click();
168 await expect.poll(async () => (await api.get("OD Settings", "OD Settings")).brand_logo).toBeFalsy();
169
170 // Signup saves as soon as it is toggled.
171 await dialog.getByRole("button", { name: "Portal Settings", exact: true }).click();
172 await settingRow(dialog, "Allow signup").getByRole("switch").click();
173 await expect
174 .poll(async () => (await api.get("Website Settings", "Website Settings")).disable_signup)
175 .toBe(signup ? 0 : 1);
176 } finally {
177 await restore();
178 await api.update("Website Settings", "Website Settings", { disable_signup: signup });
179 }
180});
181
182test("agents can be filtered, re-enabled and promoted to manager", async ({ page, api }) => {
183 const email = `e2e-role-${uid()}@example.com`;
184 await createThrowawayAgent(api, email);
185 await api.update("OD Agent", email, { is_active: 0 });
186 await page.goto("/opendesk/tickets");
187 const dialog = await openSettings(page, "Agents");
188
189 await dialog.getByRole("button", { name: "Active", exact: true }).click();
190 await page.getByRole("menuitem", { name: "Inactive" }).click();
191 await dialog.getByRole("textbox", { name: "Search" }).fill(email.split("@")[0]);
192 const row = dialog.locator("div.group").filter({ hasText: email });
193 await expect(row.getByText("Inactive", { exact: true })).toBeVisible();
194 await row.getByRole("button").last().click();
195 await page.getByRole("menuitem", { name: "Enable Agent" }).click();
196 await expect.poll(async () => (await api.get("OD Agent", email)).is_active).toBe(1);
197
198 await dialog.getByRole("button", { name: "Inactive", exact: true }).click();
199 await page.getByRole("menuitem", { name: "All" }).click();
200 await row.getByRole("button", { name: "Agent" }).click();
201 await page.getByRole("menuitem", { name: "Manager" }).click();
202 await expect.poll(() => roles(api, email)).toContain("Agent Manager");
203 await row.getByRole("button", { name: "Manager" }).click();
204 await page.getByRole("menuitem", { name: "Agent" }).click();
205 await expect.poll(() => roles(api, email)).not.toContain("Agent Manager");
206
207 await dialog.getByRole("button", { name: "New", exact: true }).click();
208 await expect(dialog.getByRole("heading", { name: "Invite Agents" })).toBeVisible();
209});
210
211test("a new team needs a member, can start disabled, and warns before discarding", async ({ page, api }) => {
212 const name = `E2E Team ${uid()}`;
213 const member = personas.agent2.email;
214 try {
215 await page.goto("/opendesk/tickets");
216 const dialog = await openSettings(page, "Teams");
217 await dialog.getByRole("button", { name: "New" }).click();
218 await dialog.getByRole("textbox", { name: /Team Name/ }).fill(name);
219 await dialog.getByRole("button", { name: "Save", exact: true }).click();
220 await expect(dialog.getByText("At least one team member is required")).toBeVisible();
221
222 await pickAgent(page, dialog, "Bela E2E");
223 await dialog.getByRole("button", { name: member }).locator("svg").last().click();
224 await expect(dialog.getByRole("button", { name: member })).toHaveCount(0);
225 await pickAgent(page, dialog, "Bela E2E");
226 await dialog.getByRole("switch").click();
227 await dialog.getByRole("button", { name: "Save", exact: true }).click();
228 await expect.poll(async () => (await api.exists("OD Team", { name })) && (await api.get("OD Team", name)).disabled).toBe(1);
229 expect((await api.get("OD Team", name)).users.map((row) => row.user)).toEqual([member]);
230
231 await dialog.getByRole("button", { name, exact: true }).click();
232 await dialog.getByRole("button", { name: "New" }).click();
233 await dialog.getByRole("textbox", { name: /Team Name/ }).fill(`${name} Draft`);
234 await dialog.getByRole("button", { name: `${name} Draft` }).click();
235 await page.getByRole("dialog", { name: "Unsaved changes" }).getByRole("button", { name: "Confirm" }).click();
236 const listed = dialog.getByText(name, { exact: true }).locator("xpath=..");
237 await expect(listed.getByText("Disabled", { exact: true })).toBeVisible();
238 expect(await api.exists("OD Team", { name: `${name} Draft` })).toBeFalsy();
239 } finally {
240 if (await api.exists("OD Team", { name })) await api.delete("OD Team", name);
241 }
242});
243
244async function loginAsNewAgent(page: Page, api: Api) {
245 const email = `e2e-profile-${uid()}@example.com`;
246 await createThrowawayAgent(api, email, PASSWORD);
247 await page.request.post("/api/method/login", { form: { usr: email, pwd: PASSWORD } });
248 await skipGettingStarted(page, email);
249 await page.goto("/opendesk/tickets");
250 return email;
251}
252
253function settingRow(dialog: Locator, label: string) {
254 return dialog.getByText(label, { exact: true }).locator("xpath=ancestor::div[.//button][1]");
255}
256
257async function pickFromDropdown(page: Page, row: Locator, option: string) {
258 await row.getByRole("button").first().click();
259 await page.locator("[data-reka-popper-content-wrapper]").getByText(option, { exact: true }).click();
260}
261
262async function pickAgent(page: Page, dialog: Locator, agentName: string) {
263 await dialog.getByPlaceholder("Type agent name or email").fill(agentName);
264 await page.getByRole("option", { name: agentName }).click();
265}
266
267async function roles(api: Api, email: string) {
268 return (await api.get("User", email)).roles.map((row) => row.role);
269}
270