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.
This commit is contained in:
parent
c8d802d78f
commit
6ba25f53b7
43 changed files with 1721 additions and 204 deletions
337
apps/desktop/tests/e2e/red_team_cycle3.spec.ts
Normal file
337
apps/desktop/tests/e2e/red_team_cycle3.spec.ts
Normal file
|
|
@ -0,0 +1,337 @@
|
|||
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([]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue