-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workspace): name tonight's first pickup plan on the map #1037
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
2e01017
3537f2f
b262875
717b217
6cd55d0
04a7c61
9218b2b
5fa6f8b
2bdafc1
6d988f8
12d1b37
dc8ceea
b2ad8ab
06ee753
26340b1
0377ad0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { expect, it } from "vitest"; | ||
| import { FirstPickupPlanCallout } from "./FirstPickupPlanCallout"; | ||
|
|
||
| it("gives co-mounted pickup-plan callouts distinct DOM identities", () => { | ||
| render( | ||
| <> | ||
| <FirstPickupPlanCallout song={createDemoRehearsalSong()} /> | ||
| <FirstPickupPlanCallout song={createDemoRehearsalSong()} /> | ||
| </> | ||
| ); | ||
|
|
||
| const callouts = screen.getAllByRole("complementary", { | ||
| name: "Tonight's first pickup plan" | ||
| }); | ||
| const ids = callouts.map((callout) => callout.id); | ||
|
|
||
| expect(ids.every((id) => id.length > 0)).toBe(true); | ||
| expect(new Set(ids).size).toBe(callouts.length); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import { render } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstPickupPlanCallout } from "./FirstPickupPlanCallout"; | ||
|
|
||
| function songWithPickupPlan() { | ||
| const song = createDemoRehearsalSong(); | ||
| const verse = song.sections[0]!; | ||
| const intro = structuredClone(verse); | ||
| intro.id = "intro-1"; | ||
| intro.label = "intro"; | ||
| intro.timeRange = { start: 0, end: verse.timeRange.start }; | ||
| intro.roles = intro.roles.map((role) => { | ||
| const clone = { ...role }; | ||
| delete clone.pickupPlan; | ||
| delete clone.pickupPlanSource; | ||
| return clone; | ||
| }); | ||
| intro.partGraph = intro.partGraph.map((node) => ({ | ||
| ...node, | ||
| is_active: node.role_id !== "bass-guitar" | ||
| })); | ||
| song.sections = [intro, verse]; | ||
| return song; | ||
| } | ||
|
|
||
| describe("FirstPickupPlanCallout resolver reuse", () => { | ||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| it("does not rescan role metadata when a parent rerenders the same song object", () => { | ||
| const song = songWithPickupPlan(); | ||
| const role = song.sections[1]!.roles.find((candidate) => candidate.id === "bass-guitar")!; | ||
| const descriptorSpy = vi.spyOn(Object, "getOwnPropertyDescriptor"); | ||
|
|
||
| const { rerender } = render(<FirstPickupPlanCallout song={song} />); | ||
| const firstScanCount = descriptorSpy.mock.calls.filter(([target]) => target === role).length; | ||
| expect(firstScanCount).toBeGreaterThan(0); | ||
|
|
||
| rerender(<FirstPickupPlanCallout song={song} />); | ||
| const secondScanCount = descriptorSpy.mock.calls.filter(([target]) => target === role).length; | ||
|
|
||
| expect(secondScanCount).toBe(firstScanCount); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { describe, expect, it } from "vitest"; | ||
| import { FirstPickupPlanCallout } from "./FirstPickupPlanCallout"; | ||
|
|
||
| function songWithPickupPlan() { | ||
| const song = createDemoRehearsalSong(); | ||
| const verse = song.sections[0]!; | ||
| const intro = structuredClone(verse); | ||
| intro.id = "intro-1"; | ||
| intro.label = "intro"; | ||
| intro.timeRange = { start: 0, end: verse.timeRange.start }; | ||
| intro.roles = intro.roles.map((role) => { | ||
| const clone = { ...role }; | ||
| delete clone.pickupPlan; | ||
| delete clone.pickupPlanSource; | ||
| return clone; | ||
| }); | ||
| intro.partGraph = intro.partGraph.map((node) => ({ | ||
| ...node, | ||
| is_active: node.role_id !== "bass-guitar" | ||
| })); | ||
| song.sections = [intro, verse]; | ||
| return song; | ||
| } | ||
|
|
||
| describe("FirstPickupPlanCallout navigation failure", () => { | ||
| it("tells the user when the named pickup cannot be opened on the rendered map", () => { | ||
| render(<FirstPickupPlanCallout song={songWithPickupPlan()} />); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "Open Bass Guitar pickup at 0:10" })); | ||
|
|
||
| expect( | ||
| screen.getByText( | ||
| "Could not open this pickup on the song map. Use the map below to find the section." | ||
| ) | ||
| ).toBeTruthy(); | ||
| expect( | ||
| screen.queryByText(/Play that pickup on Bass Guitar at 0:10 before the downbeat lands./) | ||
| ).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstPickupPlanCallout } from "./FirstPickupPlanCallout"; | ||
|
|
||
| function songWithKoreanPickup( | ||
| pickupPlan: string, | ||
| pickupPlanSource?: "model" | "user" | ||
| ) { | ||
| const song = createDemoRehearsalSong(); | ||
| const verse = song.sections[0]!; | ||
| const companion = verse.roles.find((role) => role.id === "bass-guitar")!; | ||
| verse.roles = [ | ||
| { | ||
| ...verse.roles[2]!, | ||
| id: "piano", | ||
| name: "피아노", | ||
| rehearsalPriority: "high", | ||
| pickupPlan, | ||
| ...(pickupPlanSource ? { pickupPlanSource } : {}) | ||
| }, | ||
| companion | ||
| ]; | ||
| verse.partGraph = [ | ||
| { role_id: "piano", is_active: true, handoff_to: [], handoff_from: [] }, | ||
| { role_id: "bass-guitar", is_active: true, handoff_to: [], handoff_from: [] } | ||
| ]; | ||
| const intro = structuredClone(verse); | ||
| intro.id = "intro-1"; | ||
| intro.label = "intro"; | ||
| intro.timeRange = { start: 0, end: verse.timeRange.start }; | ||
| intro.roles = intro.roles.map((role) => { | ||
| const clone = { ...role }; | ||
| delete clone.pickupPlan; | ||
| delete clone.pickupPlanSource; | ||
| return clone; | ||
| }); | ||
| intro.partGraph = intro.partGraph.map((node) => ({ | ||
| ...node, | ||
| is_active: node.role_id !== "piano" | ||
| })); | ||
| song.sections = [intro, verse]; | ||
| return song; | ||
| } | ||
|
|
||
| describe("FirstPickupPlanCallout Korean role copy", () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("keeps vowel-ending role names particle-safe before and after the pickup action", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const song = songWithKoreanPickup( | ||
| "Play this pickup with Lead Vocal on the verse last beat; land the chorus downbeat together." | ||
| ); | ||
|
|
||
| const grid = document.createElement("div"); | ||
| grid.dataset.testid = "song-structure-grid"; | ||
| grid.setAttribute("role", "region"); | ||
| grid.setAttribute("aria-label", "Scrollable song structure timeline"); | ||
| const target = document.createElement("div"); | ||
| target.dataset.sectionIndex = "1"; | ||
| Object.defineProperty(target, "scrollIntoView", { | ||
| configurable: true, | ||
| value: vi.fn() | ||
| }); | ||
| grid.appendChild(target); | ||
| document.body.appendChild(grid); | ||
|
|
||
| render(<FirstPickupPlanCallout song={song} />); | ||
|
|
||
| expect(screen.getByText("0:10 벌스에서 피아노 파트의 픽업 계획이 있습니다.")).toBeTruthy(); | ||
| expect(screen.queryByText(/피아노이/)).toBeNull(); | ||
| expect(screen.queryByText(/피아노가/)).toBeNull(); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "0:10 피아노 픽업 열기" })); | ||
|
|
||
| expect(screen.getByText("0:10에서 피아노 파트의 픽업을 넣은 다음 합주를 시작하세요.")).toBeTruthy(); | ||
| expect(screen.queryByText(/피아노과/)).toBeNull(); | ||
|
|
||
| grid.remove(); | ||
| }); | ||
|
|
||
| it("localizes the analysis-engine pickup template instead of exposing English guidance", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const song = songWithKoreanPickup( | ||
| "Play this pickup with Lead Vocal; land the downbeat together.", | ||
| "model" | ||
| ); | ||
|
|
||
| render(<FirstPickupPlanCallout song={song} />); | ||
|
|
||
| expect( | ||
| screen.getByText("Lead Vocal 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.") | ||
| ).toBeTruthy(); | ||
| expect( | ||
| screen.queryByText("Play this pickup with Lead Vocal; land the downbeat together.") | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it("localizes the rest-of-band pickup template instead of exposing English guidance", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const song = songWithKoreanPickup( | ||
| "Play this pickup with the rest of the band; land the downbeat together.", | ||
| "model" | ||
| ); | ||
|
|
||
| render(<FirstPickupPlanCallout song={song} />); | ||
|
|
||
| expect( | ||
| screen.getByText("나머지 밴드와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.") | ||
| ).toBeTruthy(); | ||
| expect( | ||
| screen.queryByText("Play this pickup with the rest of the band; land the downbeat together.") | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it("preserves the generated template shape when long target names are bounded", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const targetRole = `Lead-${"A".repeat(180)}`; | ||
| const song = songWithKoreanPickup( | ||
| `Play this pickup with ${targetRole}; land the downbeat together.`, | ||
| "model" | ||
| ); | ||
| const landing = song.sections[1]!; | ||
| landing.roles[1] = { ...landing.roles[1]!, name: targetRole }; | ||
|
|
||
| render(<FirstPickupPlanCallout song={song} />); | ||
|
|
||
| expect(screen.queryByText(/^Play this pickup with /)).toBeNull(); | ||
| expect( | ||
| screen.getByText( | ||
| (content) => | ||
| content.startsWith("Lead-") && | ||
| content.endsWith("파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.") | ||
| ) | ||
| ).toBeTruthy(); | ||
| }); | ||
|
seonghobae marked this conversation as resolved.
|
||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstPickupPlanCallout } from "./FirstPickupPlanCallout"; | ||
|
|
||
| function songWithKoreanPickup( | ||
| pickupPlan: string, | ||
| pickupPlanSource?: "model" | "user" | ||
| ) { | ||
| const song = createDemoRehearsalSong(); | ||
| const verse = song.sections[0]!; | ||
| const companion = verse.roles.find((role) => role.id === "bass-guitar")!; | ||
| verse.roles = [ | ||
| { | ||
| ...verse.roles[2]!, | ||
| id: "piano", | ||
| name: "피아노", | ||
| rehearsalPriority: "high", | ||
| pickupPlan, | ||
| ...(pickupPlanSource ? { pickupPlanSource } : {}) | ||
| }, | ||
| companion | ||
| ]; | ||
| verse.partGraph = [ | ||
| { role_id: "piano", is_active: true, handoff_to: [], handoff_from: [] }, | ||
| { role_id: "bass-guitar", is_active: true, handoff_to: [], handoff_from: [] } | ||
| ]; | ||
| const intro = structuredClone(verse); | ||
| intro.id = "intro-1"; | ||
| intro.label = "intro"; | ||
| intro.timeRange = { start: 0, end: verse.timeRange.start }; | ||
| intro.roles = intro.roles.map((role) => { | ||
| const clone = { ...role }; | ||
| delete clone.pickupPlan; | ||
| delete clone.pickupPlanSource; | ||
| return clone; | ||
| }); | ||
| intro.partGraph = intro.partGraph.map((node) => ({ | ||
| ...node, | ||
| is_active: node.role_id !== "piano" | ||
| })); | ||
| song.sections = [intro, verse]; | ||
| return song; | ||
| } | ||
|
|
||
| describe("FirstPickupPlanCallout pickup-plan provenance", () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("preserves user pickup guidance that happens to match the engine sentence shape", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const customPlan = "Play this pickup with Lead Vocal; land the downbeat together."; | ||
| const song = songWithKoreanPickup(customPlan, "user"); | ||
|
|
||
| render(<FirstPickupPlanCallout song={song} />); | ||
|
|
||
| expect(screen.getByText(customPlan)).toBeTruthy(); | ||
| expect( | ||
| screen.queryByText("Bass Guitar 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.") | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it("does not infer model authority when persisted pickup guidance has no source", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const legacyPlan = "Play this pickup with Lead Vocal; land the downbeat together."; | ||
| const song = songWithKoreanPickup(legacyPlan); | ||
|
|
||
| render(<FirstPickupPlanCallout song={song} />); | ||
|
|
||
| expect(screen.getByText(legacyPlan)).toBeTruthy(); | ||
| expect( | ||
| screen.queryByText("Bass Guitar 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.") | ||
| ).toBeNull(); | ||
| }); | ||
|
|
||
| it("localizes model guidance from structured landing topology instead of display sentence wording", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const changedDisplayCopy = "Pickup display wording changed upstream."; | ||
| const song = songWithKoreanPickup(changedDisplayCopy, "model"); | ||
|
|
||
| render(<FirstPickupPlanCallout song={song} />); | ||
|
|
||
| expect( | ||
| screen.getByText("Bass Guitar 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.") | ||
|
Check failure on line 85 in apps/desktop/src/features/workspace/FirstPickupPlanCallout.provenance.test.tsx
|
||
| ).toBeTruthy(); | ||
| expect(screen.queryByText(changedDisplayCopy)).toBeNull(); | ||
| }); | ||
| }); | ||
|
Comment on lines
+77
to
+89
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Line 80에서
🧰 Tools🪛 GitHub Actions: ci / 0_ci _ build-and-test.txt[error] 85-85: Vitest test failed: Testing Library could not find the expected localized text "Bass Guitar 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.". 🪛 GitHub Actions: ci / ci _ build-and-test[error] 85-85: Vitest test failed: expected the localized model guidance 'Bass Guitar 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요.' but the rendered callout displayed different content. Test command: npm run test --workspaces --if-present. 🪛 GitHub Actions: release / 0_release-preflight.txt[error] 85-85: Vitest test failed: expected localized model guidance text "Bass Guitar 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요." was not found. TestingLibraryElementError. 🪛 GitHub Actions: release / release-preflight[error] 85-85: Vitest/Testing Library test failed: expected localized model guidance text "Bass Guitar 파트와 이 픽업을 맞추세요. 첫 박에 함께 들어가세요." was not found in the rendered output. 🪛 GitHub Check: ci / build-and-test[failure] 85-85: src/features/workspace/FirstPickupPlanCallout.provenance.test.tsx > FirstPickupPlanCallout pickup-plan provenance > localizes model guidance from structured landing topology instead of display sentence wording Ignored nodes: comments, script, style 오늘 첫 픽업 계획 0:10 벌스에서 피아노 파트의 픽업 계획이 있습니다. Pickup display wording changed upstream. 0:10 피아노 픽업 열기🪛 GitHub Check: release-preflight[failure] 85-85: src/features/workspace/FirstPickupPlanCallout.provenance.test.tsx > FirstPickupPlanCallout pickup-plan provenance > localizes model guidance from structured landing topology instead of display sentence wording Ignored nodes: comments, script, style 오늘 첫 픽업 계획 0:10 벌스에서 피아노 파트의 픽업 계획이 있습니다. Pickup display wording changed upstream. 0:10 피아노 픽업 열기🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
Uh oh!
There was an error while loading. Please reload this page.