271 lines
8.5 KiB
TypeScript
271 lines
8.5 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
vi.mock('../../stores/projectStore', () => ({
|
|
useProjectStore: Object.assign(
|
|
(selector: (state: Record<string, unknown>) => 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);
|
|
});
|
|
});
|