1import type { Page } from "@playwright/test";
2import type { Api } from "../../../helpers/api";
3import { raiseTicket } from "../../../helpers/factories";
4import { FormScripts } from "../../../helpers/form-script";
5import { expect, test, uid, usePersona } from "../../../helpers/fixtures";
6import { personas } from "../../../helpers/personas";
7import { runTicketMethod, setOpendeskSetting } from "../../../helpers/ticket";
8
9usePersona("customer");
10
11function agentReply(agent: Api, ticket: string, html: string) {
12 return runTicketMethod(agent, ticket, "reply_via_agent", { message: html });
13}
14
15/** The email iframe showing `text`, never the page's own composer. */
16async function frameWith(page: Page, text: string) {
17 const frames = () => page.frames().filter((frame) => frame !== page.mainFrame());
18 await expect
19 .poll(async () => {
20 for (const frame of frames()) if (await frame.getByText(text).count()) return true;
21 return false;
22 })
23 .toBe(true);
24 for (const frame of frames()) if (await frame.getByText(text).count()) return frame;
25 throw new Error(`no frame shows ${text}`);
26}
27
28/** The header's Close, not the X of a dialog that shares its name. */
29function closeButton(page: Page) {
30 return page.getByRole("button", { name: "Close", exact: true }).first();
31}
32
33function confirmClose(page: Page) {
34 return page.getByRole("dialog").getByRole("button", { name: "Close ticket" }).click();
35}
36
37function composer(page: Page) {
38 return page.locator(".portal-reply-composer .ProseMirror");
39}
40
41// Closed on arrival; its bar opens it.
42async function openComposer(page: Page) {
43 await page.getByText("Type a message").click();
44 return composer(page);
45}
46
47async function communication(api: Api, ticket: string, text: string) {
48 const rows = await api.list("Communication", {
49 filters: { reference_doctype: "OD Ticket", reference_name: ticket },
50 fields: ["name", "sender", "content"],
51 });
52 return rows.find((row) => row.content.includes(text));
53}
54
55// Studio evaluates component inputs once per input, so `{{ }}` in data runs (frappe StudioComponentWrapper.vue).
56test.fixme("a subject that looks like an expression shows as text", async ({ page, apiAs }) => {
57 const subject = `E2E ${uid()} {{ 7 * 7 }}`;
58 const ticket = await raiseTicket(await apiAs("customer"), subject);
59 await page.goto(`/help/my-tickets/${ticket.name}`);
60 await expect(page.getByText(subject).first()).toBeVisible();
61 await expect(page.locator("body")).not.toContainText(subject.replace("{{ 7 * 7 }}", "49"));
62});
63
64test.describe("access", () => {
65 test("someone else's ticket, or none, reads as not found", async ({ page, apiAs }) => {
66 const colleague = await raiseTicket(await apiAs("customerManager"));
67 for (const name of [colleague.name, "99999999"]) {
68 await page.goto(`/help/my-tickets/${name}`);
69 await expect(page.getByText("Ticket not found")).toBeVisible();
70 await expect(
71 page.getByText("It may have been removed, or you may not have access to it.")
72 ).toBeVisible();
73 }
74 });
75
76 test("a manager opens, answers and closes a colleague's ticket", async ({ pageAs, api, apiAs }) => {
77 const colleague = await raiseTicket(await apiAs("customer"));
78 const page = await pageAs("customerManager");
79 await page.goto(`/help/my-tickets/${colleague.name}`);
80 await expect(page.getByRole("button", { name: colleague.subject })).toBeVisible();
81
82 const reply = `Manager note ${uid()}`;
83 await (await openComposer(page)).fill(reply);
84 await page.getByRole("button", { name: "Send" }).click();
85 await frameWith(page, reply);
86 await expect
87 .poll(async () => (await communication(api, colleague.name, reply))?.sender)
88 .toBe(personas.customerManager.email);
89
90 // no agent reply yet, so closing never asks for a rating
91 await closeButton(page).click();
92 await confirmClose(page);
93 await expect.poll(async () => (await api.get("OD Ticket", colleague.name)).status).toBe("Closed");
94 });
95
96 test("an agent opens any ticket on the portal", async ({ pageAs, apiAs }) => {
97 const ticket = await raiseTicket(await apiAs("customer"));
98 const page = await pageAs("agent");
99 await page.goto(`/help/my-tickets/${ticket.name}`);
100 await expect(page.getByRole("button", { name: ticket.subject })).toBeVisible();
101 await expect(closeButton(page)).toBeVisible();
102 });
103});
104
105test.describe("conversation", () => {
106 test("agent emails are sandboxed: links open a new tab, scripts are stripped, quotes fold", async ({
107 page,
108 apiAs,
109 ticket,
110 }) => {
111 const marker = `Sandboxed ${uid()}`;
112 await agentReply(
113 await apiAs("agent"),
114 ticket.name,
115 `<p>${marker} <a href="https://example.com/doc">the doc</a></p>
116 <img src="x" onerror="window.__e2eXss=1"><script>window.__e2eXss=1</script>
117 <div class="gmail_quote">Quoted ${marker}</div>`
118 );
119 await page.goto(`/help/my-tickets/${ticket.name}`);
120 const frame = await frameWith(page, marker);
121 // by its own target or the frame's <base target>
122 const target = await frame.getByRole("link", { name: "the doc" }).evaluate(
123 (link: HTMLAnchorElement) => link.target || document.querySelector("base")?.target || ""
124 );
125 expect(target).toBe("_blank");
126 expect(await frame.locator("body script").count()).toBe(0);
127 expect(await frame.locator("[onerror]").count()).toBe(0);
128 expect(await page.evaluate(() => (window as any).__e2eXss)).toBeUndefined();
129
130 const quote = frame.getByText(`Quoted ${marker}`);
131 await expect(quote).toBeHidden();
132 await frame.getByRole("button").filter({ hasText: "..." }).or(frame.getByText("...", { exact: true })).first().click();
133 await expect(quote).toBeVisible();
134 });
135
136 test("whitespace alone cannot be sent", async ({ page, ticket }) => {
137 await page.goto(`/help/my-tickets/${ticket.name}`);
138 await (await openComposer(page)).fill(" ");
139 await expect(page.getByRole("button", { name: "Send" })).toBeDisabled();
140 });
141
142 test("a reply to a resolved ticket reopens it; there is no separate reopen", async ({ page, api, ticket }) => {
143 await api.update("OD Ticket", ticket.name, { status: "Resolved" });
144 await page.goto(`/help/my-tickets/${ticket.name}`);
145 await expect(page.getByRole("button", { name: /Reopen/ })).toHaveCount(0);
146 const reply = `Still broken ${uid()}`;
147 await (await openComposer(page)).fill(reply);
148 await page.getByRole("button", { name: "Send" }).click();
149 await frameWith(page, reply);
150 await expect.poll(async () => (await api.get("OD Ticket", ticket.name)).status).toBe("Open");
151 });
152
153 test("a reply carries an attached file", async ({ page, api, ticket }) => {
154 await page.goto(`/help/my-tickets/${ticket.name}`);
155 await openComposer(page);
156 const chooser = page.waitForEvent("filechooser");
157 await page.getByRole("button", { name: "Attach file" }).click();
158 await (await chooser).setFiles({ name: "log.txt", mimeType: "text/plain", buffer: Buffer.from("trace") });
159 await expect(page.getByText("log.txt")).toBeVisible();
160 const reply = `With a log ${uid()}`;
161 await composer(page).fill(reply);
162 await page.getByRole("button", { name: "Send" }).click();
163 await frameWith(page, reply);
164 await expect.poll(() => communication(api, ticket.name, reply)).toBeTruthy();
165 const sent = (await communication(api, ticket.name, reply))!;
166 await expect
167 .poll(async () =>
168 (await api.list("File", {
169 filters: { attached_to_doctype: "Communication", attached_to_name: sent.name },
170 fields: ["file_name"],
171 })).map((file) => file.file_name)
172 )
173 .toEqual(["log.txt"]);
174 });
175
176 test("the composer collapses after sending and reopens from its bar", async ({ page, ticket }) => {
177 await page.goto(`/help/my-tickets/${ticket.name}`);
178 const reply = `Collapse ${uid()}`;
179 await (await openComposer(page)).fill(reply);
180 await page.getByRole("button", { name: "Send" }).click();
181 await frameWith(page, reply);
182 await expect(composer(page)).toBeHidden();
183 const bar = page.getByText("Type a message");
184 await bar.click();
185 await expect(composer(page)).toBeVisible();
186 });
187
188 test("the composer resizes from the keyboard and keeps its height", async ({ page, ticket }) => {
189 await page.goto(`/help/my-tickets/${ticket.name}`);
190 await openComposer(page);
191 const handle = page.getByRole("separator", { name: "Resize composer" });
192 await handle.focus();
193 await page.keyboard.press("ArrowUp");
194 await page.keyboard.press("ArrowUp");
195 const stored = await page.evaluate(() => localStorage.getItem("kb:composer-height"));
196 expect(Number(stored)).toBeGreaterThan(0);
197 await page.reload();
198 // The closed bar still keeps the thread's end clear of it.
199 await expect(page.locator(".portal-thread")).toHaveAttribute("style", /--composer-reserve: [1-9]/);
200 await openComposer(page);
201 await expect(page.getByRole("separator", { name: "Resize composer" })).toBeVisible();
202 expect(await page.evaluate(() => localStorage.getItem("kb:composer-height"))).toBe(stored);
203 });
204
205 test("a composer dragged shut shows its draft in the bar", async ({ page, ticket }) => {
206 await page.goto(`/help/my-tickets/${ticket.name}`);
207 const draft = `Draft ${uid()}`;
208 await (await openComposer(page)).fill(draft);
209 const box = (await page.getByRole("separator", { name: "Resize composer" }).boundingBox())!;
210 await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
211 await page.mouse.down();
212 await page.mouse.move(box.x + box.width / 2, box.y + 400, { steps: 8 });
213 await page.mouse.up();
214 await expect(composer(page)).toBeHidden();
215 await expect(page.getByRole("button", { name: draft })).toBeVisible();
216 });
217
218 test("an agent's reply elsewhere shows without a reload", async ({ page, apiAs, ticket }) => {
219 await page.goto(`/help/my-tickets/${ticket.name}`);
220 await expect(closeButton(page)).toBeVisible();
221 const live = `Live ${uid()}`;
222 await agentReply(await apiAs("agent"), ticket.name, `<p>${live}</p>`);
223 await frameWith(page, live);
224 });
225});
226
227test.describe("layouts", () => {
228 async function useChat(page: Page) {
229 await page.addInitScript(() => localStorage.setItem("kb:conversation-layout", "chat"));
230 }
231
232 async function centreX(page: Page, frameText: string) {
233 for (const frame of page.frames()) {
234 if (!(await frame.getByText(frameText).count())) continue;
235 const box = await (await frame.frameElement()).boundingBox();
236 return box!.x + box!.width / 2;
237 }
238 throw new Error(frameText);
239 }
240
241 test("chat: the customer's messages on the right, the agent's on the left; closes are centred events", async ({
242 page,
243 api,
244 apiAs,
245 ticket,
246 }) => {
247 const answer = `Chat answer ${uid()}`;
248 await agentReply(await apiAs("agent"), ticket.name, `<p>${answer}</p>`);
249 await api.update("OD Ticket", ticket.name, { status: "Closed" });
250 await api.update("OD Ticket", ticket.name, { status: "Open" });
251 await useChat(page);
252 await page.goto(`/help/my-tickets/${ticket.name}`);
253 await frameWith(page, answer);
254 const width = page.viewportSize()!.width;
255 expect(await centreX(page, `${ticket.subject} description`)).toBeGreaterThan(width / 3);
256 expect(await centreX(page, answer)).toBeLessThan(width / 3);
257 await expect(page.getByText(/ closed the ticket/)).toBeVisible();
258 await expect(page.getByText(/ reopened the ticket/)).toBeVisible();
259 });
260
261 test("chat: an attached image is a chip that previews on click; an inline one stays inline", async ({
262 page,
263 apiAs,
264 ticket,
265 }) => {
266 const agent = await apiAs("agent");
267 const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
268 const name = `chat-${uid()}.png`;
269 const file = await agent.insert("File", { file_name: name, content: PNG, decode: 1, is_private: 0 });
270 const inline = `Inline ${uid()}`;
271 await runTicketMethod(agent, ticket.name, "reply_via_agent", {
272 message: `<p>${inline}</p><img src="data:image/png;base64,${PNG}" alt="inline">`,
273 attachments: [file.name],
274 });
275 await useChat(page);
276 await page.goto(`/help/my-tickets/${ticket.name}`);
277 const frame = await frameWith(page, inline);
278 await expect(frame.getByRole("img", { name: "inline" })).toBeVisible();
279 await expect(page.locator(`img[src$="${name}"]`)).toHaveCount(0);
280 await page.getByRole("button", { name }).click();
281 await expect(page.getByRole("dialog", { name })).toBeVisible();
282 });
283
284 test("timeline: closes and reopens read as log rows", async ({ page, api, ticket }) => {
285 await api.update("OD Ticket", ticket.name, { status: "Closed" });
286 await api.update("OD Ticket", ticket.name, { status: "Open" });
287 await page.goto(`/help/my-tickets/${ticket.name}`);
288 await expect(page.getByText(/ closed the ticket/)).toBeVisible();
289 await expect(page.getByText(/ reopened the ticket/)).toBeVisible();
290 });
291
292 test("chat: an agent sees the same sides, their own replies on the left", async ({ pageAs, apiAs, ticket }) => {
293 const answer = `Own reply ${uid()}`;
294 await agentReply(await apiAs("agent"), ticket.name, `<p>${answer}</p>`);
295 const page = await pageAs("agent");
296 await useChat(page);
297 await page.goto(`/help/my-tickets/${ticket.name}`);
298 await frameWith(page, answer);
299 const width = page.viewportSize()!.width;
300 expect(await centreX(page, `${ticket.subject} description`)).toBeGreaterThan(width / 3);
301 expect(await centreX(page, answer)).toBeLessThan(width / 3);
302 });
303});
304
305test.describe("closing and rating", () => {
306 test("close asks to confirm; cancel keeps it open", async ({ page, api, ticket }) => {
307 await page.goto(`/help/my-tickets/${ticket.name}`);
308 await closeButton(page).click();
309 const dialog = page.getByRole("dialog");
310 await expect(dialog.getByText("Are you sure you want to close this ticket?")).toBeVisible();
311 await dialog.getByRole("button", { name: "Cancel" }).click();
312 await expect(dialog).toHaveCount(0);
313 expect((await api.get("OD Ticket", ticket.name)).status).toBe("Open");
314
315 await closeButton(page).click();
316 await confirmClose(page);
317 await expect(page.getByRole("button", { name: "Raise a ticket" })).toBeVisible();
318 await expect(composer(page)).toHaveCount(0);
319 expect((await api.get("OD Ticket", ticket.name)).status).toBe("Closed");
320 });
321
322 test("the rating dialog: stars reload options, submit needs an option, details show after", async ({
323 page,
324 api,
325 apiAs,
326 ticket,
327 }) => {
328 await agentReply(await apiAs("agent"), ticket.name, "<p>Done</p>");
329 const restore = await setOpendeskSetting(api, "is_feedback_mandatory", 1);
330 try {
331 await page.goto(`/help/my-tickets/${ticket.name}`);
332 await closeButton(page).click();
333 const dialog = page.getByRole("dialog", { name: "Rate this ticket" });
334 const submit = dialog.getByRole("button", { name: "Submit" });
335
336 await dialog.getByRole("radio").nth(3).click();
337 await expect(dialog.getByRole("button", { name: "Prompt, informative support" })).toBeVisible();
338 await expect(dialog.getByRole("button", { name: "Exceptional support experience" })).toHaveCount(0);
339 await expect(submit).toBeDisabled();
340 await dialog.getByRole("button", { name: "Prompt, informative support" }).click();
341 await expect(submit).toBeEnabled();
342
343 // a new star value clears the chosen option
344 await dialog.getByRole("radio").nth(4).click();
345 await expect(submit).toBeDisabled();
346 await dialog.getByRole("button", { name: "Exceptional support experience" }).click();
347 await dialog.getByPlaceholder("Tell us more").fill("Thanks");
348 await submit.click();
349
350 await expect.poll(async () => (await api.get("OD Ticket", ticket.name)).status).toBe("Closed");
351 await expect(page.getByText("Exceptional support experience")).toBeVisible();
352 await expect(page.getByText("Thanks", { exact: true })).toBeVisible();
353 } finally {
354 await restore();
355 }
356 });
357
358 test("closing the rating dialog resets it", async ({ page, api, apiAs, ticket }) => {
359 await agentReply(await apiAs("agent"), ticket.name, "<p>Done</p>");
360 const restore = await setOpendeskSetting(api, "is_feedback_mandatory", 1);
361 try {
362 await page.goto(`/help/my-tickets/${ticket.name}`);
363 await closeButton(page).click();
364 const dialog = page.getByRole("dialog", { name: "Rate this ticket" });
365 await dialog.getByRole("radio").nth(4).click();
366 await dialog.getByRole("button", { name: "Exceptional support experience" }).click();
367 await page.keyboard.press("Escape");
368 await closeButton(page).click();
369 await expect(dialog.getByRole("button", { name: "Submit" })).toBeDisabled();
370 await expect(dialog.getByRole("button", { name: "Exceptional support experience" })).toHaveCount(0);
371 } finally {
372 await restore();
373 }
374 });
375
376 test("with feedback optional, closing never offers a rating", async ({ page, api, apiAs, ticket }) => {
377 await agentReply(await apiAs("agent"), ticket.name, "<p>Done</p>");
378 const restore = await setOpendeskSetting(api, "is_feedback_mandatory", 0);
379 try {
380 await page.goto(`/help/my-tickets/${ticket.name}`);
381 await closeButton(page).click();
382 await expect(page.getByRole("dialog").getByText("Are you sure you want to close this ticket?")).toBeVisible();
383 await expect(page.getByRole("dialog", { name: "Rate this ticket" })).toHaveCount(0);
384 } finally {
385 await restore();
386 }
387 });
388
389 test("an agent closing on the portal is not asked to rate", async ({ pageAs, api, apiAs, ticket }) => {
390 await agentReply(await apiAs("agent"), ticket.name, "<p>Done</p>");
391 const restore = await setOpendeskSetting(api, "is_feedback_mandatory", 1);
392 try {
393 const page = await pageAs("agent");
394 await page.goto(`/help/my-tickets/${ticket.name}`);
395 await closeButton(page).click();
396 await expect(page.getByRole("dialog").getByText("Are you sure you want to close this ticket?")).toBeVisible({
397 timeout: 5_000,
398 });
399 } finally {
400 await restore();
401 }
402 });
403});
404
405test.describe("details panel", () => {
406 test("contact, status, team, priority and template fields; sections collapse", async ({
407 page,
408 api,
409 ticket,
410 }) => {
411 const [team] = await api.list("OD Team", { fields: ["name"], limit: 1 });
412 if (team) await api.update("OD Ticket", ticket.name, { agent_group: team.name });
413 await page.goto(`/help/my-tickets/${ticket.name}`);
414 await expect(page.getByText(`#${ticket.name}`)).toBeVisible();
415 await expect(page.getByText("Status")).toBeVisible();
416 await expect(page.getByText("Priority")).toBeVisible();
417 if (team) await expect(page.getByText(team.name).first()).toBeVisible();
418
419 const toggle = page.getByRole("button", { name: "Timeline" });
420 await expect(toggle).toHaveAttribute("aria-expanded", "true");
421 await toggle.click();
422 await expect(toggle).toHaveAttribute("aria-expanded", "false");
423 });
424
425 test("timeline steps: received, first response due, resolution due", async ({ page, ticket }) => {
426 await page.goto(`/help/my-tickets/${ticket.name}`);
427 await expect(page.getByText("Request received")).toBeVisible();
428 await expect(page.getByText("First response")).toBeVisible();
429 await expect(page.getByText("Resolution")).toBeVisible();
430 await expect(page.getByText(/^Due /).first()).toBeVisible();
431 });
432
433 test("closed before any reply: assignment and first response skipped, closing resolves", async ({ page, api, ticket }) => {
434 await api.update("OD Ticket", ticket.name, { status: "Closed" });
435 await page.goto(`/help/my-tickets/${ticket.name}`);
436 await expect(page.getByText("Skipped")).toHaveCount(2);
437 await expect(page.getByText("Resolution")).toBeVisible();
438 await expect(page.getByText(/Closed directly/)).toHaveCount(0);
439 await expect(page.getByText(/^Due /)).toHaveCount(0);
440 });
441
442 test("an email field stacks an avatar per address, named on hover; a bad one shows the raw text", async ({ page, api, ticket }) => {
443 if (!(await api.exists("Custom Field", { name: "OD Ticket-e2e_emails" }))) {
444 await api.insert("Custom Field", {
445 dt: "OD Ticket",
446 fieldname: "e2e_emails",
447 label: "E2E Emails",
448 fieldtype: "Data",
449 insert_after: "description",
450 });
451 }
452 const { fields } = await api.get("OD Ticket Template", "Default");
453 await api.update("OD Ticket Template", "Default", {
454 fields: [{ fieldname: "e2e_emails", visible_to: "Everyone", required: 0 }],
455 });
456 try {
457 const emails = page.locator('[data-component-id="PortalEmailList-ticket-basics"]');
458 await api.update("OD Ticket", ticket.name, { e2e_emails: "ann@example.com; bob@example.com" });
459 await page.goto(`/help/my-tickets/${ticket.name}`);
460 await expect(emails).toContainText("ann@example.com, bob@example.com");
461 await emails.getByText("b", { exact: true }).hover();
462 await expect(page.locator("[data-slot=bubble]")).toHaveText("bob@example.com");
463
464 await api.update("OD Ticket", ticket.name, { e2e_emails: "ann@example.com, not an email" });
465 await page.reload();
466 await expect(page.getByText("ann@example.com, not an email")).toBeVisible();
467 await expect(emails).toHaveCount(0);
468 } finally {
469 await api.update("OD Ticket Template", "Default", { fields });
470 }
471 });
472
473 test("a paused status puts the next step on hold", async ({ page, api, ticket }) => {
474 await api.update("OD Ticket", ticket.name, { status: "Replied" });
475 await page.goto(`/help/my-tickets/${ticket.name}`);
476 await expect(page.getByText("On hold")).toBeVisible();
477 });
478
479 test("the out-of-hours banner dismisses for the day", async ({ page, ticket }) => {
480 const message = `Back on Monday ${uid()}`;
481 await page.route("**/*od_ticket.api.get_one*", async (route) => {
482 const response = await route.fetch();
483 const json = await response.json();
484 json.message.outside_hours_banner = { show: true, msg: message };
485 await route.fulfill({ response, json });
486 });
487 await page.goto(`/help/my-tickets/${ticket.name}`);
488 await expect(page.getByRole("alert").getByText(message)).toBeVisible();
489 await page.getByRole("button", { name: "Dismiss" }).click();
490 await expect(page.getByText(message)).toHaveCount(0);
491 await page.reload();
492 await expect(closeButton(page)).toBeVisible();
493 await expect(page.getByText(message)).toHaveCount(0);
494 const keys = await page.evaluate(() => Object.keys(localStorage).filter((key) => key.startsWith("kb:banner-dismissed:")));
495 expect(keys.some((key) => key.includes(ticket.name))).toBe(true);
496 });
497});
498
499test.describe("form scripts", () => {
500 test("grouped, labelled and More actions menus on the portal", async ({ page, api, ticket }) => {
501 const scripts = new FormScripts(api);
502 await scripts.create(
503 `return { actions: [
504 { group: "E2E Group", options: [{ label: "E2E Option", onClick: () => {} }] },
505 { group: "E2E Labelled", buttonLabel: "E2E Menu", label: "E2E Menu Item", onClick: () => {} },
506 ] };`,
507 { portal: true }
508 );
509 try {
510 await page.goto(`/help/my-tickets/${ticket.name}`);
511 await page.getByRole("button", { name: "E2E Menu" }).click();
512 await expect(page.getByRole("menuitem", { name: "E2E Menu Item" })).toBeVisible();
513 await page.keyboard.press("Escape");
514 await page.getByRole("button", { name: "More actions" }).click();
515 await expect(page.getByRole("menuitem", { name: "E2E Option" })).toBeVisible();
516 } finally {
517 await scripts.removeAll();
518 }
519 });
520
521 test("an agent on the portal gets the portal's scripts, not the desk's", async ({ pageAs, api, ticket }) => {
522 const scripts = new FormScripts(api);
523 await scripts.create(`return { actions: [{ label: "E2E Agent Scoped", onClick: () => {} }] };`);
524 await scripts.create(`return { actions: [{ label: "E2E Portal Scoped", onClick: () => {} }] };`, {
525 portal: true,
526 });
527 try {
528 const page = await pageAs("agent");
529 await page.goto(`/help/my-tickets/${ticket.name}`);
530 await expect(page.getByRole("button", { name: "E2E Portal Scoped" })).toBeVisible();
531 await expect(page.getByRole("button", { name: "E2E Agent Scoped" })).toBeHidden();
532 } finally {
533 await scripts.removeAll();
534 }
535 });
536});
537
538test.describe("phone", () => {
539 test.use({ viewport: { width: 390, height: 800 } });
540
541 test("the header's raise button is shortened to Create", async ({ page }) => {
542 await page.goto("/help/my-tickets");
543 await page.getByRole("button", { name: "Create", exact: true }).click();
544 await expect(page).toHaveURL(/\/help\/my-tickets\/new(\?|$)/);
545 });
546
547 test("ticket details open in a drawer and close on Escape", async ({ page, ticket }) => {
548 await page.goto(`/help/my-tickets/${ticket.name}`);
549 await expect(page.getByText("Request received")).toBeHidden();
550 await page.getByRole("button", { name: "Ticket details" }).click();
551 await expect(page.getByText("Request received")).toBeVisible();
552 await page.keyboard.press("Escape");
553 await expect(page.getByText("Request received")).toBeHidden();
554 await page.getByRole("button", { name: "Ticket details" }).click();
555 await page.getByRole("button", { name: "Close details" }).click();
556 await expect(page.getByText("Request received")).toBeHidden();
557 });
558});
559