diff --git a/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx
index 75a19924..3e4ee7c5 100644
--- a/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx
+++ b/apps/desktop/src/features/workspace/SectionRoadmap.test.tsx
@@ -1,4 +1,4 @@
-import { fireEvent, render, screen } from "@testing-library/react";
+import { fireEvent, render, screen, within } from "@testing-library/react";
import { createDemoRehearsalSong } from "@bandscope/shared-types";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SectionRoadmap } from "./SectionRoadmap";
@@ -12,10 +12,23 @@ function setNavigatorLanguage(language: string) {
});
}
+function getChordEditButton(roleName = "Bass Guitar", sectionLabel = "verse", chord = "C#m7") {
+ return screen.getByRole("button", {
+ name: new RegExp(`${roleName}.*${sectionLabel}.*${chord}`)
+ });
+}
+
+function roleCard(roleName: string) {
+ const card = screen.getByText(roleName).closest(".border-l-4");
+ if (!card) throw new Error(`Role card not found for ${roleName}`);
+ return card as HTMLElement;
+}
+
describe("SectionRoadmap", () => {
afterEach(() => {
setNavigatorLanguage(originalLanguage);
vi.restoreAllMocks();
+ vi.unstubAllGlobals();
});
it("localizes roadmap controls and provenance badges", () => {
@@ -25,12 +38,12 @@ describe("SectionRoadmap", () => {
render();
- expect(screen.getByText("더 많은 구간은 옆으로 스크롤하세요 →")).toBeTruthy();
+ expect(screen.getByText("더 많은 구간은 옆으로 스크롤하세요 →")).toBeInTheDocument();
expect(screen.getAllByText("그루브").length).toBeGreaterThan(0);
expect(screen.getAllByText("코드").length).toBeGreaterThan(0);
expect(screen.getAllByText("큐").length).toBeGreaterThan(0);
expect(screen.getAllByTitle("우선순위: high").length).toBeGreaterThan(0);
- expect(screen.getByText("사용자")).toBeTruthy();
+ expect(screen.getByText("사용자")).toBeInTheDocument();
});
it("uses localized copy for chord edit prompts and control labels", () => {
@@ -41,7 +54,7 @@ describe("SectionRoadmap", () => {
render();
- fireEvent.click(screen.getByRole("button", { name: "Bass Guitar의 verse 코드 수정, 현재 C#m7" }));
+ fireEvent.click(getChordEditButton());
expect(promptSpy).toHaveBeenCalledWith("새 코드 입력:", "C#m7");
expect(screen.getAllByTitle("코드 수정").length).toBeGreaterThan(0);
@@ -56,8 +69,102 @@ describe("SectionRoadmap", () => {
render();
- fireEvent.click(screen.getByRole("button", { name: "Edit chord for Bass Guitar in verse, current C#m7" }));
+ fireEvent.click(getChordEditButton());
+
+ expect(onSongUpdate).not.toHaveBeenCalled();
+ });
+
+ it("cancels chord editing without updating the song", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+ const onSongUpdate = vi.fn();
+ const promptSpy = vi.spyOn(window, "prompt").mockReturnValue(null);
+
+ render();
+
+ fireEvent.click(getChordEditButton());
+ expect(promptSpy).toHaveBeenCalledWith("새 코드 입력:", "C#m7");
expect(onSongUpdate).not.toHaveBeenCalled();
});
+
+ it("disables chord editing when no update handler is provided", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+
+ render();
+
+ expect(getChordEditButton()).toBeDisabled();
+ });
+
+ it("renders priority indicators for high, medium, and low roles", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+
+ song.sections[0].roles.push({
+ ...song.sections[0].roles[0],
+ id: "medium-priority-role",
+ name: "Medium Role",
+ rehearsalPriority: "medium"
+ });
+ song.sections[0].roles.push({
+ ...song.sections[0].roles[0],
+ id: "low-priority-role",
+ name: "Low Role",
+ rehearsalPriority: "low"
+ });
+
+ render();
+
+ expect(within(roleCard("Bass Guitar")).getByTitle("우선순위: high")).toBeInTheDocument();
+ expect(within(roleCard("Medium Role")).getByTitle("우선순위: medium")).toBeInTheDocument();
+ expect(within(roleCard("Low Role")).getByTitle("우선순위: low")).toBeInTheDocument();
+ });
+
+ it("renders low confidence badges for low-confidence sections and roles", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+ song.sections[0].confidence.level = "low";
+ song.sections[0].roles[0].confidence.level = "low";
+
+ render();
+
+ expect(screen.getAllByText("확신이 낮음").length).toBeGreaterThan(0);
+ });
+
+ it("filters the roadmap to the active role", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+
+ render();
+
+ expect(screen.getByText("Bass Guitar")).toBeInTheDocument();
+ expect(screen.queryByText("Keyboard 1 Right Hand")).toBeNull();
+ });
+
+ it("does not update when the entered chord is empty or whitespace", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+ const onSongUpdate = vi.fn();
+ const promptSpy = vi.spyOn(window, "prompt");
+
+ promptSpy.mockReturnValueOnce("").mockReturnValueOnce(" ");
+ render();
+
+ const editButton = getChordEditButton();
+ fireEvent.click(editButton);
+ fireEvent.click(editButton);
+
+ expect(promptSpy).toHaveBeenCalledTimes(2);
+ expect(onSongUpdate).not.toHaveBeenCalled();
+ });
+
+ it("renders overlap warnings", () => {
+ setNavigatorLanguage("ko-KR");
+ const song = createDemoRehearsalSong();
+
+ render();
+
+ expect(screen.getByText(/Density warning/i)).toBeInTheDocument();
+ });
});