1import type { Locator, Page } from "@playwright/test";
2import type { Api } from "../../helpers/api";
3import { expect, test, uid, usePersona } from "../../helpers/fixtures";
4import { openSettings } from "../../helpers/settings";
5
6const SLA = "OD Service Level Agreement";
7const HOLIDAYS = "OD Service Holiday List";
8const PRIORITIES = ["Low", "Medium", "High", "Urgent"];
9
10usePersona("admin");
11
12const created: { doctype: string; name: string }[] = [];
13
14test.afterEach(async ({ api }) => {
15 // SLAs link to holiday lists, so they go first.
16 const records = created.splice(0);
17 const ordered = [...records.filter((r) => r.doctype === SLA), ...records.filter((r) => r.doctype !== SLA)];
18 for (const { doctype, name } of ordered) {
19 if (await api.exists(doctype, { name })) await api.delete(doctype, name);
20 }
21});
22
23test("an SLA is disabled, duplicated and deleted from the policy list", async ({ page, api }) => {
24 const sla = await createSla(api, await createHolidayList(api));
25 const copy = `${sla} (Copy)`;
26 created.push({ doctype: SLA, name: copy });
27 await page.goto("/opendesk/tickets");
28 const dialog = await openSettings(page, "SLA Policies");
29
30 await listRow(dialog, sla).getByRole("switch").click();
31 await expect.poll(async () => (await api.get(SLA, sla)).enabled).toBe(0);
32
33 await listRow(dialog, sla).getByRole("button").click();
34 await page.getByRole("menuitem", { name: "Duplicate" }).click();
35 const duplicate = page.getByRole("dialog", { name: "Duplicate SLA Policy" });
36 await expect(duplicate.getByRole("textbox")).toHaveValue(copy);
37 await duplicate.getByRole("button", { name: "Duplicate" }).click();
38 await expect.poll(() => api.exists(SLA, { name: copy })).toBeTruthy();
39 const duplicated = await api.get(SLA, copy);
40 expect(duplicated.default_sla).toBe(0);
41 expect(duplicated.priorities.map((row) => row.priority).sort()).toEqual([...PRIORITIES].sort());
42
43 await dialog.getByRole("button", { name: copy, exact: true }).click();
44 await deleteFromMenu(page, listRow(dialog, copy).getByRole("button"));
45 await expect.poll(() => api.exists(SLA, { name: copy })).toBeFalsy();
46 await expect(listRow(dialog, sla)).toBeVisible();
47});
48
49test("an SLA's targets, work days and holiday list are edited and saved", async ({ page, api }) => {
50 const firstList = await createHolidayList(api);
51 const secondList = await createHolidayList(api);
52 const sla = await createSla(api, firstList);
53 await page.goto("/opendesk/tickets");
54 const dialog = await openSettings(page, "SLA Policies");
55 await listRow(dialog, sla).getByText(sla, { exact: true }).click();
56
57 await rowMenu(page, tableRow(dialog, "High"), "Edit");
58 const targets = page.getByRole("dialog", { name: "Edit response and resolution" });
59 await targets.getByText("1 hour", { exact: true }).click();
60 const hours = page.locator("div.group", { hasText: "Hrs" }).locator("input");
61 await hours.fill("2");
62 await hours.press("Tab");
63 await targets.getByRole("checkbox", { name: "Set default priority" }).check();
64 await targets.getByRole("button", { name: "Save" }).click();
65 await deleteFromMenu(page, tableRow(dialog, "Urgent").getByRole("button"));
66
67 await rowMenu(page, tableRow(dialog, "Tuesday"), "Edit");
68 const workday = page.getByRole("dialog", { name: "Edit workday" });
69 await workday.getByLabel("End Time").fill("08:00");
70 await workday.getByRole("button", { name: "Save" }).click();
71 await expect(workday.getByText("End time must be after start time")).toBeVisible();
72 await workday.getByLabel("End Time").fill("18:30");
73 await workday.getByRole("button", { name: "Save" }).click();
74 await expect(workday).toBeHidden();
75 await deleteFromMenu(page, tableRow(dialog, "Monday").getByRole("button"));
76 await dialog.getByRole("button", { name: "Add row" }).last().click();
77
78 await dialog.getByRole("button", { name: firstList, exact: true }).click();
79 await page.getByText(secondList, { exact: true }).click();
80 await page.keyboard.press("Escape");
81 await dialog.getByRole("button", { name: "Save", exact: true }).click();
82
83 await expect.poll(async () => (await api.get(SLA, sla)).holiday_list).toBe(secondList);
84 const saved = await api.get(SLA, sla);
85 const byPriority = Object.fromEntries(saved.priorities.map((row) => [row.priority, row]));
86 expect(Object.keys(byPriority).sort()).toEqual(["High", "Low", "Medium"]);
87 expect(byPriority.High).toMatchObject({ default_priority: 1, response_time: 7200 });
88 expect(byPriority.Medium.default_priority).toBe(0);
89 expect(
90 saved.support_and_resolution.map((row) => [row.workday, row.start_time, row.end_time])
91 ).toEqual([
92 ["Tuesday", "9:00:00", "18:30:00"],
93 ["Monday", "9:00:00", "17:00:00"],
94 ]);
95});
96
97test("a holiday list gets a recurring weekend and its holidays are edited", async ({ page, api }) => {
98 const list = await createHolidayList(api);
99 await page.goto("/opendesk/tickets");
100 const dialog = await openSettings(page, "Business Holidays");
101 await dialog.getByText(list, { exact: true }).click();
102
103 await dialog.getByRole("button", { name: "Add Recurring Holiday" }).click();
104 const recurring = page.getByRole("dialog", { name: "Add Recurring Holiday" });
105 await recurring.getByRole("combobox").click();
106 await page.getByRole("option", { name: "Saturday" }).click();
107 await recurring.getByRole("button", { name: "Add Holiday" }).click();
108 await expect(page.getByText("Please select at least one repetition option")).toBeVisible();
109 await recurring.getByRole("checkbox", { name: "Every week", exact: true }).check();
110 await recurring.getByRole("button", { name: "Add Holiday" }).click();
111 await expect(dialog.getByText(`There are in total ${daysIn2027(6).length + 1} holidays`)).toBeVisible();
112
113 await rowMenu(page, recurringRow(dialog, "Saturday"), "Edit");
114 const editRecurring = page.getByRole("dialog", { name: "Edit Recurring Holiday" });
115 await editRecurring.getByRole("checkbox", { name: "Every week", exact: true }).uncheck();
116 await editRecurring.getByRole("checkbox", { name: "Every first week" }).check();
117 await editRecurring.getByRole("button", { name: "Update Holiday" }).click();
118 await expect(recurringRow(dialog, "Saturday")).toContainText("Every first");
119 await expect(dialog.getByText("There are in total 13 holidays")).toBeVisible();
120
121 await dialog.getByRole("radio", { name: "List" }).click();
122 await rowMenu(page, holidayRow(dialog, "26 Jan 2027"), "Edit");
123 const editHoliday = page.getByRole("dialog", { name: "Edit Holiday" });
124 await editHoliday.getByPlaceholder("National holiday, etc.").fill("Republic Day, observed");
125 await editHoliday.getByRole("button", { name: "Add Holiday" }).click();
126
127 await addHoliday(page, dialog, "2028-01-05", "Out of range");
128 await expect(page.getByText(/Holiday date must be between/)).toBeVisible();
129 await page.getByRole("dialog", { name: "Add Holiday" }).getByRole("button", { name: "Cancel" }).click();
130 await addHoliday(page, dialog, "2027-08-15", "Independence Day");
131 await deleteFromMenu(page, holidayRow(dialog, "15 Aug 2027").getByRole("button"));
132 await expect(holidayRow(dialog, "15 Aug 2027")).toHaveCount(0);
133 await dialog.getByRole("button", { name: "Save", exact: true }).click();
134
135 await expect.poll(async () => JSON.parse((await api.get(HOLIDAYS, list)).recurring_holidays || "[]")).toEqual([
136 expect.objectContaining({ day: "Saturday", repetition: expect.objectContaining({ all: false, first: true }) }),
137 ]);
138 const saved = await api.get(HOLIDAYS, list);
139 const weeklyOffs = saved.holidays.filter((row) => row.weekly_off).map((row) => row.holiday_date);
140 expect(weeklyOffs.sort()).toEqual(firstOfEachMonth(daysIn2027(6)));
141 const others = saved.holidays.filter((row) => !row.weekly_off);
142 expect(others.map((row) => [row.holiday_date, row.description])).toEqual([["2027-01-26", "Republic Day, observed"]]);
143});
144
145test("a holiday list spanning two years is browsed, duplicated and deleted", async ({ page, api }) => {
146 const list = await createHolidayList(api, { to_date: "2028-12-31" });
147 const copy = `${list} (Copy)`;
148 created.push({ doctype: HOLIDAYS, name: copy });
149 await page.goto("/opendesk/tickets");
150 const dialog = await openSettings(page, "Business Holidays");
151 await dialog.getByText(list, { exact: true }).click();
152
153 await dialog.getByRole("button", { name: /^20\d\d$/ }).click();
154 await page.getByText("2028", { exact: true }).click();
155 await expect(dialog.getByText("January, 2028")).toBeVisible();
156 await dialog.getByRole("button", { name: "2028" }).click();
157 await page.getByText("2027", { exact: true }).click();
158 await dialog.locator(".bg-surface-yellow-2", { hasText: /^26$/ }).click();
159 const editHoliday = page.getByRole("dialog", { name: "Edit Holiday" });
160 await expect(editHoliday.getByPlaceholder("National holiday, etc.")).toHaveValue("Republic Day");
161 await editHoliday.getByRole("button", { name: "Cancel" }).click();
162
163 await dialog.getByRole("button", { name: list, exact: true }).click();
164 await listRow(dialog, list).getByRole("button").click();
165 await page.getByRole("menuitem", { name: "Duplicate" }).click();
166 const duplicate = page.getByRole("dialog", { name: "Duplicate Holiday List" });
167 await expect(duplicate.getByRole("textbox")).toHaveValue(copy);
168 await duplicate.getByRole("button", { name: "Duplicate" }).click();
169 await expect.poll(() => api.exists(HOLIDAYS, { name: copy })).toBeTruthy();
170 expect((await api.get(HOLIDAYS, copy)).holidays.map((row) => row.holiday_date)).toEqual(["2027-01-26"]);
171
172 await dialog.getByRole("button", { name: copy, exact: true }).click();
173 await deleteFromMenu(page, listRow(dialog, copy).getByRole("button"));
174 await expect.poll(() => api.exists(HOLIDAYS, { name: copy })).toBeFalsy();
175});
176
177test("the holiday calendar opens on the list's first year", async ({ page, api }) => {
178 const list = await createHolidayList(api, { to_date: "2028-12-31" });
179 await page.goto("/opendesk/tickets");
180 const dialog = await openSettings(page, "Business Holidays");
181 await dialog.getByText(list, { exact: true }).click();
182 await expect(dialog.getByText("January, 2027")).toBeVisible();
183});
184
185test("every first Sunday includes a month that starts on a Sunday", async ({ page, api }) => {
186 const list = await createHolidayList(api);
187 await page.goto("/opendesk/tickets");
188 const dialog = await openSettings(page, "Business Holidays");
189 await dialog.getByText(list, { exact: true }).click();
190 await dialog.getByRole("button", { name: "Add Recurring Holiday" }).click();
191 const recurring = page.getByRole("dialog", { name: "Add Recurring Holiday" });
192 await recurring.getByRole("combobox").click();
193 await page.getByRole("option", { name: "Sunday" }).click();
194 await recurring.getByRole("checkbox", { name: "Every first week" }).check();
195 await recurring.getByRole("button", { name: "Add Holiday" }).click();
196 await dialog.getByRole("button", { name: "Save", exact: true }).click();
197 await expect
198 .poll(async () => (await api.get(HOLIDAYS, list)).holidays.filter((row) => row.weekly_off).map((row) => row.holiday_date).sort())
199 .toEqual(firstOfEachMonth(daysIn2027(0)));
200});
201
202async function createHolidayList(api: Api, overrides: Record<string, unknown> = {}) {
203 const name = `E2E Holidays ${uid()}`;
204 created.push({ doctype: HOLIDAYS, name });
205 await api.insert(HOLIDAYS, {
206 holiday_list_name: name,
207 from_date: "2027-01-01",
208 to_date: "2027-12-31",
209 holidays: [{ holiday_date: "2027-01-26", description: "Republic Day", weekly_off: 0 }],
210 ...overrides,
211 });
212 return name;
213}
214
215async function createSla(api: Api, holidayList: string) {
216 const name = `E2E SLA ${uid()}`;
217 created.push({ doctype: SLA, name });
218 await api.insert(SLA, {
219 service_level: name,
220 holiday_list: holidayList,
221 enabled: 1,
222 default_sla: 0,
223 apply_sla_for_resolution: 1,
224 priorities: PRIORITIES.map((priority) => ({
225 priority,
226 response_time: 3600,
227 resolution_time: 86400,
228 default_priority: priority === "Medium" ? 1 : 0,
229 })),
230 support_and_resolution: ["Monday", "Tuesday"].map((workday) => ({
231 workday,
232 start_time: "09:00:00",
233 end_time: "17:00:00",
234 })),
235 });
236 return name;
237}
238
239/** A row of the SLA or holiday list, found by its exact name. */
240function listRow(dialog: Locator, name: string) {
241 return dialog.locator("div.cursor-pointer").filter({ has: dialog.page().getByText(name, { exact: true }) });
242}
243
244/** A priority or work day row, found by the value selected in its dropdown. */
245function tableRow(dialog: Locator, value: string) {
246 return dialog.locator("div.grid").filter({ has: dialog.page().locator(`select option:checked[value="${value}"]`) });
247}
248
249/** A recurring holiday row, found by its day. */
250function recurringRow(dialog: Locator, day: string) {
251 return dialog.locator("div.grid").filter({ has: dialog.page().getByText(day, { exact: true }) });
252}
253
254function holidayRow(dialog: Locator, date: string) {
255 return dialog.locator("div.grid").filter({ has: dialog.page().getByText(date, { exact: true }) });
256}
257
258async function rowMenu(page: Page, row: Locator, action: string) {
259 await row.getByRole("button").click();
260 await page.getByRole("menuitem", { name: action }).click();
261}
262
263async function deleteFromMenu(page: Page, menuButton: Locator) {
264 await menuButton.click();
265 await page.getByRole("menuitem", { name: "Delete" }).click();
266 await page.getByRole("menuitem", { name: "Confirm Delete" }).click();
267}
268
269async function addHoliday(page: Page, dialog: Locator, date: string, description: string) {
270 await dialog.getByRole("button", { name: "Add Holiday" }).click();
271 const modal = page.getByRole("dialog", { name: "Add Holiday" });
272 await modal.getByRole("combobox").fill(date);
273 await modal.getByRole("combobox").press("Enter");
274 await modal.getByPlaceholder("National holiday, etc.").fill(description);
275 await modal.getByRole("button", { name: "Add Holiday" }).click();
276}
277
278/** Every date in 2027 that falls on a weekday, 0 being Sunday. */
279function daysIn2027(weekday: number) {
280 const dates: string[] = [];
281 for (let day = new Date(Date.UTC(2027, 0, 1)); day.getUTCFullYear() === 2027; day.setUTCDate(day.getUTCDate() + 1)) {
282 if (day.getUTCDay() === weekday) dates.push(day.toISOString().slice(0, 10));
283 }
284 return dates;
285}
286
287function firstOfEachMonth(dates: string[]) {
288 return dates.filter((date) => Number(date.slice(8)) <= 7);
289}
290