1import type { Page } from "@playwright/test";
2import { expectDenied, type Api } from "../../helpers/api";
3import {
4 createContact,
5 createContactWithUser,
6 createCustomer,
7 customerUrl,
8 pageMenu,
9} from "../../helpers/directory";
10import { expect, test, uid, usePersona } from "../../helpers/fixtures";
11
12usePersona("manager");
13
14test("create a customer with a primary contact, promote a member, list its tickets", async ({
15 page,
16 api,
17}) => {
18 const id = uid();
19 const name = `E2E Customer ${id}`;
20 await page.goto("/opendesk/customers");
21 await page.getByRole("button", { name: "Create" }).click();
22 const dialog = page.getByRole("dialog", { name: "Create Customer" });
23 await dialog.getByRole("textbox", { name: "Name (required)" }).fill(name);
24 await dialog.getByRole("textbox", { name: "First Name", exact: true }).fill("Omar");
25 await dialog.getByRole("textbox", { name: "Email", exact: true }).fill(`e2e-primary-${id}@example.com`);
26 await dialog.getByRole("button", { name: "Create" }).click();
27 await expect(page).toHaveURL(new RegExp(customerUrl(name)));
28
29 const member = await createContact(api, "Member");
30 const doc = await api.get("OD Customer", name);
31 await api.update("OD Customer", name, {
32 contacts: [...doc.contacts, { contact_name: member.name }],
33 });
34 const subject = `E2E customer ticket ${id}`;
35 await api.insert("OD Ticket", { subject, description: subject, raised_by: member.email, customer: name });
36
37 await page.reload();
38 await expect(page.getByText(subject)).toBeVisible();
39 await page.getByRole("tab", { name: /Contacts/ }).click();
40 await openCardMenu(page, member.name);
41 await page.getByRole("menuitem", { name: "Role" }).hover();
42 await page.getByRole("menuitem", { name: "Customer Manager" }).click();
43 await confirm(page);
44
45 await expect.poll(async () => (await memberRow(api, name, member.name))?.is_manager).toBe(1);
46});
47
48test("a manager edits a customer's domain, then renames it", async ({ page, api }) => {
49 const name = await createCustomer(api);
50 const renamed = `${name} Renamed`;
51 await page.goto(customerUrl(name));
52
53 await page.getByRole("button", { name: "Edit" }).click();
54 await page.getByRole("dialog").getByRole("textbox", { name: "Domain" }).fill("e2e.example.com");
55 await page.getByRole("dialog").getByRole("button", { name: "Save" }).click();
56 await expect(page.getByText("e2e.example.com")).toBeVisible();
57 expect((await api.get("OD Customer", name)).domain).toBe("e2e.example.com");
58
59 await page.getByRole("button", { name: "Edit" }).click();
60 await page.getByRole("dialog").getByRole("textbox", { name: /^Name/ }).fill(renamed);
61 await page.getByRole("dialog").getByRole("button", { name: "Save" }).click();
62 await expect(page).toHaveURL(new RegExp(customerUrl(renamed)));
63 expect(await api.exists("OD Customer", { name: renamed })).toBe(renamed);
64 expect(await api.exists("OD Customer", { name })).toBeUndefined();
65});
66
67test("inviting adds an existing user at once and keeps a new email pending until revoked", async ({
68 page,
69 api,
70}) => {
71 const existing = await createContactWithUser(api, "Uma");
72 const newcomer = `e2e-newcomer-${uid()}@example.com`;
73 const name = await createCustomer(api);
74
75 await page.goto(`${customerUrl(name)}#contacts`);
76 await page.getByRole("button", { name: "Invite" }).click();
77 const dialog = page.getByRole("dialog", { name: "Invite Contact" });
78 const input = dialog.getByPlaceholder("Enter email address");
79 for (const email of [existing.email, newcomer]) {
80 await input.fill(email);
81 await input.press("Enter");
82 }
83 // Escape closes the open suggestions without blurring, so the picker's delayed refocus can't reopen them
84 await input.press("Escape");
85 await expect(page.getByRole("listbox")).toHaveCount(0);
86 await dialog.getByRole("button", { name: "Invite", exact: true }).click();
87
88 await expect(card(page, existing.name)).toBeVisible();
89 expect(await memberRow(api, name, existing.name)).toBeTruthy();
90 const pending = dialog.locator("div.justify-between").filter({ hasText: newcomer });
91 await expect(pending).toBeVisible();
92 expect(await invitationStatus(api, newcomer)).toBe("Pending");
93
94 await pending.getByRole("button").click();
95 await pending.getByRole("button", { name: "Confirm" }).click();
96 await expect(pending).toHaveCount(0);
97 expect(await invitationStatus(api, newcomer)).toBe("Cancelled");
98});
99
100test("contact cards set the primary contact, revoke manager access and remove a contact", async ({
101 page,
102 api,
103}) => {
104 const lead = await createContact(api, "Lead");
105 const deputy = await createContact(api, "Deputy");
106 const name = await createCustomer(api, [{ name: lead.name, isManager: true }, { name: deputy.name }]);
107 await page.goto(`${customerUrl(name)}#contacts`);
108
109 await openCardMenu(page, deputy.name);
110 await page.getByRole("menuitem", { name: "Set as Primary" }).click();
111 await confirm(page);
112 await expect(card(page, deputy.name).getByLabel("Primary")).toBeVisible();
113 expect((await api.get("OD Customer", name)).primary_contact).toBe(deputy.name);
114
115 await openCardMenu(page, lead.name);
116 await page.getByRole("menuitem", { name: "Role" }).hover();
117 await page.getByRole("menuitem", { name: "Customer", exact: true }).click();
118 await confirm(page);
119 await expect.poll(async () => (await memberRow(api, name, lead.name))?.is_manager).toBe(0);
120
121 await openCardMenu(page, lead.name);
122 await page.getByRole("menuitem", { name: "Remove Contact" }).click();
123 await confirm(page);
124 await expect(card(page, lead.name)).toHaveCount(0);
125 expect(await memberRow(api, name, lead.name)).toBeUndefined();
126});
127
128test("deleting a customer keeps its tickets and clears their customer", async ({ page, api }) => {
129 const member = await createContact(api, "Keeper");
130 const name = await createCustomer(api, [{ name: member.name }]);
131 const ticket = await api.insert("OD Ticket", {
132 subject: `E2E kept ${uid()}`,
133 raised_by: member.email,
134 customer: name,
135 });
136 await page.goto(customerUrl(name));
137
138 await pageMenu(page).click();
139 await page.getByRole("menuitem", { name: "Delete" }).click();
140 const dialog = page.getByRole("dialog", { name: "Delete Customer" });
141 await expect(dialog.getByRole("checkbox", { name: "Delete 1 ticket(s)" })).not.toBeChecked();
142 await dialog.getByRole("button", { name: "Delete" }).click();
143
144 await expect(page).toHaveURL(/\/opendesk\/customers$/);
145 expect(await api.exists("OD Customer", { name })).toBeUndefined();
146 expect((await api.get("OD Ticket", ticket.name)).customer).toBeFalsy();
147});
148
149test.describe("agent", () => {
150 usePersona("agent");
151
152 test("an agent reads customers but gets none of the manager actions", async ({ page, api, apiAs }) => {
153 const member = await createContact(api, "Viewer");
154 const name = await createCustomer(api, [{ name: member.name }]);
155
156 await page.goto(`${customerUrl(name)}#contacts`);
157 await expect(card(page, member.name)).toBeVisible();
158 await expect(page.getByRole("button", { name: "Edit" })).toHaveCount(0);
159 await expect(page.getByRole("button", { name: "Invite" })).toHaveCount(0);
160 await expect(card(page, member.name).getByRole("button")).toHaveCount(0);
161
162 const agent = await apiAs("agent");
163 await expectDenied(
164 agent.raw("frappe.client.set_value", { doctype: "OD Customer", name, fieldname: "domain", value: "x.io" })
165 );
166 await expectDenied(agent.raw("opendesk.api.customer.create_customer", { customer: { customer_name: `${name} 2` } }));
167 });
168});
169
170function card(page: Page, contact: string) {
171 return page.locator("div.rounded-5").filter({ hasText: contact });
172}
173
174async function openCardMenu(page: Page, contact: string) {
175 await card(page, contact).getByRole("button").click();
176}
177
178function confirm(page: Page) {
179 return page.getByRole("dialog").getByRole("button", { name: "Confirm" }).click();
180}
181
182async function memberRow(api: Api, customer: string, contact: string) {
183 const doc = await api.get("OD Customer", customer);
184 return doc.contacts.find((row: { contact_name: string }) => row.contact_name === contact);
185}
186
187async function invitationStatus(api: Api, email: string) {
188 const [invite] = await api.list("User Invitation", { fields: ["status"], filters: { email } });
189 return invite?.status;
190}
191