import type { Api } from "../../../helpers/api"; import { raiseTicket } from "../../../helpers/factories"; import { expect, test, uid, usePersona } from "../../../helpers/fixtures"; import { TicketList, resetDefaultTicketView, } from "../../../helpers/list"; import { personas } from "../../../helpers/personas"; usePersona("manager"); test.beforeEach(async ({ api }) => { await resetDefaultTicketView(api, personas.manager.email); }); test.afterEach(async ({ api }) => { await resetDefaultTicketView(api, personas.manager.email); }); test("filters and quick filters narrow the list and clear back", async ({ page, api, apiAs, }) => { const id = uid(); const customer = await apiAs("customer"); const high = await raiseTicket(customer, `${id} high`); const low = await raiseTicket(customer, `${id} low`); await api.update("OD Ticket", high.name, { priority: "High" }); await api.update("OD Ticket", low.name, { priority: "Low" }); const org = `E2E Org ${id}`; await api.insert("OD Customer", { customer_name: org }); await api.update("OD Ticket", low.name, { customer: org }); const list = new TicketList(page); await list.goto(); await list.searchSubject(id); await expect(list.rows()).toHaveCount(2); await list.addFilter("Priority", "High"); await expect(list.rows()).toHaveCount(1); await expect(list.row(`${id} high`)).toBeVisible(); const popover = page.getByRole("dialog", { name: "Filter" }); await popover .locator(".group", { hasText: "Priority is High" }) .getByRole("button", { name: "Remove filter" }) .click(); await expect(list.rows()).toHaveCount(2); await page.keyboard.press("Escape"); await page.getByRole("combobox", { name: "Customer" }).click(); await page.getByRole("option", { name: org }).click(); await expect(list.rows()).toHaveCount(1); await expect(list.row(`${id} low`)).toBeVisible(); await page.getByRole("button", { name: "Filter", exact: true }).click(); await popover.getByRole("button", { name: "Clear all" }).click(); await expect(page.getByRole("textbox", { name: "Subject" })).toHaveValue(""); await expect(list.rows().nth(2)).toBeVisible(); }); test("a date quick filter keeps its value and narrows the list", async ({ page, api, apiAs, }) => { const id = uid(); const customer = await apiAs("customer"); await raiseTicket(customer, `${id} today`); const old = await raiseTicket(customer, `${id} old`); await api.update("OD Ticket", old.name, { opening_date: "2020-01-15" }); const setter = await api.insert("Property Setter", { doctype_or_field: "DocField", doc_type: "OD Ticket", field_name: "opening_date", property: "in_standard_filter", property_type: "Check", value: "1", }); try { // The meta cache is cleared before the setter commits, so a concurrent read can re-cache it stale. await expect .poll(async () => { const filters = await api.call("opendesk.api.doc.get_quick_filters", { doctype: "OD Ticket" }); const found = filters.some((filter: { name: string }) => filter.name === "opening_date"); if (!found) await api.update("Property Setter", setter.name, { value: "1" }); return found; }) .toBe(true); const list = new TicketList(page); await list.goto(); await list.searchSubject(id); await expect(list.rows()).toHaveCount(2); const dateFilter = page.getByRole("combobox", { name: "Opening Date" }); await dateFilter.click(); await page.getByRole("gridcell", { name: /\(Today\)/ }).click(); await expect(list.rows()).toHaveCount(1); await expect(list.row(`${id} today`)).toBeVisible(); await expect(dateFilter).not.toHaveValue(""); } finally { await api.delete("Property Setter", setter.name); } }); test("sort by a picked field, flip direction and clear", async ({ page, apiAs }) => { const id = uid(); const customer = await apiAs("customer"); for (const word of ["bravo", "alpha", "charlie"]) { await raiseTicket(customer, `${id} ${word}`); } const list = new TicketList(page); await list.goto(); await list.searchSubject(id); await expect(list.rows()).toHaveCount(3); const order = async () => (await list.rows().allInnerTexts()).map((text) => ["alpha", "bravo", "charlie"].find((word) => text.includes(`${id} ${word}`)) ); await page.getByRole("button", { name: "Last Modified" }).click(); const sortPopover = page.getByRole("dialog", { name: "Last Modified" }); await sortPopover.getByRole("button", { name: "Last Modified" }).click(); await page.getByRole("option", { name: "Subject", exact: true }).click(); await expect.poll(order).toEqual(["charlie", "bravo", "alpha"]); await page.getByRole("dialog", { name: "Subject" }).locator("button").first().click(); await expect.poll(order).toEqual(["alpha", "bravo", "charlie"]); await page.getByRole("button", { name: "Clear Sort" }).click(); await page.keyboard.press("Escape"); await expect(page.getByRole("button", { name: "Sort", exact: true })).toBeVisible(); }); test("an added datetime column renders relative time and can be removed", async ({ page, apiAs, }) => { const subject = `${uid()} columns`; await raiseTicket(await apiAs("customer"), subject); const list = new TicketList(page); await list.goto(); await list.searchSubject(subject); await expect(list.rows()).toHaveCount(1); const row = list.row(subject); await expect(row).not.toContainText(/ago.*ago/); await page.getByRole("button", { name: "Columns" }).click(); await page.getByRole("button", { name: "Add Column" }).click(); await page.getByPlaceholder("Select option").fill("Last Modified"); await page.getByRole("option", { name: "Last Modified", exact: true }).click(); await page.keyboard.press("Escape"); // Regression: the column was saved with type "option" and showed the raw value. await expect(row).toContainText(/ago.*ago/); await expect(row).not.toContainText(/\d{4}-\d{2}-\d{2}/); await page.getByRole("button", { name: "Columns" }).click(); await page .getByRole("dialog", { name: "Columns" }) .locator(".cursor-grab", { hasText: "Last Modified" }) .getByRole("button") .last() .click(); await expect(row).not.toContainText(/ago.*ago/); }); test("page length and load more page through the list", async ({ page, apiAs }) => { const id = uid(); const customer = await apiAs("customer"); for (let index = 0; index < 21; index++) { await raiseTicket(customer, `${id} page ${index}`); } const list = new TicketList(page); await list.goto(); await list.searchSubject(id); await expect(list.rows()).toHaveCount(20); await page.getByRole("radio", { name: "50" }).click(); await expect(list.rows()).toHaveCount(21); await page.getByRole("radio", { name: "20" }).click(); await expect(list.rows()).toHaveCount(20); await page.getByRole("button", { name: "Load More" }).click(); await expect(list.rows()).toHaveCount(21); }); test("clicking an assignee avatar filters the list by that agent", async ({ page, api, apiAs, }) => { const id = uid(); const customer = await apiAs("customer"); const { agent, agent2 } = personas; const assignments = { solo: [agent.email], other: [agent2.email], both: [agent.email, agent2.email], }; for (const [label, users] of Object.entries(assignments)) { const ticket = await raiseTicket(customer, `${id} ${label}`); await assign(api, ticket.name, users); } const list = new TicketList(page); await list.goto(); await list.searchSubject(id); await expect(list.rows()).toHaveCount(3); // Regression: a single assignee arrives as a string and the click did nothing. await list.row(`${id} solo`).locator(`.user-avatar[data-name="${agent.email}"]`).click(); await expect(list.rows()).toHaveCount(2); await expect(list.row(`${id} other`)).toHaveCount(0); // the avatar filter is auto saved to the default view, so a reload would keep it await list.clearFilters(); await list.searchSubject(id); await expect(list.rows()).toHaveCount(3); await list.row(`${id} both`).locator(`.user-avatar[data-name="${agent2.email}"]`).click(); await expect(list.rows()).toHaveCount(2); await expect(list.row(`${id} solo`)).toHaveCount(0); }); test("bulk edit, assign and reply act on every selected ticket", async ({ page, api, apiAs, }) => { const id = uid(); const customer = await apiAs("customer"); const first = await raiseTicket(customer, `${id} first`); const second = await raiseTicket(customer, `${id} second`); const names = [first.name, second.name]; const list = new TicketList(page); await list.goto(); await list.searchSubject(id); await expect(list.rows()).toHaveCount(2); await list.select(`${id} first`, `${id} second`); await list.selectionBar().getByRole("button", { name: "Assign" }).click(); const assignDialog = page.getByRole("dialog", { name: "Assign" }); await assignDialog.getByRole("button", { name: "Select agents" }).click(); const picker = page.getByRole("dialog", { name: "Select agents" }); await picker.getByRole("textbox").fill(personas.agent2.firstName); await picker.getByRole("button", { name: new RegExp(personas.agent2.firstName) }).click(); await page.keyboard.press("Escape"); await assignDialog.getByRole("button", { name: "Assign 2 tickets" }).click(); await expect(page.getByText("Assigned 2 ticket(s)")).toHaveCount(1); for (const name of names) { await expect.poll(() => assignees(api, name)).toContain(personas.agent2.email); } await list.select(`${id} first`, `${id} second`); await list.selectionMenu("Edit"); const editDialog = page.getByRole("dialog", { name: "Edit" }); await editDialog.getByRole("combobox").click(); await page.getByRole("option", { name: "Priority", exact: true }).click(); await editDialog.getByRole("combobox").nth(1).click(); await page.getByRole("option", { name: "Urgent" }).click(); await editDialog.getByRole("button", { name: "Update 2 tickets" }).click(); await expect(page.getByText("Updated 2 tickets")).toHaveCount(1); for (const name of names) { await expect.poll(async () => (await api.get("OD Ticket", name)).priority).toBe("Urgent"); } await list.select(`${id} first`, `${id} second`); await list.selectionBar().getByRole("button", { name: "Reply" }).click(); const replyDialog = page.getByRole("dialog"); await replyDialog.locator("[contenteditable=true]").fill(`${id} bulk reply`); await replyDialog.getByRole("button", { name: /Send|Reply/ }).last().click(); for (const name of names) { await expect.poll(() => replyCount(api, name, id)).toBe(1); } }); test("selected tickets export as CSV", async ({ page, apiAs }) => { const subject = `${uid()} export`; await raiseTicket(await apiAs("customer"), subject); const list = new TicketList(page); await list.goto(); await list.searchSubject(subject); await list.select(subject); await list.selectionMenu("Export"); const dialog = page.getByRole("dialog"); await dialog.getByRole("combobox").click(); await page.getByRole("option", { name: "CSV" }).click(); const download = page.waitForEvent("download"); await dialog.getByRole("button", { name: "Download" }).click(); const file = await download; expect(file.suggestedFilename()).toMatch(/\.csv$/); const content = await (await file.createReadStream()).toArray(); expect(Buffer.concat(content).toString()).toContain(subject); }); async function assign(api: Api, name: string, users: string[]) { await api.call("frappe.desk.form.assign_to.add", { doctype: "OD Ticket", name, assign_to: JSON.stringify(users), }); } async function assignees(api: Api, name: string) { const todos = await api.list("ToDo", { fields: ["allocated_to"], filters: { reference_type: "OD Ticket", reference_name: name, status: "Open" }, }); return todos.map((todo) => todo.allocated_to); } async function replyCount(api: Api, name: string, text: string) { const replies = await api.list("Communication", { fields: ["content"], filters: { reference_doctype: "OD Ticket", reference_name: name, sent_or_received: "Sent" }, }); return replies.filter((reply) => reply.content.includes(text)).length; }