1import type { Page, Route } from "@playwright/test";
2import type { Api } from "../../../helpers/api";
3import { ensureTemplate } from "../../../helpers/form-script";
4import { expect, test, uid, usePersona } from "../../../helpers/fixtures";
5import { personas } from "../../../helpers/personas";
6
7const PIXEL = "e2e/fixtures/pixel.png";
8const DEFAULTED_FIELDS = [
9 { fieldname: "e2e_frozen", read_only: 1 },
10 { fieldname: "e2e_answer", read_only: 0 },
11].map((field) => ({ ...field, fieldtype: "Select", options: "No\nYes", default: "No" }));
12
13usePersona("customer");
14
15test.describe("raising a ticket", () => {
16 test("with a custom field and an inline image", async ({ page, api }) => {
17 const template = await api.get("OD Ticket Template", "Default");
18 await api.update("OD Ticket Template", "Default", {
19 fields: [{ fieldname: "ticket_type", required: 0, visible_to: "Everyone" }],
20 });
21 try {
22 const subject = `E2E portal ${uid()}`;
23 await page.goto("/help/my-tickets/new");
24 await page.getByRole("combobox", { name: "Ticket Type" }).click();
25 await page.getByRole("option", { name: "Question" }).click();
26 await page.getByPlaceholder("A short description").fill(subject);
27 await page.locator(".ProseMirror").fill("Screenshot of the error");
28 await attachImage(page);
29 await page.getByRole("button", { name: "Create ticket" }).click();
30
31 await expect(page).toHaveURL(/\/help\/my-tickets\/\d+$/);
32 const [ticket] = await api.list("OD Ticket", {
33 filters: { subject },
34 fields: ["description", "ticket_type", "raised_by"],
35 });
36 expect(ticket.description).toContain("<img");
37 expect(ticket.ticket_type).toBe("Question");
38 expect(ticket.raised_by).toBe(personas.customer.email);
39 } finally {
40 await api.update("OD Ticket Template", "Default", { fields: template.fields });
41 }
42 });
43
44 test("accepts a description that is only an image", async ({ page }) => {
45 await page.goto("/help/my-tickets/new");
46 await page.getByPlaceholder("A short description").fill(`E2E image only ${uid()}`);
47 await attachImage(page);
48 await expect(page.getByRole("button", { name: "Create ticket" })).toBeEnabled();
49 });
50
51 test("keeps Create ticket disabled while an image is uploading", async ({ page }) => {
52 let release: () => void = () => {};
53 const held = new Promise<void>((resolve) => (release = resolve));
54 await page.route("**/api/method/upload_file", async (route: Route) => {
55 await held;
56 await route.continue();
57 });
58
59 await page.goto("/help/my-tickets/new");
60 await page.getByPlaceholder("A short description").fill(`E2E upload ${uid()}`);
61 await page.locator(".ProseMirror").fill("See the screenshot");
62 const submit = page.getByRole("button", { name: "Create ticket" });
63 await expect(submit).toBeEnabled();
64
65 await chooseImage(page);
66 await expect(submit).toBeDisabled();
67 release();
68 await expect(uploadedImage(page)).toBeVisible();
69 await expect(submit).toBeEnabled();
70 });
71});
72
73test.describe("the form", () => {
74 test("a read-only field is disabled and untouched fields keep their defaults", async ({
75 page,
76 api,
77 }) => {
78 await ensureDefaultedFields(api);
79 const restore = await useTemplate(api, DEFAULTED_FIELDS);
80 try {
81 await page.goto("/help/my-tickets/new");
82 await expect(page.getByRole("combobox", { name: /^E2E frozen/ })).toBeDisabled();
83 await expect(page.getByRole("combobox", { name: /^E2E answer/ })).toBeEnabled();
84 const subject = await fillBasics(page);
85 await page.getByRole("button", { name: "Create ticket" }).click();
86
87 await expect(page).toHaveURL(/\/help\/my-tickets\/\d+$/);
88 const [ticket] = await api.list("OD Ticket", {
89 filters: { subject },
90 fields: ["e2e_frozen", "e2e_answer"],
91 });
92 expect(ticket).toEqual({ e2e_frozen: "No", e2e_answer: "No" });
93 } finally {
94 await restore();
95 }
96 });
97
98 test("about links open in a new tab", async ({ page, api }) => {
99 const restore = await useTemplate(api, [], {
100 about: '<p><a href="https://example.com/register" target="_blank">Register your site</a></p>',
101 });
102 try {
103 await page.goto("/help/my-tickets/new");
104 const link = page.getByRole("link", { name: "Register your site" });
105 await expect(link).toHaveAttribute("target", "_blank");
106 await expect(link).toHaveAttribute("rel", "noopener noreferrer");
107 } finally {
108 await restore();
109 }
110 });
111
112 test("says what is still required on submit and ignores a blank subject", async ({ page }) => {
113 let creates = 0;
114 page.on("request", (request) => request.url().includes("od_ticket.api.new") && creates++);
115 await page.goto("/help/my-tickets/new");
116 const hint = page.getByRole("alert").filter({ hasText: /^Required:/ });
117 const submit = page.getByRole("button", { name: "Create ticket" });
118 await expect(hint).toBeHidden();
119 await expect(submit).toBeEnabled();
120
121 await submit.click();
122 await expect(hint).toHaveText("Required: Subject, Description");
123 await page.getByPlaceholder("A short description").fill(" ");
124 await page.locator(".ProseMirror").fill("Details");
125 await expect(hint).toHaveText("Required: Subject");
126 await submit.click();
127 expect(creates).toBe(0);
128 await page.getByPlaceholder("A short description").fill(`E2E ${uid()}`);
129 await expect(hint).toBeHidden();
130 });
131
132 test("a url_method field asks to select an option", async ({ page, api }) => {
133 await ensureTemplate(api);
134 const restore = await useTemplate(api, [
135 { fieldname: "e2e_needed", url_method: "opendesk.e2e_options" },
136 ]);
137 await page.route("**/api/method/opendesk.e2e_options*", (route) =>
138 route.fulfill({ json: { message: ["Red"] } })
139 );
140 try {
141 await page.goto("/help/my-tickets/new");
142 const picker = page.locator('[data-fieldname="e2e_needed"] button').first();
143 await expect(picker).toContainText("Select an option");
144 } finally {
145 await restore();
146 }
147 });
148
149 test("creating a ticket confirms with a toast", async ({ page }) => {
150 await page.goto("/help/my-tickets/new");
151 await fillBasics(page);
152 await page.getByRole("button", { name: "Create ticket" }).click();
153 await expect(page.getByText("Ticket created")).toBeVisible();
154 });
155
156 test("the editor does not offer Embed", async ({ page }) => {
157 await page.goto("/help/my-tickets/new");
158 await expect(page.getByRole("button", { name: "Image", exact: true })).toBeVisible();
159 await expect(page.getByRole("button", { name: "Embed" })).toHaveCount(0);
160 });
161});
162
163async function ensureDefaultedFields(api: Api) {
164 for (const field of DEFAULTED_FIELDS) {
165 if (await api.exists("Custom Field", { name: `OD Ticket-${field.fieldname}` })) continue;
166 await api.insert("Custom Field", {
167 dt: "OD Ticket",
168 label: field.fieldname.replace("e2e_", "E2E "),
169 insert_after: "description",
170 ...field,
171 });
172 }
173}
174
175async function useTemplate(api: Api, rows: Record<string, any>[], extra: Record<string, any> = {}) {
176 const before = await api.get("OD Ticket Template", "Default");
177 await api.update("OD Ticket Template", "Default", {
178 fields: rows.map(({ fieldname, url_method }) => ({
179 fieldname,
180 url_method,
181 visible_to: "Everyone",
182 required: 0,
183 })),
184 ...extra,
185 });
186 return () =>
187 api.update("OD Ticket Template", "Default", {
188 fields: before.fields,
189 about: before.about || "",
190 });
191}
192
193async function fillBasics(page: Page) {
194 const subject = `E2E form ${uid()}`;
195 await page.getByPlaceholder("A short description").fill(subject);
196 await page.locator(".ProseMirror").fill("Details");
197 return subject;
198}
199
200async function chooseImage(page: Page) {
201 const chooser = page.waitForEvent("filechooser");
202 await page.getByRole("button", { name: "Image", exact: true }).click();
203 await (await chooser).setFiles(PIXEL);
204}
205
206async function attachImage(page: Page) {
207 await chooseImage(page);
208 await expect(uploadedImage(page)).toBeVisible();
209}
210
211function uploadedImage(page: Page) {
212 return page.locator('.ProseMirror img[src*="/files/"]');
213}
214