1import type { Page } from "@playwright/test";
2import { raiseTicket } from "../../../helpers/factories";
3import { expect, test, uid, usePersona } from "../../../helpers/fixtures";
4import { personas } from "../../../helpers/personas";
5import { createArticle, createCategory } from "../../../helpers/portal";
6
7usePersona("customer");
8
9function ticketRows(page: Page) {
10 return page.getByRole("button", { name: /^\d{4} / });
11}
12
13test("an article without headings scrolls without errors", async ({ page, api }) => {
14 const category = await createCategory(api);
15 const article = await createArticle(api, category.name, {
16 content: Array.from({ length: 60 }, (_, i) => `<p>Line ${i}</p>`).join(""),
17 });
18 const errors: string[] = [];
19 page.on("pageerror", (error) => errors.push(error.message));
20 await page.goto(`/help/articles/${article.name}`);
21 await page.getByText("Line 30", { exact: true }).hover();
22 await page.mouse.wheel(0, 2000);
23 await expect(page.getByText("Line 59", { exact: true })).toBeInViewport();
24 expect(errors).toEqual([]);
25});
26
27test("Back after creating a ticket returns to the list", async ({ page }) => {
28 await page.goto("/help/my-tickets");
29 await page.getByRole("button", { name: "Raise a ticket" }).click();
30 await page.getByPlaceholder("A short description").fill(`E2E back ${uid()}`);
31 await page.locator(".ProseMirror").fill("Back should skip the form");
32 await page.getByRole("button", { name: "Create ticket" }).click();
33 await expect(page).toHaveURL(/\/help\/my-tickets\/\d+$/);
34
35 await page.goBack();
36 await expect(page).toHaveURL(/\/help\/my-tickets$/);
37});
38
39test("closing a ticket confirms with a uniquely named button", async ({ page, api, apiAs }) => {
40 const ticket = await raiseTicket(await apiAs("customer"));
41 await page.goto(`/help/my-tickets/${ticket.name}`);
42 await page.getByRole("button", { name: "Close", exact: true }).click();
43 const dialog = page.getByRole("dialog");
44 // the dialog's X keeps the name Close
45 await expect(dialog.getByRole("button", { name: "Close", exact: true })).toHaveCount(1);
46 await dialog.getByRole("button", { name: "Close ticket" }).click();
47 await expect.poll(async () => (await api.get("OD Ticket", ticket.name)).status).toBe("Closed");
48});
49
50test("tables and code blocks in a description are styled as on the desk", async ({ page, apiAs }) => {
51 const subject = `E2E ${uid()} styled`;
52 const ticket = await (await apiAs("customer")).call("opendesk.opendesk.doctype.od_ticket.api.new", {
53 doc: {
54 subject,
55 description: "<table><tbody><tr><th>Head</th></tr><tr><td>Cell</td></tr></tbody></table><pre><code>let a = 1</code></pre>",
56 },
57 });
58 await page.goto(`/help/my-tickets/${ticket.name}`);
59 const frame = page.frameLocator("iframe").first();
60 await expect(frame.getByText("let a = 1")).toBeVisible();
61 // desk's .prose-f: a grey code block and a rule under the header row
62 await expect(frame.locator("pre")).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
63 await expect(frame.locator("tr").first()).not.toHaveCSS("border-bottom-width", "0px");
64});
65
66test("Load More rows survive opening a ticket and going back", async ({ page, api }) => {
67 const token = uid();
68 // as admin: the portal's create endpoint is rate limited per user
69 for (let i = 0; i < 22; i++)
70 await api.insert("OD Ticket", { subject: `${token} more ${i}`, raised_by: personas.customer.email });
71 await page.goto("/help/my-tickets");
72 await expect(ticketRows(page).first()).toBeVisible();
73 const reloaded = page.waitForRequest(
74 (request) => /frappe.client.get_count/.test(request.url()) && (request.postData() || "").includes(token)
75 );
76 await page.getByRole("textbox", { name: "Subject" }).fill(token);
77 await reloaded;
78 // a Load More before the filtered page lands appends to the unfiltered one
79 await expect(ticketRows(page).filter({ hasNotText: token })).toHaveCount(0);
80 await expect(ticketRows(page)).toHaveCount(20);
81 await page.getByRole("button", { name: "Load More" }).click();
82 await expect(ticketRows(page)).toHaveCount(22);
83
84 await ticketRows(page).last().click();
85 await expect(page).toHaveURL(/\/help\/my-tickets\/\d+$/);
86 await page.goBack();
87 await expect(ticketRows(page)).toHaveCount(22);
88});
89
90test("ticket details show a tooltip only for text that is cut off", async ({ page, ticket }) => {
91 const longName = `Acme ${"Holdings ".repeat(12)}Ltd`;
92 await page.route("**/api/method/opendesk.opendesk.doctype.od_ticket.api.get_one*", async (route) => {
93 const response = await route.fetch();
94 const body = await response.json();
95 body.message.customer = longName;
96 await route.fulfill({ response, json: body });
97 });
98 await page.goto(`/help/my-tickets/${ticket.name}`);
99
100 // A tooltip repeats its text, so a second copy means it opened.
101 const priority = page.getByText("Priority", { exact: true });
102 await priority.hover();
103 await page.waitForTimeout(800); // past the tooltip's 500ms delay
104 await expect(priority).toHaveCount(1);
105
106 await page.getByText(longName).first().hover();
107 await expect.poll(() => page.getByText(longName, { exact: true }).count()).toBeGreaterThan(1);
108});
109