Repository navigation
feat(workspace): name using your own song as the first next action #980
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
Changes from 2 commits
a6d5b6f
e929b60
843023f
a35a90e
18caa7a
3126f8e
5fa1ad0
b347b84
cda99c2
9bc2ead
571b913
ab719fa
bc92235
14dfb02
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,92 @@ | ||
| import { fireEvent, render, screen, waitFor } from "@testing-library/react"; | ||
| import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
| import { App } from "./App"; | ||
|
|
||
| const analysisMocks = vi.hoisted(() => ({ | ||
| getAnalysisJobStatus: vi.fn(), | ||
| importYoutubeUrl: vi.fn(), | ||
| isSupportedYoutubeUrl: vi.fn(() => false), | ||
| loadProject: vi.fn(), | ||
| saveProject: vi.fn(), | ||
| selectLocalAudioSource: vi.fn(), | ||
| startAnalysisJob: vi.fn(), | ||
| subscribeToAnalysisJobUpdates: vi.fn(async () => () => undefined), | ||
| })); | ||
|
|
||
| vi.mock("./lib/analysis", () => ({ | ||
| createDefaultAnalysisRequest: () => ({ | ||
| sourceKind: "demo", | ||
| sourceLabel: "Late Night Set", | ||
| roleFocus: ["bass-guitar", "keys-right", "lead-vocal"], | ||
| }), | ||
| getAnalysisJobStatus: analysisMocks.getAnalysisJobStatus, | ||
| importYoutubeUrl: analysisMocks.importYoutubeUrl, | ||
| isSupportedYoutubeUrl: analysisMocks.isSupportedYoutubeUrl, | ||
| loadProject: analysisMocks.loadProject, | ||
| MAX_YOUTUBE_URL_LENGTH: 2048, | ||
| saveProject: analysisMocks.saveProject, | ||
| selectLocalAudioSource: analysisMocks.selectLocalAudioSource, | ||
| startAnalysisJob: analysisMocks.startAnalysisJob, | ||
| subscribeToAnalysisJobUpdates: analysisMocks.subscribeToAnalysisJobUpdates, | ||
| })); | ||
|
|
||
| vi.mock("./features/score/ScoreView", () => ({ | ||
| ScoreView: () => <div>Score view</div>, | ||
| })); | ||
|
|
||
| const selectedBootstrap = { | ||
| projectId: "project-local-intake", | ||
| sourceMode: "reference", | ||
| projectRoot: "/tmp/bandscope/projects/project-local-intake", | ||
| cacheRoot: "/tmp/bandscope/cache/project-local-intake", | ||
| tempRoot: "/tmp/bandscope/temp/project-local-intake", | ||
| source: { | ||
| sourcePath: "/Users/test/Music/selected-song.wav", | ||
| fileName: "selected-song.wav", | ||
| extension: "wav", | ||
| fileSizeBytes: 1024, | ||
| }, | ||
| }; | ||
|
|
||
| /** | ||
| * Security Notes: | ||
| * - The selected path is a synthetic test fixture and is not asserted as buyer-visible copy. | ||
| * - This test mocks the existing local-picker boundary and adds no filesystem, network, or IPC authority. | ||
| */ | ||
| describe("App local song intake concurrency", () => { | ||
| beforeEach(() => { | ||
| for (const mock of Object.values(analysisMocks)) { | ||
| mock.mockReset(); | ||
| } | ||
| analysisMocks.isSupportedYoutubeUrl.mockReturnValue(false); | ||
| analysisMocks.subscribeToAnalysisJobUpdates.mockResolvedValue(() => undefined); | ||
| }); | ||
|
|
||
| it("allows only one local picker while the first selection is pending", async () => { | ||
| let resolveSelection: ((value: { ok: true; bootstrap: typeof selectedBootstrap }) => void) | undefined; | ||
| analysisMocks.selectLocalAudioSource.mockImplementation( | ||
| () => | ||
| new Promise((resolve) => { | ||
| resolveSelection = resolve; | ||
| }), | ||
| ); | ||
|
|
||
| render(<App />); | ||
|
|
||
| const emptyAction = screen.getByRole("button", { name: "Use my own song" }); | ||
| const headerAction = screen.getByRole("button", { name: "Choose local audio" }); | ||
| fireEvent.click(emptyAction); | ||
|
|
||
| await waitFor(() => { | ||
| expect(emptyAction).toBeDisabled(); | ||
|
Check failure on line 81 in apps/desktop/src/App.localSelectionConcurrency.test.tsx
|
||
| expect(headerAction).toBeDisabled(); | ||
| }); | ||
|
|
||
| fireEvent.click(emptyAction); | ||
| fireEvent.click(headerAction); | ||
| expect(analysisMocks.selectLocalAudioSource).toHaveBeenCalledTimes(1); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| resolveSelection?.({ ok: true, bootstrap: selectedBootstrap }); | ||
| await waitFor(() => expect(screen.getByText("selected-song.wav")).toBeTruthy()); | ||
| }); | ||
| }); | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
seonghobae marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { fireEvent, render, screen } from "@testing-library/react"; | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import { EmptyState, LoadingState } from "./WorkspaceStates"; | ||
|
|
||
| const originalLanguage = window.navigator.language; | ||
|
|
||
| function setNavigatorLanguage(language: string) { | ||
| Object.defineProperty(window.navigator, "language", { | ||
| configurable: true, | ||
| value: language | ||
| }); | ||
| } | ||
|
|
||
| describe("WorkspaceStates empty first-run card", () => { | ||
| afterEach(() => { | ||
| setNavigatorLanguage(originalLanguage); | ||
| }); | ||
|
|
||
| it("names using a local song as the next rehearsal action", () => { | ||
| const onUseOwnSong = vi.fn(); | ||
| render(<EmptyState onUseOwnSong={onUseOwnSong} />); | ||
|
|
||
| fireEvent.click(screen.getByRole("button", { name: "Use my own song" })); | ||
|
|
||
| expect(onUseOwnSong).toHaveBeenCalledTimes(1); | ||
| expect(screen.getByRole("heading", { name: "Start tonight's rehearsal" })).toBeTruthy(); | ||
| expect(screen.getByText(/keeps the file local/i)).toBeTruthy(); | ||
| expect(screen.queryByRole("button", { name: /try the demo/i })).toBeNull(); | ||
| }); | ||
|
|
||
| it("disables the next action while intake is already running", () => { | ||
| const onUseOwnSong = vi.fn(); | ||
| render(<EmptyState onUseOwnSong={onUseOwnSong} chooseDisabled />); | ||
|
|
||
| expect(screen.getByRole("button", { name: "Use my own song" })).toBeDisabled(); | ||
| }); | ||
|
|
||
| it("localizes the empty next-action copy", () => { | ||
| setNavigatorLanguage("ko-KR"); | ||
| render( | ||
| <> | ||
| <EmptyState onUseOwnSong={vi.fn()} /> | ||
| <LoadingState /> | ||
| </> | ||
| ); | ||
|
|
||
| expect(screen.getByRole("heading", { name: "오늘 합주를 시작하세요" })).toBeTruthy(); | ||
| expect(screen.getByRole("button", { name: "내 곡 사용하기" })).toBeTruthy(); | ||
| expect(screen.getByRole("heading", { name: "오디오 분석 중" })).toBeTruthy(); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.