import { beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('../../stores/projectStore', () => ({ useProjectStore: Object.assign( (selector: (state: Record) => unknown) => selector({ maxBars: 8, tracks: [], updateTrack: vi.fn(), timeSignature: { numerator: 4, denominator: 4 }, showChatBox: false, showKGOnePanel: false, showEventListPanel: false, showInstrumentSelection: false, keySignature: 'C major', selectedMode: 'ionian', setSelectedMode: vi.fn(), playheadPosition: 0, isPlaying: false, autoScrollEnabled: false, bpm: 120, pianoRollScrollRequest: null, selectedNoteIds: [], automationRedrawVersion: 0, }), { getState: () => ({ setAutoScrollEnabled: vi.fn(), }), setState: vi.fn(), } ), })); import { createPendingModeSwitchRequest, getRegionStartScrollLeft, getRegionPlayheadRelation, getScrollLeftForViewportRequest, } from './PianoRoll'; import type { SheetMeasureMetric } from './sheetNotationTypes'; function createContainer({ clientWidth, scrollWidth }: { clientWidth: number; scrollWidth: number }): HTMLDivElement { return { clientWidth, scrollWidth, } as HTMLDivElement; } describe('PianoRoll viewport switch helpers', () => { beforeEach(() => { document.documentElement.style.setProperty('--region-grid-beat-width', '40px'); document.documentElement.style.setProperty('--region-piano-key-width', '60px'); }); it('classifies playhead position relative to the active region', () => { expect(getRegionPlayheadRelation(15, 16, 24)).toBe('before'); expect(getRegionPlayheadRelation(20, 16, 24)).toBe('inside'); expect(getRegionPlayheadRelation(25, 16, 24)).toBe('after'); }); it('uses the zoomed beat width when scrolling to a different region in piano-roll view', () => { document.documentElement.style.setProperty('--region-grid-beat-width', '80px'); document.documentElement.style.setProperty('--region-grid-bar-width', 'calc(var(--region-grid-beat-width) * var(--time-signature-numerator))'); expect(getRegionStartScrollLeft(16)).toBe(1280); }); it('centers an in-region playhead when switching to region-scope sheet view', () => { const request = createPendingModeSwitchRequest({ playheadBeat: 20, regionStartBeat: 16, regionEndBeat: 24, sourceSheetMusicViewEnabled: false, destinationSheetMusicViewEnabled: true, destinationSheetMusicTrackScopeEnabled: false, }); expect(request).toMatchObject({ alignment: 'center', anchorBeat: 20, clampScope: 'region', }); const metrics: SheetMeasureMetric[] = [ { barIndex: 0, startBeat: 0, endBeat: 4, leftPx: 0, widthPx: 200 }, { barIndex: 1, startBeat: 4, endBeat: 8, leftPx: 200, widthPx: 200 }, ]; const scrollLeft = getScrollLeftForViewportRequest({ request, container: createContainer({ clientWidth: 200, scrollWidth: 400 }), sheetMeasureMetrics: metrics, activeRegionStartBeat: 16, activeRegionEndBeat: 24, songEndBeat: 32, }); expect(scrollLeft).toBe(100); }); it('snaps to the region start when the playhead is before the active region', () => { const request = createPendingModeSwitchRequest({ playheadBeat: 12, regionStartBeat: 16, regionEndBeat: 24, sourceSheetMusicViewEnabled: true, destinationSheetMusicViewEnabled: false, destinationSheetMusicTrackScopeEnabled: false, }); expect(request).toMatchObject({ alignment: 'region-start', anchorBeat: 16, clampScope: 'region', }); const scrollLeft = getScrollLeftForViewportRequest({ request, container: createContainer({ clientWidth: 260, scrollWidth: 2000 }), sheetMeasureMetrics: [], activeRegionStartBeat: 16, activeRegionEndBeat: 24, songEndBeat: 32, }); expect(scrollLeft).toBe(640); }); it('snaps to the region end when the playhead is after the active region', () => { const request = createPendingModeSwitchRequest({ playheadBeat: 28, regionStartBeat: 16, regionEndBeat: 24, sourceSheetMusicViewEnabled: true, destinationSheetMusicViewEnabled: true, destinationSheetMusicTrackScopeEnabled: false, }); expect(request).toMatchObject({ alignment: 'region-end', anchorBeat: 24, clampScope: 'region', }); const metrics: SheetMeasureMetric[] = [ { barIndex: 0, startBeat: 0, endBeat: 4, leftPx: 0, widthPx: 200 }, { barIndex: 1, startBeat: 4, endBeat: 8, leftPx: 200, widthPx: 200 }, ]; const scrollLeft = getScrollLeftForViewportRequest({ request, container: createContainer({ clientWidth: 200, scrollWidth: 400 }), sheetMeasureMetrics: metrics, activeRegionStartBeat: 16, activeRegionEndBeat: 24, songEndBeat: 32, }); expect(scrollLeft).toBe(200); }); it('uses the track-scope special case only when entering sheet music from piano roll', () => { const specialCaseRequest = createPendingModeSwitchRequest({ playheadBeat: 28, regionStartBeat: 16, regionEndBeat: 24, sourceSheetMusicViewEnabled: false, destinationSheetMusicViewEnabled: true, destinationSheetMusicTrackScopeEnabled: true, }); const regularTrackScopeRequest = createPendingModeSwitchRequest({ playheadBeat: 28, regionStartBeat: 16, regionEndBeat: 24, sourceSheetMusicViewEnabled: true, destinationSheetMusicViewEnabled: true, destinationSheetMusicTrackScopeEnabled: true, }); expect(specialCaseRequest).toMatchObject({ alignment: 'center', anchorBeat: 28, clampScope: 'track', }); expect(regularTrackScopeRequest).toMatchObject({ alignment: 'region-end', anchorBeat: 24, clampScope: 'region', }); }); it('treats sheet-music to piano-roll switches as region-scoped even when source sheet view was track-scoped', () => { const request = createPendingModeSwitchRequest({ playheadBeat: 20, regionStartBeat: 16, regionEndBeat: 24, sourceSheetMusicViewEnabled: true, destinationSheetMusicViewEnabled: false, destinationSheetMusicTrackScopeEnabled: false, }); expect(request).toMatchObject({ alignment: 'center', anchorBeat: 20, clampScope: 'region', }); }); it('clamps centered piano-roll scroll at the start and end of the active region', () => { const container = createContainer({ clientWidth: 260, scrollWidth: 2000 }); const startClamp = getScrollLeftForViewportRequest({ request: { sourceSheetMusicViewEnabled: false, destinationSheetMusicViewEnabled: false, destinationSheetMusicTrackScopeEnabled: false, alignment: 'center', anchorBeat: 16, clampScope: 'region', }, container, sheetMeasureMetrics: [], activeRegionStartBeat: 16, activeRegionEndBeat: 24, songEndBeat: 32, }); const endClamp = getScrollLeftForViewportRequest({ request: { sourceSheetMusicViewEnabled: false, destinationSheetMusicViewEnabled: false, destinationSheetMusicTrackScopeEnabled: false, alignment: 'center', anchorBeat: 24, clampScope: 'region', }, container, sheetMeasureMetrics: [], activeRegionStartBeat: 16, activeRegionEndBeat: 24, songEndBeat: 32, }); expect(startClamp).toBe(640); expect(endClamp).toBe(760); }); it('centers the playhead in track-scope sheet view with song-bound clamping', () => { const request = createPendingModeSwitchRequest({ playheadBeat: 14, regionStartBeat: 16, regionEndBeat: 24, sourceSheetMusicViewEnabled: false, destinationSheetMusicViewEnabled: true, destinationSheetMusicTrackScopeEnabled: true, }); const metrics: SheetMeasureMetric[] = [ { barIndex: 0, startBeat: 0, endBeat: 4, leftPx: 0, widthPx: 200 }, { barIndex: 1, startBeat: 4, endBeat: 8, leftPx: 200, widthPx: 200 }, { barIndex: 2, startBeat: 8, endBeat: 12, leftPx: 400, widthPx: 200 }, { barIndex: 3, startBeat: 12, endBeat: 16, leftPx: 600, widthPx: 200 }, ]; const scrollLeft = getScrollLeftForViewportRequest({ request, container: createContainer({ clientWidth: 200, scrollWidth: 800 }), sheetMeasureMetrics: metrics, activeRegionStartBeat: 16, activeRegionEndBeat: 24, songEndBeat: 16, }); expect(scrollLeft).toBe(600); }); });