-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workspace): name tonight's first ear check on the map #1001
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
Open
seonghobae
wants to merge
38
commits into
develop
Choose a base branch
from
feat/workspace-first-ear-check
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,149
−14
Open
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
f620ed6
feat(workspace): name tonight's first ear check on the map
seonghobae 9b89b61
test(workspace): cover ear-check ownership stability
seonghobae bd9e9e3
fix(workspace): keep ear-check notes with named part
seonghobae 327e5db
fix(workspace): preserve ear-check state for stable songs
seonghobae 394001f
test(workspace): scope ear-check navigation
seonghobae c0f0fec
fix(workspace): scope ear-check navigation
seonghobae 96d2e37
test(workspace): require section in Korean ear-check completion
seonghobae e5d64df
fix(workspace): keep section in Korean ear-check completion
seonghobae 109b86c
test(workspace): reject inactive ear-check hint leakage
seonghobae cc2f7c2
fix(workspace): keep inactive ear-check notes private
seonghobae 68e3fbc
test(workspace): bind band-wide ear-check hints to section owner
seonghobae 72d34c0
test(workspace): align inherited ear-check hint contract
seonghobae 15f189b
test(workspace): require initial Korean ear-check wording
seonghobae 38781c8
fix(workspace): clarify Korean ear-check next action
seonghobae bec4991
fix(workspace): harden first ear check identity, scoping, and candida…
seonghobae 8dd54ac
test(workspace): require ear-check coverage ownership
seonghobae 66bb7d1
fix(coverage): include ear-check production paths
seonghobae 1c12a37
test(workspace): reject truncated ear-check identity collisions
seonghobae d04ea19
fix(workspace): fail closed on oversized ear-check identity
seonghobae db57800
test(workspace): align Korean ear-check tone
seonghobae df35aed
fix(workspace): align Korean ear-check status tone
seonghobae cb86ea2
test(workspace): align Korean ear-check copy contract
seonghobae 9e7bbea
test(workspace): reject hostile ear-check sections access
seonghobae 685e701
test(workspace): align Korean ear-check expectations
seonghobae aa61010
test(accessibility): reject duplicate ear-check landmark ids
seonghobae da9c1a7
fix(accessibility): keep ear-check landmark ids unique
seonghobae c5d3a74
test(i18n): reject noncanonical ear-check section labels
seonghobae c8804b7
fix(i18n): require canonical ear-check section labels
seonghobae 6762e26
Merge branch 'develop' into feat/workspace-first-ear-check
seonghobae d3bbd82
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae af4d632
chore(workspace): remove unrelated palette drift
seonghobae 56a7b3b
chore(workspace): restore base palette provenance
seonghobae 62c0217
test(workspace): reject repeated ear-check sections reads
seonghobae d11451e
fix(workspace): bind ear-check resolution to sections snapshot
seonghobae 98bdef0
fix(workspace): reuse validated ear-check section snapshot
seonghobae 9874ae0
test(workspace): track the snapshot-bound ear-check resolver
seonghobae 8c1fe66
test(workspace): reject nested ear-check identity accessors
seonghobae 33eb08f
fix(workspace): make ear-check identity fingerprint descriptor-bound
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
17 changes: 17 additions & 0 deletions
17
apps/desktop/src/features/workspace/FirstEarCheckCallout.concurrent-id.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { render } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { expect, it } from "vitest"; | ||
| import { FirstEarCheckCallout } from "./FirstEarCheckCallout"; | ||
|
|
||
| it("keeps concurrent ear-check landmarks free of duplicate ids", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const { container } = render( | ||
| <> | ||
| <FirstEarCheckCallout song={song} /> | ||
| <FirstEarCheckCallout song={song} /> | ||
| </> | ||
| ); | ||
|
|
||
| const ids = Array.from(container.querySelectorAll<HTMLElement>("[id]"), (element) => element.id); | ||
| expect(ids).toEqual(Array.from(new Set(ids))); | ||
| }); |
64 changes: 64 additions & 0 deletions
64
apps/desktop/src/features/workspace/FirstEarCheckCallout.identity-bound.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstEarCheckCallout } from "./FirstEarCheckCallout"; | ||
|
|
||
| function appendSongStructureTarget() { | ||
| const timeline = document.createElement("div"); | ||
| timeline.setAttribute("role", "region"); | ||
| timeline.setAttribute("aria-label", "Scrollable song structure timeline"); | ||
| const target = document.createElement("div"); | ||
| target.dataset.sectionIndex = "0"; | ||
| Object.defineProperty(target, "scrollIntoView", { | ||
| configurable: true, | ||
| value: vi.fn() | ||
| }); | ||
| timeline.appendChild(target); | ||
| document.body.appendChild(timeline); | ||
| return timeline; | ||
| } | ||
|
|
||
| function songWithThirtyThreeSections() { | ||
| const song = createDemoRehearsalSong(); | ||
| const filler = structuredClone(song.sections[1] ?? song.sections[0]!); | ||
| song.sections = [ | ||
| structuredClone(song.sections[0]!), | ||
| ...Array.from({ length: 32 }, (_, index) => ({ | ||
| ...structuredClone(filler), | ||
| id: `identity-filler-${index + 1}`, | ||
| timeRange: { | ||
| ...structuredClone(filler.timeRange), | ||
| start: 100 + index * 10, | ||
| end: 105 + index * 10 | ||
| } | ||
| })) | ||
| ]; | ||
| return song; | ||
| } | ||
|
|
||
| describe("FirstEarCheckCallout bounded song identity", () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("resets armed guidance when same-id oversized songs differ beyond the fingerprint bound", () => { | ||
| const firstSong = songWithThirtyThreeSections(); | ||
| const secondSong = structuredClone(firstSong); | ||
| secondSong.sections[32]!.id = "different-tail-section"; | ||
| const timeline = appendSongStructureTarget(); | ||
|
|
||
| try { | ||
| const { rerender } = render(<FirstEarCheckCallout song={firstSong} />); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "Open Bass Guitar ear check at 0:10" })); | ||
| expect(screen.getByText(/Confirm Bass Guitar by ear at 0:10 before the room starts./)).toBeTruthy(); | ||
|
|
||
| rerender(<FirstEarCheckCallout song={secondSong} />); | ||
|
|
||
| expect(screen.getByText("Bass Guitar still needs an ear check in the verse at 0:10.")).toBeTruthy(); | ||
| expect(screen.queryByText(/Confirm Bass Guitar by ear at 0:10 before the room starts./)).toBeNull(); | ||
| } finally { | ||
| timeline.remove(); | ||
| } | ||
| }); | ||
| }); |
94 changes: 94 additions & 0 deletions
94
apps/desktop/src/features/workspace/FirstEarCheckCallout.particle.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstEarCheckCallout } from "./FirstEarCheckCallout"; | ||
|
|
||
| describe("FirstEarCheckCallout Korean role copy", () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("keeps vowel-ending dynamic role names particle-safe before and after the ear-check action", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const song = createDemoRehearsalSong(); | ||
| const seed = song.sections[0]!; | ||
| seed.roles = [ | ||
| { | ||
| ...seed.roles[0]!, | ||
| id: "piano", | ||
| name: "피아노", | ||
| rehearsalPriority: "high", | ||
| confidence: { | ||
| level: "medium", | ||
| source: "model", | ||
| notes: "Top voicing may need a quick ear check." | ||
| } | ||
| } | ||
| ]; | ||
| seed.partGraph = [{ role_id: "piano", is_active: true, handoff_to: [], handoff_from: [] }]; | ||
|
|
||
| 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 = "0"; | ||
| Object.defineProperty(target, "scrollIntoView", { | ||
| configurable: true, | ||
| value: vi.fn() | ||
| }); | ||
| grid.appendChild(target); | ||
| document.body.appendChild(grid); | ||
|
|
||
| render(<FirstEarCheckCallout 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("keeps the section locator after opening a band-wide ear check", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const song = createDemoRehearsalSong(); | ||
| const seed = song.sections[0]!; | ||
| seed.roles = [ | ||
| { | ||
| ...seed.roles[0]!, | ||
| confidence: { | ||
| level: "medium", | ||
| source: "model", | ||
| notes: "Confirm the section by ear." | ||
| } | ||
| } | ||
| ]; | ||
| seed.partGraph = []; | ||
|
|
||
| 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 = "0"; | ||
| Object.defineProperty(target, "scrollIntoView", { | ||
| configurable: true, | ||
| value: vi.fn() | ||
| }); | ||
| grid.appendChild(target); | ||
| document.body.appendChild(grid); | ||
|
|
||
| render(<FirstEarCheckCallout song={song} />); | ||
|
|
||
| expect(screen.getByText("0:10 벌스에서 아직 귀 확인이 필요합니다.")).toBeTruthy(); | ||
| fireEvent.click(screen.getByRole("button", { name: "0:10 첫 귀 확인 위치 열기" })); | ||
| expect(screen.getByText("0:10 벌스에서 귀로 확인한 다음 합주를 시작하세요.")).toBeTruthy(); | ||
|
|
||
| grid.remove(); | ||
| }); | ||
| }); |
43 changes: 43 additions & 0 deletions
43
apps/desktop/src/features/workspace/FirstEarCheckCallout.reduced-motion.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstEarCheckCallout } from "./FirstEarCheckCallout"; | ||
|
|
||
| describe("FirstEarCheckCallout reduced motion", () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("scrolls immediately when the operating system requests reduced motion", () => { | ||
| vi.stubGlobal("matchMedia", (query: string) => ({ | ||
| matches: query === "(prefers-reduced-motion: reduce)", | ||
| media: query, | ||
| onchange: null, | ||
| addListener: vi.fn(), | ||
| removeListener: vi.fn(), | ||
| addEventListener: vi.fn(), | ||
| removeEventListener: vi.fn(), | ||
| dispatchEvent: vi.fn() | ||
| })); | ||
|
|
||
| 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 = "0"; | ||
| const scrollIntoView = vi.fn(); | ||
| Object.defineProperty(target, "scrollIntoView", { | ||
| configurable: true, | ||
| value: scrollIntoView | ||
| }); | ||
| grid.appendChild(target); | ||
| document.body.appendChild(grid); | ||
|
|
||
| render(<FirstEarCheckCallout song={createDemoRehearsalSong()} />); | ||
| fireEvent.click(screen.getByRole("button", { name: "Open Bass Guitar ear check at 0:10" })); | ||
| expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest", behavior: "auto" }); | ||
|
|
||
| grid.remove(); | ||
| }); | ||
| }); |
63 changes: 63 additions & 0 deletions
63
apps/desktop/src/features/workspace/FirstEarCheckCallout.sections-accessor.test.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong, type RehearsalSong } from "@bandscope/shared-types"; | ||
| import { expect, it, vi } from "vitest"; | ||
| import { FirstEarCheckCallout } from "./FirstEarCheckCallout"; | ||
|
|
||
| it("rejects a hostile song sections accessor without invoking it", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const readSections = vi.fn(() => { | ||
| throw new Error("hostile song sections getter"); | ||
| }); | ||
| Object.defineProperty(song, "sections", { | ||
| configurable: true, | ||
| enumerable: true, | ||
| get: readSections | ||
| }); | ||
|
|
||
| expect(() => render(<FirstEarCheckCallout song={song as RehearsalSong} />)).not.toThrow(); | ||
| expect(readSections).not.toHaveBeenCalled(); | ||
| expect( | ||
| screen.getByText( | ||
| "Nothing still needs an ear check. Stay on tonight's map until a part is marked uncertain." | ||
| ) | ||
| ).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("uses the owned sections snapshot without invoking a proxy get trap", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const readSections = vi.fn(() => { | ||
| throw new Error("hostile sections get trap"); | ||
| }); | ||
| const proxiedSong = new Proxy(song, { | ||
| get(target, property, receiver) { | ||
| if (property === "sections") { | ||
| return readSections(); | ||
| } | ||
| return Reflect.get(target, property, receiver); | ||
| } | ||
| }); | ||
|
|
||
| expect(() => render(<FirstEarCheckCallout song={proxiedSong as RehearsalSong} />)).not.toThrow(); | ||
| expect(readSections).not.toHaveBeenCalled(); | ||
| expect(screen.getByRole("button", { name: "Open Bass Guitar ear check at 0:10" })).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("rejects a hostile nested time-range accessor without invoking it for identity", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const readTimeRange = vi.fn(() => { | ||
| throw new Error("hostile section time-range getter"); | ||
| }); | ||
| Object.defineProperty(song.sections[0]!, "timeRange", { | ||
| configurable: true, | ||
| enumerable: true, | ||
| get: readTimeRange | ||
| }); | ||
|
|
||
| expect(() => render(<FirstEarCheckCallout song={song as RehearsalSong} />)).not.toThrow(); | ||
| expect(readTimeRange).not.toHaveBeenCalled(); | ||
| expect( | ||
| screen.getByText( | ||
| "Nothing still needs an ear check. Stay on tonight's map until a part is marked uncertain." | ||
| ) | ||
| ).toBeTruthy(); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.