1import type { Page } from "@playwright/test";
2import { expect, test } from "../../helpers/fixtures";
3import {
4 FormScripts,
5 TEMPLATE,
6 clicksOn,
7 countingAction,
8 ensureTemplate,
9} from "../../helpers/form-script";
10
11const NEW_WITH_TEMPLATE = `/opendesk/tickets/new/${encodeURIComponent(TEMPLATE)}`;
12
13let scripts: FormScripts;
14
15test.beforeEach(({ api }) => {
16 scripts = new FormScripts(api);
17});
18
19test.afterEach(async () => {
20 await scripts.removeAll();
21});
22
23test("a flat action fires exactly once on each page", async ({ pageAs, ticket }) => {
24 await scripts.create(countingAction("E2E Agent Ticket"));
25 await scripts.create(countingAction("E2E Portal Ticket"), { portal: true });
26 await scripts.create(countingAction("E2E Agent New"), { newPage: true });
27
28 const surfaces = [
29 { persona: "agent", url: `/opendesk/tickets/${ticket.name}`, label: "E2E Agent Ticket" },
30 { persona: "customer", url: `/help/my-tickets/${ticket.name}`, label: "E2E Portal Ticket" },
31 { persona: "agent", url: "/opendesk/tickets/new", label: "E2E Agent New" },
32 ] as const;
33 for (const surface of surfaces) {
34 const page = await pageAs(surface.persona);
35 await page.goto(surface.url);
36 await page.getByRole("button", { name: surface.label }).click();
37 await expect.poll(() => clicksOn(page)).toBe(1);
38 }
39});
40
41test("grouped actions render, including the legacy items shape", async ({ pageAs, ticket }) => {
42 await scripts.create(
43 countingAction(
44 "E2E Flat",
45 `, { group: "E2E Group", options: [{ label: "E2E Option", onClick: () => {} }] },
46 { group: "E2E Legacy", items: [{ label: "E2E Legacy Item", onClick: () => {
47 window.__e2eClicks = (window.__e2eClicks || 0) + 1;
48 } }] },
49 { group: "E2E Labelled", buttonLabel: "E2E Menu", label: "E2E Menu Item", onClick: () => {} }`
50 )
51 );
52 const page = await pageAs("agent");
53 await page.goto(`/opendesk/tickets/${ticket.name}`);
54
55 await page.getByRole("button", { name: "E2E Menu" }).click();
56 await expect(page.getByRole("menuitem", { name: "E2E Menu Item" })).toBeVisible();
57 await page.keyboard.press("Escape");
58
59 await openMoreMenu(page);
60 await expect(page.getByRole("menuitem", { name: "E2E Option" })).toBeVisible();
61 await page.getByRole("menuitem", { name: "E2E Legacy Item" }).click();
62 await expect.poll(() => clicksOn(page)).toBe(1);
63});
64
65test("actions can update fields, call the server, open dialogs and route", async ({
66 api,
67 pageAs,
68 ticket,
69}) => {
70 await scripts.create(`return { actions: [
71 { label: "E2E Set Priority", onClick: () => ctx.updateField("priority", "High") },
72 { label: "E2E Call", onClick: () => ctx.call("frappe.client.get_count", { doctype: "OD Ticket Priority" })
73 .then((count) => { window.__e2eCount = count; }) },
74 { label: "E2E Dialog", onClick: () => ctx.$dialog({ title: "E2E Dialog Title", message: "From a form script" }) },
75 { label: "E2E Route", onClick: () => ctx.router.push({ name: "TicketsAgent" }) },
76 ] };`);
77 const page = await pageAs("agent");
78 await page.goto(`/opendesk/tickets/${ticket.name}`);
79
80 await page.getByRole("button", { name: "E2E Set Priority" }).click();
81 await expect.poll(async () => (await api.get("OD Ticket", ticket.name)).priority).toBe("High");
82
83 await page.getByRole("button", { name: "E2E Call" }).click();
84 await expect.poll(() => page.evaluate(() => (window as any).__e2eCount)).toBeGreaterThan(0);
85
86 await page.getByRole("button", { name: "E2E Dialog" }).click();
87 await expect(page.getByRole("dialog").getByText("E2E Dialog Title")).toBeVisible();
88 await page.keyboard.press("Escape");
89
90 await page.getByRole("button", { name: "E2E Route" }).click();
91 await expect(page).toHaveURL(/\/opendesk\/tickets$/);
92});
93
94test("toast shows a message", async ({ pageAs, ticket }) => {
95 await scripts.create(`return { actions: [
96 { label: "E2E Toast", onClick: () => ctx.toast.success("E2E plain toast") },
97 ] };`);
98 const page = await pageAs("agent");
99 await page.goto(`/opendesk/tickets/${ticket.name}`);
100
101 await page.getByRole("button", { name: "E2E Toast" }).click();
102 await expect(page.getByText("E2E plain toast", { exact: true })).toBeVisible();
103});
104
105test("createToast reads duration as seconds", async ({ pageAs, ticket }) => {
106 // Form scripts pass seconds; vue-sonner reads milliseconds.
107 await scripts.create(`return { actions: [
108 { label: "E2E Timed Toast", onClick: () => ctx.createToast({ message: "E2E timed toast", type: "success", duration: 5 }) },
109 ] };`);
110 const page = await pageAs("agent");
111 await page.goto(`/opendesk/tickets/${ticket.name}`);
112
113 await page.getByRole("button", { name: "E2E Timed Toast" }).click();
114 await expect(page.getByText("E2E timed toast", { exact: true })).toBeVisible();
115 // A 5 second toast must still be up after 1.5s; read as ms it is long gone.
116 await page.waitForTimeout(1500);
117 await expect(page.getByText("E2E timed toast", { exact: true })).toBeVisible();
118});
119
120test.describe("new ticket form", () => {
121 test.beforeEach(async ({ api }) => {
122 await ensureTemplate(api);
123 });
124
125 test("onChange narrows a dependent field with applyFilters", async ({ pageAs }) => {
126 await scripts.create(
127 `return { actions: [], onChange: { e2e_trigger: (value) =>
128 ctx.applyFilters("e2e_choice", value === "Show" ? ["Alpha", "Beta"] : null) } };`,
129 { newPage: true }
130 );
131 const page = await pageAs("agent");
132 await page.goto(NEW_WITH_TEMPLATE);
133
134 await pick(page, "E2E trigger", "Show");
135 await field(page, "E2E choice").getByRole("combobox").click();
136 await expect(page.getByRole("option")).toHaveText(["Alpha", "Beta"]);
137 await page.keyboard.press("Escape");
138
139 await pick(page, "E2E trigger", "Hide");
140 await expect(field(page, "E2E choice").getByRole("combobox")).toBeDisabled();
141 });
142
143 test("depends_on and mandatory_depends_on follow other values", async ({ pageAs }) => {
144 const page = await pageAs("agent");
145 await page.goto(NEW_WITH_TEMPLATE);
146 await expect(field(page, "E2E shown")).toHaveCount(0);
147 await expect(field(page, "E2E needed").getByText("*")).toHaveCount(0);
148
149 await pick(page, "E2E trigger", "Show");
150 await expect(field(page, "E2E shown")).toBeVisible();
151 await expect(field(page, "E2E needed").getByText("*")).toBeVisible();
152 });
153
154 test("read_only_depends_on locks a field", async ({ pageAs }) => {
155 const page = await pageAs("agent");
156 await page.goto(NEW_WITH_TEMPLATE);
157 await pick(page, "E2E trigger", "Show");
158 await expect(field(page, "E2E locked").getByRole("textbox")).toBeDisabled();
159 });
160});
161
162test.describe("scoping", () => {
163 test("portal flag keeps agent and portal scripts apart", async ({ pageAs, ticket }) => {
164 await scripts.create(countingAction("E2E Agent Only"));
165 await scripts.create(countingAction("E2E Portal Only"), { portal: true });
166
167 const agent = await pageAs("agent");
168 await agent.goto(`/opendesk/tickets/${ticket.name}`);
169 await expect(agent.getByRole("button", { name: "E2E Agent Only" })).toBeVisible();
170 await expect(agent.getByRole("button", { name: "E2E Portal Only" })).toHaveCount(0);
171
172 const customer = await pageAs("customer");
173 await customer.goto(`/help/my-tickets/${ticket.name}`);
174 await expect(customer.getByRole("button", { name: "E2E Portal Only" })).toBeVisible();
175 await expect(customer.getByRole("button", { name: "E2E Agent Only" })).toHaveCount(0);
176 });
177
178 test("a disabled script never runs and two enabled scripts combine", async ({
179 pageAs,
180 ticket,
181 }) => {
182 await scripts.create(countingAction("E2E First"));
183 await scripts.create(countingAction("E2E Second"));
184 await scripts.create(countingAction("E2E Disabled"), { enabled: false });
185
186 const page = await pageAs("agent");
187 await page.goto(`/opendesk/tickets/${ticket.name}`);
188 await expect(page.getByRole("button", { name: "E2E First" })).toBeVisible();
189 await expect(page.getByRole("button", { name: "E2E Second" })).toBeVisible();
190 await expect(page.getByRole("button", { name: "E2E Disabled" })).toHaveCount(0);
191 });
192
193 test.fixme("new ticket scripts respect the portal flag", async ({ pageAs }) => {
194 // od_ticket_template/api.py get_one hardcodes is_customer_portal=False.
195 await scripts.create(countingAction("E2E Agent New Only"), { newPage: true });
196 await scripts.create(countingAction("E2E Portal New Only"), { newPage: true, portal: true });
197
198 const customer = await pageAs("customer");
199 await customer.goto("/help/my-tickets/new");
200 await expect(customer.getByRole("button", { name: "E2E Portal New Only" })).toBeVisible();
201 await expect(customer.getByRole("button", { name: "E2E Agent New Only" })).toHaveCount(0);
202 });
203});
204
205function field(page: Page, label: string) {
206 return page.locator(".space-y-1\\.5").filter({
207 has: page.getByText(new RegExp(`^${label}\\b`)),
208 });
209}
210
211async function pick(page: Page, label: string, option: string) {
212 await field(page, label).getByRole("combobox").click();
213 await page.getByRole("option", { name: option, exact: true }).click();
214}
215
216async function openMoreMenu(page: Page) {
217 await page.getByRole("banner").getByRole("button", { name: "More actions" }).click();
218}
219