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,66 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('Landing Page & Scenario Navigation', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Clear localStorage to ensure fresh deterministic initial state
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear();
|
||||
window.localStorage.setItem('cognitive-clarity-settings', JSON.stringify({
|
||||
voiceName: 'Zephyr',
|
||||
vocalStyle: 'Helpful',
|
||||
genderPreference: 'Neutral',
|
||||
openRouterApiKey: 'sk-or-v1-playwright-mock-key',
|
||||
roleplayModel: 'google/gemini-3.8-flash',
|
||||
supervisorModel: 'google/gemini-3.8-flash'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
test('01 - Home View Initial Render and Visual Snapshot', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify Brand Hero & Elements
|
||||
await expect(page.locator('h1')).toContainText('Cognitive Clarity');
|
||||
await expect(page.getByText('Evidence-Based Framework')).toBeVisible();
|
||||
await expect(page.getByText(/Vocal Partner:/)).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Training Scenarios' })).toBeVisible();
|
||||
|
||||
// Verify all 14 default scenarios are rendered
|
||||
const scenarioCards = page.locator('[data-testid="scenarios-grid"] > *');
|
||||
await expect(scenarioCards).toHaveCount(15); // 14 scenarios + 1 custom trigger button
|
||||
|
||||
// Capture visual snapshot
|
||||
const shotDir = path.join(process.cwd(), 'docs/screenshots', testInfo.project.name);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, '01-home-initial.png'), fullPage: true });
|
||||
});
|
||||
|
||||
test('02 - Filter Scenarios by Difficulty', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Filter by Beginner
|
||||
await page.getByRole('button', { name: 'Beginner', exact: true }).click();
|
||||
await expect(page.getByText('The Silent Friend')).toBeVisible();
|
||||
await expect(page.getByText('Inner Critic Echo')).toBeVisible();
|
||||
|
||||
const shotDir = path.join(process.cwd(), 'docs/screenshots', testInfo.project.name);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, '02-filter-beginner.png'), fullPage: true });
|
||||
|
||||
// Filter by Advanced
|
||||
await page.getByRole('button', { name: 'Advanced', exact: true }).click();
|
||||
await expect(page.getByText('The Social Spotlight')).toBeVisible();
|
||||
await expect(page.getByText('The Unpaid Debt')).toBeVisible();
|
||||
await expect(page.getByText('The Silent Friend')).not.toBeVisible();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '03-filter-advanced.png'), fullPage: true });
|
||||
|
||||
// Reset to All
|
||||
await page.getByRole('button', { name: 'All', exact: true }).click();
|
||||
await expect(page.getByText('The Silent Friend')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('Modals & Clinical Dialogs Suite', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear();
|
||||
window.localStorage.setItem('cognitive-clarity-settings', JSON.stringify({
|
||||
voiceName: 'Zephyr',
|
||||
vocalStyle: 'Helpful',
|
||||
genderPreference: 'Neutral',
|
||||
openRouterApiKey: 'sk-or-v1-playwright-test-key',
|
||||
roleplayModel: 'google/gemini-3.8-flash',
|
||||
supervisorModel: 'google/gemini-3.8-flash'
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
test('01 - Settings Modal and Model Selector', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click Settings button
|
||||
const settingsBtn = page.locator('button:has(svg)').first();
|
||||
await settingsBtn.click();
|
||||
|
||||
// Verify Settings Modal elements
|
||||
await expect(page.getByRole('heading', { name: 'Application Settings' })).toBeVisible();
|
||||
await expect(page.getByText('Vocal Profile')).toBeVisible();
|
||||
await expect(page.getByText('Fenrir (Salt of the Earth)')).toBeVisible();
|
||||
await expect(page.getByText('Puck')).toBeVisible();
|
||||
await expect(page.getByText('Kore')).toBeVisible();
|
||||
await expect(page.getByText('Natural Voice Testing Lab')).toBeVisible();
|
||||
|
||||
// Select Fenrir (Salt of the Earth) persona
|
||||
await page.getByText('Fenrir (Salt of the Earth)').click();
|
||||
await expect(page.getByText(/Auditioning/)).toContainText('Fenrir (Salt of the Earth)');
|
||||
|
||||
// Click quick audition demo button
|
||||
const auditionBtn = page.getByRole('button', { name: 'Audition Voice' });
|
||||
await expect(auditionBtn).toBeVisible();
|
||||
await auditionBtn.click();
|
||||
|
||||
const shotDir = path.join(process.cwd(), 'docs/screenshots', testInfo.project.name);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, '04-settings-modal-voice.png'), fullPage: true });
|
||||
|
||||
// Switch to OpenRouter AI Tab
|
||||
await page.getByRole('tab', { name: 'OpenRouter AI' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'OpenRouter API Key' })).toBeVisible();
|
||||
await expect(page.getByText('Google Gemini 3.8 Flash', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Frontier Ultra Low Latency')).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '05-settings-modal-openrouter.png'), fullPage: true });
|
||||
|
||||
// Close Modal
|
||||
await page.getByRole('button', { name: 'Save Preferences' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Application Settings' })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('02 - CBT Clinical Reference Glossary & Concept Search', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Open Reference Guide
|
||||
await page.getByRole('button', { name: 'Reference Guide' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'CBT Clinical Reference' })).toBeVisible();
|
||||
|
||||
const shotDir = path.join(process.cwd(), 'docs/screenshots', testInfo.project.name);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, '06-cbt-reference-all.png'), fullPage: true });
|
||||
|
||||
// Filter by Distortion category
|
||||
await page.getByRole('button', { name: 'Distortion', exact: true }).click();
|
||||
await expect(page.getByRole('heading', { name: 'All-or-Nothing Thinking' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'Catastrophizing' })).toBeVisible();
|
||||
|
||||
// Search for a specific concept
|
||||
const searchInput = page.getByPlaceholder('Search concepts, distortions, or definitions...');
|
||||
await searchInput.fill('Mind Reading');
|
||||
await expect(page.getByRole('heading', { name: 'Mind Reading' })).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: 'All-or-Nothing Thinking' })).not.toBeVisible();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '07-cbt-reference-search.png'), fullPage: true });
|
||||
|
||||
// Close Reference Guide
|
||||
await page.locator('div.fixed button:has(svg)').first().click();
|
||||
});
|
||||
|
||||
test('03 - Custom Scenario Architect Generator', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Open Custom Scenario Generator
|
||||
await page.getByRole('button', { name: 'Create Custom' }).first().click();
|
||||
await expect(page.getByRole('heading', { name: 'Clinical Scenario Architect' })).toBeVisible();
|
||||
await expect(page.getByText('Target Dynamic & Situation')).toBeVisible();
|
||||
await expect(page.getByText('Emotional Resistance & Difficulty')).toBeVisible();
|
||||
|
||||
const shotDir = path.join(process.cwd(), 'docs/screenshots', testInfo.project.name);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, '08-scenario-generator-modal.png'), fullPage: true });
|
||||
|
||||
// Fill scenario prompt
|
||||
await page.getByPlaceholder('e.g. Asking my manager for a delayed performance review').fill(
|
||||
'Negotiating a contract renewal with a skeptical supplier who wants to raise rates by 40%.'
|
||||
);
|
||||
|
||||
// Select difficulty
|
||||
await page.getByRole('button', { name: 'Advanced Severe Friction' }).click();
|
||||
|
||||
// Select CBT focus distortion
|
||||
await page.getByRole('button', { name: 'Emotional Reasoning' }).click();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '09-scenario-generator-filled.png'), fullPage: true });
|
||||
|
||||
// Cancel modal
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Clinical Scenario Architect' })).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('Chat Roleplay Simulation & Supervision Suite', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear();
|
||||
window.localStorage.setItem('cognitive-clarity-settings', JSON.stringify({
|
||||
voiceName: 'Zephyr',
|
||||
vocalStyle: 'Helpful',
|
||||
genderPreference: 'Neutral',
|
||||
openRouterApiKey: 'sk-or-v1-playwright-test-key',
|
||||
roleplayModel: 'google/gemini-3.8-flash',
|
||||
supervisorModel: 'google/gemini-3.8-flash'
|
||||
}));
|
||||
});
|
||||
|
||||
// Mock OpenRouter API requests deterministically
|
||||
await page.route('https://openrouter.ai/api/v1/chat/completions', async (route) => {
|
||||
const request = route.request();
|
||||
const postData = JSON.parse(request.postData() || '{}');
|
||||
|
||||
// 1. Streaming Roleplay Chat
|
||||
if (postData.stream) {
|
||||
const sseBody = [
|
||||
'data: {"choices":[{"delta":{"content":"Hey, sorry I didn\'t "}}]}\n\n',
|
||||
'data: {"choices":[{"delta":{"content":"get back to you right away. Work was totally overwhelming yesterday."}}]}\n\n',
|
||||
'data: [DONE]\n\n'
|
||||
].join('');
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'text/event-stream',
|
||||
body: sseBody
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Turn Supervisor or Session Report Analysis
|
||||
const isSessionReport = postData.messages?.some((m: any) =>
|
||||
typeof m.content === 'string' && m.content.includes('SESSION EVALUATION PROMPT')
|
||||
);
|
||||
|
||||
if (isSessionReport) {
|
||||
const mockSummaryReport = {
|
||||
analysis: {
|
||||
distortions: ['Mind Reading'],
|
||||
isAdaptive: true,
|
||||
evidence: 'User clearly asked for context before jumping to catastrophic conclusions.'
|
||||
},
|
||||
rating: {
|
||||
score: 4,
|
||||
reason: 'Demonstrated measured emotional self-regulation and clear communication.'
|
||||
},
|
||||
suggestions: {
|
||||
alternativeThought: 'A delayed message is not proof of abandonment or malice.',
|
||||
behavioralStep: 'Practice pausing for 2 minutes before assuming negative intentions.'
|
||||
}
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
choices: [{ message: { content: JSON.stringify(mockSummaryReport) } }]
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Turn supervisor analysis
|
||||
const mockTurnInsight = {
|
||||
analysis: {
|
||||
distortions: [],
|
||||
isAdaptive: true,
|
||||
evidence: 'Checked in calmly without accusation.'
|
||||
},
|
||||
rating: {
|
||||
score: 5,
|
||||
reason: 'Balanced and open inquiry.'
|
||||
},
|
||||
suggestions: {
|
||||
alternativeThought: 'Waiting for factual data is adaptive.',
|
||||
behavioralStep: 'Continue listening to the friend\'s explanation.'
|
||||
}
|
||||
};
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
choices: [{ message: { content: JSON.stringify(mockTurnInsight) } }]
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('01 - Launch Text Chat, Stream Response, Live Insights, and Export Report', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click 'Chat' on the first scenario (The Silent Friend)
|
||||
const chatBtn = page.getByRole('button', { name: 'Chat' }).first();
|
||||
await chatBtn.click();
|
||||
|
||||
// Verify initializing transition screen
|
||||
await expect(page.getByText('Initializing Session')).toBeVisible();
|
||||
|
||||
// Wait for Chat Session view
|
||||
await expect(page.getByText('The Silent Friend')).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Complete Interaction')).toBeVisible();
|
||||
|
||||
const shotDir = path.join(process.cwd(), 'docs/screenshots', testInfo.project.name);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, '10-chat-session-initial.png'), fullPage: true });
|
||||
|
||||
// Send a message
|
||||
const chatInput = page.getByPlaceholder('Type your response in character...');
|
||||
await chatInput.fill('Hey Sam, no worries at all! Just wanted to check if you were doing okay.');
|
||||
await page.keyboard.press('Enter');
|
||||
|
||||
// Verify bot streamed message appears
|
||||
await expect(
|
||||
page.getByText('Work was totally overwhelming yesterday')
|
||||
).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Verify Live Insight Panel updates
|
||||
await expect(page.getByText('Live Clinical Insight')).toBeVisible();
|
||||
await expect(page.getByText('Waiting for factual data is adaptive')).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '11-chat-interaction-streamed.png'), fullPage: true });
|
||||
|
||||
// Complete the interaction
|
||||
await page.getByRole('button', { name: 'Complete Interaction' }).click();
|
||||
|
||||
// Verify Session Summary Report renders
|
||||
await expect(page.getByRole('heading', { name: 'Session Summary Report' })).toBeVisible({ timeout: 6000 });
|
||||
await expect(page.getByText('Adaptive Growth Observed')).toBeVisible();
|
||||
await expect(page.getByText('Master Reframe')).toBeVisible();
|
||||
await expect(page.getByText('Waiting for factual data is adaptive')).toBeVisible();
|
||||
|
||||
// Verify Export actions are present
|
||||
await expect(page.getByTitle('Download Markdown Report')).toBeVisible();
|
||||
await expect(page.getByTitle('Download JSON Report')).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '12-session-report-summary.png'), fullPage: true });
|
||||
|
||||
// Close Report and return to home
|
||||
await page.getByRole('button', { name: 'Close Report & Return' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Training Scenarios' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
test.describe('Voice Roleplay Simulation & Visualizer Suite', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Stub getUserMedia and AudioContext before loading page
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear();
|
||||
window.localStorage.setItem('cognitive-clarity-settings', JSON.stringify({
|
||||
voiceName: 'Kore',
|
||||
vocalStyle: 'Calm',
|
||||
genderPreference: 'Female',
|
||||
openRouterApiKey: 'sk-or-v1-playwright-test-key',
|
||||
roleplayModel: 'google/gemini-3.8-flash',
|
||||
supervisorModel: 'google/gemini-3.8-flash'
|
||||
}));
|
||||
|
||||
// Mock MediaDevices with real native MediaStream from AudioContext
|
||||
if (navigator.mediaDevices) {
|
||||
navigator.mediaDevices.getUserMedia = async () => {
|
||||
try {
|
||||
const AudioCtx = window.AudioContext || (window as any).webkitAudioContext;
|
||||
const ctx = new AudioCtx();
|
||||
const dest = ctx.createMediaStreamDestination();
|
||||
return dest.stream;
|
||||
} catch (e) {
|
||||
return new MediaStream();
|
||||
}
|
||||
};
|
||||
navigator.mediaDevices.enumerateDevices = async () => [
|
||||
{
|
||||
deviceId: 'default',
|
||||
kind: 'audioinput',
|
||||
label: 'Studio Microphone Pro',
|
||||
groupId: 'group-1',
|
||||
toJSON: () => {}
|
||||
} as MediaDeviceInfo
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
// Mock OpenRouter Speech synthesis API
|
||||
await page.route('https://openrouter.ai/api/v1/audio/speech', async (route) => {
|
||||
// Return empty audio buffer
|
||||
const dummyPcm = new Uint8Array(2048);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'audio/mpeg',
|
||||
body: Buffer.from(dummyPcm)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('01 - Launch Voice Session, Visualizer Orb, and Transcript Drawer', async ({ page }, testInfo) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click 'Voice' on 'Critical Feedback'
|
||||
const voiceBtn = page.getByRole('button', { name: 'Voice' }).first();
|
||||
await voiceBtn.click();
|
||||
|
||||
// Verify initializing transition screen
|
||||
await expect(page.getByText('Initializing Session')).toBeVisible();
|
||||
|
||||
// Wait for Voice Session cockpit
|
||||
await expect(page.getByText(/Active Roleplay Session/)).toBeVisible({ timeout: 5000 });
|
||||
await expect(page.getByText('Begin Voice Simulation')).toBeVisible();
|
||||
await expect(page.getByText('Kore Vocal Persona Active')).toBeVisible();
|
||||
|
||||
const shotDir = path.join(process.cwd(), 'docs/screenshots', testInfo.project.name);
|
||||
fs.mkdirSync(shotDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(shotDir, '13-voice-cockpit-initial.png'), fullPage: true });
|
||||
|
||||
// Click 'Begin Voice Simulation'
|
||||
await page.getByRole('button', { name: 'Begin Voice Simulation' }).click();
|
||||
|
||||
// Toggle Transcript Drawer
|
||||
const transcriptToggleBtn = page.getByTitle('Toggle Transcript Drawer');
|
||||
await transcriptToggleBtn.click();
|
||||
|
||||
// Verify Transcript Drawer opens
|
||||
await expect(page.getByText('Live Conversation')).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '14-voice-transcript-drawer.png'), fullPage: true });
|
||||
|
||||
// Close Transcript Drawer by toggling it back off
|
||||
await transcriptToggleBtn.click();
|
||||
await expect(page.getByText('Live Conversation')).not.toBeVisible();
|
||||
|
||||
// Toggle Mute
|
||||
const muteBtn = page.getByTitle('Mute Microphone');
|
||||
await muteBtn.click();
|
||||
await expect(page.getByTitle('Unmute Microphone')).toBeVisible();
|
||||
|
||||
// Open Audio Hardware Settings
|
||||
const audioSettingsBtn = page.getByTitle('Audio Settings');
|
||||
await audioSettingsBtn.click();
|
||||
await expect(page.getByText('Select Audio Input Device')).toBeVisible();
|
||||
|
||||
await page.screenshot({ path: path.join(shotDir, '15-voice-audio-settings-modal.png'), fullPage: true });
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user