1import type { Locator, Page } from "@playwright/test";
2import type { Api } from "../../helpers/api";
3import { expect, test, uid, usePersona } from "../../helpers/fixtures";
4import { PASSWORD } from "../../helpers/personas";
5import { createThrowawayAgent, openSettings, snapshotSettings } from "../../helpers/settings";
6
7const ACCOUNT = "Email Account";
8
9usePersona("admin");
10
11test("the new email account form validates each provider and reports a bad server", async ({ page, api }) => {
12 const name = `E2E Add ${uid()}`;
13 await page.goto("/opendesk/tickets");
14 const dialog = await openSettings(page, "Email Accounts");
15 await dialog.getByRole("button", { name: "New" }).click();
16 const create = dialog.getByRole("button", { name: "Create" });
17
18 await dialog.getByText("GMail", { exact: true }).click();
19 await create.click();
20 await expect(dialog.getByText("Account name is required")).toBeVisible();
21 await dialog.getByRole("textbox", { name: "Account name" }).fill(name);
22 await create.click();
23 await expect(dialog.getByText("Email ID is required")).toBeVisible();
24 await dialog.getByRole("textbox", { name: "Email ID" }).fill("not-an-email");
25 await create.click();
26 await expect(dialog.getByText("Invalid email ID")).toBeVisible();
27 await dialog.getByRole("textbox", { name: "Email ID" }).fill(`e2e-add-${uid()}@opendesk.test`);
28 await create.click();
29 await expect(dialog.getByText("Password is required")).toBeVisible();
30
31 await dialog.getByText("Frappe Mail", { exact: true }).click();
32 await create.click();
33 await expect(dialog.getByText("API Key is required")).toBeVisible();
34
35 await dialog.getByText("Custom", { exact: true }).click();
36 await create.click();
37 await expect(dialog.getByText("Email Domain or manual server settings are required")).toBeVisible();
38 await fillServers(dialog, "993", "587");
39 await dialog.getByRole("textbox", { name: "Password" }).fill("secret");
40 await create.click();
41 await expect(dialog.getByText("Failed to create email account, Invalid credentials")).toBeVisible();
42 expect(await api.exists(ACCOUNT, { email_account_name: name })).toBeFalsy();
43
44 await dialog.getByRole("button", { name: "Back" }).click();
45 await expect(dialog.getByRole("heading", { name: "Email Accounts" })).toBeVisible();
46});
47
48test("an email account is edited, renamed, and rejects outgoing mail without a password", async ({ page, api }) => {
49 const name = `E2E Edit ${uid()}`;
50 const renamed = `${name} Renamed`;
51 await api.insert(ACCOUNT, {
52 email_account_name: name,
53 email_id: `e2e-edit-${uid()}@opendesk.test`,
54 email_server: "imap.e2e.invalid",
55 smtp_server: "smtp.e2e.invalid",
56 incoming_port: "993",
57 smtp_port: "587",
58 enable_incoming: 0,
59 enable_outgoing: 0,
60 });
61 try {
62 await page.goto("/opendesk/tickets");
63 const dialog = await openSettings(page, "Email Accounts");
64 const card = accountCard(dialog, name);
65 await expect(card.getByText("Inbox", { exact: true })).toBeVisible();
66 await card.click();
67
68 const update = dialog.getByRole("button", { name: "Update Account" });
69 await expect(update).toHaveCount(0);
70 await dialog.getByRole("textbox", { name: "Outgoing Port" }).fill("2525");
71 await update.click();
72 await expect.poll(async () => (await api.get(ACCOUNT, name)).smtp_port).toBe("2525");
73
74 await accountCard(dialog, name).click();
75 await dialog.getByRole("textbox", { name: "Account name" }).fill(renamed);
76 await update.click();
77 await expect.poll(() => api.exists(ACCOUNT, { name: renamed })).toBeTruthy();
78
79 await accountCard(dialog, renamed).click();
80 await dialog.getByRole("checkbox", { name: "Enable Outgoing" }).check();
81 await update.click();
82 await expect(dialog.getByText("Failed to update email account, Invalid credentials")).toBeVisible();
83 expect((await api.get(ACCOUNT, renamed)).enable_outgoing).toBe(0);
84 } finally {
85 for (const account of [name, renamed]) {
86 if (await api.exists(ACCOUNT, { name: account })) await api.delete(ACCOUNT, account);
87 }
88 }
89});
90
91const NOTIFICATIONS = [
92 { label: "Reply from contact", enabled: "enable_reply_email_to_agent", content: "reply_email_to_agent_content" },
93 { label: "Reply from agent", enabled: "enable_reply_email_via_agent", content: "reply_via_agent_email_content" },
94 { label: "Share feedback", enabled: "enable_email_ticket_feedback", content: "feedback_email_content" },
95];
96
97test("each notification saves its content and switch, and share feedback its status", async ({ page, api }) => {
98 const fields = NOTIFICATIONS.flatMap((n) => [n.enabled, n.content]);
99 const restore = await snapshotSettings(api, [...fields, "send_email_feedback_on_status"]);
100 const before = await api.get("OD Settings", "OD Settings");
101 try {
102 await page.goto("/opendesk/tickets");
103 const dialog = await openSettings(page, "Email Notifications");
104 for (const notification of NOTIFICATIONS) {
105 const content = `<p>E2E ${notification.label} ${uid()}</p>`;
106 await openNotification(dialog, notification.label);
107 if (notification.label === "Share feedback") {
108 await dialog.getByRole("combobox", { name: /On ticket status/ }).click();
109 await page.getByRole("option", { name: "Resolved" }).click();
110 }
111 await dialog.getByRole("textbox", { name: /Email Content/ }).fill(content);
112 await dialog.getByRole("switch", { name: "Enabled" }).click();
113 await dialog.getByRole("button", { name: "Save", exact: true }).click();
114 await expect.poll(() => setting(api, notification.content)).toBe(content);
115 expect(await setting(api, notification.enabled)).toBe(before[notification.enabled] ? 0 : 1);
116 await dialog.getByRole("button", { name: "back to email event list" }).click();
117 }
118 expect(await setting(api, "send_email_feedback_on_status")).toBe("Resolved");
119 } finally {
120 await restore();
121 }
122});
123
124test("notification content resets to the default, and leaving unsaved edits asks first", async ({ page, api }) => {
125 await page.goto("/opendesk/tickets");
126 const dialog = await openSettings(page, "Email Notifications");
127 await openNotification(dialog, "Reply from agent");
128 const content = dialog.getByRole("textbox", { name: /Email Content/ });
129 const reset = dialog.getByRole("button", { name: "Reset Content" });
130 await content.fill("<p>E2E draft that is never saved</p>");
131 await expect(dialog.getByText("Unsaved", { exact: true })).toBeVisible();
132
133 await reset.click();
134 await page.getByRole("dialog", { name: "Reset content" }).getByRole("button", { name: "Confirm" }).click();
135 await expect(reset).toBeDisabled();
136 await expect(content).not.toHaveValue("<p>E2E draft that is never saved</p>");
137
138 await content.fill("<p>E2E second draft</p>");
139 await dialog.getByRole("button", { name: "back to email event list" }).click();
140 await page.getByRole("dialog", { name: "Unsaved changes" }).getByRole("button", { name: "Confirm" }).click();
141 await expect(dialog.getByRole("heading", { name: "Reply from agent", level: 2 })).toBeVisible();
142 expect(await setting(api, "reply_via_agent_email_content")).not.toContain("E2E second draft");
143});
144
145test("an admin links and unlinks an outgoing email account", async ({ page, api }) => {
146 const before = (await api.get("User", "Administrator")).user_emails;
147 try {
148 await page.goto("/opendesk/tickets");
149 const dialog = await openSettings(page, / Profile$/);
150 await dialog.getByRole("button", { name: "Configure" }).click();
151 await linkEmailAccount(page, dialog);
152 await expect.poll(() => linkedAccounts(api, "Administrator")).toContain("E2E Support");
153
154 await dialog.getByText("E2E Support", { exact: true }).locator("xpath=..").getByRole("button", { name: "Remove" }).click();
155 await dialog.getByRole("button", { name: "Update" }).click();
156 await expect.poll(() => linkedAccounts(api, "Administrator")).not.toContain("E2E Support");
157 } finally {
158 await api.update("User", "Administrator", { user_emails: before });
159 }
160});
161
162test("a plain agent links an outgoing email account", async ({ page, api }) => {
163 const email = `e2e-link-${uid()}@example.com`;
164 await createThrowawayAgent(api, email, PASSWORD);
165 await page.context().clearCookies();
166 await page.request.post("/api/method/login", { form: { usr: email, pwd: PASSWORD } });
167 await page.goto("/opendesk/tickets");
168 const dialog = await openSettings(page, / Profile$/);
169 await dialog.getByRole("button", { name: "Configure" }).click();
170 await linkEmailAccount(page, dialog);
171 await expect.poll(() => linkedAccounts(api, email)).toEqual(["E2E Support"]);
172});
173
174async function linkEmailAccount(page: Page, dialog: Locator) {
175 await dialog.getByRole("button", { name: "Add Email" }).click();
176 await page.getByRole("option", { name: /E2E Support/ }).click();
177 await dialog.getByRole("button", { name: "Update" }).click();
178}
179
180async function linkedAccounts(api: Api, user: string) {
181 return (await api.get("User", user)).user_emails.map((row) => row.email_account);
182}
183
184function accountCard(dialog: Locator, name: string) {
185 return dialog.getByText(name, { exact: true }).locator("xpath=ancestor::div[contains(@class, 'cursor-pointer')][1]");
186}
187
188async function fillServers(dialog: Locator, incomingPort: string, outgoingPort: string) {
189 await dialog.getByRole("textbox", { name: "Incoming Server (IMAP/POP)" }).fill("imap.e2e.invalid");
190 await dialog.getByRole("textbox", { name: "Incoming Port" }).fill(incomingPort);
191 await dialog.getByRole("textbox", { name: "Outgoing Server (SMTP)" }).fill("smtp.e2e.invalid");
192 await dialog.getByRole("textbox", { name: "Outgoing Port" }).fill(outgoingPort);
193}
194
195async function openNotification(dialog: Locator, label: string) {
196 await dialog.getByRole("listitem").filter({ hasText: label }).click();
197 await expect(dialog.getByRole("heading", { name: label, level: 1 })).toBeVisible();
198}
199
200async function setting(api: Api, field: string) {
201 return (await api.get("OD Settings", "OD Settings"))[field];
202}
203
204