added tests
This commit is contained in:
127
frontend/src/store/aiStore.test.ts
Normal file
127
frontend/src/store/aiStore.test.ts
Normal file
@ -0,0 +1,127 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { useAIStore } from './aiStore';
|
||||
|
||||
|
||||
function mockElectronAPI() {
|
||||
(window as any).electronAPI = {
|
||||
encryptString: vi.fn().mockResolvedValue('encrypted-value'),
|
||||
decryptString: vi.fn().mockResolvedValue('decrypted-key'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('aiStore', () => {
|
||||
beforeEach(() => {
|
||||
mockElectronAPI();
|
||||
useAIStore.setState({
|
||||
providers: {
|
||||
ollama: { provider: 'ollama', baseUrl: 'http://localhost:11434', model: 'llama3' },
|
||||
openai: { provider: 'openai', apiKey: '', model: 'gpt-4o' },
|
||||
claude: { provider: 'claude', apiKey: '', model: 'claude-sonnet-4-20250514' },
|
||||
},
|
||||
defaultProvider: 'ollama',
|
||||
customFillerWords: '',
|
||||
fillerResult: null,
|
||||
clipSuggestions: [],
|
||||
isProcessing: false,
|
||||
processingMessage: '',
|
||||
_keysHydrated: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('setProviderConfig', () => {
|
||||
test('updates Ollama base URL', () => {
|
||||
useAIStore.getState().setProviderConfig('ollama', { baseUrl: 'http://custom:11434' });
|
||||
expect(useAIStore.getState().providers.ollama.baseUrl).toBe('http://custom:11434');
|
||||
});
|
||||
|
||||
test('updates Ollama model', () => {
|
||||
useAIStore.getState().setProviderConfig('ollama', { model: 'llama3.2' });
|
||||
expect(useAIStore.getState().providers.ollama.model).toBe('llama3.2');
|
||||
});
|
||||
|
||||
test('updates OpenAI apiKey and encrypts', async () => {
|
||||
useAIStore.getState().setProviderConfig('openai', { apiKey: 'sk-test123' });
|
||||
expect(useAIStore.getState().providers.openai.apiKey).toBe('sk-test123');
|
||||
expect((window as any).electronAPI.encryptString).toHaveBeenCalledWith('sk-test123');
|
||||
});
|
||||
|
||||
test('updates Claude model', () => {
|
||||
useAIStore.getState().setProviderConfig('claude', { model: 'claude-opus-4-20250514' });
|
||||
expect(useAIStore.getState().providers.claude.model).toBe('claude-opus-4-20250514');
|
||||
});
|
||||
|
||||
test('preserves existing config when updating partial fields', () => {
|
||||
useAIStore.getState().setProviderConfig('openai', { apiKey: 'sk-new', model: 'gpt-4o-mini' });
|
||||
expect(useAIStore.getState().providers.openai.apiKey).toBe('sk-new');
|
||||
expect(useAIStore.getState().providers.openai.model).toBe('gpt-4o-mini');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDefaultProvider', () => {
|
||||
test('changes default provider', () => {
|
||||
useAIStore.getState().setDefaultProvider('openai');
|
||||
expect(useAIStore.getState().defaultProvider).toBe('openai');
|
||||
});
|
||||
|
||||
test('can switch to claude', () => {
|
||||
useAIStore.getState().setDefaultProvider('claude');
|
||||
expect(useAIStore.getState().defaultProvider).toBe('claude');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCustomFillerWords', () => {
|
||||
test('sets custom filler words', () => {
|
||||
useAIStore.getState().setCustomFillerWords('okay, alright, anyway');
|
||||
expect(useAIStore.getState().customFillerWords).toBe('okay, alright, anyway');
|
||||
});
|
||||
|
||||
test('clears custom filler words', () => {
|
||||
useAIStore.getState().setCustomFillerWords('test');
|
||||
useAIStore.getState().setCustomFillerWords('');
|
||||
expect(useAIStore.getState().customFillerWords).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setFillerResult', () => {
|
||||
test('sets filler result', () => {
|
||||
const result = { fillers: [{ word: 'um', start: 1.0, end: 1.3 }], totalCount: 1 };
|
||||
useAIStore.getState().setFillerResult(result as any);
|
||||
expect(useAIStore.getState().fillerResult).toEqual(result);
|
||||
});
|
||||
|
||||
test('clears filler result', () => {
|
||||
useAIStore.getState().setFillerResult({ fillers: [], totalCount: 0 } as any);
|
||||
useAIStore.getState().setFillerResult(null);
|
||||
expect(useAIStore.getState().fillerResult).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setProcessing', () => {
|
||||
test('sets processing true with message', () => {
|
||||
useAIStore.getState().setProcessing(true, 'Analyzing transcript...');
|
||||
expect(useAIStore.getState().isProcessing).toBe(true);
|
||||
expect(useAIStore.getState().processingMessage).toBe('Analyzing transcript...');
|
||||
});
|
||||
|
||||
test('sets processing false', () => {
|
||||
useAIStore.getState().setProcessing(true, 'Working...');
|
||||
useAIStore.getState().setProcessing(false);
|
||||
expect(useAIStore.getState().isProcessing).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setClipSuggestions', () => {
|
||||
test('sets clip suggestions', () => {
|
||||
const clips = [{ title: 'Best moment', start: 10, end: 40, reason: 'Engaging' }];
|
||||
useAIStore.getState().setClipSuggestions(clips as any);
|
||||
expect(useAIStore.getState().clipSuggestions).toEqual(clips);
|
||||
});
|
||||
|
||||
test('clears clip suggestions', () => {
|
||||
useAIStore.getState().setClipSuggestions([{ title: 'x', start: 0, end: 10, reason: 'y' }] as any);
|
||||
useAIStore.getState().setClipSuggestions([]);
|
||||
expect(useAIStore.getState().clipSuggestions).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -3,30 +3,401 @@ import { beforeEach, describe, expect, test } from 'vitest';
|
||||
import { useEditorStore } from './editorStore';
|
||||
|
||||
|
||||
describe('editorStore basics', () => {
|
||||
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();
|
||||
});
|
||||
|
||||
test('clamps global gain to valid bounds', () => {
|
||||
const state = useEditorStore.getState();
|
||||
describe('global gain', () => {
|
||||
test('clamps to upper bound', () => {
|
||||
useEditorStore.getState().setGlobalGainDb(100);
|
||||
expect(useEditorStore.getState().globalGainDb).toBe(24);
|
||||
});
|
||||
|
||||
state.setGlobalGainDb(100);
|
||||
expect(useEditorStore.getState().globalGainDb).toBe(24);
|
||||
test('clamps to lower bound', () => {
|
||||
useEditorStore.getState().setGlobalGainDb(-100);
|
||||
expect(useEditorStore.getState().globalGainDb).toBe(-24);
|
||||
});
|
||||
|
||||
state.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);
|
||||
});
|
||||
});
|
||||
|
||||
test('adds gain range to store', () => {
|
||||
const state = useEditorStore.getState();
|
||||
describe('zone ranges', () => {
|
||||
beforeEach(() => {
|
||||
useEditorStore.getState().setDuration(100);
|
||||
});
|
||||
|
||||
state.addGainRange(1.2, 2.4, 3.5);
|
||||
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);
|
||||
});
|
||||
|
||||
const ranges = useEditorStore.getState().gainRanges;
|
||||
expect(ranges.length).toBe(1);
|
||||
expect(ranges[0].start).toBe(1.2);
|
||||
expect(ranges[0].end).toBe(2.4);
|
||||
expect(ranges[0].gainDb).toBe(3.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.length).toBe(1);
|
||||
expect(project.muteRanges.length).toBe(1);
|
||||
expect(project.gainRanges.length).toBe(1);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
183
frontend/src/store/licenseStore.test.ts
Normal file
183
frontend/src/store/licenseStore.test.ts
Normal file
@ -0,0 +1,183 @@
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { useLicenseStore } from './licenseStore';
|
||||
|
||||
|
||||
function mockElectronAPI(overrides: Record<string, any> = {}) {
|
||||
(window as any).electronAPI = {
|
||||
getAppStatus: vi.fn().mockResolvedValue({ tag: 'Expired' }),
|
||||
activateLicense: vi.fn().mockResolvedValue(null),
|
||||
deactivateLicense: vi.fn().mockResolvedValue(undefined),
|
||||
hasLicenseFeature: vi.fn().mockResolvedValue(false),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('licenseStore', () => {
|
||||
beforeEach(() => {
|
||||
mockElectronAPI();
|
||||
useLicenseStore.setState({ status: null, isLoaded: false, canEdit: true, showDialog: false });
|
||||
});
|
||||
|
||||
describe('canEdit', () => {
|
||||
test('is true for Licensed status', async () => {
|
||||
mockElectronAPI({
|
||||
getAppStatus: vi.fn().mockResolvedValue({ tag: 'Licensed', license: { license_id: 'x', tier: 'pro' } }),
|
||||
});
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().canEdit).toBe(true);
|
||||
});
|
||||
|
||||
test('is true for Trial status', async () => {
|
||||
mockElectronAPI({
|
||||
getAppStatus: vi.fn().mockResolvedValue({ tag: 'Trial', days_remaining: 20, started_at: Date.now() }),
|
||||
});
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().canEdit).toBe(true);
|
||||
});
|
||||
|
||||
test('is false for Expired status', async () => {
|
||||
mockElectronAPI({
|
||||
getAppStatus: vi.fn().mockResolvedValue({ tag: 'Expired' }),
|
||||
});
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().canEdit).toBe(false);
|
||||
});
|
||||
|
||||
test('is false when status is null', () => {
|
||||
useLicenseStore.setState({ status: null, canEdit: true });
|
||||
useLicenseStore.getState().setStatus(null);
|
||||
expect(useLicenseStore.getState().canEdit).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkStatus', () => {
|
||||
test('sets status to Licensed when backend returns Licensed', async () => {
|
||||
const license = { license_id: 'l1', tier: 'pro', customer_email: 'a@b.com', expires_at: 9999999999, features: [], issued_at: 1, max_activations: 1 };
|
||||
mockElectronAPI({ getAppStatus: vi.fn().mockResolvedValue({ tag: 'Licensed', license }) });
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Licensed');
|
||||
});
|
||||
|
||||
test('sets status to Trial when backend returns Trial', async () => {
|
||||
mockElectronAPI({ getAppStatus: vi.fn().mockResolvedValue({ tag: 'Trial', days_remaining: 15, started_at: Date.now() }) });
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Trial');
|
||||
});
|
||||
|
||||
test('sets status to Expired when backend returns Expired', async () => {
|
||||
mockElectronAPI({ getAppStatus: vi.fn().mockResolvedValue({ tag: 'Expired' }) });
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Expired');
|
||||
});
|
||||
|
||||
test('handles API error gracefully', async () => {
|
||||
mockElectronAPI({ getAppStatus: vi.fn().mockRejectedValue(new Error('network error')) });
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Expired');
|
||||
expect(useLicenseStore.getState().canEdit).toBe(false);
|
||||
});
|
||||
|
||||
test('handles missing electronAPI', async () => {
|
||||
delete (window as any).electronAPI;
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Expired');
|
||||
expect(useLicenseStore.getState().canEdit).toBe(false);
|
||||
});
|
||||
|
||||
test('sets isLoaded to true after check', async () => {
|
||||
await useLicenseStore.getState().checkStatus();
|
||||
expect(useLicenseStore.getState().isLoaded).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('activateLicense', () => {
|
||||
test('sets Licensed on valid key', async () => {
|
||||
const license = { license_id: 'l2', tier: 'pro', customer_email: 'x@y.com', expires_at: 9999999999, features: ['bg_removal'], issued_at: 1, max_activations: 1 };
|
||||
mockElectronAPI({ activateLicense: vi.fn().mockResolvedValue(license) });
|
||||
const result = await useLicenseStore.getState().activateLicense('talkedit_v1_validKey');
|
||||
expect(result).toBe(true);
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Licensed');
|
||||
expect(useLicenseStore.getState().canEdit).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false on invalid key', async () => {
|
||||
mockElectronAPI({ activateLicense: vi.fn().mockResolvedValue(null) });
|
||||
const result = await useLicenseStore.getState().activateLicense('invalid-key');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false on API error', async () => {
|
||||
mockElectronAPI({ activateLicense: vi.fn().mockRejectedValue(new Error('bad key')) });
|
||||
const result = await useLicenseStore.getState().activateLicense('bad-key');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('closes dialog on success', async () => {
|
||||
useLicenseStore.setState({ showDialog: true });
|
||||
const license = { license_id: 'l3', tier: 'business', customer_email: 'z@z.com', expires_at: 9999999999, features: [], issued_at: 1, max_activations: 5 };
|
||||
mockElectronAPI({ activateLicense: vi.fn().mockResolvedValue(license) });
|
||||
await useLicenseStore.getState().activateLicense('talkedit_v1_key');
|
||||
expect(useLicenseStore.getState().showDialog).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deactivateLicense', () => {
|
||||
test('sets Expired when trial is over', async () => {
|
||||
mockElectronAPI({
|
||||
deactivateLicense: vi.fn().mockResolvedValue(undefined),
|
||||
getAppStatus: vi.fn().mockResolvedValue({ tag: 'Expired' }),
|
||||
});
|
||||
await useLicenseStore.getState().deactivateLicense();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Expired');
|
||||
expect(useLicenseStore.getState().canEdit).toBe(false);
|
||||
});
|
||||
|
||||
test('restores Trial when trial is still valid', async () => {
|
||||
mockElectronAPI({
|
||||
deactivateLicense: vi.fn().mockResolvedValue(undefined),
|
||||
getAppStatus: vi.fn().mockResolvedValue({ tag: 'Trial', days_remaining: 5, started_at: Date.now() }),
|
||||
});
|
||||
await useLicenseStore.getState().deactivateLicense();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Trial');
|
||||
expect(useLicenseStore.getState().canEdit).toBe(true);
|
||||
});
|
||||
|
||||
test('handles API error', async () => {
|
||||
mockElectronAPI({ deactivateLicense: vi.fn().mockRejectedValue(new Error('fail')) });
|
||||
useLicenseStore.setState({ status: { tag: 'Licensed', license: {} as any }, canEdit: true });
|
||||
await useLicenseStore.getState().deactivateLicense();
|
||||
expect(useLicenseStore.getState().status?.tag).toBe('Expired');
|
||||
expect(useLicenseStore.getState().canEdit).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasFeature', () => {
|
||||
test('returns true when feature exists', async () => {
|
||||
mockElectronAPI({ hasLicenseFeature: vi.fn().mockResolvedValue(true) });
|
||||
const result = await useLicenseStore.getState().hasFeature('bg_removal');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false when feature missing', async () => {
|
||||
mockElectronAPI({ hasLicenseFeature: vi.fn().mockResolvedValue(false) });
|
||||
const result = await useLicenseStore.getState().hasFeature('nonexistent');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('returns false on API error', async () => {
|
||||
mockElectronAPI({ hasLicenseFeature: vi.fn().mockRejectedValue(new Error('fail')) });
|
||||
const result = await useLicenseStore.getState().hasFeature('bg_removal');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setShowDialog', () => {
|
||||
test('toggles dialog', () => {
|
||||
useLicenseStore.getState().setShowDialog(true);
|
||||
expect(useLicenseStore.getState().showDialog).toBe(true);
|
||||
useLicenseStore.getState().setShowDialog(false);
|
||||
expect(useLicenseStore.getState().showDialog).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user