diff --git a/.jules/palette.md b/.jules/palette.md index c05638899..04f7363d7 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -1,3 +1,6 @@ ## 2024-05-19 - Replace HTML disabled with aria-disabled="true" for Accessible Tooltips **Learning:** Native HTML `disabled` attributes completely hide elements from screen readers and block all pointer/hover events, preventing tooltips from functioning for disabled elements. **Action:** Replace `disabled` with `aria-disabled="true"`, enforce block click handlers via `e.preventDefault()`, and add a title tooltip directly to the element to maintain full tooltip accessibility and keyboard focus support for visually impaired and mouse users. +## 2025-02-12 - Playwright Frontend Verification for Score Components +**Learning:** In headless Playwright scripts interacting with the BandScope desktop frontend on `localhost:5173`, interacting with dynamic or conditionally rendered components (like the "Open Project" button, demo track rows, or nested tabs like "Score") requires careful handling of element counts and using `force=True` on `.first` locator clicks due to overlapping layers or strict mode violations in complex UIs. The "Score" tab specifically can be consistently found using `page.locator("nav a").filter(has_text="Score")`. +**Action:** When writing Playwright verification scripts for this UI, always handle potential `count() == 0` edge cases for conditional elements (e.g., demo lists vs home states), use `.first.click(force=True)` to bypass overlapping pointer-events interception, and ensure enough `page.wait_for_timeout` delays for CSS transitions/data loading before capturing screenshots of disabled or state-dependent buttons. diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e7685d6f0..647047e31 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -20,7 +20,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.24.0", - "pdfjs-dist": "6.1.200", + "pdfjs-dist": "^6.2.108", "react": "^19.2.4", "react-dom": "^19.2.7", "sonner": "^2.0.7", diff --git a/apps/desktop/src/features/score/ScoreView.test.tsx b/apps/desktop/src/features/score/ScoreView.test.tsx index de4ccb95c..ab7b87148 100644 --- a/apps/desktop/src/features/score/ScoreView.test.tsx +++ b/apps/desktop/src/features/score/ScoreView.test.tsx @@ -85,7 +85,7 @@ describe("ScoreView", () => { expect(screen.getByRole("heading", { name: /Score ยท Late Night Set/i })).toBeInTheDocument(); expect(screen.getByText("No scores attached to this song yet.")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Add score" })).toBeEnabled(); + expect(screen.getByRole("button", { name: "Add score" })).not.toHaveAttribute("aria-disabled", "true"); expect(screen.getByTestId("score-viewer")).toHaveTextContent("no-data"); expect(mockInvoke).not.toHaveBeenCalled(); }); @@ -95,11 +95,24 @@ describe("ScoreView", () => { render(); expect(screen.getByText("Scores attach to the active analysis project.")).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Add score" })).toBeDisabled(); - expect(screen.getByRole("button", { name: "Open score: opener.pdf" })).toBeDisabled(); - expect(screen.getByRole("button", { name: "Remove: opener.pdf" })).toBeDisabled(); - fireEvent.click(screen.getByRole("button", { name: "Open score: opener.pdf" })); + const addBtn = screen.getByRole("button", { name: "Add score" }); + const openBtn = screen.getByRole("button", { name: "Open score: opener.pdf" }); + const removeBtn = screen.getByRole("button", { name: "Remove: opener.pdf" }); + + expect(addBtn).toHaveAttribute("aria-disabled", "true"); + expect(openBtn).toHaveAttribute("aria-disabled", "true"); + expect(removeBtn).toHaveAttribute("aria-disabled", "true"); + + const preventDefaultSpy = vi.spyOn(Event.prototype, 'preventDefault'); + + fireEvent.click(addBtn); + fireEvent.click(openBtn); + fireEvent.click(removeBtn); + + expect(preventDefaultSpy).toHaveBeenCalledTimes(3); + preventDefaultSpy.mockRestore(); + expect(mockInvoke).not.toHaveBeenCalled(); }); @@ -143,7 +156,7 @@ describe("ScoreView", () => { "Choose a PDF file to attach as a score." ); expect(onSongUpdate).not.toHaveBeenCalled(); - expect(screen.getByRole("button", { name: "Add score" })).toBeEnabled(); + expect(screen.getByRole("button", { name: "Add score" })).not.toHaveAttribute("aria-disabled", "true"); }); it("falls back to the generic attach failure for malformed bridge responses", async () => { diff --git a/apps/desktop/src/features/score/ScoreView.tsx b/apps/desktop/src/features/score/ScoreView.tsx index 72732450f..bacc9bfcc 100644 --- a/apps/desktop/src/features/score/ScoreView.tsx +++ b/apps/desktop/src/features/score/ScoreView.tsx @@ -134,8 +134,14 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {

{t("scoreViewSubtitle")}

@@ -305,8 +311,14 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps size="icon-lg" className="size-14" aria-label={t("scoreViewerNextPage")} - disabled={pageNumber >= pageCount} - onClick={goToNextPage} + aria-disabled={pageNumber >= pageCount} + onClick={(e) => { + if (pageNumber >= pageCount) { + e.preventDefault(); + } else { + goToNextPage(); + } + }} >