feat: implement global chord track

This commit is contained in:
Xiaohan-Tian
2026-05-24 23:06:08 -07:00
parent 77fb0e4c1f
commit 1879df8033
30 changed files with 2757 additions and 20 deletions
+86 -2
View File
@@ -4,8 +4,9 @@ import { fireEvent, render, screen } from '@testing-library/react';
import MainContent from './MainContent';
import { KGMidiRegion } from '../core/region/KGMidiRegion';
import { KGAudioRegion } from '../core/region/KGAudioRegion';
import { KGChordRegion } from '../core/region/KGChordRegion';
import { KGAudioTrack } from '../core/track/KGAudioTrack';
import { createDefaultGlobalTracks } from '../core/global-track';
import { createDefaultGlobalTracks, GlobalTrackType } from '../core/global-track';
import { createMockMidiTrack } from '../test/utils/mock-data';
const executeCommandMock = vi.fn();
@@ -138,9 +139,11 @@ vi.mock('./piano-roll/PianoRoll', () => ({
describe('MainContent', () => {
beforeEach(() => {
storeState.globalTracks = createDefaultGlobalTracks();
storeState.selectedRegionIds = [];
storeState.activeRegionId = null;
storeState.showPianoRoll = false;
storeState.playheadPosition = 0;
storeState.timeSignature = { numerator: 4, denominator: 4 };
storeState.clearAllSelections.mockClear();
storeState.setSelectedTrack.mockClear();
@@ -332,7 +335,8 @@ describe('MainContent', () => {
expect(screen.queryByText('Chord')).not.toBeInTheDocument();
});
it('routes the tempo global track add button through a command and keeps chord visual-only', () => {
it('routes the tempo and chord global track add buttons through commands', () => {
storeState.playheadPosition = 5;
render(<MainContent />);
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
@@ -344,7 +348,87 @@ describe('MainContent', () => {
expect(storeState.addTrack).not.toHaveBeenCalled();
expect(storeState.addAudioTrack).not.toHaveBeenCalled();
expect(executeCommandMock).toHaveBeenCalledTimes(2);
expect((executeCommandMock.mock.calls[1][0] as { startBeat?: number }).startBeat).toBe(5);
});
it('uses split-insert chord command when the playhead is inside an existing chord region', () => {
const globalTracks = createDefaultGlobalTracks();
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
chordTrack?.addRegion(new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 8));
storeState.globalTracks = globalTracks;
storeState.playheadPosition = 3;
render(<MainContent />);
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
fireEvent.click(screen.getByRole('button', { name: 'Add Chord global track item' }));
expect(executeCommandMock).toHaveBeenCalledTimes(1);
expect((executeCommandMock.mock.calls[0][0] as { insertBeat?: number }).insertBeat).toBe(3);
storeState.globalTracks = createDefaultGlobalTracks();
});
it('tabs the open chord popup to the next chord region before the next bar', () => {
const globalTracks = createDefaultGlobalTracks();
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
chordTrack?.setRegions([
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 3),
new KGChordRegion('chord-2', chordTrack.getId(), chordTrack.getTrackIndex(), 'G', 2, 2),
]);
storeState.globalTracks = globalTracks;
render(<MainContent />);
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
fireEvent.doubleClick(screen.getByText('Am'));
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab' });
expect(storeState.selectedRegionIds).toEqual(['chord-2']);
storeState.globalTracks = createDefaultGlobalTracks();
});
it('tabs the open chord popup to insert at the next exact bar when no chord starts before it', () => {
const globalTracks = createDefaultGlobalTracks();
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
chordTrack?.setRegions([
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 8),
]);
storeState.globalTracks = globalTracks;
render(<MainContent />);
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
fireEvent.doubleClick(screen.getByText('Am'));
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab' });
expect(executeCommandMock).toHaveBeenCalledTimes(1);
expect((executeCommandMock.mock.calls[0][0] as { insertBeat?: number }).insertBeat).toBe(4);
storeState.globalTracks = createDefaultGlobalTracks();
});
it('shift-tabs the open chord popup to the previous existing chord without creating a new one', () => {
const globalTracks = createDefaultGlobalTracks();
const chordTrack = globalTracks.find(track => track.getType() === GlobalTrackType.Chord);
chordTrack?.setRegions([
new KGChordRegion('chord-1', chordTrack.getId(), chordTrack.getTrackIndex(), 'Am', 0, 4),
new KGChordRegion('chord-2', chordTrack.getId(), chordTrack.getTrackIndex(), 'G', 8, 4),
]);
storeState.globalTracks = globalTracks;
render(<MainContent />);
fireEvent.click(screen.getByRole('button', { name: 'Show global tracks' }));
fireEvent.doubleClick(screen.getByText('G'));
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Tab', shiftKey: true });
expect(storeState.selectedRegionIds).toEqual(['chord-1']);
expect(executeCommandMock).toHaveBeenCalledTimes(0);
storeState.globalTracks = createDefaultGlobalTracks();
});
it('routes the signature global track add button through a command', () => {