feat: added Global tab to Event List panel
This commit is contained in:
@@ -5,7 +5,16 @@ import EventListPanel from './EventListPanel';
|
||||
import { KGMidiControllerEvent } from '../core/midi/KGMidiControllerEvent';
|
||||
import { KGMidiNote } from '../core/midi/KGMidiNote';
|
||||
import { KGMidiPitchBend } from '../core/midi/KGMidiPitchBend';
|
||||
import { KGProject } from '../core/KGProject';
|
||||
import { KGChordRegion } from '../core/region/KGChordRegion';
|
||||
import { KGKeySignatureRegion } from '../core/region/KGKeySignatureRegion';
|
||||
import { KGMarkerRegion } from '../core/region/KGMarkerRegion';
|
||||
import { KGRegion } from '../core/region/KGRegion';
|
||||
import { KGTempoRegion } from '../core/region/KGTempoRegion';
|
||||
import { KGChordTrack } from '../core/global-track/KGChordTrack';
|
||||
import { KGMarkerTrack } from '../core/global-track/KGMarkerTrack';
|
||||
import { KGSignatureTrack } from '../core/global-track/KGSignatureTrack';
|
||||
import { KGTempoTrack } from '../core/global-track/KGTempoTrack';
|
||||
import { KGAudioRegion } from '../core/region/KGAudioRegion';
|
||||
import { KGAudioTrack } from '../core/track/KGAudioTrack';
|
||||
import { KGTrackAutomationPoint } from '../core/track/KGTrackAutomationPoint';
|
||||
@@ -16,6 +25,7 @@ import {
|
||||
createMockMidiRegion,
|
||||
createMockMidiTrack,
|
||||
} from '../test/utils/mock-data';
|
||||
import { showAlert } from '../util/dialogUtil';
|
||||
|
||||
const clickDropdownOption = (label: string) => {
|
||||
const option = Array.from(document.querySelectorAll('.quant-option'))
|
||||
@@ -58,8 +68,23 @@ const audioTrack = new KGAudioTrack('Audio Track', 2, -3);
|
||||
audioTrack.setTrackIndex(1);
|
||||
audioTrack.setRegions([audioRegion]);
|
||||
|
||||
const markerTrack = new KGMarkerTrack();
|
||||
const tempoTrack = new KGTempoTrack();
|
||||
const signatureTrack = new KGSignatureTrack();
|
||||
const chordTrack = new KGChordTrack();
|
||||
|
||||
const markerRegion = new KGMarkerRegion('marker-1', markerTrack.getId(), markerTrack.getTrackIndex(), 'Intro', 0, 4);
|
||||
const tempoRegionA = new KGTempoRegion('tempo-1', tempoTrack.getId(), tempoTrack.getTrackIndex(), 120, 0, 4, 4);
|
||||
const tempoRegionB = new KGTempoRegion('tempo-2', tempoTrack.getId(), tempoTrack.getTrackIndex(), 140, 4, 28, 4);
|
||||
const keySignatureRegionA = new KGKeySignatureRegion('signature-1', signatureTrack.getId(), signatureTrack.getTrackIndex(), 'C major', 0, 4, 4);
|
||||
const keySignatureRegionB = new KGKeySignatureRegion('signature-2', signatureTrack.getId(), signatureTrack.getTrackIndex(), 'G major', 4, 28, 4);
|
||||
const chordRegion = new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Bm7b5', 4, 4);
|
||||
|
||||
let project = new KGProject();
|
||||
|
||||
type MockStoreState = {
|
||||
tracks: Array<typeof midiTrack | typeof audioTrack>;
|
||||
globalTracks: Array<typeof markerTrack | typeof tempoTrack | typeof signatureTrack | typeof chordTrack>;
|
||||
activeRegionId: string | null;
|
||||
selectedRegionIds: string[];
|
||||
selectedTrackId: string | null;
|
||||
@@ -77,6 +102,7 @@ type MockStoreState = {
|
||||
|
||||
const storeState: MockStoreState = {
|
||||
tracks: [midiTrack, audioTrack],
|
||||
globalTracks: [markerTrack, tempoTrack, signatureTrack, chordTrack],
|
||||
activeRegionId: 'region-1',
|
||||
selectedRegionIds: ['region-1'],
|
||||
selectedTrackId: '1',
|
||||
@@ -92,7 +118,9 @@ const storeState: MockStoreState = {
|
||||
bumpTrackAutomationRedrawVersion: vi.fn(),
|
||||
};
|
||||
|
||||
let selectedItems: Array<KGRegion | KGMidiNote | KGMidiPitchBend | KGMidiControllerEvent | KGTrackAutomationPoint> = [];
|
||||
let selectedItems: Array<
|
||||
KGRegion | KGMidiNote | KGMidiPitchBend | KGMidiControllerEvent | KGTrackAutomationPoint
|
||||
> = [];
|
||||
|
||||
const syncStoreSelectionFromCore = () => {
|
||||
storeState.selectedRegionIds = selectedItems
|
||||
@@ -151,19 +179,37 @@ vi.mock('../core/KGCore', () => ({
|
||||
command.execute();
|
||||
syncStoreSelectionFromCore();
|
||||
},
|
||||
getCurrentProject: () => ({
|
||||
getTracks: () => storeState.tracks,
|
||||
}),
|
||||
getCurrentProject: () => project,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('EventListPanel', () => {
|
||||
beforeEach(() => {
|
||||
project = new KGProject(
|
||||
'Test Project',
|
||||
32,
|
||||
0,
|
||||
120,
|
||||
{ numerator: 4, denominator: 4 },
|
||||
'C major',
|
||||
'ionian',
|
||||
false,
|
||||
[0, 0],
|
||||
1,
|
||||
[midiTrack, audioTrack],
|
||||
);
|
||||
|
||||
selectedItems = [midiRegion];
|
||||
midiRegion.select();
|
||||
secondMidiRegion.deselect();
|
||||
audioRegion.deselect();
|
||||
markerRegion.deselect();
|
||||
tempoRegionA.deselect();
|
||||
tempoRegionB.deselect();
|
||||
keySignatureRegionA.deselect();
|
||||
keySignatureRegionB.deselect();
|
||||
chordRegion.deselect();
|
||||
|
||||
midiRegion.setStartFromBeat(4);
|
||||
midiRegion.setLength(4);
|
||||
@@ -183,9 +229,34 @@ describe('EventListPanel', () => {
|
||||
midiTrack.getVolumeAutomation().forEach(point => point.deselect());
|
||||
midiTrack.getPanAutomation().forEach(point => point.deselect());
|
||||
|
||||
markerRegion.setName('Intro');
|
||||
markerRegion.setStartFromBeat(0);
|
||||
markerRegion.setLength(4);
|
||||
markerTrack.setRegions([markerRegion]);
|
||||
|
||||
tempoRegionA.setBpm(120);
|
||||
tempoRegionA.setBarRange(0, 4, 4);
|
||||
tempoRegionB.setBpm(140);
|
||||
tempoRegionB.setBarRange(4, 28, 4);
|
||||
tempoTrack.setRegions([tempoRegionA, tempoRegionB]);
|
||||
|
||||
keySignatureRegionA.setKeySignature('C major');
|
||||
keySignatureRegionA.setBarRange(0, 4, 4);
|
||||
keySignatureRegionB.setKeySignature('G major');
|
||||
keySignatureRegionB.setBarRange(4, 28, 4);
|
||||
signatureTrack.setRegions([keySignatureRegionA, keySignatureRegionB]);
|
||||
|
||||
chordRegion.setSymbol('Bm7b5');
|
||||
chordRegion.setStartFromBeat(4);
|
||||
chordRegion.setLength(4);
|
||||
chordTrack.setRegions([chordRegion]);
|
||||
|
||||
project.setGlobalTracks([markerTrack, tempoTrack, signatureTrack, chordTrack]);
|
||||
|
||||
storeState.activeRegionId = 'region-1';
|
||||
storeState.selectedRegionIds = ['region-1'];
|
||||
storeState.selectedTrackId = '1';
|
||||
storeState.globalTracks = [markerTrack, tempoTrack, signatureTrack, chordTrack];
|
||||
storeState.selectedNoteIds = [];
|
||||
storeState.selectedPitchBendIds = [];
|
||||
storeState.selectedControllerEventIds = [];
|
||||
@@ -195,6 +266,7 @@ describe('EventListPanel', () => {
|
||||
storeState.refreshProjectState.mockClear();
|
||||
storeState.bumpAutomationRedrawVersion.mockClear();
|
||||
storeState.bumpTrackAutomationRedrawVersion.mockClear();
|
||||
vi.mocked(showAlert).mockClear();
|
||||
});
|
||||
|
||||
it('defaults to Region tab and preserves existing event rows', () => {
|
||||
@@ -202,6 +274,7 @@ describe('EventListPanel', () => {
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Region' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Track' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Global' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Pitch Bend')).toBeInTheDocument();
|
||||
expect(screen.getByText('Raw 12288 | 0.500 | 1.00 st')).toBeInTheDocument();
|
||||
});
|
||||
@@ -376,4 +449,109 @@ describe('EventListPanel', () => {
|
||||
|
||||
await waitFor(() => expect(midiTrack.getVolumeAutomation()[0].getValue()).toBe(-3));
|
||||
});
|
||||
|
||||
it('switches to Global tab and lists global track rows without requiring selection', () => {
|
||||
storeState.selectedTrackId = null;
|
||||
storeState.activeRegionId = null;
|
||||
storeState.selectedRegionIds = [];
|
||||
|
||||
render(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Global' }));
|
||||
|
||||
expect(screen.getAllByRole('button', { name: 'Marker' })[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Tempo' })[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Key Signature' })[0]).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('button', { name: 'Chord' })[0]).toBeInTheDocument();
|
||||
expect(screen.getByText('Intro')).toBeInTheDocument();
|
||||
expect(screen.getByText('120')).toBeInTheDocument();
|
||||
expect(screen.getByText('G major')).toBeInTheDocument();
|
||||
expect(screen.getByText('Bm7b5')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Qua. Pos.')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Qua. Len.')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles global filters independently', () => {
|
||||
render(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Global' }));
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Marker' })[0]);
|
||||
|
||||
expect(screen.queryByText('Intro')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('120')).toBeInTheDocument();
|
||||
expect(screen.getByText('Bm7b5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('creates marker and tempo rows from the Global tab with type-specific snapping', async () => {
|
||||
const { rerender } = render(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Global' }));
|
||||
|
||||
storeState.playheadPosition = 5.6;
|
||||
rerender(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByTitle('Add marker region at playhead'));
|
||||
await waitFor(() => expect(markerTrack.getRegions()).toHaveLength(2));
|
||||
const createdMarker = markerTrack.getRegions().find(region => region.getId() !== 'marker-1');
|
||||
expect(createdMarker?.getStartFromBeat()).toBe(6);
|
||||
|
||||
fireEvent.click(screen.getAllByRole('button', { name: 'Marker' })[1]);
|
||||
clickDropdownOption('Tempo');
|
||||
storeState.playheadPosition = 6.2;
|
||||
rerender(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByTitle('Add tempo region at playhead'));
|
||||
|
||||
await waitFor(() => expect(tempoTrack.getRegions()).toHaveLength(3));
|
||||
const createdTempo = tempoTrack.getRegions().find(region => region.getId() !== 'tempo-1' && region.getId() !== 'tempo-2') as KGTempoRegion | undefined;
|
||||
expect(createdTempo?.getStartBar()).toBe(2);
|
||||
});
|
||||
|
||||
it('edits global values inline and shows validation dialogs for invalid input', async () => {
|
||||
render(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Global' }));
|
||||
|
||||
fireEvent.doubleClick(screen.getByText('Intro'));
|
||||
const markerInput = screen.getByDisplayValue('Intro');
|
||||
fireEvent.change(markerInput, { target: { value: 'Verse A' } });
|
||||
fireEvent.keyDown(markerInput, { key: 'Enter' });
|
||||
await waitFor(() => expect(markerRegion.getName()).toBe('Verse A'));
|
||||
|
||||
fireEvent.doubleClick(screen.getByText('140'));
|
||||
const tempoInput = screen.getByDisplayValue('140');
|
||||
fireEvent.change(tempoInput, { target: { value: 'fast' } });
|
||||
fireEvent.keyDown(tempoInput, { key: 'Enter' });
|
||||
await waitFor(() => expect(vi.mocked(showAlert)).toHaveBeenCalledWith(expect.stringContaining('Example: 128')));
|
||||
expect(tempoRegionB.getBpm()).toBe(140);
|
||||
});
|
||||
|
||||
it('edits global position and length inline', async () => {
|
||||
render(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Global' }));
|
||||
|
||||
fireEvent.doubleClick(screen.getByText('2 1 0'));
|
||||
const positionInput = screen.getByDisplayValue('2 1 0');
|
||||
fireEvent.change(positionInput, { target: { value: '3 1 0' } });
|
||||
fireEvent.keyDown(positionInput, { key: 'Enter' });
|
||||
await waitFor(() => expect(chordRegion.getStartFromBeat()).toBe(8));
|
||||
|
||||
fireEvent.doubleClick(screen.getAllByText('4 0')[0]);
|
||||
const lengthInput = screen.getByDisplayValue('4 0');
|
||||
fireEvent.change(lengthInput, { target: { value: '8 0' } });
|
||||
fireEvent.keyDown(lengthInput, { key: 'Enter' });
|
||||
await waitFor(() => expect(markerRegion.getLength()).toBe(8));
|
||||
});
|
||||
|
||||
it('deletes mixed global selections with type-aware commands', async () => {
|
||||
const { rerender } = render(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Global' }));
|
||||
|
||||
fireEvent.click(screen.getByText('Intro').closest('tr')!);
|
||||
rerender(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByText('120').closest('tr')!, { ctrlKey: true });
|
||||
rerender(<EventListPanel isVisible={true} />);
|
||||
fireEvent.click(screen.getByTitle('Delete visible selected rows'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(markerTrack.getRegions()).toHaveLength(0);
|
||||
expect(tempoTrack.getRegions()).toHaveLength(1);
|
||||
expect(tempoTrack.getRegions()[0].getId()).toBe('tempo-2');
|
||||
expect((tempoTrack.getRegions()[0] as KGTempoRegion).getStartBar()).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user