-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workspace): guide tonight's first intro on map and player #943
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
Draft
seonghobae
wants to merge
48
commits into
bolt-performance-chart-export-13223013812255847379
Choose a base branch
from
feat/workspace-first-intro-count-in
base: bolt-performance-chart-export-13223013812255847379
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.
Draft
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
832109a
feat(workspace): guide tonight's first intro on map and player
seonghobae ff219f5
test(workspace): lock intro tie-breaking across locales
seonghobae ebfd15b
fix(workspace): make intro tie-breaking locale independent
seonghobae 990f6f5
test(i18n): reject inherited section-label lookups
seonghobae 1e1a937
test(player): require localized section-count copy
seonghobae a41090c
fix(i18n): keep inherited keys out of section labels
seonghobae 85c425d
fix(player): localize section-count labels
seonghobae 9f4dd36
fix(player): localize section count copy
seonghobae 862cae4
test(player): require localized intro badge
seonghobae 9a7a465
fix(player): localize intro section badge
seonghobae 8b48181
test(workspace): lock particle-safe Korean intro copy
seonghobae d3d3c40
fix(workspace): keep Korean intro role copy particle-safe
seonghobae 0cb60b5
test(workspace): cover Korean intro armed particle safety
seonghobae 93f49dc
fix(workspace): keep Korean intro armed copy particle-safe
seonghobae b35424c
test(workspace): align Korean intro copy oracle
seonghobae c9180f7
docs(changelog): preserve tempo badge entry
seonghobae 26b7f10
test(workspace): reject inherited intro metadata
seonghobae cbea560
fix(workspace): reject inherited intro metadata
seonghobae f06234b
docs(workspace): record intro metadata trust boundary
seonghobae 37f6de8
test(i18n): require Korean section badge labels
seonghobae 36a93c9
fix(i18n): localize all section form badges
seonghobae 41063fe
test(workspace): reject exceptional intro metadata
seonghobae 785dbef
fix(workspace): contain exceptional intro metadata
seonghobae 3f6217a
test(runtime): bound synthetic section collection scans
seonghobae bd5b1ea
fix(runtime): bound intro section density scans
seonghobae 8b1e402
fix(player): bound runtime section density scans
seonghobae b473ab5
refactor(i18n): remove unreachable label fallback branch
seonghobae 0f62b37
test(player): localize playback availability hint
seonghobae 45a877b
feat(i18n): add localized player playback hint
seonghobae 8cbe4b3
feat(i18n): add Korean player playback hint
seonghobae ff9ab44
fix(player): localize playback availability hint
seonghobae 9b7a1c6
test(workspace): require localized timeline summary
seonghobae 3984ba0
test(workspace): use shared localized timeline labels
seonghobae 3e81d49
fix(workspace): localize timeline summary
seonghobae 96985e1
test(workspace): cover summary term capitalization
seonghobae 6d38625
fix(workspace): capitalize confidence summary label
seonghobae 14db3d7
test(workspace): isolate malformed first-intro candidates
seonghobae 7ad0956
fix(workspace): isolate malformed first-intro metadata
seonghobae b627d31
test(player): reject accessor-backed section collections
seonghobae 4855357
fix(player): contain section collection accessors
seonghobae 396d003
Merge branch 'develop' into feat/workspace-first-intro-count-in
seonghobae d22b6ca
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae 39bad56
fix(workspace): read intro sections through trap-safe own-data path
seonghobae ff5e47d
fix(workspace): fail closed on throwing runtime metadata
seonghobae 39ed0be
test(workspace): require canonical Korean section term
seonghobae 7e88240
test(player): reject mixed Korean section terminology
seonghobae 066cc31
fix(i18n): align Korean section terminology
seonghobae 5e357e8
chore(workspace): restack first-intro lane on formatter prerequisite
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
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,144 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import { PlayerFeature } from "./index"; | ||
|
|
||
| function songWithIntro() { | ||
| const song = createDemoRehearsalSong(); | ||
| const verse = song.sections[0]!; | ||
| const intro = structuredClone(verse); | ||
| intro.id = "intro-1"; | ||
| intro.label = "intro"; | ||
| intro.timeRange = { start: 0, end: 8 }; | ||
| intro.roles = [ | ||
| { | ||
| ...verse.roles[0]!, | ||
| id: "drums", | ||
| name: "Drums", | ||
| rehearsalPriority: "high" | ||
| } | ||
| ]; | ||
| intro.partGraph = [ | ||
| { | ||
| role_id: "drums", | ||
| is_active: true, | ||
| handoff_to: [], | ||
| handoff_from: [] | ||
| } | ||
| ]; | ||
| song.sections = [intro, verse]; | ||
| return song; | ||
| } | ||
|
|
||
| describe("PlayerFeature", () => { | ||
| it("asks the room to analyze first when no song is loaded", () => { | ||
| render(<PlayerFeature title="Player" />); | ||
| expect( | ||
| screen.getByText("Analyze tonight's song first, then hear the first intro from this player.") | ||
| ).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("keeps the intro hear action unavailable without a player playback callback", () => { | ||
| render(<PlayerFeature title="Player" song={songWithIntro()} />); | ||
|
|
||
| expect(screen.queryByRole("button", { name: "Hear Drums start at 0:00" })).toBeNull(); | ||
| expect(screen.getByText("Drums starts the intro at 0:00.")).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("delegates the intro hear action to the owning player callback", () => { | ||
| const onPlayFromSeconds = vi.fn(); | ||
| render(<PlayerFeature title="Player" song={songWithIntro()} onPlayFromSeconds={onPlayFromSeconds} />); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "Hear Drums start at 0:00" })); | ||
|
|
||
| expect(onPlayFromSeconds).toHaveBeenCalledTimes(1); | ||
| expect(onPlayFromSeconds).toHaveBeenCalledWith(0); | ||
| }); | ||
|
|
||
| it("localizes the section count, labels, and playback hint instead of mixing English player copy", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| try { | ||
| render(<PlayerFeature title="Player" song={songWithIntro()} />); | ||
| expect(screen.getByText("2개 구간")).toBeTruthy(); | ||
| expect(screen.queryByText("2개 섹션")).toBeNull(); | ||
| expect(screen.queryByText("2 sections")).toBeNull(); | ||
| expect(screen.getByText("인트로")).toBeTruthy(); | ||
| expect(screen.queryByText("intro")).toBeNull(); | ||
| expect(screen.getByText("오디오 재생은 로컬 오디오 소스가 있는 데스크톱 앱에서 사용할 수 있습니다.")).toBeTruthy(); | ||
| expect(screen.queryByText("Audio playback requires the desktop app with a local audio source.")).toBeNull(); | ||
| } finally { | ||
| vi.unstubAllGlobals(); | ||
| } | ||
| }); | ||
|
|
||
| it("renders a safe empty summary when the runtime section collection is not an array", () => { | ||
| const song = songWithIntro(); | ||
| (song as unknown as { sections: unknown }).sections = null; | ||
|
|
||
| render(<PlayerFeature title="Player" song={song} />); | ||
|
|
||
| expect(screen.getByText("No intro yet. Stay on tonight's map until the start is labeled.")).toBeTruthy(); | ||
| expect(screen.getByText("0 sections")).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("renders a safe empty summary when the runtime section collection is sparse", () => { | ||
| const song = songWithIntro(); | ||
| const sparseSections: typeof song.sections = new Array(2); | ||
| sparseSections[1] = song.sections[1]!; | ||
| song.sections = sparseSections; | ||
|
|
||
| render(<PlayerFeature title="Player" song={song} />); | ||
|
|
||
| expect(screen.getByText("No intro yet. Stay on tonight's map until the start is labeled.")).toBeTruthy(); | ||
| expect(screen.getByText("0 sections")).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("renders a safe empty summary when sections is a throwing own accessor", () => { | ||
| const song = songWithIntro(); | ||
| Object.defineProperty(song, "sections", { | ||
| configurable: true, | ||
| enumerable: true, | ||
| get() { | ||
| throw new Error("sections getter must stay data"); | ||
| } | ||
| }); | ||
|
|
||
| expect(() => render(<PlayerFeature title="Player" song={song} />)).not.toThrow(); | ||
| expect(screen.getByText("No intro yet. Stay on tonight's map until the start is labeled.")).toBeTruthy(); | ||
| expect(screen.getByText("0 sections")).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("renders a safe empty summary when a song Proxy throws on sections access", () => { | ||
| const song = songWithIntro(); | ||
| const proxiedSong = new Proxy(song, { | ||
| get(target, key, receiver) { | ||
| if (key === "sections") { | ||
| throw new Error("sections get trap"); | ||
| } | ||
| return Reflect.get(target, key, receiver); | ||
| } | ||
| }); | ||
|
|
||
| expect(() => render(<PlayerFeature title="Player" song={proxiedSong} />)).not.toThrow(); | ||
| expect(screen.getByText("No intro yet. Stay on tonight's map until the start is labeled.")).toBeTruthy(); | ||
| expect(screen.getByText("0 sections")).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("omits malformed runtime section elements without crashing the player summary", () => { | ||
| const song = songWithIntro(); | ||
| song.sections = [null, song.sections[1]!] as unknown as typeof song.sections; | ||
|
|
||
| render(<PlayerFeature title="Player" song={song} />); | ||
|
|
||
| expect(screen.getByText("1 section")).toBeTruthy(); | ||
| expect(screen.getByText("verse")).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("does not pass an object-valued runtime song title into React copy", () => { | ||
| const song = songWithIntro(); | ||
| (song as unknown as { title: unknown }).title = { unsafe: "not-copy" }; | ||
|
|
||
| expect(() => render(<PlayerFeature title="Player" song={song} />)).not.toThrow(); | ||
| expect(screen.queryByText("not-copy")).toBeNull(); | ||
| }); | ||
| }); |
|
devin-ai-integration[bot] marked this conversation as resolved.
|
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
37 changes: 37 additions & 0 deletions
37
apps/desktop/src/features/workspace/FirstIntroCallout.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,37 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstIntroCallout } from "./FirstIntroCallout"; | ||
|
|
||
| describe("FirstIntroCallout Korean role copy", () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("keeps vowel-ending dynamic role names particle-safe before and after the intro action", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const song = createDemoRehearsalSong(); | ||
| const seed = song.sections[0]!; | ||
| const intro = structuredClone(seed); | ||
| intro.id = "intro-particle"; | ||
| intro.label = "intro"; | ||
| intro.timeRange = { start: 0, end: 8 }; | ||
| intro.roles = [{ ...seed.roles[0]!, id: "piano", name: "피아노", rehearsalPriority: "high" }]; | ||
| intro.partGraph = [ | ||
| { role_id: "piano", is_active: true, handoff_to: [], handoff_from: [] } | ||
| ]; | ||
| song.sections = [intro]; | ||
|
|
||
| const onHearIntro = vi.fn(); | ||
| render(<FirstIntroCallout song={song} actionMode="callback-only" onHearIntro={onHearIntro} />); | ||
|
|
||
| expect(screen.getByText("0:00 인트로에서 피아노 파트가 시작합니다.")).toBeTruthy(); | ||
| expect(screen.queryByText(/피아노이/)).toBeNull(); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "0:00에 피아노 시작 듣기" })); | ||
|
|
||
| expect(onHearIntro).toHaveBeenCalledWith(0); | ||
| expect(screen.getByText("0:00에서 피아노 파트와 함께 카운트인하세요. 같이 시작하세요.")).toBeTruthy(); | ||
| expect(screen.queryByText(/피아노과/)).toBeNull(); | ||
| }); | ||
| }); |
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.