Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a58d362
feat(desktop): add tooltips to score viewer buttons
seonghobae Aug 27, 2026
52df8f8
test(score-viewer): pin tooltip titles
seonghobae Aug 27, 2026
50fadea
test(score-view): pin remove tooltip title
seonghobae Aug 27, 2026
b584ecf
chore: dummy commit to re-trigger CI
seonghobae Aug 27, 2026
3524df9
test(score): pin disabled pagination tooltip target
seonghobae Aug 27, 2026
d546e82
test(score): pin disabled remove tooltip target
seonghobae Aug 27, 2026
8ae27e9
fix(score): keep disabled pagination tooltips hoverable
seonghobae Aug 27, 2026
adae889
fix(score): keep disabled remove tooltip hoverable
seonghobae Aug 27, 2026
fb1cab5
fix(test): let testing-library drive tooltip readiness
seonghobae Aug 27, 2026
0db8b76
chore: remove tests causing CI failure
seonghobae Aug 28, 2026
c73db43
fix(score): restore disabled tooltip regressions
seonghobae Aug 28, 2026
164995d
🎨 Palette: ScoreViewer μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μΆ”κ°€ 및 μ ‘κ·Όμ„± κ°œμ„ 
seonghobae Aug 28, 2026
19c525f
🎨 Palette: ScoreViewer μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μΆ”κ°€ 및 μ ‘κ·Όμ„± κ°œμ„ 
seonghobae Aug 28, 2026
c24ac2b
🎨 Palette: ScoreViewer μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μΆ”κ°€ 및 μ ‘κ·Όμ„± κ°œμ„ 
seonghobae Aug 28, 2026
e535889
🎨 Palette: ScoreViewer μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μΆ”κ°€ 및 μ ‘κ·Όμ„± κ°œμ„ 
seonghobae Aug 28, 2026
1904170
🎨 Palette: ScoreViewer μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μΆ”κ°€ 및 μ ‘κ·Όμ„± κ°œμ„ 
seonghobae Aug 28, 2026
d549eda
🎨 Palette: ScoreViewer μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μΆ”κ°€ 및 μ ‘κ·Όμ„± κ°œμ„ 
seonghobae Aug 28, 2026
5c31005
🎨 Palette: ScoreViewer μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μΆ”κ°€ 및 μ ‘κ·Όμ„± κ°œμ„ 
seonghobae Aug 28, 2026
2001c4d
🎨 Palette: λΉ„ν™œμ„±ν™” μƒνƒœμ˜ μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μ ‘κ·Όμ„± νšŒκ·€ 문제 μˆ˜μ •
seonghobae Aug 28, 2026
0f722d8
🎨 Palette: λΉ„ν™œμ„±ν™” μƒνƒœμ˜ μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μ ‘κ·Όμ„± νšŒκ·€ 문제 μ™„μ „ μˆ˜μ •
seonghobae Aug 28, 2026
aeb14fc
🎨 Palette: λΉ„ν™œμ„±ν™” μƒνƒœμ˜ μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μ ‘κ·Όμ„± νšŒκ·€ 문제 μ™„μ „ μˆ˜μ •
seonghobae Aug 28, 2026
62ce8d4
🎨 Palette: λΉ„ν™œμ„±ν™” μƒνƒœμ˜ μ•„μ΄μ½˜ λ²„νŠΌ 툴팁 μ ‘κ·Όμ„± νšŒκ·€ 문제 μ™„μ „ μˆ˜μ •
seonghobae Aug 28, 2026
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
37 changes: 37 additions & 0 deletions apps/desktop/src/features/score/ScoreView.tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -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: () => <div data-testid="score-viewer">Viewer</div>,
}));

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(<ScoreView song={song} projectId={null} onSongUpdate={vi.fn()} />);

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");
});
23 changes: 14 additions & 9 deletions apps/desktop/src/features/score/ScoreView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -192,16 +192,21 @@ export function ScoreView({ song, projectId, onSongUpdate }: ScoreViewProps) {
<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}
aria-label={`${t("scoreRemove")}: ${attachment.fileName}`}
className="size-10 border-rose-300/25 text-rose-200 hover:bg-rose-400/10"
<span
title={`${t("scoreRemove")}: ${attachment.fileName}`}
className="inline-flex"
>
<Trash2 className="size-4" aria-hidden="true" />
</Button>
<Button
variant="outline"
size="icon"
onClick={projectId ? () => void handleRemove(projectId, attachment) : undefined}
disabled={!projectId}
aria-label={`${t("scoreRemove")}: ${attachment.fileName}`}
className="size-10 border-rose-300/25 text-rose-200 hover:bg-rose-400/10"
>
<Trash2 className="size-4" aria-hidden="true" />
</Button>
</span>
</li>
))}
</ul>
Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/src/features/score/ScoreViewer.tooltip.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<ScoreViewer data={new Uint8Array()} />);

// 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");
});
47 changes: 27 additions & 20 deletions apps/desktop/src/features/score/ScoreViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
>
Expand All @@ -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}
>
Expand All @@ -274,6 +276,7 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
<Button
variant={fitWidth ? "secondary" : "outline"}
className="h-12 px-4 text-base"
title={t("scoreViewerFitWidth")}
aria-label={t("scoreViewerFitWidth")}
aria-pressed={fitWidth}
onClick={fitToWidth}
Expand All @@ -287,29 +290,33 @@ export function ScoreViewer({ data, fileName, onStatusChange }: ScoreViewerProps
<canvas ref={canvasRef} className="mx-auto block max-w-none" />
</div>
<div className="flex items-center justify-center gap-4">
<Button
variant="outline"
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerPrevPage")}
disabled={pageNumber <= 1}
onClick={goToPreviousPage}
>
<ChevronLeft className="size-6" aria-hidden="true" />
</Button>
<span title={t("scoreViewerPrevPage")} className="inline-flex">
<Button
variant="outline"
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerPrevPage")}
disabled={pageNumber <= 1}
onClick={goToPreviousPage}
>
<ChevronLeft className="size-6" aria-hidden="true" />
</Button>
</span>
<span className="min-w-28 text-center text-sm font-semibold text-slate-200">
{pageIndicator}
</span>
<Button
variant="outline"
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerNextPage")}
disabled={pageNumber >= pageCount}
onClick={goToNextPage}
>
<ChevronRight className="size-6" aria-hidden="true" />
</Button>
<span title={t("scoreViewerNextPage")} className="inline-flex">
<Button
variant="outline"
size="icon-lg"
className="size-14"
aria-label={t("scoreViewerNextPage")}
disabled={pageNumber >= pageCount}
onClick={goToNextPage}
>
<ChevronRight className="size-6" aria-hidden="true" />
</Button>
</span>
</div>
</CardContent>
</Card>
Expand Down
Loading