Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
25 changes: 19 additions & 6 deletions apps/desktop/src/features/score/ScoreView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Expand All @@ -95,11 +95,24 @@ describe("ScoreView", () => {
render(<ScoreView song={song} projectId={null} onSongUpdate={vi.fn()} />);

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();
});

Expand Down Expand Up @@ -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 () => {
Expand Down
32 changes: 25 additions & 7 deletions apps/desktop/src/features/score/ScoreView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,14 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
<p className="mt-1 max-w-2xl text-sm text-slate-400">{t("scoreViewSubtitle")}</p>
</div>
<Button
onClick={projectId ? () => void handleAttach(projectId) : undefined}
disabled={!projectId || isAttaching}
onClick={(e) => {
if (!projectId || isAttaching) {
e.preventDefault();
} else {
void handleAttach(projectId);
}
}}
Comment on lines +137 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Info: Click guards match the aria-disabled conditions

Each new onClick guard uses the same condition as its aria-disabled attribute (e.g. ScoreViewer.tsx:297, ScoreViewer.tsx:316), so clicking or keyboard-activating a visually-disabled button only calls preventDefault() and never runs the action. No action leaks through.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

aria-disabled={!projectId || isAttaching}
Comment on lines +137 to +144

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Disabled attach button still shows no tooltip

The attach button was changed from disabled to aria-disabled with no title added, unlike every other aria-disabled control in the repo (App.tsx:574, PracticeProgress.tsx:58) and the palette.md rule that requires one. Hovering the disabled Add score button renders no tooltip.

Prompt for agents
The Add score button now uses aria-disabled but has no title attribute, so no hover tooltip appears. The repo convention (App.tsx, PracticeProgress.tsx) and .jules/palette.md require pairing aria-disabled with a title tooltip. Add a title attribute to this Button conveying the disabled reason (e.g. the scoreRequiresProject / scoreAttaching copy), using an existing i18n key so both ko and en locales are covered.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Info: Enabled state renders aria-disabled="false" instead of omitting it

aria-disabled={!projectId || isAttaching} renders a literal aria-disabled="false" when enabled, whereas the repo pattern (PracticeProgress.tsx:55, App.tsx:573) uses ? "true" : undefined to omit it. This is valid ARIA and tests still pass, but is inconsistent with the codebase.

Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

variant="secondary"
className="min-h-11 border border-cyan-300/20 bg-cyan-300/10 font-semibold text-cyan-50 hover:bg-cyan-300/20"
>
Expand Down Expand Up @@ -183,20 +189,32 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
>
<button
type="button"
onClick={projectId ? () => void openAttachment(projectId, attachment) : undefined}
disabled={!projectId}
onClick={(e) => {
if (!projectId) {
e.preventDefault();
} else {
void openAttachment(projectId, attachment);
}
}}
aria-disabled={!projectId}
Comment on lines +192 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Disabled open-score button shows no tooltip

The open-score list button uses aria-disabled with only an aria-label and no title, breaking the repo convention (App.tsx:574) and palette.md. Hovering it while disabled renders no tooltip.

Prompt for agents
The open-score button now uses aria-disabled but only has aria-label, no title, so no hover tooltip appears when disabled. Per .jules/palette.md and the App.tsx/PracticeProgress.tsx convention, add a title attribute conveying the disabled reason using an existing i18n key covering ko and en.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

aria-current={selected?.id === attachment.id ? "true" : undefined}
aria-label={`${t("scoreOpen")}: ${attachment.fileName}`}
className="flex min-h-10 min-w-0 flex-1 items-center gap-2 text-left text-sm font-semibold text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 disabled:cursor-not-allowed disabled:opacity-60"
className="flex min-h-10 min-w-0 flex-1 items-center gap-2 text-left text-sm font-semibold text-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-cyan-300 aria-disabled:cursor-not-allowed aria-disabled:opacity-60"
>
<FileMusic className="size-4 shrink-0 text-cyan-300" aria-hidden="true" />
<span className="truncate">{attachment.fileName}</span>
</button>
<Button
variant="outline"
size="icon"
onClick={projectId ? () => void handleRemove(projectId, attachment) : undefined}
disabled={!projectId}
onClick={(e) => {
if (!projectId) {
e.preventDefault();
} else {
void handleRemove(projectId, attachment);
}
}}
aria-disabled={!projectId}
Comment on lines +210 to +217

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Disabled remove-score button shows no tooltip

The remove button uses aria-disabled with no title, unlike the repo's other disabled controls and palette.md. Hovering it while disabled renders no tooltip.

Prompt for agents
The remove button now uses aria-disabled but has no title attribute, so no hover tooltip appears when disabled. Per .jules/palette.md and the App.tsx/PracticeProgress.tsx convention, add a title attribute conveying the disabled reason using an existing i18n key covering ko and en.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

aria-label={`${t("scoreRemove")}: ${attachment.fileName}`}
className="size-10 border-rose-300/25 text-rose-200 hover:bg-rose-400/10"
>
Expand Down
20 changes: 16 additions & 4 deletions apps/desktop/src/features/score/ScoreViewer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ describe("ScoreViewer", () => {
expect(page.render).toHaveBeenCalled();
});
expect(page.getViewport).toHaveBeenCalledWith({ scale: 1 });
expect(screen.getByRole("button", { name: "Previous page" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Next page" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Previous page" })).toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("button", { name: "Next page" })).not.toHaveAttribute("aria-disabled", "true");
});

it("shows the file name when provided", async () => {
Expand Down Expand Up @@ -174,14 +174,26 @@ describe("ScoreViewer", () => {
expect(await screen.findByText("Page 1 of 3")).toBeInTheDocument();
const previousButton = screen.getByRole("button", { name: "Previous page" });
const nextButton = screen.getByRole("button", { name: "Next page" });
expect(previousButton).toBeDisabled();
expect(previousButton).toHaveAttribute("aria-disabled", "true");

// Simulate clicking previous when disabled
const preventDefaultSpy = vi.spyOn(Event.prototype, 'preventDefault');
fireEvent.click(previousButton);
expect(preventDefaultSpy).toHaveBeenCalled();
preventDefaultSpy.mockRestore();

fireEvent.click(nextButton);
expect(screen.getByText("Page 2 of 3")).toBeInTheDocument();

fireEvent.click(nextButton);
expect(screen.getByText("Page 3 of 3")).toBeInTheDocument();
expect(nextButton).toBeDisabled();
expect(nextButton).toHaveAttribute("aria-disabled", "true");

// Simulate clicking next when disabled
const preventDefaultSpyNext = vi.spyOn(Event.prototype, 'preventDefault');
fireEvent.click(nextButton);
expect(preventDefaultSpyNext).toHaveBeenCalled();
preventDefaultSpyNext.mockRestore();

await waitFor(() => {
expect(doc.getPage).toHaveBeenCalledWith(3);
Expand Down
20 changes: 16 additions & 4 deletions apps/desktop/src/features/score/ScoreViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,14 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerPrevPage")}
disabled={pageNumber <= 1}
onClick={goToPreviousPage}
aria-disabled={pageNumber <= 1}
onClick={(e) => {
if (pageNumber <= 1) {
e.preventDefault();
} else {
goToPreviousPage();
}
}}
Comment on lines +295 to +302

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Disabled previous-page button shows no tooltip

The previous-page button uses aria-disabled with no title, unlike the repo's other disabled controls and palette.md. Hovering it on the first page renders no tooltip.

Prompt for agents
The previous-page button now uses aria-disabled but has no title attribute, so no hover tooltip appears when disabled. Per .jules/palette.md and the App.tsx/PracticeProgress.tsx convention, add a title attribute (e.g. the scoreViewerPrevPage label) using an existing i18n key covering ko and en.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

>
<ChevronLeft className="size-6" aria-hidden="true" />
</Button>
Expand All @@ -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();
}
}}
Comment on lines +314 to +321

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟑 Disabled next-page button shows no tooltip

The next-page button uses aria-disabled with no title, unlike the repo's other disabled controls and palette.md. Hovering it on the last page renders no tooltip.

Prompt for agents
The next-page button now uses aria-disabled but has no title attribute, so no hover tooltip appears when disabled. Per .jules/palette.md and the App.tsx/PracticeProgress.tsx convention, add a title attribute (e.g. the scoreViewerNextPage label) using an existing i18n key covering ko and en.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

>
<ChevronRight className="size-6" aria-hidden="true" />
</Button>
Expand Down
Loading
Loading