-
Notifications
You must be signed in to change notification settings - Fork 1
feat(workspace): set up tonight's part before the first entrance #910
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
37
commits into
develop
Choose a base branch
from
feat/workspace-setup-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.
Draft
Changes from all commits
Commits
Show all changes
37 commits
Select commit
Hold shift + click to select a range
5e55876
feat(workspace): arm tonight's first setup on the role strip
seonghobae 25bd74f
fix(workspace): preserve setup copy and transposition detail
seonghobae 1db3ff0
test(workspace): lock review regressions
seonghobae 12b1c4a
fix(workspace): make groove map role-aware and accessible
seonghobae ef4b354
fix(workspace): require actionable start evidence
seonghobae 1c841b0
fix(workspace): explain missing start evidence
seonghobae 6f686fe
fix(workspace): localize missing start evidence
seonghobae 6d838ca
test(workspace): assert native setup disable
seonghobae 098f634
docs(architecture): refresh current guidance date
seonghobae d27bff6
docs(design): keep groove-map contract code-current
seonghobae fd36651
docs(changelog): record workspace review repairs
seonghobae 184ad06
test(workspace): expect role-specific groove map label
seonghobae b06be3f
test(workspace): keep setup placeholders literal
seonghobae 0ad6315
fix(workspace): interpolate setup copy once
seonghobae 16abeb1
test(security): forbid dynamic regex copy interpolation
seonghobae 512c860
fix(security): use fixed copy interpolation pattern
seonghobae 61aa51f
test(workspace): make setup regressions portable
seonghobae c49d0f3
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae d14634b
docs(changelog): remove duplicated test entry
seonghobae a6bc38d
test(workspace): require visible setup label in accessible name
seonghobae 4850852
fix(a11y): include visible setup action in accessible name
seonghobae 8d1fdd2
fix(a11y): align Korean setup accessible names
seonghobae 1cc2699
test(a11y): align setup accessible-name oracle
seonghobae 4e252a4
test(changelog): preserve shipped security fix classification
seonghobae cce6ff4
fix(changelog): preserve protected security history
seonghobae fbb6973
Merge remote-tracking branch 'origin/develop' into HEAD
seonghobae b6bcecb
fix(workspace): restore setup test contract
seonghobae ec61228
test(workspace): preserve visible stem control names
seonghobae 30ebfcc
fix(a11y): preserve visible stem control names
seonghobae ad2daa5
test(workspace): align disabled control contract
seonghobae 9721802
Merge protected develop into workspace accessibility repair
seonghobae 1c3248a
test(workspace): reject malformed setup ranges
seonghobae 87e8640
fix(workspace): validate setup range evidence
seonghobae 9bd8e1b
test(release): preserve semantic setup boundaries
seonghobae 94f1c00
test(i18n): cover workspace rehearsal controls
seonghobae 0044cd5
fix(i18n): localize rehearsal controls
seonghobae 39a12a9
docs(workspace): define validated range fallback
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
180 changes: 180 additions & 0 deletions
180
apps/desktop/src/features/workspace/Workspace.review.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,180 @@ | ||
| import { readFileSync } from "node:fs"; | ||
| import { resolve } from "node:path"; | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { createDemoRehearsalSong } from "@bandscope/shared-types"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { GrooveMap } from "./GrooveMap"; | ||
| import { Workspace } from "./Workspace"; | ||
|
|
||
| /** Replace every copy of one rehearsal role so cross-section aggregation stays deterministic. */ | ||
| function replaceRole(song: ReturnType<typeof createDemoRehearsalSong>, roleId: string, replace: (role: (typeof song.sections)[number]["roles"][number]) => (typeof song.sections)[number]["roles"][number]) { | ||
| song.sections = song.sections.map((section) => ({ | ||
| ...section, | ||
| roles: section.roles.map((role) => (role.id === roleId ? replace(role) : role)) | ||
| })); | ||
| } | ||
|
|
||
| describe("Workspace review regressions", () => { | ||
| afterEach(() => { | ||
| vi.unstubAllGlobals(); | ||
| }); | ||
|
|
||
| it("keeps copy interpolation free of dynamically constructed regular expressions", () => { | ||
| const source = readFileSync(resolve(process.cwd(), "src/features/workspace/Workspace.tsx"), "utf8"); | ||
| expect(source).not.toContain("new RegExp("); | ||
| }); | ||
|
|
||
| it("labels a non-bass groove map by role, keeps keyboard focus visible, and emits one entrance anchor", () => { | ||
| render( | ||
| <GrooveMap | ||
| roleName="Lead Guitar" | ||
| entranceOnset={1} | ||
| notes={[ | ||
| { pitch: "E4", onset: 1, offset: 1.5, velocity: 0.8 }, | ||
| { pitch: "G4", onset: 1, offset: 1.5, velocity: 0.75 } | ||
| ]} | ||
| /> | ||
| ); | ||
|
|
||
| const region = screen.getByRole("region", { name: "Lead Guitar transcription groove map" }); | ||
| expect(region.className).toContain("focus-visible:ring-2"); | ||
| expect(document.querySelectorAll("#workspace-groove-entrance")).toHaveLength(1); | ||
| expect(screen.getAllByTitle(/Tonight's entrance/)).toHaveLength(2); | ||
| }); | ||
|
|
||
| it("uses the selected role name in groove-map empty and loading copy", () => { | ||
| const { rerender } = render(<GrooveMap roleName="Lead Guitar" notes={[]} />); | ||
| expect(screen.getByText("No Lead Guitar transcription yet. Use it when you want to check the groove before rehearsal.")).toBeTruthy(); | ||
|
|
||
| rerender(<GrooveMap roleName="Lead Guitar" notes={[]} isLoading />); | ||
| expect(screen.getByText("Checking the Lead Guitar line... 45%")).toBeTruthy(); | ||
| }); | ||
|
|
||
| it("localizes GrooveMap states and the unavailable loop control", () => { | ||
| vi.stubGlobal("navigator", { language: "ko-KR" }); | ||
| const song = createDemoRehearsalSong(); | ||
| const roleName = song.sections[0]!.roles[0]!.name; | ||
| const { rerender } = render(<GrooveMap roleName={roleName} notes={[]} />); | ||
|
|
||
| expect(screen.getByText(`${roleName} 채보가 아직 없습니다. 합주 전에 그루브를 확인할 때 사용하세요.`)).toBeTruthy(); | ||
|
|
||
| rerender(<GrooveMap roleName={roleName} notes={[]} isLoading />); | ||
| expect(screen.getByText(`${roleName} 파트를 확인하는 중... 45%`)).toBeTruthy(); | ||
| expect(screen.getByRole("button", { name: "취소" })).toBeTruthy(); | ||
|
|
||
| rerender(<Workspace song={song} />); | ||
| fireEvent.click(screen.getByRole("tab", { name: roleName })); | ||
| expect(screen.getByRole("button", { name: /구간 반복/ })).toHaveTextContent("구간 반복"); | ||
| }); | ||
|
|
||
| it("keeps range-backed setup available when no exact first note exists", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const roleId = song.sections[0]!.roles[0]!.id; | ||
| replaceRole(song, roleId, (role) => ({ | ||
| ...role, | ||
| setupNote: "Tune down a whole step.", | ||
| transcription: undefined, | ||
| range: { | ||
| ...role.range, | ||
| lowestNote: "C#2", | ||
| highestNote: "E3" | ||
| } | ||
| })); | ||
|
|
||
| render(<Workspace song={song} />); | ||
| fireEvent.click(screen.getByRole("tab", { name: song.sections[0]!.roles[0]!.name })); | ||
|
|
||
| const setupButton = screen.getByRole("button", { name: /then start in C#2–E3/i }); | ||
| expect(setupButton).toBeEnabled(); | ||
| const visibleLabel = setupButton.textContent?.trim() ?? ""; | ||
| expect(visibleLabel).not.toBe(""); | ||
| expect(setupButton.getAttribute("aria-label")).toContain(visibleLabel); | ||
| }); | ||
|
|
||
| it("keeps placeholder-looking role names literal in setup copy", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const roleId = song.sections[0]!.roles[0]!.id; | ||
| replaceRole(song, roleId, (role) => ({ | ||
| ...role, | ||
| name: "{low}", | ||
| setupNote: "Tune down a whole step.", | ||
| transcription: undefined, | ||
| range: { | ||
| ...role.range, | ||
| lowestNote: "C#2", | ||
| highestNote: "E3" | ||
| } | ||
| })); | ||
|
|
||
| render(<Workspace song={song} />); | ||
| fireEvent.click(screen.getByRole("tab", { name: "{low}" })); | ||
|
|
||
| expect( | ||
| screen.getByRole("button", { | ||
| name: "Set up {low} · then start in C#2–E3. Setup: Tune down a whole step. Use tonight's map" | ||
| }) | ||
| ).toBeEnabled(); | ||
| }); | ||
|
|
||
| it("keeps disabled stem controls discoverable by their visible labels", () => { | ||
| const song = createDemoRehearsalSong(); | ||
|
|
||
| render(<Workspace song={song} />); | ||
|
|
||
| fireEvent.click(screen.getByRole("tab", { name: song.sections[0]!.roles[0]!.name })); | ||
| expect(screen.getByRole("button", { name: /Play stem/ })).toHaveTextContent("Play stem"); | ||
| expect(screen.getByRole("button", { name: /Solo \/ mute others/ })).toHaveTextContent( | ||
| "Solo / mute others" | ||
| ); | ||
| }); | ||
|
|
||
| it("natively disables setup when a cue has neither an entrance nor a playable range", () => { | ||
| const song = createDemoRehearsalSong(); | ||
| const roleId = song.sections[0]!.roles[0]!.id; | ||
| replaceRole(song, roleId, (role) => ({ | ||
| ...role, | ||
| setupNote: "Tune down a whole step.", | ||
| transcription: undefined, | ||
| range: { | ||
| ...role.range, | ||
| lowestNote: " ", | ||
| highestNote: " " | ||
| } | ||
| })); | ||
|
|
||
| render(<Workspace song={song} />); | ||
| fireEvent.click(screen.getByRole("tab", { name: song.sections[0]!.roles[0]!.name })); | ||
|
|
||
| const setupButton = screen.getByRole("button", { | ||
| name: "No first entrance or playable range yet. Stay on tonight's map." | ||
| }); | ||
| expect(setupButton).toBeDisabled(); | ||
| }); | ||
|
|
||
| it.each([ | ||
| ["none", "none"], | ||
| ["E3", "C#2"], | ||
| ["low", "high"] | ||
| ])("rejects malformed setup range %s–%s", (lowestNote, highestNote) => { | ||
| const song = createDemoRehearsalSong(); | ||
| const roleId = song.sections[0]!.roles[0]!.id; | ||
| replaceRole(song, roleId, (role) => ({ | ||
| ...role, | ||
| setupNote: "Tune down a whole step.", | ||
| transcription: undefined, | ||
| range: { | ||
| ...role.range, | ||
| lowestNote, | ||
| highestNote | ||
| } | ||
| })); | ||
|
|
||
| render(<Workspace song={song} />); | ||
| fireEvent.click(screen.getByRole("tab", { name: song.sections[0]!.roles[0]!.name })); | ||
|
|
||
| const setupButton = screen.getByRole("button", { | ||
| name: "No first entrance or playable range yet. Stay on tonight's map." | ||
| }); | ||
| expect(setupButton).toBeDisabled(); | ||
| }); | ||
| }); |
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.