1import type { Page } from "@playwright/test";
2import { expect, test, uid, usePersona } from "../../../helpers/fixtures";
3
4usePersona("customer");
5
6test("conversation: customer and agent replies both show, internal comments never do", async ({
7 page,
8 apiAs,
9 pageAs,
10 ticket,
11}) => {
12 const agent = await apiAs("agent");
13 const agentReply = `Agent answer ${uid()}`;
14 const note = `Internal note ${uid()}`;
15 const customerReply = `Customer follow up ${uid()}`;
16 await agent.call("run_doc_method", {
17 dt: "OD Ticket",
18 dn: ticket.name,
19 method: "reply_via_agent",
20 args: { message: `<p>${agentReply}</p>` },
21 });
22 await agent.call("run_doc_method", {
23 dt: "OD Ticket",
24 dn: ticket.name,
25 method: "new_comment",
26 args: { content: `<p>${note}</p>` },
27 });
28
29 await page.goto(`/help/my-tickets/${ticket.name}`);
30 await expectInConversation(page, agentReply);
31 // closed on arrival; its bar opens it
32 await page.getByText("Type a message").click();
33 await expect(page.locator(".portal-reply-composer .ProseMirror")).toBeFocused();
34 await page.locator(".ProseMirror").fill(customerReply);
35 await page.getByRole("button", { name: "Send" }).click();
36 await expectInConversation(page, customerReply);
37 for (const frame of page.frames()) expect(await frame.content()).not.toContain(note);
38
39 const agentPage = await pageAs("agent");
40 await agentPage.goto(`/opendesk/tickets/${ticket.name}`);
41 await expectInConversation(agentPage, customerReply);
42});
43
44test("closing after an agent reply asks for a rating when feedback is mandatory", async ({
45 page,
46 api,
47 apiAs,
48 ticket,
49}) => {
50 await (await apiAs("agent")).call("run_doc_method", {
51 dt: "OD Ticket",
52 dn: ticket.name,
53 method: "reply_via_agent",
54 args: { message: "<p>Fixed on our side</p>" },
55 });
56 const mandatory = (await api.get("OD Settings", "OD Settings")).is_feedback_mandatory;
57 await api.update("OD Settings", "OD Settings", { is_feedback_mandatory: 1 });
58 try {
59 await page.goto(`/help/my-tickets/${ticket.name}`);
60 await page.getByRole("button", { name: "Close" }).click();
61 const dialog = page.getByRole("dialog", { name: "Rate this ticket" });
62 await dialog.getByRole("radio").nth(4).click();
63 await dialog.getByRole("button", { name: "Exceptional support experience" }).click();
64 await dialog.getByPlaceholder("Tell us more").fill("Quick turnaround");
65 await dialog.getByRole("button", { name: "Submit" }).click();
66
67 await expect
68 .poll(async () => {
69 const doc = await api.get("OD Ticket", ticket.name);
70 return [doc.status, doc.feedback, doc.feedback_extra];
71 })
72 .toEqual(["Closed", "Exceptional support experience", "Quick turnaround"]);
73 await expect(page.getByRole("button", { name: "Close" })).toHaveCount(0);
74 } finally {
75 await api.update("OD Settings", "OD Settings", { is_feedback_mandatory: mandatory });
76 }
77});
78
79test("a ticket rated by email closes without asking for a rating again", async ({ page, api, apiAs, ticket }) => {
80 await (await apiAs("agent")).call("run_doc_method", {
81 dt: "OD Ticket",
82 dn: ticket.name,
83 method: "reply_via_agent",
84 args: { message: "<p>Fixed on our side</p>" },
85 });
86 // Email feedback sets only the rating; the customer then wrote back and reopened it.
87 await api.update("OD Ticket", ticket.name, { feedback_rating: 0.8, status: "Open" });
88 const mandatory = (await api.get("OD Settings", "OD Settings")).is_feedback_mandatory;
89 await api.update("OD Settings", "OD Settings", { is_feedback_mandatory: 1 });
90 try {
91 await page.goto(`/help/my-tickets/${ticket.name}`);
92 await page.getByRole("button", { name: "Close" }).click();
93 await expect(page.getByRole("dialog", { name: "Rate this ticket" })).toHaveCount(0);
94 await page.getByRole("dialog", { name: "Close ticket" }).getByRole("button", { name: "Close ticket" }).click();
95 await expect.poll(async () => (await api.get("OD Ticket", ticket.name)).status).toBe("Closed");
96 } finally {
97 await api.update("OD Settings", "OD Settings", { is_feedback_mandatory: mandatory });
98 }
99});
100
101/** Email bodies render inside iframes, so wait until any frame shows the text. */
102async function expectInConversation(page: Page, text: string) {
103 await expect
104 .poll(async () => {
105 for (const frame of page.frames()) {
106 if (await frame.getByText(text).count()) return true;
107 }
108 return false;
109 })
110 .toBe(true);
111}
112