Files
TalkEdit/frontend/src/store/editorStore.test.ts
2026-05-06 16:05:04 -06:00

408 lines
15 KiB
TypeScript

import { beforeEach, describe, expect, test } from 'vitest';
import { useEditorStore } from './editorStore';
function seedWords(count: number) {
const words: { word: string; start: number; end: number; confidence: number }[] = [];
for (let i = 0; i < count; i++) {
words.push({ word: `word${i}`, start: i * 0.5, end: i * 0.5 + 0.4, confidence: 0.95 });
}
const segments = [{
id: 0, start: 0, end: count * 0.5,
text: words.map(w => w.word).join(' '),
words,
globalStartIndex: 0,
}];
useEditorStore.getState().setTranscription({ words, segments, language: 'en' });
}
describe('editorStore', () => {
beforeEach(() => {
useEditorStore.getState().reset();
});
describe('global gain', () => {
test('clamps to upper bound', () => {
useEditorStore.getState().setGlobalGainDb(100);
expect(useEditorStore.getState().globalGainDb).toBe(24);
});
test('clamps to lower bound', () => {
useEditorStore.getState().setGlobalGainDb(-100);
expect(useEditorStore.getState().globalGainDb).toBe(-24);
});
test('rejects NaN by falling back to 0', () => {
useEditorStore.getState().setGlobalGainDb(NaN);
expect(useEditorStore.getState().globalGainDb).toBe(0);
});
test('rejects Infinity', () => {
useEditorStore.getState().setGlobalGainDb(Infinity);
expect(useEditorStore.getState().globalGainDb).toBe(0);
});
test('accepts value in range', () => {
useEditorStore.getState().setGlobalGainDb(6);
expect(useEditorStore.getState().globalGainDb).toBe(6);
});
});
describe('zone ranges', () => {
beforeEach(() => {
useEditorStore.getState().setDuration(100);
});
test('addCutRange creates a zone with correct times', () => {
useEditorStore.getState().addCutRange(1, 5);
const ranges = useEditorStore.getState().cutRanges;
expect(ranges.length).toBe(1);
expect(ranges[0].start).toBe(1);
expect(ranges[0].end).toBe(5);
});
test('addCutRange generates unique ids', () => {
useEditorStore.getState().addCutRange(1, 2);
useEditorStore.getState().addCutRange(3, 4);
const ranges = useEditorStore.getState().cutRanges;
expect(ranges[0].id).not.toBe(ranges[1].id);
});
test('addCutRange rejects start >= end', () => {
useEditorStore.getState().addCutRange(5, 5);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
test('addCutRange rejects start > end', () => {
useEditorStore.getState().addCutRange(5, 1);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
test('addCutRange rejects duration < 0.01s', () => {
useEditorStore.getState().addCutRange(0, 0.005);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
test('addCutRange rejects negative start', () => {
useEditorStore.getState().addCutRange(-1, 5);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
test('addCutRange rejects NaN values', () => {
useEditorStore.getState().addCutRange(NaN, 5);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
test('addMuteRange creates a zone', () => {
useEditorStore.getState().addMuteRange(2, 6);
const ranges = useEditorStore.getState().muteRanges;
expect(ranges.length).toBe(1);
expect(ranges[0].start).toBe(2);
expect(ranges[0].end).toBe(6);
});
test('addGainRange creates a zone with gain value', () => {
useEditorStore.getState().addGainRange(1, 4, 3.5);
const ranges = useEditorStore.getState().gainRanges;
expect(ranges.length).toBe(1);
expect(ranges[0].gainDb).toBe(3.5);
});
test('addSpeedRange creates a zone with speed value', () => {
useEditorStore.getState().addSpeedRange(0, 10, 1.5);
const ranges = useEditorStore.getState().speedRanges;
expect(ranges.length).toBe(1);
expect(ranges[0].speed).toBe(1.5);
});
test('removeCutRange removes by id', () => {
useEditorStore.getState().addCutRange(1, 2);
const id = useEditorStore.getState().cutRanges[0].id;
useEditorStore.getState().removeCutRange(id);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
test('removeCutRange does nothing for missing id', () => {
useEditorStore.getState().addCutRange(1, 2);
useEditorStore.getState().removeCutRange('nonexistent');
expect(useEditorStore.getState().cutRanges.length).toBe(1);
});
test('updateCutRange updates bounds', () => {
useEditorStore.getState().addCutRange(1, 5);
const id = useEditorStore.getState().cutRanges[0].id;
useEditorStore.getState().updateCutRange(id, 2, 8);
const range = useEditorStore.getState().cutRanges[0];
expect(range.start).toBe(2);
expect(range.end).toBe(8);
});
test('removeMuteRange, removeGainRange, removeSpeedRange work', () => {
useEditorStore.getState().addMuteRange(1, 2);
useEditorStore.getState().addGainRange(2, 4, 3);
useEditorStore.getState().addSpeedRange(3, 6, 1.2);
useEditorStore.getState().removeMuteRange(useEditorStore.getState().muteRanges[0].id);
useEditorStore.getState().removeGainRange(useEditorStore.getState().gainRanges[0].id);
useEditorStore.getState().removeSpeedRange(useEditorStore.getState().speedRanges[0].id);
expect(useEditorStore.getState().muteRanges.length).toBe(0);
expect(useEditorStore.getState().gainRanges.length).toBe(0);
expect(useEditorStore.getState().speedRanges.length).toBe(0);
});
test('rejects zones beyond duration', () => {
useEditorStore.getState().setDuration(10);
useEditorStore.getState().addCutRange(5, 20);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
test('rejects zone with end beyond duration', () => {
useEditorStore.getState().setDuration(5);
useEditorStore.getState().addCutRange(1, 10);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
});
describe('word selection', () => {
beforeEach(() => { seedWords(10); });
test('setSelectedWordIndices updates selection', () => {
useEditorStore.getState().setSelectedWordIndices([0, 1, 2]);
expect(useEditorStore.getState().selectedWordIndices).toEqual([0, 1, 2]);
});
test('setSelectedWordIndices handles empty', () => {
useEditorStore.getState().setSelectedWordIndices([0]);
useEditorStore.getState().setSelectedWordIndices([]);
expect(useEditorStore.getState().selectedWordIndices).toEqual([]);
});
test('updateWordText updates the word at index', () => {
useEditorStore.getState().updateWordText(0, 'hello');
expect(useEditorStore.getState().words[0].word).toBe('hello');
});
test('updateWordText preserves timing', () => {
const origStart = useEditorStore.getState().words[3].start;
useEditorStore.getState().updateWordText(3, 'changed');
expect(useEditorStore.getState().words[3].start).toBe(origStart);
});
test('updateWordText rejects out-of-bounds index', () => {
useEditorStore.getState().updateWordText(999, 'oops');
expect(useEditorStore.getState().words.length).toBe(10);
});
test('updateWordText rejects empty string', () => {
useEditorStore.getState().updateWordText(0, '');
expect(useEditorStore.getState().words[0].word).toBe('word0');
});
test('replaceWordRange replaces words in middle', () => {
const newWords = [
{ word: 'new1', start: 1.5, end: 1.9, confidence: 0.99 },
{ word: 'new2', start: 2.0, end: 2.4, confidence: 0.99 },
];
useEditorStore.getState().replaceWordRange(3, 5, newWords);
const words = useEditorStore.getState().words;
expect(words.length).toBe(10 - (5 - 3 + 1) + 2);
expect(words[3].word).toBe('new1');
expect(words[4].word).toBe('new2');
});
test('getWordAtTime returns correct index', () => {
const idx = useEditorStore.getState().getWordAtTime(1.0);
expect(idx).toBe(2);
});
test('getWordAtTime returns 0 for time before first word', () => {
const idx = useEditorStore.getState().getWordAtTime(-1);
expect(idx).toBe(0);
});
test('getWordAtTime returns -1 for no words', () => {
useEditorStore.getState().reset();
expect(useEditorStore.getState().getWordAtTime(0)).toBe(-1);
});
});
describe('markers', () => {
beforeEach(() => {
useEditorStore.getState().setDuration(120);
});
test('setMarkInTime sets and clears', () => {
useEditorStore.getState().setMarkInTime(10);
expect(useEditorStore.getState().markInTime).toBe(10);
useEditorStore.getState().setMarkInTime(null);
expect(useEditorStore.getState().markInTime).toBeNull();
});
test('setMarkInTime rejects NaN', () => {
useEditorStore.getState().setMarkInTime(NaN);
expect(useEditorStore.getState().markInTime).toBeNull();
});
test('clearMarkRange clears both', () => {
useEditorStore.getState().setMarkInTime(5);
useEditorStore.getState().setMarkOutTime(10);
useEditorStore.getState().clearMarkRange();
expect(useEditorStore.getState().markInTime).toBeNull();
expect(useEditorStore.getState().markOutTime).toBeNull();
});
test('addTimelineMarker adds with correct data', () => {
useEditorStore.getState().addTimelineMarker(5, 'Intro', '#ef4444');
const markers = useEditorStore.getState().timelineMarkers;
expect(markers.length).toBe(1);
expect(markers[0].time).toBe(5);
expect(markers[0].label).toBe('Intro');
expect(markers[0].color).toBe('#ef4444');
});
test('addTimelineMarker defaults empty label to Marker', () => {
useEditorStore.getState().addTimelineMarker(10, '', '#6366f1');
expect(useEditorStore.getState().timelineMarkers[0].label).toBe('Marker');
});
test('addTimelineMarker rejects NaN time', () => {
useEditorStore.getState().addTimelineMarker(NaN, 'test', '#6366f1');
expect(useEditorStore.getState().timelineMarkers.length).toBe(0);
});
test('removeTimelineMarker removes by id', () => {
useEditorStore.getState().addTimelineMarker(5, 'Intro', '#ef4444');
const id = useEditorStore.getState().timelineMarkers[0].id;
useEditorStore.getState().removeTimelineMarker(id);
expect(useEditorStore.getState().timelineMarkers.length).toBe(0);
});
test('updateTimelineMarker updates label and color', () => {
useEditorStore.getState().addTimelineMarker(5, 'Intro', '#ef4444');
const id = useEditorStore.getState().timelineMarkers[0].id;
useEditorStore.getState().updateTimelineMarker(id, { label: 'Chapter 1', color: '#22c55e' });
const m = useEditorStore.getState().timelineMarkers[0];
expect(m.label).toBe('Chapter 1');
expect(m.color).toBe('#22c55e');
});
});
describe('transcription', () => {
test('setTranscription sets words and segments', () => {
seedWords(5);
expect(useEditorStore.getState().words.length).toBe(5);
expect(useEditorStore.getState().segments.length).toBe(1);
});
test('setTranscription clears segments when words are empty', () => {
useEditorStore.getState().setTranscription({ words: [], segments: [], language: 'en' });
expect(useEditorStore.getState().segments.length).toBe(0);
});
test('setTranscriptionModel ignores null', () => {
useEditorStore.getState().setTranscriptionModel('base');
useEditorStore.getState().setTranscriptionModel(null);
expect(useEditorStore.getState().transcriptionModel).toBe('base');
});
test('setTranscriptionModel ignores empty string', () => {
useEditorStore.getState().setTranscriptionModel('base');
useEditorStore.getState().setTranscriptionModel('');
expect(useEditorStore.getState().transcriptionModel).toBe('base');
});
test('setTranscribing toggles state and status', () => {
useEditorStore.getState().setTranscribing(true, 50, 'Loading...');
expect(useEditorStore.getState().isTranscribing).toBe(true);
expect(useEditorStore.getState().transcriptionProgress).toBe(50);
expect(useEditorStore.getState().transcriptionStatus).toBe('Loading...');
});
});
describe('project file', () => {
test('saveProject includes all zone types', () => {
useEditorStore.getState().loadVideo('test.mp4');
useEditorStore.getState().setDuration(100);
useEditorStore.getState().addCutRange(1, 2);
useEditorStore.getState().addMuteRange(2, 3);
useEditorStore.getState().addGainRange(3, 4, 3);
useEditorStore.getState().addSpeedRange(4, 5, 1.5);
const project = useEditorStore.getState().saveProject();
expect(project.cutRanges).toBeDefined();
expect(project.cutRanges!.length).toBe(1);
expect(project.muteRanges).toBeDefined();
expect(project.muteRanges!.length).toBe(1);
expect(project.gainRanges).toBeDefined();
expect(project.gainRanges!.length).toBe(1);
expect(project.speedRanges).toBeDefined();
expect(project.speedRanges!.length).toBe(1);
});
test('setProjectFilePath sets and reads back', () => {
useEditorStore.getState().setProjectFilePath('/path/to/project.aive');
expect(useEditorStore.getState().projectFilePath).toBe('/path/to/project.aive');
});
});
describe('duration and current time', () => {
test('setDuration sets duration value', () => {
useEditorStore.getState().setDuration(120);
expect(useEditorStore.getState().duration).toBe(120);
});
test('setCurrentTime sets time without clamping', () => {
useEditorStore.getState().setDuration(60);
useEditorStore.getState().setCurrentTime(120);
expect(useEditorStore.getState().currentTime).toBe(120);
});
test('setCurrentTime accepts negative values', () => {
useEditorStore.getState().setCurrentTime(-10);
expect(useEditorStore.getState().currentTime).toBe(-10);
});
test('setIsPlaying toggles', () => {
useEditorStore.getState().setIsPlaying(true);
expect(useEditorStore.getState().isPlaying).toBe(true);
useEditorStore.getState().setIsPlaying(false);
expect(useEditorStore.getState().isPlaying).toBe(false);
});
});
describe('loadVideo', () => {
test('loadVideo rejects empty path', () => {
useEditorStore.getState().loadVideo('');
expect(useEditorStore.getState().videoUrl).toBeNull();
});
test('loadVideo resets state', () => {
seedWords(5);
useEditorStore.getState().addCutRange(1, 2);
useEditorStore.getState().loadVideo('new-video.mp4');
expect(useEditorStore.getState().words.length).toBe(0);
expect(useEditorStore.getState().cutRanges.length).toBe(0);
});
});
describe('zone preview padding', () => {
test('sets padding value', () => {
useEditorStore.getState().setZonePreviewPaddingSeconds(3);
expect(useEditorStore.getState().zonePreviewPaddingSeconds).toBe(3);
});
test('rejects NaN', () => {
useEditorStore.getState().setZonePreviewPaddingSeconds(2);
useEditorStore.getState().setZonePreviewPaddingSeconds(NaN);
expect(useEditorStore.getState().zonePreviewPaddingSeconds).toBe(2);
});
test('clamps to upper bound', () => {
useEditorStore.getState().setZonePreviewPaddingSeconds(20);
expect(useEditorStore.getState().zonePreviewPaddingSeconds).toBe(10);
});
});
});