feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice
- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer - Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support - Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline - Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots) - Migrate core application to Svelte 5 runes (, , ) with reactive stores - Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
This commit is contained in:
1 parent
c82b112624
commit
1feea40684
148 files changed
+11230
-1706
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
||||
import { UserSettingsStore } from '../src/lib/stores/UserSettingsStore.svelte';
|
||||
import { AppStateStore } from '../src/lib/stores/AppStateStore.svelte';
|
||||
import type { Scenario } from '../types';
|
||||
|
||||
describe('Svelte 5 Reactive Stores', () => {
|
||||
const mockStorage: Record<string, string> = {};
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key in mockStorage) {
|
||||
delete mockStorage[key];
|
||||
}
|
||||
|
||||
vi.stubGlobal('localStorage', {
|
||||
getItem: (key: string) => mockStorage[key] || null,
|
||||
setItem: (key: string, val: string) => {
|
||||
mockStorage[key] = val;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete mockStorage[key];
|
||||
},
|
||||
clear: () => {
|
||||
for (const key in mockStorage) {
|
||||
delete mockStorage[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('UserSettingsStore initializes with 3.8 models and correct defaults', () => {
|
||||
const store = new UserSettingsStore();
|
||||
expect(store.voiceName).toBe('Zephyr');
|
||||
expect(store.roleplayModel).toBe('google/gemini-3.8-flash');
|
||||
expect(store.supervisorModel).toBe('google/gemini-3.8-flash');
|
||||
expect(store.genderPreference).toBe('Neutral');
|
||||
});
|
||||
|
||||
test('UserSettingsStore updates properties cleanly', () => {
|
||||
const store = new UserSettingsStore();
|
||||
store.update({
|
||||
voiceName: 'Kore',
|
||||
openRouterApiKey: 'sk-or-v1-svelte-test-key',
|
||||
roleplayModel: 'google/gemini-3.7-flash'
|
||||
});
|
||||
|
||||
expect(store.voiceName).toBe('Kore');
|
||||
expect(store.openRouterApiKey).toBe('sk-or-v1-svelte-test-key');
|
||||
expect(store.roleplayModel).toBe('google/gemini-3.7-flash');
|
||||
|
||||
const plain = store.toUserSettings();
|
||||
expect(plain.voiceName).toBe('Kore');
|
||||
expect(plain.openRouterApiKey).toBe('sk-or-v1-svelte-test-key');
|
||||
});
|
||||
|
||||
test('UserSettingsStore recovers gracefully from corrupted localStorage', () => {
|
||||
mockStorage['cognitive-clarity-settings'] = 'MALFORMED_JSON_STRING';
|
||||
const store = new UserSettingsStore();
|
||||
expect(store.voiceName).toBe('Zephyr');
|
||||
expect(store.roleplayModel).toBe('google/gemini-3.8-flash');
|
||||
});
|
||||
|
||||
test('AppStateStore manages scenario catalog and filtering', () => {
|
||||
const app = new AppStateStore();
|
||||
expect(app.allScenarios.length).toBe(14);
|
||||
|
||||
app.selectedDifficulty = 'Beginner';
|
||||
expect(app.filteredScenarios.every((s) => s.difficulty === 'Beginner')).toBe(true);
|
||||
|
||||
app.selectedDifficulty = 'Advanced';
|
||||
expect(app.filteredScenarios.every((s) => s.difficulty === 'Advanced')).toBe(true);
|
||||
|
||||
app.selectedDifficulty = 'All';
|
||||
expect(app.filteredScenarios.length).toBe(14);
|
||||
});
|
||||
|
||||
test('AppStateStore adds and removes custom scenarios', () => {
|
||||
const app = new AppStateStore();
|
||||
const customScenario: Scenario = {
|
||||
id: 'custom-test-1',
|
||||
title: 'Custom Job Negotiation',
|
||||
description: 'Negotiating offer',
|
||||
category: 'Workplace',
|
||||
difficulty: 'Intermediate',
|
||||
systemInstruction: 'INTERNAL CBT LOGIC: Be firm.',
|
||||
initialMessage: 'Here is our final offer.',
|
||||
cbtFocus: ['All-or-Nothing Thinking'],
|
||||
characterName: 'Hiring Lead'
|
||||
};
|
||||
|
||||
app.addCustomScenario(customScenario);
|
||||
expect(app.customScenarios.length).toBe(1);
|
||||
expect(app.allScenarios.length).toBe(15);
|
||||
expect(app.allScenarios[0].id).toBe('custom-test-1');
|
||||
|
||||
app.deleteCustomScenario('custom-test-1');
|
||||
expect(app.customScenarios.length).toBe(0);
|
||||
expect(app.allScenarios.length).toBe(14);
|
||||
});
|
||||
|
||||
test('AppStateStore selectScenario and exitSession lifecycle', async () => {
|
||||
vi.useFakeTimers();
|
||||
const app = new AppStateStore();
|
||||
const targetScenario = app.allScenarios[0];
|
||||
|
||||
app.selectScenario(targetScenario, 'voice');
|
||||
expect(app.isPreparing).toBe(true);
|
||||
expect(app.activeScenario).toBe(null);
|
||||
|
||||
vi.advanceTimersByTime(1400);
|
||||
|
||||
expect(app.isPreparing).toBe(false);
|
||||
expect(app.activeScenario?.id).toBe(targetScenario.id);
|
||||
expect(app.sessionMode).toBe('voice');
|
||||
|
||||
app.exitSession();
|
||||
expect(app.activeScenario).toBe(null);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user