d3ro-voice/apps/desktop/tests/e2e/red_team_cycle3.spec.ts
Yun Chan 6ba25f53b7 fix(desktop): surface configuration and provider failures instead of hiding them
Several desktop paths quietly substituted defaults or partial results: a
config write could fall back to a throwaway in-memory store, speech provider
errors were absorbed into empty transcriptions, and meeting exports built
file names from raw titles.

Writes now fail explicitly when the store is unavailable, provider and model
failures reach the UI as errors, and export names pass through one
sanitizer. Settings, license, ad, and support surfaces use the shared theme
tokens, unused hotkey helpers are gone, and the package gains strict
node/renderer typecheck configs plus red-team e2e scenarios for these flows.
2026-09-16 23:23:58 +09:00

337 lines
12 KiB
TypeScript

import { test, expect, _electron as electron, ElectronApplication, Page } from '@playwright/test';
import path from 'path';
import fs from 'fs';
const SCREENSHOT_DIR = path.resolve('C:/Users/encep/.gemini/antigravity/brain/bbff18a3-721d-4c43-989f-1d6964e15be7/screenshots');
test.describe.serial('Extreme Red Team - Cycle 3: Modals, Deep Configuration & System Shell', () => {
let app: ElectronApplication;
let window: Page;
let testUserDataDir: string;
const uncaughtExceptions: string[] = [];
test.beforeAll(async () => {
if (!fs.existsSync(SCREENSHOT_DIR)) {
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
}
testUserDataDir = path.join(
'C:/Users/encep/AppData/Local/Temp',
'playwright-redteam-cycle3-' + Date.now()
);
app = await electron.launch({
args: [
'out/main/index.js',
'--disable-gpu',
'--no-sandbox',
`--user-data-dir=${testUserDataDir}`,
],
env: {
...process.env,
NODE_ENV: 'test',
},
});
// Find main window
for (let i = 0; i < 50; i++) {
for (const w of app.windows()) {
try {
const url = w.url();
if (url && url.includes('index.html') && !url.includes('popups/')) {
window = w;
break;
}
} catch {
// window navigating
}
}
if (window) break;
await new Promise((r) => setTimeout(r, 400));
}
if (!window) {
window = app.windows()[0] || (await app.firstWindow());
}
window.on('pageerror', (err) => {
console.error('[PAGEERROR in Electron Window]:', err.message);
uncaughtExceptions.push(err.message);
});
await window.waitForLoadState('domcontentloaded');
// Bypass Onboarding modal for testing
await window.evaluate(async () => {
if (window.electronAPI?.config) {
await window.electronAPI.config.set({ key: 'onboardingCompleted', value: true });
await window.electronAPI.config.set({ key: 'appUsageMode', value: 'online' });
await window.electronAPI.config.set({ key: 'llmBackend', value: 'online' });
}
});
await window.waitForTimeout(500);
await window.reload();
await window.waitForLoadState('domcontentloaded');
await window.waitForTimeout(1000);
});
test.afterAll(async () => {
if (app) {
await app.close();
}
});
test('RT-10: SettingsModal - Deep Tab Traversal, Live Theme Switching, Language Switch & Audio Test', async () => {
// 1. Open SettingsModal from sidebar
const settingsTrigger = window.getByText('설정', { exact: true });
await expect(settingsTrigger).toBeVisible({ timeout: 5000 });
await settingsTrigger.click();
await window.waitForTimeout(600);
// Verify modal dialog opened
await expect(window.getByText(/설정|SETTINGS/i).first()).toBeVisible({ timeout: 5000 });
// 2. Tab 0: General - Voice Mode Cards & Theme / Language
await expect(window.getByText(/받아쓰기|Dictation/i).first()).toBeVisible();
await expect(window.getByText(/원터치|Hands-Free/i).first()).toBeVisible();
await expect(window.getByText(/자막|Caption/i).first()).toBeVisible();
// Test Theme Selector
const themeSelect = window.locator('div[role="combobox"]').first();
if (await themeSelect.isVisible()) {
await themeSelect.click();
await window.waitForTimeout(300);
// Select Light theme
const lightOption = window.getByRole('option', { name: /라이트|Light/i });
if (await lightOption.isVisible()) {
await lightOption.click();
await window.waitForTimeout(400);
}
// Select Dark theme back
await themeSelect.click();
await window.waitForTimeout(300);
const darkOption = window.getByRole('option', { name: /다크|Dark/i });
if (await darkOption.isVisible()) {
await darkOption.click();
await window.waitForTimeout(400);
}
}
// 3. Tab 1: Audio Tab
const audioTab = window.getByRole('tab', { name: /오디오|Audio/i });
await expect(audioTab).toBeVisible();
await audioTab.click();
await window.waitForTimeout(500);
// Verify Audio Settings: Input device selector & Mic Test
await expect(window.getByText(/입력 장치|마이크|Microphone/i).first()).toBeVisible();
const micTestBtn = window.getByRole('button', { name: /테스트|Test/i }).first();
if (await micTestBtn.isVisible()) {
await micTestBtn.click();
await window.waitForTimeout(400);
// Click again to stop test
await micTestBtn.click();
await window.waitForTimeout(300);
}
// 4. Tab 2: STT Tab
const sttTab = window.getByRole('tab', { name: 'STT' });
await expect(sttTab).toBeVisible();
await sttTab.click();
await window.waitForTimeout(500);
// Verify STT Provider / Model configurations
await expect(window.getByText(/STT 엔진|faster-whisper|모델|Provider/i).first()).toBeVisible();
// 5. Tab 3: LLM Tab
const llmTab = window.getByRole('tab', { name: 'LLM' });
await expect(llmTab).toBeVisible();
await llmTab.click();
await window.waitForTimeout(500);
// Verify Ollama Server URL input
await expect(window.getByText(/Ollama|Server|URL/i).first()).toBeVisible();
const refreshLlmBtn = window.locator('button:has(svg.lucide-refresh-cw)').first();
if (await refreshLlmBtn.isVisible()) {
await refreshLlmBtn.click();
await window.waitForTimeout(400);
}
// 6. Tab 4: License Tab
const licenseTab = window.getByRole('tab', { name: /라이선스|License/i });
await expect(licenseTab).toBeVisible();
await licenseTab.click();
await window.waitForTimeout(500);
// Verify Current Tier Display
await expect(window.getByText(/현재 라이선스|현재 플랜|FREE|PRO/i).first()).toBeVisible();
// 7. Tab 5: Cloud Sync Tab
const cloudTab = window.getByRole('tab', { name: /클라우드|Cloud/i });
if (await cloudTab.isVisible()) {
await cloudTab.click();
await window.waitForTimeout(500);
await expect(window.getByText(/Cloud|동기화|Sync/i).first()).toBeVisible();
}
// 8. Tab 6: About Tab
const aboutTab = window.getByRole('tab', { name: /정보|About/i });
if (await aboutTab.isVisible()) {
await aboutTab.click();
await window.waitForTimeout(500);
await expect(window.getByText(/D3RO Voice|버전|Version/i).first()).toBeVisible();
}
// Screenshot SettingsModal
await window.screenshot({
path: path.join(SCREENSHOT_DIR, 'rt10_settings_modal_all_tabs.png'),
});
// Close SettingsModal
const closeBtn = window.locator('div[role="dialog"] button:has(svg.lucide-x)').first();
await expect(closeBtn).toBeVisible();
await closeBtn.click();
await window.waitForTimeout(500);
// Verify modal is closed
await expect(window.locator('div[role="dialog"]')).not.toBeVisible();
expect(uncaughtExceptions).toEqual([]);
});
test('RT-11: HotkeyRecordModal - Interactive Hotkey Recording & Conflict Protection', async () => {
// 1. Re-open SettingsModal to General Tab
const settingsTrigger = window.getByText('설정', { exact: true });
await settingsTrigger.click();
await window.waitForTimeout(600);
// Click General tab
const generalTab = window.getByRole('tab', { name: /일반|General/i });
await generalTab.click();
await window.waitForTimeout(400);
// 2. Click pencil icon on Dictation shortcut to open HotkeyRecordModal
const editHotkeyBtn = window.locator('button:has(svg.lucide-pencil)').first();
await expect(editHotkeyBtn).toBeVisible();
await editHotkeyBtn.click();
await window.waitForTimeout(500);
// Verify HotkeyRecordModal is open
await expect(window.getByText(/단축키 설정|단축키 녹화|키 조합/i).first()).toBeVisible({ timeout: 5000 });
// 3. Test pressing a key (F9)
await window.keyboard.press('F9');
await window.waitForTimeout(400);
// Verify chip shows F9
await expect(window.getByText('F9', { exact: true })).toBeVisible();
// Take screenshot while modal is open with captured key
await window.screenshot({
path: path.join(SCREENSHOT_DIR, 'rt11_hotkey_modal_verified.png'),
});
// 4. Click Cancel button to close HotkeyRecordModal without corrupting bindings
const cancelBtn = window.getByRole('button', { name: /취소|Cancel/i });
await expect(cancelBtn).toBeVisible();
await cancelBtn.click();
await window.waitForTimeout(500);
// Close SettingsModal
const closeSettingsBtn = window.locator('div[role="dialog"] button:has(svg.lucide-x)').first();
if (await closeSettingsBtn.isVisible()) {
await closeSettingsBtn.click();
await window.waitForTimeout(500);
}
expect(uncaughtExceptions).toEqual([]);
});
test('RT-12: SupportModal - AI Diagnostics, FAQ Accordion, Refund Query & Telemetry', async () => {
// 1. Open SupportModal from sidebar
const supportTrigger = window.locator('text=Customer Support').first();
await expect(supportTrigger).toBeVisible({ timeout: 5000 });
await supportTrigger.click();
await window.waitForTimeout(600);
// Verify SupportModal opens
await expect(window.getByText(/Customer Assistance|고객지원/i).first()).toBeVisible({ timeout: 5000 });
// 2. Tab 0: AI Assistant Chat & Quick Prompt Chips
await expect(window.getByText(/AI 어시스턴트|AI Assistant/i).first()).toBeVisible();
// Click quick prompt chip: "🎤 마이크 인식 오류"
const micPromptChip = window.getByRole('button', { name: /마이크/i }).first();
if (await micPromptChip.isVisible()) {
await micPromptChip.click();
await window.waitForTimeout(800);
// Verify AI answer appears
await expect(window.getByText(/마이크 입력 진단 결과|개인정보 권한/i)).toBeVisible({ timeout: 5000 });
}
// 3. Tab 1: FAQ Accordion
const faqTab = window.getByRole('tab', { name: /FAQ|자주 묻는 질문/i });
await expect(faqTab).toBeVisible();
await faqTab.click();
await window.waitForTimeout(500);
// Verify FAQ questions render
await expect(window.getByText(/마이크 음성 인식이 작동하지 않거나/i)).toBeVisible();
await expect(window.getByText(/NVIDIA CUDA GPU 가속/i)).toBeVisible();
// 4. Tab 3 (Tab index 3 in DOM): 7일 자동 환불 확인
const refundTab = window.getByRole('tab', { name: /환불|Refund/i });
if (await refundTab.isVisible()) {
await refundTab.click();
await window.waitForTimeout(500);
// Verify refund check button
const checkRefundBtn = window.getByRole('button', { name: /환불 자격 조회/i });
if (await checkRefundBtn.isVisible()) {
await checkRefundBtn.click();
await window.waitForTimeout(1000);
// Verify eligibility result
await expect(window.getByText(/전액 환불 가능|조회 완료|환불 자격/i).first()).toBeVisible();
}
}
// Take screenshot
await window.screenshot({
path: path.join(SCREENSHOT_DIR, 'rt12_support_diagnostics_modal.png'),
});
// Close SupportModal
await window.keyboard.press('Escape');
await window.waitForTimeout(500);
expect(uncaughtExceptions).toEqual([]);
});
test('RT-13: LicenseModal & Custom Events Fail-Closed Monetization Guardrails', async () => {
// 1. Dispatch custom event to open LicenseModal
await window.evaluate(() => {
window.dispatchEvent(new CustomEvent('d3ro:open-license-modal'));
});
await window.waitForTimeout(600);
// Verify LicenseModal opens
await expect(window.getByText(/라이선스|License/i).first()).toBeVisible({ timeout: 5000 });
await expect(window.getByText(/현재 등급|FREE/i).first()).toBeVisible();
// Verify Tier Comparison Table exists
await expect(window.getByText(/등급 비교|Tier Comparison/i)).toBeVisible();
// Take screenshot
await window.screenshot({
path: path.join(SCREENSHOT_DIR, 'rt13_license_monetization_modal.png'),
});
// Close LicenseModal via Escape
await window.keyboard.press('Escape');
await window.waitForTimeout(500);
expect(uncaughtExceptions).toEqual([]);
});
});