diff --git a/apps/desktop/src/features/score/ScoreView.tooltip.test.tsx b/apps/desktop/src/features/score/ScoreView.tooltip.test.tsx
new file mode 100644
index 000000000..6eea21eb8
--- /dev/null
+++ b/apps/desktop/src/features/score/ScoreView.tooltip.test.tsx
@@ -0,0 +1,37 @@
+import { render, screen } from "@testing-library/react";
+import { expect, test, vi } from "vitest";
+import { ScoreView } from "./ScoreView";
+import type { RehearsalSong } from "@bandscope/shared-types";
+
+vi.mock("../../i18n", () => ({
+ detectPreferredLocale: () => "en",
+ createTranslator: () => (key: string) => key,
+}));
+vi.mock("./scoreStorage", () => ({
+ readScorePdf: vi.fn(),
+ attachScorePdf: vi.fn(),
+ removeScorePdf: vi.fn(),
+}));
+vi.mock("./ScoreViewer", () => ({
+ ScoreViewer: () =>
Viewer
,
+}));
+
+test("ScoreView places title on wrapper when remove button is disabled", () => {
+ const song: RehearsalSong = {
+ id: "song-1",
+ title: "Test Song",
+ scoreAttachments: [{ id: "att-1", fileName: "test.pdf" }],
+ } as unknown as RehearsalSong;
+
+ // No projectId -> buttons should be disabled
+ render();
+
+ const removeButton = screen.getByRole("button", { name: "scoreRemove: test.pdf" });
+ expect(removeButton).toBeDisabled();
+
+ // title should be on the wrapper, NOT the button
+ expect(removeButton).not.toHaveAttribute("title");
+
+ const wrapper = removeButton.parentElement;
+ expect(wrapper).toHaveAttribute("title", "scoreRemove: test.pdf");
+});
diff --git a/apps/desktop/src/features/score/ScoreView.tsx b/apps/desktop/src/features/score/ScoreView.tsx
index 72732450f..67eca634a 100644
--- a/apps/desktop/src/features/score/ScoreView.tsx
+++ b/apps/desktop/src/features/score/ScoreView.tsx
@@ -192,16 +192,21 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
{attachment.fileName}
-
+
+
))}
diff --git a/apps/desktop/src/features/score/ScoreViewer.tooltip.test.tsx b/apps/desktop/src/features/score/ScoreViewer.tooltip.test.tsx
new file mode 100644
index 000000000..f7a99f209
--- /dev/null
+++ b/apps/desktop/src/features/score/ScoreViewer.tooltip.test.tsx
@@ -0,0 +1,45 @@
+import { render, screen } from "@testing-library/react";
+import { expect, test, vi } from "vitest";
+
+vi.mock("../../i18n", () => ({
+ detectPreferredLocale: () => "en",
+ createTranslator: () => (key: string) => key,
+}));
+
+vi.mock("./pdfjs", () => ({
+ loadScorePdf: vi.fn(() => ({
+ // Resolve immediately to enter READY state, allowing buttons to render
+ promise: Promise.resolve({
+ numPages: 2,
+ getPage: vi.fn(() => Promise.resolve({
+ getViewport: vi.fn(() => ({ width: 800, height: 600, scale: 1 })),
+ render: vi.fn(() => ({ promise: Promise.resolve(), cancel: vi.fn() }))
+ }))
+ }),
+ destroy: vi.fn(() => Promise.resolve()),
+ })),
+}));
+
+import { ScoreViewer } from "./ScoreViewer";
+
+test("ScoreViewer places title on wrapper when prev/next buttons are disabled", async () => {
+ render();
+
+ // Wait for READY state
+ const prevButton = await screen.findByRole("button", { name: "scoreViewerPrevPage" });
+ const nextButton = await screen.findByRole("button", { name: "scoreViewerNextPage" });
+
+ // On page 1, prev is disabled, next is enabled (since numPages = 2)
+ expect(prevButton).toBeDisabled();
+ expect(nextButton).not.toBeDisabled();
+
+ // title should be on the wrapper for both
+ expect(prevButton).not.toHaveAttribute("title");
+ expect(nextButton).not.toHaveAttribute("title");
+
+ const prevWrapper = prevButton.parentElement;
+ const nextWrapper = nextButton.parentElement;
+
+ expect(prevWrapper).toHaveAttribute("title", "scoreViewerPrevPage");
+ expect(nextWrapper).toHaveAttribute("title", "scoreViewerNextPage");
+});
diff --git a/apps/desktop/src/features/score/ScoreViewer.tsx b/apps/desktop/src/features/score/ScoreViewer.tsx
index 82692469e..335083413 100644
--- a/apps/desktop/src/features/score/ScoreViewer.tsx
+++ b/apps/desktop/src/features/score/ScoreViewer.tsx
@@ -257,6 +257,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
variant="outline"
size="icon-lg"
className="size-12"
+ title={t("scoreViewerZoomOut")}
aria-label={t("scoreViewerZoomOut")}
onClick={zoomOut}
>
@@ -266,6 +267,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
variant="outline"
size="icon-lg"
className="size-12"
+ title={t("scoreViewerZoomIn")}
aria-label={t("scoreViewerZoomIn")}
onClick={zoomIn}
>
@@ -274,6 +276,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
-
+
+
+
{pageIndicator}
-
+
+
+