1import type { Locator, Page } from "@playwright/test";
2import type { Api } from "../../helpers/api";
3import { expect, test, uid, usePersona } from "../../helpers/fixtures";
4import { personas } from "../../helpers/personas";
5import { openSettings } from "../../helpers/settings";
6
7const RULE = "Assignment Rule";
8const DEPENDENCY = "Field Dependency-ticket_type-priority";
9const DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
10
11usePersona("admin");
12
13test.describe("field dependencies", () => {
14 test.afterEach(async ({ api }) => {
15 if (await api.exists("OD Form Script", { name: DEPENDENCY })) await api.delete("OD Form Script", DEPENDENCY);
16 });
17
18 test("the builder maps values per parent, then edits, disables and deletes the dependency", async ({ page, api }) => {
19 await page.goto("/opendesk/tickets");
20 const dialog = await openSettings(page, "Field Dependencies");
21 await dialog.getByRole("button", { name: "New" }).click();
22 await pickOption(page, dialog.getByRole("combobox").first(), "Ticket Type");
23 await pickOption(page, dialog.getByRole("combobox").nth(1), "Priority");
24
25 // Search narrows the child list, and Select All only takes the filtered values.
26 await parentValue(dialog, "Bug").click();
27 await dialog.getByPlaceholder("Search Bug values").fill("Urg");
28 await dialog.getByText("Select All", { exact: true }).click();
29 await expect(dialog.getByText("1 value selected")).toBeVisible();
30 await dialog.getByPlaceholder("Search Bug values").fill("");
31 await expect(childValue(dialog, "Low").getByRole("checkbox")).not.toBeChecked();
32
33 await parentValue(dialog, "Question").click();
34 await childValue(dialog, "Low").click();
35 await childValue(dialog, "Medium").click();
36 await expect(dialog.getByText("2 values selected")).toBeVisible();
37 await expect(parentValue(dialog, "Bug")).toContainText("1");
38
39 await turnOffCriteria(dialog);
40 await dialog.getByRole("button", { name: "Save", exact: true }).click();
41 await expect.poll(() => mapping(api)).toEqual({ Bug: ["Urgent"], Question: ["Low", "Medium"] });
42
43 const row = dialog.locator("div.grid-cols-11").filter({ hasText: "Ticket Type → Priority" });
44 await row.getByText("Ticket Type → Priority").click();
45 await expect(dialog.getByRole("combobox").first()).toBeDisabled();
46 await parentValue(dialog, "Question").click();
47 await childValue(dialog, "Medium").click();
48 await dialog.getByRole("button", { name: "Save", exact: true }).click();
49 await expect.poll(() => mapping(api)).toEqual({ Bug: ["Urgent"], Question: ["Low"] });
50
51 // The back button carries the dependency's label.
52 await dialog.getByRole("button", { name: /Ticket Type → Priority/ }).first().click();
53 await row.getByRole("switch").click();
54 await expect.poll(async () => (await api.get("OD Form Script", DEPENDENCY)).enabled).toBe(0);
55
56 await row.getByRole("button").last().click();
57 await page.getByRole("menuitem", { name: "Delete" }).click();
58 await page.getByRole("menuitem", { name: "Confirm Delete" }).click();
59 await expect.poll(() => api.exists("OD Form Script", { name: DEPENDENCY })).toBeFalsy();
60 });
61
62 test("going back with unsaved choices asks before discarding them", async ({ page, api }) => {
63 await page.goto("/opendesk/tickets");
64 const dialog = await openSettings(page, "Field Dependencies");
65 await dialog.getByRole("button", { name: "New" }).click();
66 await pickOption(page, dialog.getByRole("combobox").first(), "Ticket Type");
67 await dialog.getByRole("button", { name: "New Field Dependency" }).click();
68 const confirm = page.getByRole("dialog", { name: "Unsaved changes" });
69 await confirm.getByRole("button", { name: "Confirm" }).click();
70 await expect(dialog.getByRole("button", { name: "New", exact: true })).toBeVisible();
71 expect(await api.exists("OD Form Script", { name: DEPENDENCY })).toBeFalsy();
72 });
73});
74
75test.describe("assignment rules", () => {
76 let rule: string;
77
78 test.beforeEach(async ({ api }) => {
79 rule = `E2E Rule ${uid()}`;
80 // The condition names a team that never exists, so the rule never grabs real tickets.
81 await api.insert(RULE, {
82 name: rule,
83 assignment_rule_name: rule,
84 document_type: "OD Ticket",
85 description: "Route nothing",
86 rule: "Round Robin",
87 priority: 0,
88 assign_condition: `agent_group == "${rule}"`,
89 users: [{ user: personas.agent2.email }],
90 assignment_days: DAYS.map((day) => ({ day })),
91 });
92 });
93
94 test.afterEach(async ({ api }) => {
95 for (const name of [rule, `${rule} (Copy)`, `${rule} Renamed`]) {
96 if (await api.exists(RULE, { name })) await api.delete(RULE, name);
97 }
98 });
99
100 test("the list changes priority, disables, duplicates and deletes a rule", async ({ page, api }) => {
101 await page.goto("/opendesk/tickets");
102 const dialog = await openSettings(page, "Assignment Rules");
103 const row = ruleRow(dialog, rule);
104
105 await row.locator("select").selectOption({ label: "High" });
106 await expect.poll(async () => (await api.get(RULE, rule)).priority).toBe(4);
107 await row.getByRole("switch").click();
108 await expect.poll(async () => (await api.get(RULE, rule)).disabled).toBe(1);
109
110 await row.getByRole("button").last().click();
111 await page.getByRole("menuitem", { name: "Duplicate" }).click();
112 const duplicate = page.getByRole("dialog", { name: "Duplicate Assignment Rule" });
113 await expect(duplicate.getByRole("textbox")).toHaveValue(`${rule} (Copy)`);
114 await duplicate.getByRole("button", { name: "Duplicate" }).click();
115 await expect.poll(() => api.exists(RULE, { name: `${rule} (Copy)` })).toBeTruthy();
116 expect((await api.get(RULE, `${rule} (Copy)`)).users.map((u: { user: string }) => u.user)).toEqual([
117 personas.agent2.email,
118 ]);
119
120 await dialog.getByRole("button", { name: `${rule} (Copy)` }).first().click();
121 await ruleRow(dialog, `${rule} (Copy)`).getByRole("button").last().click();
122 await page.getByRole("menuitem", { name: "Delete" }).click();
123 await page.getByRole("menuitem", { name: "Confirm Delete" }).click();
124 await expect.poll(() => api.exists(RULE, { name: `${rule} (Copy)` })).toBeFalsy();
125 expect(await api.exists(RULE, { name: rule })).toBeTruthy();
126 });
127
128 test("editing a rule with an old condition renames it and keeps that condition", async ({ page, api }) => {
129 await page.goto("/opendesk/tickets");
130 const dialog = await openSettings(page, "Assignment Rules");
131 await ruleRow(dialog, rule).getByText(rule, { exact: true }).click();
132 await expect(dialog.getByText("Old Condition").first()).toBeVisible();
133
134 await dialog.getByRole("textbox", { name: /^Name/ }).fill(`${rule} Renamed`);
135 await dialog.getByRole("textbox", { name: /^Description/ }).fill("Still routes nothing");
136 await dialog.getByText("Low", { exact: true }).click();
137 await page.getByText("Medium-High", { exact: true }).click();
138 await dialog.getByRole("button", { name: "Save", exact: true }).click();
139
140 await expect.poll(() => api.exists(RULE, { name: `${rule} Renamed` })).toBeTruthy();
141 const saved = await api.get(RULE, `${rule} Renamed`);
142 expect(saved).toMatchObject({ description: "Still routes nothing", priority: 3 });
143 expect(saved.assign_condition).toBe(`agent_group == "${rule}"`);
144 });
145
146 test("saving without a name is refused", async ({ page }) => {
147 await page.goto("/opendesk/tickets");
148 const dialog = await openSettings(page, "Assignment Rules");
149 await ruleRow(dialog, rule).getByText(rule, { exact: true }).click();
150 await dialog.getByRole("textbox", { name: /^Name/ }).fill("");
151 await dialog.getByRole("button", { name: "Save", exact: true }).click();
152 await expect(page.getByText("Invalid fields, check if all are filled in and values are correct.")).toBeVisible();
153 });
154});
155
156async function pickOption(page: Page, combobox: Locator, option: string) {
157 await combobox.click();
158 await page.getByRole("option", { name: option, exact: true }).click();
159}
160
161function parentValue(dialog: Locator, value: string) {
162 return dialog.getByRole("listitem").filter({ hasText: new RegExp(`^${value}\\d*$`) }).first();
163}
164
165function childValue(dialog: Locator, value: string) {
166 return dialog.getByRole("listitem").filter({ hasText: new RegExp(`^${value}$`) }).last();
167}
168
169// Leave the visibility and mandatory rules off so OD Ticket's schema stays untouched.
170async function turnOffCriteria(dialog: Locator) {
171 for (const rule of [/^Show Priority if/, /^Make Priority mandatory if/]) {
172 await dialog.getByText(rule).locator("xpath=ancestor::div[.//button[@role='switch']][1]").getByRole("switch").click();
173 }
174}
175
176async function mapping(api: Api) {
177 if (!(await api.exists("OD Form Script", { name: DEPENDENCY }))) return null;
178 const dependency = await api.call("opendesk.api.settings.field_dependency.get_field_dependency", {
179 name: DEPENDENCY,
180 });
181 return JSON.parse(dependency.parent_child_mapping);
182}
183
184function ruleRow(dialog: Locator, name: string) {
185 return dialog.locator("div.grid-cols-12").filter({ has: dialog.page().getByText(name, { exact: true }) });
186}
187