import { expect, type Page } from "@playwright/test"; import type { Api } from "./api"; /** The agent ticket list at /helpdesk/tickets. */ export class TicketList { constructor(private page: Page) {} async goto() { await this.page.goto("/helpdesk/tickets"); await expect(this.page.getByRole("button", { name: "Filter", exact: true })).toBeVisible(); } rows() { return this.page.locator("#list-rows a"); } row(subject: string) { return this.rows().filter({ hasText: subject }); } /** Narrow the list to this spec's tickets through the Subject quick filter. */ async searchSubject(text: string) { const reloaded = this.page.waitForResponse(/get_list_data/); await this.page.getByRole("textbox", { name: "Subject" }).fill(text); await reloaded; } async select(...subjects: string[]) { for (const subject of subjects) { await this.row(subject).getByRole("checkbox").check(); } } /** The floating bar that appears once rows are selected. */ selectionBar() { return this.page .getByText(/\d+ rows? selected/) .locator("xpath=ancestor::div[contains(@class, 'shadow-2xl')]"); } async selectionMenu(item: string) { await this.selectionBar().locator("[aria-haspopup=menu]").click(); await this.page.getByRole("menuitem", { name: item }).click(); } /** Save the current filters as a new view and return its HD View name. */ async createView(api: Api, label: string) { await this.page.getByRole("button", { name: "List", exact: true }).click(); await this.page.getByRole("menuitem", { name: "Create View" }).click(); const dialog = this.page.getByRole("dialog", { name: "Create View" }); await dialog.getByRole("textbox").first().fill(label); await dialog.getByRole("button", { name: "Create" }).click(); await expect(this.page).toHaveURL(/\?view=/); await expect.poll(() => api.exists("HD View", { label })).toBeTruthy(); return (await api.exists("HD View", { label }))!; } /** Run an action from a view's kebab menu in the breadcrumb dropdown. */ async viewAction(label: string, action: string) { await this.page.getByRole("banner").locator("button[aria-haspopup=menu]").first().click(); const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // standard views carry a "Standard" badge in their name const name = new RegExp(`^${escaped}( Standard)?$`); const item = this.page.getByRole("menuitem", { name }); await item.hover(); await item.getByRole("button").click(); await this.page.getByRole("menuitem", { name: action }).click(); } /** Clear every filter from the Filter popover, the way an agent resets the list. */ async clearFilters() { await this.page.getByRole("button", { name: "Filter", exact: true }).click(); await this.page.getByRole("dialog", { name: "Filter" }).getByRole("button", { name: "Clear all" }).click(); await this.page.keyboard.press("Escape"); } async addFilter(field: string, value: string) { await this.chooseFilterField(field); await this.page.getByRole("option", { name: value, exact: true }).click(); } filterPopover() { return this.page.getByRole("dialog", { name: "Filter" }); } /** Open the Filter popover and pick a field, leaving its value editor open. */ async chooseFilterField(field: string) { await this.page.getByRole("button", { name: "Filter", exact: true }).click(); const popover = this.filterPopover(); await expect(popover).toBeVisible(); const addFilter = popover.getByRole("button", { name: "Add filter" }); if (await addFilter.isVisible()) await addFilter.click(); await popover.getByRole("textbox").fill(field); await this.page.getByRole("option", { name: field, exact: true }).click(); } async chooseFilterOperator(operator: string) { await this.filterPopover().locator("button[aria-haspopup]").click(); await this.page.getByRole("menuitem", { name: operator, exact: true }).click(); } } /** Drop the auto saved default view and any view a spec left behind. */ export async function resetDefaultTicketView(api: Api, user: string) { const views = await api.list("HD View", { filters: { user, dt: "HD Ticket", is_default: 1 }, }); views.push(...(await api.list("HD View", { filters: { label: ["like", "E2E view %"] } }))); // the list may still be auto saving the default view, which locks it for a moment for (const view of views) { await expect(async () => { if (await api.exists("HD View", { name: view.name })) await api.delete("HD View", view.name); }).toPass(); } }