Files
KGStudio/src/components/piano-roll/PianoRoll.test.ts
T

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);
});
});