From 919748a274894fb8879da9f909853b40f6f4404b Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Thu, 20 Aug 2026 17:20:10 -0300 Subject: [PATCH] fix(work-orders): expose type filter selection state to AT Menu roles and aria-checked match the multi-select. Trigger uses AAP Tag icon. --- e2e/work-orders/type-filter.spec.ts | 8 +- .../list/filter-bar/wo-type-filter.tsx | 14 ++- src/components/ui/checkbox-menu-item.tsx | 1 + .../workorders/wo-type-filter.test.tsx | 102 +++++++++++++----- ...ork-orders-advanced-search-wiring.test.tsx | 4 +- 5 files changed, 93 insertions(+), 36 deletions(-) diff --git a/e2e/work-orders/type-filter.spec.ts b/e2e/work-orders/type-filter.spec.ts index 06a6e3d3..6e829c88 100644 --- a/e2e/work-orders/type-filter.spec.ts +++ b/e2e/work-orders/type-filter.spec.ts @@ -78,16 +78,16 @@ test.describe("Work Orders type filter (SH-243)", () => { await expect(trigger).toBeVisible(); await trigger.click(); const typeMenu = page.getByTestId("wo-type-filter-menu"); - await expect(typeMenu.getByRole("button", { name: "PM" })).toBeVisible(); - await typeMenu.getByRole("button", { name: "PM" }).click(); - await typeMenu.getByRole("button", { name: "Emergency" }).click(); + await expect(typeMenu.getByRole("menuitemcheckbox", { name: "PM" })).toBeVisible(); + await typeMenu.getByRole("menuitemcheckbox", { name: "PM" }).click(); + await typeMenu.getByRole("menuitemcheckbox", { name: "Emergency" }).click(); await expect(trigger).toContainText("2 types selected"); await expect .poll(() => boardUrls.some((url) => url.includes("types=2") && url.includes("types=3"))) .toBe(true); - await typeMenu.getByRole("button", { name: "All types" }).click(); + await typeMenu.getByRole("menuitemcheckbox", { name: "All types" }).click(); await expect(trigger).toContainText("All types"); }); }); diff --git a/src/app/(protected)/workorders/_components/list/filter-bar/wo-type-filter.tsx b/src/app/(protected)/workorders/_components/list/filter-bar/wo-type-filter.tsx index 0affe8a4..caf77692 100644 --- a/src/app/(protected)/workorders/_components/list/filter-bar/wo-type-filter.tsx +++ b/src/app/(protected)/workorders/_components/list/filter-bar/wo-type-filter.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { Popover } from "@mui/material"; -import { ChevronDown, Layers } from "lucide-react"; +import { ChevronDown, Tag } from "lucide-react"; import { CheckboxMenuItem } from "@/components/ui/checkbox-menu-item"; import { MenuAnchorButton } from "@/components/ui/menu-anchor-button"; import { WO_TYPES, type WOType } from "@/domain/work-orders/types/work-order-wizard"; @@ -43,7 +43,7 @@ export function WoTypeFilter({ selected, onChange, disabled = false }: WoTypeFil onAnchor={setAnchor} open={open} disabled={disabled} - aria-haspopup="listbox" + aria-label={`Work order types, ${label}`} data-testid="wo-type-filter-trigger" className={cn( "flex h-[38px] w-auto shrink-0 items-center gap-2 rounded-md border px-3 text-[13px] font-semibold whitespace-nowrap transition-colors", @@ -52,7 +52,7 @@ export function WoTypeFilter({ selected, onChange, disabled = false }: WoTypeFil : "border-[#bfe2f5] bg-(--color-primary-tint) text-primary", )} > - + {label} @@ -65,21 +65,25 @@ export function WoTypeFilter({ selected, onChange, disabled = false }: WoTypeFil transformOrigin={{ vertical: "top", horizontal: "left" }} slotProps={{ paper: { - className: "mt-1 w-[220px] rounded-lg border border-border p-1.5 shadow-md", + className: "mt-1 w-[180px] rounded-lg border border-border p-1.5 shadow-md", }, }} > -
+
onChange(new Set())} /> {WO_TYPES.map((type) => ( toggle(type)} /> ))} diff --git a/src/components/ui/checkbox-menu-item.tsx b/src/components/ui/checkbox-menu-item.tsx index 473bde76..4ef1e5e5 100644 --- a/src/components/ui/checkbox-menu-item.tsx +++ b/src/components/ui/checkbox-menu-item.tsx @@ -32,6 +32,7 @@ export function CheckboxMenuItem({ className, )} {...props} + aria-checked={checked} > {left} diff --git a/src/test/app/(protected)/workorders/wo-type-filter.test.tsx b/src/test/app/(protected)/workorders/wo-type-filter.test.tsx index 3314c986..e9cde5af 100644 --- a/src/test/app/(protected)/workorders/wo-type-filter.test.tsx +++ b/src/test/app/(protected)/workorders/wo-type-filter.test.tsx @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; import { WoTypeFilter } from "@/app/(protected)/workorders/_components/list/filter-bar/wo-type-filter"; import type { WOType } from "@/domain/work-orders/types/work-order-wizard"; import { renderWithProviders } from "@/test/test-utils"; -import { screen } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; function TypeFilterHarness({ initial = new Set(), @@ -17,37 +17,78 @@ function TypeFilterHarness({ return ; } -async function openTypeMenu(user: ReturnType, triggerName: string) { +async function openTypeMenu( + user: ReturnType, + triggerName: string | RegExp, +) { await user.click(screen.getByRole("button", { name: triggerName })); + return screen.getByRole("menu", { name: "Work order types" }); } -describe("WoTypeFilter (SH-243)", () => { +describe("WoTypeFilter", () => { + it("exposes a menu trigger, not a listbox", () => { + renderWithProviders(, { withAuth: false }); + + const trigger = screen.getByRole("button", { name: /all types/i }); + expect(trigger).toHaveAttribute("aria-haspopup", "menu"); + expect(trigger).not.toHaveAttribute("aria-haspopup", "listbox"); + }); + it("shows All types on the trigger and lists the four types in the menu", async () => { const user = userEvent.setup(); renderWithProviders(, { withAuth: false }); - expect(screen.getByRole("button", { name: "All types" })).toBeEnabled(); - await openTypeMenu(user, "All types"); + expect(screen.getByRole("button", { name: /all types/i })).toBeEnabled(); + const menu = await openTypeMenu(user, /all types/i); - expect(screen.getByRole("button", { name: "PM" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Reactive" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Emergency" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Overdue" })).toBeInTheDocument(); + expect(within(menu).getByRole("menuitemcheckbox", { name: "PM" })).toBeInTheDocument(); + expect(within(menu).getByRole("menuitemcheckbox", { name: "Reactive" })).toBeInTheDocument(); + expect(within(menu).getByRole("menuitemcheckbox", { name: "Emergency" })).toBeInTheDocument(); + expect(within(menu).getByRole("menuitemcheckbox", { name: "Overdue" })).toBeInTheDocument(); + }); + + it("exposes selected and unselected type state on the menu", async () => { + const user = userEvent.setup(); + renderWithProviders(, { withAuth: false }); + + const menu = await openTypeMenu(user, /all types/i); + expect(within(menu).getByRole("menuitemcheckbox", { name: "All types" })).toHaveAttribute( + "aria-checked", + "true", + ); + expect(within(menu).getByRole("menuitemcheckbox", { name: "PM" })).toHaveAttribute( + "aria-checked", + "false", + ); + + await user.click(within(menu).getByRole("menuitemcheckbox", { name: "PM" })); + expect(within(menu).getByRole("menuitemcheckbox", { name: "All types" })).toHaveAttribute( + "aria-checked", + "false", + ); + expect(within(menu).getByRole("menuitemcheckbox", { name: "PM" })).toHaveAttribute( + "aria-checked", + "true", + ); }); it("labels a single type and multiple types on the trigger", async () => { const user = userEvent.setup(); renderWithProviders(, { withAuth: false }); - await openTypeMenu(user, "All types"); - await user.click(screen.getByRole("button", { name: "PM" })); + const menu = await openTypeMenu(user, /all types/i); + await user.click(within(menu).getByRole("menuitemcheckbox", { name: "PM" })); expect( - screen.getByRole("button", { name: "PM", hidden: true, expanded: true }), + screen.getByRole("button", { name: /work order types, pm/i, hidden: true, expanded: true }), ).toBeInTheDocument(); - await user.click(screen.getByRole("button", { name: "Emergency" })); + await user.click(within(menu).getByRole("menuitemcheckbox", { name: "Emergency" })); expect( - screen.getByRole("button", { name: "2 types selected", hidden: true, expanded: true }), + screen.getByRole("button", { + name: /2 types selected/i, + hidden: true, + expanded: true, + }), ).toBeInTheDocument(); }); @@ -55,11 +96,15 @@ describe("WoTypeFilter (SH-243)", () => { const user = userEvent.setup(); renderWithProviders(, { withAuth: false }); - expect(screen.getByRole("button", { name: "PM" })).toBeInTheDocument(); - await openTypeMenu(user, "PM"); - await user.click(screen.getByRole("button", { name: "All types" })); + expect(screen.getByRole("button", { name: /work order types, pm/i })).toBeInTheDocument(); + const menu = await openTypeMenu(user, /work order types, pm/i); + await user.click(within(menu).getByRole("menuitemcheckbox", { name: "All types" })); expect( - screen.getByRole("button", { name: "All types", hidden: true, expanded: true }), + screen.getByRole("button", { + name: /work order types, all types/i, + hidden: true, + expanded: true, + }), ).toBeInTheDocument(); }); @@ -67,10 +112,14 @@ describe("WoTypeFilter (SH-243)", () => { const user = userEvent.setup(); renderWithProviders(, { withAuth: false }); - await openTypeMenu(user, "PM"); - await user.click(screen.getByRole("button", { name: "PM" })); + const menu = await openTypeMenu(user, /work order types, pm/i); + await user.click(within(menu).getByRole("menuitemcheckbox", { name: "PM" })); expect( - screen.getByRole("button", { name: "All types", hidden: true, expanded: true }), + screen.getByRole("button", { + name: /work order types, all types/i, + hidden: true, + expanded: true, + }), ).toBeInTheDocument(); }); @@ -78,18 +127,21 @@ describe("WoTypeFilter (SH-243)", () => { const user = userEvent.setup(); renderWithProviders(, { withAuth: false }); - const trigger = screen.getByRole("button", { name: "All types" }); + const trigger = screen.getByRole("button", { name: /all types/i }); expect(trigger).toBeDisabled(); await user.click(trigger); - expect(screen.queryByRole("button", { name: "PM" })).not.toBeInTheDocument(); + expect(screen.queryByRole("menu", { name: "Work order types" })).not.toBeInTheDocument(); + expect(screen.queryByRole("menuitemcheckbox", { name: "PM" })).not.toBeInTheDocument(); }); it("lists All types first, then the four WO types", async () => { const user = userEvent.setup(); renderWithProviders(, { withAuth: false }); - await openTypeMenu(user, "All types"); - const labels = screen.getAllByRole("button").map((button) => button.textContent?.trim()); + const menu = await openTypeMenu(user, /all types/i); + const labels = within(menu) + .getAllByRole("menuitemcheckbox") + .map((item) => item.textContent?.trim()); expect(labels).toEqual(["All types", "PM", "Reactive", "Emergency", "Overdue"]); }); }); diff --git a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx index 1a18be5c..8ef733a2 100644 --- a/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx +++ b/src/test/app/(protected)/workorders/work-orders-advanced-search-wiring.test.tsx @@ -256,7 +256,7 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => { expect(screen.getByRole("button", { name: "All dispatchers" })).toBeDisabled(); expect(screen.getByRole("button", { name: "Week 7/20–7/24" })).toBeDisabled(); expect(screen.getByRole("tab", { name: "Mon · 07/20" })).toBeDisabled(); - expect(screen.getByRole("button", { name: "All types" })).toBeDisabled(); + expect(screen.getByRole("button", { name: /work order types, all types/i })).toBeDisabled(); expect(screen.getByText("42 work orders")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /clear all/i })); @@ -266,6 +266,6 @@ describe("WorkOrders Advanced Search page wiring (SH-121)", () => { expect(screen.queryByText("42 work orders")).not.toBeInTheDocument(); expect(screen.getByRole("button", { name: "All dispatchers" })).toBeEnabled(); - expect(screen.getByRole("button", { name: "All types" })).toBeEnabled(); + expect(screen.getByRole("button", { name: /work order types, all types/i })).toBeEnabled(); }); });