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: KeyBindingPicker - Key Recording, Reserved-Combo Protection & Cancel Safety', 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. The dictation row is rendered by KeyBindingField off KEYBINDING_ACTIONS const dictationField = window.getByTestId('keybinding-field-dictation'); await expect(dictationField).toBeVisible({ timeout: 5000 }); // Wait for the binding map to arrive, then snapshot how many bindings this action // has so we can prove afterwards that cancelling did not corrupt them. const bindingChips = dictationField.getByTestId('keybinding-binding'); await expect(bindingChips.first()).toBeVisible({ timeout: 5000 }); const bindingsBefore = await bindingChips.count(); // 3. Open the picker with the "+ Add" button (replaces the old pencil icon) await dictationField.getByTestId('keybinding-add-dictation').click(); const picker = window.getByTestId('keybinding-picker'); await expect(picker).toBeVisible({ timeout: 5000 }); // 4. Record tab: pressing F9 captures it onto a keycap const recordArea = picker.getByTestId('keybinding-record-area'); await expect(recordArea).toBeVisible(); await window.keyboard.press('F9'); await window.waitForTimeout(400); await expect(recordArea.getByText('F9', { exact: true })).toBeVisible(); // Take screenshot while the picker holds the captured key await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt11_keybinding_picker_recorded.png'), }); // 5. Reserved-combo protection: Ctrl+C must be rejected and Save must stay disabled await picker.getByTestId('keybinding-record-again').click(); await window.waitForTimeout(300); await window.keyboard.press('Control+c'); await window.waitForTimeout(400); await expect(picker.getByTestId('keybinding-rejection').first()).toBeVisible(); await expect(picker.getByTestId('keybinding-confirm')).toBeDisabled(); // 6. Cancel closes the picker without touching the stored bindings await picker.getByTestId('keybinding-cancel').click(); await window.waitForTimeout(500); await expect(picker).toBeHidden(); await expect(bindingChips).toHaveCount(bindingsBefore); // Close SettingsModal (its close button sits in DialogTitle, ahead of any field markup) 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-11b: KeyBindingPicker - Searchable Key Dropdown & Multi-Binding Add/Remove', async () => { // 1. Re-open SettingsModal to General Tab const settingsTrigger = window.getByText('설정', { exact: true }); await settingsTrigger.click(); await window.waitForTimeout(600); const generalTab = window.getByRole('tab', { name: /일반|General/i }); await generalTab.click(); await window.waitForTimeout(400); const dictationField = window.getByTestId('keybinding-field-dictation'); const bindingChips = dictationField.getByTestId('keybinding-binding'); await expect(bindingChips.first()).toBeVisible({ timeout: 5000 }); const bindingsBefore = await bindingChips.count(); // 2. Open the picker and switch to the list tab await dictationField.getByTestId('keybinding-add-dictation').click(); const picker = window.getByTestId('keybinding-picker'); await expect(picker).toBeVisible({ timeout: 5000 }); await picker .getByTestId('keybinding-picker-tabs') .getByText(/목록에서 선택|Choose from List/i) .click(); await window.waitForTimeout(300); // 3. The dropdown carries its own search field and narrows the key catalog as you type. // Its listbox is portalled to , so options are queried from the page root. const searchInput = picker.getByTestId('keybinding-search').locator('input'); await expect(searchInput).toBeVisible(); const keyOptions = window.getByTestId('keybinding-key-option'); expect(await keyOptions.count()).toBeGreaterThan(1); await searchInput.fill('f8'); await window.waitForTimeout(400); await expect(keyOptions).toHaveCount(1); await keyOptions.first().click(); await window.waitForTimeout(300); // Picked key is reflected back into the picker's selection preview await expect(picker.getByText('F8', { exact: true }).first()).toBeVisible(); // 4. Saving adds a SECOND binding to the same action (multi-binding) const confirmBtn = picker.getByTestId('keybinding-confirm'); await expect(confirmBtn).toBeEnabled({ timeout: 5000 }); await confirmBtn.click(); await expect(picker).toBeHidden(); await expect(bindingChips).toHaveCount(bindingsBefore + 1); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt11b_keybinding_multi_binding.png'), }); // 5. Remove it again so the stored config is left exactly as we found it await bindingChips.last().getByTestId('keybinding-remove').click(); await expect(bindingChips).toHaveCount(bindingsBefore); 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([]); }); });