-
Notifications
You must be signed in to change notification settings - Fork 0
feat(workspace): guide tonight's first entrance on map and player #912
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
62
commits into
develop
Choose a base branch
from
feat/player-hear-first-entrance
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.
Open
Changes from all commits
Commits
Show all changes
62 commits
Select commit
Hold shift + click to select a range
f56b4e9
feat(workspace): hear tonight's first entrance from the map
seonghobae fbaa553
test(workspace): keep entrance placeholders literal
seonghobae 5abfcf8
fix(workspace): interpolate entrance copy once
seonghobae 4050a31
test(workspace): cover first entrance song replacement
seonghobae 6141af5
fix(workspace): reset heard state for changed entrance
seonghobae bd632dd
docs(design): clarify first entrance unavailable action contract
seonghobae 03250e8
fix(changelog): preserve existing unreleased feature notes
seonghobae e991f4f
test(workspace): lock first entrance state transitions
seonghobae 5b44724
test(workspace): reject malformed entrance metadata
seonghobae 6810002
test(player): require a real entrance playback action
seonghobae 94185c1
fix(workspace): bind first entrance action to its surface
seonghobae 8bc71d8
fix(workspace): reject unsafe first entrance metadata
seonghobae a71ce94
fix(player): delegate first entrance playback
seonghobae 74cd028
fix(i18n): use musical first-entry terminology
seonghobae cad86b8
docs(changelog): describe executable first entrance actions
seonghobae 961b846
docs(architecture): align first entrance behavior
seonghobae 71bc5bb
docs(design): record first entrance Figma and anchor contract
seonghobae e59b974
test(workspace): distinguish map navigation from playback
seonghobae d496ac6
fix(workspace): label map navigation honestly
seonghobae 6ccdfab
feat(i18n): distinguish entrance navigation from playback
seonghobae db05549
feat(i18n): localize entrance map navigation
seonghobae 827f0d7
docs(workspace): describe entrance navigation honestly
seonghobae e03bd5b
fix(docs): preserve release history while clarifying entrance action
seonghobae ef3358e
docs(workspace): align entrance action architecture copy
seonghobae 03647cd
test(workspace): decouple entrance navigation from analysis ids
seonghobae a51d2d8
fix(workspace): use renderer-owned entrance navigation targets
seonghobae aa466dd
fix(workspace): keep entrance analysis ids out of DOM authority
seonghobae 55b73a8
test(workspace): align entrance map contracts with current behavior
seonghobae 0262b8e
test(a11y): require reduced-motion entrance navigation
seonghobae b754127
fix(a11y): honor reduced motion for entrance navigation
seonghobae 3c2a006
docs(a11y): record reduced-motion navigation contract
seonghobae dd9434e
docs(changelog): note reduced-motion entrance scroll
seonghobae a9b22e7
test(workspace): require active first-entrance part
seonghobae 791fa11
fix(workspace): require active first-entrance role
seonghobae 6a749bd
test(workspace): align entrance property graph
seonghobae 960eb49
test: reject non-boolean entrance activity flags
seonghobae 46a6829
fix: require boolean entrance activity evidence
seonghobae 199dbf5
test: reject malformed entrance role ids
seonghobae 5450fb7
fix: reject malformed entrance role ids
seonghobae 1201fae
test(workspace): require executed entrance actions
seonghobae f98608b
test(i18n): use grammatical Korean role copy
seonghobae 3a418a1
feat(i18n): localize entrance section forms
seonghobae a571ead
fix(workspace): arm entrance only after action
seonghobae fd3d88f
fix(workspace): harden first entrance navigation
seonghobae f9a66f7
fix(workspace): reject malformed entrance metadata
seonghobae f27ce38
fix(workspace): keep entrance section lookup typed
seonghobae 62ba822
test(workspace): lock particle-safe Korean entrance copy
seonghobae b8f588a
fix(workspace): keep Korean entrance role copy particle-safe
seonghobae 5466880
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae f6ada09
docs(changelog): remove duplicate 0.1.4 test entry
seonghobae e5d3d0e
test(workspace): require production song-structure anchor
seonghobae ec1cc3b
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae 88b5400
fix(workspace): remove test-only entrance selector
seonghobae 8fc1f21
test(workspace): fail closed on ambiguous entrance grids
seonghobae 9473742
fix(workspace): fail closed on ambiguous entrance grids
seonghobae a54ffbd
fix(workspace): stabilize entrance navigation anchor
seonghobae cead41a
test(a11y): keep first-entrance region name stable
seonghobae 697ef10
fix(a11y): keep first-entrance region name concise
seonghobae b63219f
test(player): require shipped first-entrance surface
seonghobae 6e20c4c
fix(player): localize shipped player navigation
seonghobae fcca36c
fix(player): localize shipped player navigation
seonghobae 0c39a81
fix(player): expose first-entrance guidance in shipped app
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,40 @@ | ||
| import { fireEvent, render, screen, waitFor } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { describe, expect, it, vi } from "vitest"; | ||
| import { App } from "./App"; | ||
|
|
||
| vi.mock("./features/score/pdfjs", () => ({ | ||
| configureScorePdfWorker: vi.fn(), | ||
| loadScorePdf: vi.fn(() => ({ | ||
| promise: Promise.resolve({ numPages: 1, getPage: vi.fn() }), | ||
| destroy: vi.fn(() => Promise.resolve()) | ||
| })) | ||
| })); | ||
|
|
||
| vi.mock("./lib/analysis", async (importActual) => { | ||
| const actual = await importActual<typeof import("./lib/analysis")>(); | ||
|
|
||
| return { | ||
| ...actual, | ||
| loadProject: vi.fn(async () => createDemoRehearsalSong()) | ||
| }; | ||
| }); | ||
|
|
||
| describe("App player reachability", () => { | ||
| it("exposes the analyzed song's first-entrance guidance through the shipped Player view without a dead playback action", async () => { | ||
| render(<App />); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: /open project/i })); | ||
|
|
||
| const playerButton = await screen.findByRole("button", { name: /^Player$/i }); | ||
| fireEvent.click(playerButton); | ||
|
|
||
| await waitFor(() => { | ||
| expect(screen.getByRole("heading", { name: /^Player$/i })).toBeTruthy(); | ||
| }); | ||
| expect(screen.getByText(/^Bass Guitar enters the verse at 0:10\./)).toBeTruthy(); | ||
| expect( | ||
| screen.queryByRole("button", { name: "Hear Bass Guitar enter the verse at 0:10" }) | ||
| ).toBeNull(); | ||
| }); | ||
| }); | ||
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,35 @@ | ||
| 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"; | ||
|
|
||
| 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 entrance from this player.") | ||
| ).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("names tonight's first entrance without a dead action when playback is unavailable", () => { | ||
| render(<PlayerFeature title="Player" song={createDemoRehearsalSong()} />); | ||
|
|
||
| expect(screen.getByText(/^Bass Guitar enters the verse at 0:10\./)).toBeTruthy(); | ||
| expect(screen.queryByRole("button", { name: "Hear Bass Guitar enter the verse at 0:10" })).toBeNull(); | ||
| }); | ||
|
|
||
| it("uses the player playback callback for the first entrance action", () => { | ||
| const onPlayFromSeconds = vi.fn(); | ||
| render( | ||
| <PlayerFeature | ||
| title="Player" | ||
| song={createDemoRehearsalSong()} | ||
| onPlayFromSeconds={onPlayFromSeconds} | ||
| /> | ||
| ); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "Hear Bass Guitar enter the verse at 0:10" })); | ||
| expect(onPlayFromSeconds).toHaveBeenCalledTimes(1); | ||
| expect(onPlayFromSeconds).toHaveBeenCalledWith(10); | ||
| }); | ||
| }); |
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 |
|---|---|---|
| @@ -1,56 +1,34 @@ | ||
| import type { RehearsalSong } from "@bandscope/shared-types"; | ||
| import { FirstEntranceCallout } from "../workspace/FirstEntranceCallout"; | ||
| import { createTranslator, detectPreferredLocale } from "../../i18n"; | ||
|
|
||
| /** Documented. */ | ||
| export function PlayerFeature(props: { title: string; song?: RehearsalSong | null }) { | ||
| const { title, song } = props; | ||
| type PlayerFeatureProps = { | ||
| title: string; | ||
| song?: RehearsalSong | null; | ||
| onPlayFromSeconds?: (startSeconds: number) => void; | ||
| }; | ||
|
|
||
| /** Player surface that names tonight's first entrance and delegates playback to the owning player. */ | ||
| export function PlayerFeature({ title, song, onPlayFromSeconds }: PlayerFeatureProps) { | ||
| const t = createTranslator(detectPreferredLocale()); | ||
|
|
||
| if (!song) { | ||
| return ( | ||
| <section style={{ padding: "24px" }}> | ||
| <h2>{title}</h2> | ||
| <p style={{ color: "#999" }}>No song loaded. Start an analysis to use the player.</p> | ||
| <section className="p-6"> | ||
| <h2 className="text-xl font-black text-white">{title}</h2> | ||
| <p className="mt-2 text-sm leading-6 text-slate-400">{t("firstEntranceNeedsSong")}</p> | ||
| </section> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <section style={{ padding: "24px" }}> | ||
| <h2>{title}</h2> | ||
| <div | ||
| style={{ | ||
| padding: "16px", | ||
| backgroundColor: "#fafafa", | ||
| borderRadius: "8px", | ||
| border: "1px solid #e8e8e8", | ||
| }} | ||
| > | ||
| <div style={{ marginBottom: "12px" }}> | ||
| <strong>{song.title}</strong> | ||
| <span style={{ color: "#666", marginLeft: "8px" }}> | ||
| {song.sections.length} {song.sections.length === 1 ? "section" : "sections"} | ||
| </span> | ||
| </div> | ||
| <div style={{ display: "flex", gap: "8px", flexWrap: "wrap" }}> | ||
| {song.sections.map((section) => ( | ||
| <span | ||
| key={section.id} | ||
| style={{ | ||
| padding: "4px 12px", | ||
| borderRadius: "16px", | ||
| backgroundColor: "#fff", | ||
| border: "1px solid #d9d9d9", | ||
| fontSize: "0.85em", | ||
| textTransform: "capitalize", | ||
| }} | ||
| > | ||
| {section.label} | ||
| </span> | ||
| ))} | ||
| </div> | ||
| <div style={{ marginTop: "16px", color: "#999", fontSize: "0.85em" }}> | ||
| Audio playback requires the desktop app with a local audio source. | ||
| </div> | ||
| </div> | ||
| <section className="space-y-4 p-6"> | ||
| <h2 className="text-xl font-black text-white">{title}</h2> | ||
| <FirstEntranceCallout | ||
| song={song} | ||
| actionMode="callback-only" | ||
| onHearEntrance={onPlayFromSeconds} | ||
| /> | ||
| </section> | ||
| ); | ||
|
seonghobae marked this conversation as resolved.
|
||
| } | ||
20 changes: 20 additions & 0 deletions
20
apps/desktop/src/features/workspace/FirstEntranceCallout.accessible-name.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,20 @@ | ||
| import { render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { describe, expect, it } from "vitest"; | ||
| import { FirstEntranceCallout } from "./FirstEntranceCallout"; | ||
|
|
||
| describe("FirstEntranceCallout accessible name", () => { | ||
| it("keeps the concise first-entrance region name when no entrance exists", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| song.sections = []; | ||
|
|
||
| render(<FirstEntranceCallout song={song} />); | ||
|
|
||
| expect( | ||
| screen.getByRole("complementary", { name: "Tonight's first entrance" }) | ||
| ).toBeTruthy(); | ||
| expect( | ||
| screen.getByText("No first entrance yet. Stay on tonight's map until a section has a part.") | ||
| ).toBeTruthy(); | ||
| }); | ||
| }); |
53 changes: 53 additions & 0 deletions
53
apps/desktop/src/features/workspace/FirstEntranceCallout.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,53 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { FirstEntranceCallout } from "./FirstEntranceCallout"; | ||
|
|
||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| document.querySelector("#workspace-song-structure-grid")?.remove(); | ||
| }); | ||
|
|
||
| /** Mount the renderer-owned song-structure target used by workspace navigation. */ | ||
| function appendSongStructureTarget() { | ||
| const grid = document.createElement("div"); | ||
| grid.id = "workspace-song-structure-grid"; | ||
| const target = document.createElement("div"); | ||
| const scrollIntoView = vi.fn(); | ||
| Object.defineProperty(target, "scrollIntoView", { | ||
| configurable: true, | ||
| value: scrollIntoView | ||
| }); | ||
| grid.appendChild(target); | ||
| document.body.appendChild(grid); | ||
| return scrollIntoView; | ||
| } | ||
|
|
||
| describe("FirstEntranceCallout reduced-motion navigation", () => { | ||
| it("avoids smooth scrolling when the user requests reduced motion", () => { | ||
| const scrollIntoView = appendSongStructureTarget(); | ||
| vi.stubGlobal( | ||
| "matchMedia", | ||
| vi.fn((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() | ||
| })) | ||
| ); | ||
|
|
||
| render(<FirstEntranceCallout song={createDemoRehearsalSong()} />); | ||
| fireEvent.click( | ||
| screen.getByRole("button", { name: "Open Bass Guitar entrance in the verse at 0:10" }) | ||
| ); | ||
|
|
||
| expect(scrollIntoView).toHaveBeenCalledWith({ | ||
| block: "nearest", | ||
| behavior: "auto" | ||
| }); | ||
| }); | ||
| }); |
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.