1import type { Page } from "@playwright/test";
2import type { Api } from "../../../helpers/api";
3import { raiseTicket } from "../../../helpers/factories";
4import { expect, test, uid, usePersona } from "../../../helpers/fixtures";
5import { TicketList, resetDefaultTicketView } from "../../../helpers/list";
6import { personas } from "../../../helpers/personas";
7
8usePersona("manager");
9
10test.afterEach(async ({ api }) => {
11 await resetDefaultTicketView(api, personas.manager.email);
12});
13
14test("tickets group by priority with a count per group and a total, kept on reload", async ({
15 page,
16 api,
17 apiAs,
18}) => {
19 const token = uid();
20 const customer = await apiAs("customer");
21 const priorities = {
22 [`E2E high ${token}`]: "High",
23 [`E2E high again ${token}`]: "High",
24 [`E2E low ${token}`]: "Low",
25 };
26 // one at a time, since parallel inserts deadlock
27 for (const [subject, priority] of Object.entries(priorities)) {
28 const ticket = await raiseTicket(customer, subject);
29 await api.update("OD Ticket", ticket.name, { priority });
30 }
31
32 const list = new TicketList(page);
33 await list.goto();
34 await list.searchSubject(token);
35 await expect.poll(() => savedView(api, "filters")).toContain(token);
36 await groupBy(page, "Priority", { totals: true });
37
38 await expect(groupRow(page, "High")).toContainText("2");
39 await expect(groupRow(page, "Low")).toContainText("1");
40 await expect(page.locator("[data-totals]")).toContainText("3");
41 await expect(list.rows().filter({ hasText: token })).toHaveCount(0);
42 await expect.poll(() => savedView(api, "list_group_by")).toContain("priority");
43
44 const requests: string[] = [];
45 page.on("request", (request) => requests.push(request.url()));
46 await page.reload();
47 await expect(page.getByRole("button", { name: "Group", exact: true })).toHaveText(
48 "Grouped by Priority"
49 );
50 await expect(groupRow(page, "High")).toContainText("2");
51 // the groups and their totals, without loading the hidden ticket list or counting groups
52 await page.waitForLoadState("networkidle");
53 expect(requests.filter((url) => url.includes("get_list_data"))).toHaveLength(0);
54 expect(requests.filter((url) => url.includes("get_grouped_list"))).toHaveLength(2);
55
56 await page.getByRole("button", { name: "Clear grouping" }).click();
57 await expect(list.row(`E2E low ${token}`)).toBeVisible();
58 await expect.poll(() => savedView(api, "list_group_by")).toBeFalsy();
59});
60
61test("a reopened grouped view can be saved as a new view, with Columns hidden", async ({
62 page,
63 api,
64}) => {
65 const token = uid();
66 const list = new TicketList(page);
67 await list.goto();
68 await list.searchSubject(token);
69 await expect.poll(() => savedView(api, "filters")).toContain(token);
70 await groupBy(page, "Priority");
71 await expect(page.getByRole("button", { name: "Columns" })).toHaveCount(0);
72 await expect.poll(() => savedView(api, "list_group_by")).toContain("priority");
73
74 // the reload opens the grouped view without loading the ticket list
75 await page.reload();
76 await expect(page.getByRole("button", { name: "Group", exact: true })).toHaveText(
77 "Grouped by Priority"
78 );
79 const view = await list.createView(api, `E2E view ${token}`);
80 const saved = await api.get("OD View", view);
81 expect(saved.list_group_by).toContain("priority");
82 expect(saved.filters).toContain(token);
83});
84
85test("Load More adds the next page of groups to the ones shown", async ({ page, api, apiAs }) => {
86 const token = uid();
87 const customer = await apiAs("customer");
88 // one at a time, since parallel inserts deadlock
89 for (let i = 0; i < 21; i++) await raiseTicket(customer, `E2E page ${i} ${token}`);
90 const list = new TicketList(page);
91 await list.goto();
92 await list.searchSubject(token);
93 await expect.poll(() => savedView(api, "filters")).toContain(token);
94 await groupBy(page, "Subject");
95 const groups = page.getByRole("row").filter({ hasText: token });
96 await expect(groups).toHaveCount(20);
97
98 const next = page.waitForResponse(/get_grouped_list/);
99 await page.getByRole("button", { name: "Load More" }).click();
100 expect((await next).request().postDataJSON()).toMatchObject({ limit_start: 20 });
101 await expect(groups).toHaveCount(21);
102 await expect(page.getByRole("button", { name: "Load More" })).toHaveCount(0);
103});
104
105test("grouped tickets export as CSV with one row per group", async ({ page, api, apiAs }) => {
106 const token = uid();
107 const customer = await apiAs("customer");
108 for (const subject of [`E2E export ${token}`, `E2E export again ${token}`]) {
109 const ticket = await raiseTicket(customer, subject);
110 await api.update("OD Ticket", ticket.name, { priority: "Urgent" });
111 }
112 const list = new TicketList(page);
113 await list.goto();
114 await list.searchSubject(token);
115 await expect.poll(() => savedView(api, "filters")).toContain(token);
116 await groupBy(page, "Priority");
117 await expect(groupRow(page, "Urgent")).toContainText("2");
118
119 await page.getByRole("button", { name: "Export" }).click();
120 const download = page.waitForEvent("download");
121 await page.getByRole("menuitem", { name: "CSV" }).click();
122 const file = await download;
123 expect(file.suggestedFilename()).toMatch(/\.csv$/);
124 const content = Buffer.concat(await (await file.createReadStream()).toArray()).toString();
125 expect(content).toMatch(/"?Urgent"?,"?2"?/);
126 expect(content).not.toContain(token);
127});
128
129test.describe("customer portal", () => {
130 usePersona("customer");
131
132 test("customers get no grouping on their ticket list", async ({ page }) => {
133 await page.goto("/opendesk/my-tickets");
134 await expect(page.getByRole("button", { name: "Filter", exact: true })).toBeVisible();
135 await expect(page.getByRole("button", { name: "Group", exact: true })).toHaveCount(0);
136 });
137});
138
139/** Group the list by one field from the Group popover and wait for the groups. */
140async function groupBy(page: Page, field: string, { totals = false } = {}) {
141 await page.getByRole("button", { name: "Group", exact: true }).click();
142 await page.getByRole("combobox", { name: "Select field" }).click();
143 await page.getByRole("option", { name: field, exact: true }).click();
144 if (totals) await page.getByRole("switch", { name: "Show totals" }).click();
145 const loaded = page.waitForResponse(/get_grouped_list/);
146 await page.getByRole("button", { name: "Apply" }).click();
147 await loaded;
148}
149
150function groupRow(page: Page, value: string) {
151 return page
152 .getByRole("row")
153 .filter({ has: page.getByRole("cell", { name: value, exact: true }) });
154}
155
156/** A field of the manager's auto saved default ticket view. */
157async function savedView(api: Api, field: string) {
158 const [view] = await api.list("OD View", {
159 fields: [field],
160 filters: { user: personas.manager.email, dt: "OD Ticket", is_default: 1 },
161 });
162 return view?.[field];
163}
164