// apps/desktop/tests/e2e/red_team_cycle1.spec.ts // Extreme Red Team: Headful Deep Interactive Testing for Cycle 1 (Core CRUD) import { test, expect, _electron as electron, type Page, type ElectronApplication } from '@playwright/test'; import * as fs from 'fs'; import * as path from 'path'; const SCREENSHOT_DIR = 'C:/Users/encep/.gemini/antigravity/brain/bbff18a3-721d-4c43-989f-1d6964e15be7/screenshots'; test.describe('Extreme Red Team - Cycle 1: Core CRUD & State Integrity', () => { test.describe.configure({ mode: 'serial' }); let electronApp: ElectronApplication; let window: Page; const consoleErrors: string[] = []; const uncaughtExceptions: string[] = []; test.beforeAll(async () => { if (!fs.existsSync(SCREENSHOT_DIR)) { fs.mkdirSync(SCREENSHOT_DIR, { recursive: true }); } const testUserDataDir = path.join( 'C:/Users/encep/AppData/Local/Temp', 'playwright-redteam-cycle1-' + Date.now() ); electronApp = 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 (skip popups like recording-tip, caption-overlay, etc.) for (let i = 0; i < 50; i++) { for (const w of electronApp.windows()) { try { const url = w.url(); if (url && url.includes('index.html') && !url.includes('popups/')) { window = w; break; } } catch { // window might be navigating } } if (window) break; await new Promise((r) => setTimeout(r, 400)); } if (!window) { window = electronApp.windows()[0] || (await electronApp.firstWindow()); } await window.waitForLoadState('domcontentloaded'); // Attach Error Sentinels window.on('pageerror', (err) => { const msg = `[PAGE_ERROR] ${err.message}\n${err.stack || ''}`; uncaughtExceptions.push(msg); // eslint-disable-next-line no-console console.error(msg); }); window.on('console', (msg) => { if (msg.type() === 'error') { const text = msg.text(); // Filter out benign CSP dev warnings if (!text.includes('Electron Security Warning') && !text.includes('Content Security Policy')) { consoleErrors.push(text); } } }); // Bypass Onboarding modal for testing CRUD 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 (electronApp) { await electronApp.close(); } }); test('RT-01: Full Navigation Across All Primary Sidebar Routes', async () => { // 1. Dashboard await window.locator('text=/^(Dashboard|대시보드)$/').first().click(); await expect(window.locator('text=/^(Current Backend|현재 백엔드)$/').first()).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt01_01_dashboard.png') }); // 2. History await window.locator('text=/^(History|히스토리|기록)$/').first().click(); await expect(window.locator('text=/^(History|히스토리|변환 기록)$/').first()).toBeVisible({ timeout: 5000 }); // 3. Dictionary await window.locator('text=/^(Dictionary|사전|단어장)$/').first().click(); await expect(window.locator('text=/^(Custom Dictionary|커스텀 사전|단어장)$/').first()).toBeVisible({ timeout: 5000 }); // 4. Commands await window.locator('text=/^(Commands|명령어)$/').first().click(); await expect(window.locator('text=/^(LLM Commands|LLM 명령어)$/').first()).toBeVisible({ timeout: 5000 }); // 5. Voice Conversation await window.locator('text=/^(Conversation|대화|음성 대화)$/').first().click(); await expect(window.locator('text=/^(Voice Conversation|음성 대화)$/').first()).toBeVisible({ timeout: 5000 }); // 6. Knowledge Base await window.locator('text=/^(Knowledge|지식 베이스)$/').first().click(); await expect(window.locator('text=/^(Knowledge Base|지식 베이스)$/').first()).toBeVisible({ timeout: 5000 }); // 7. Meeting Mode await window.locator('text=/^(Meeting|회의|회의 모드)$/').first().click(); await expect(window.locator('text=/^(Meeting Mode|회의 모드)$/').first()).toBeVisible({ timeout: 5000 }); expect(uncaughtExceptions).toHaveLength(0); }); test('RT-02: Dictionary Deep CRUD, Boundary Validation & Search Interaction', async () => { // Navigate to Dictionary Tab await window.locator('text=/^(Dictionary|사전|단어장)$/').first().click(); await expect(window.locator('text=/^(Custom Dictionary|커스텀 사전|단어장)$/').first()).toBeVisible(); // Click Add Word button (PhysicalButton tone="accent") const addBtn = window.locator('header button:has-text("추가"), header button:has-text("Add"), button:has-text("추가"), button:has-text("Add")').first(); await expect(addBtn).toBeVisible({ timeout: 5000 }); await addBtn.click(); await window.waitForTimeout(500); // Verify Add Dialog opens const dialogTitle = window.locator('text=/^(단어 추가|Add Word)$/').first(); await expect(dialogTitle).toBeVisible({ timeout: 5000 }); // Test Boundary: Empty input should disable save button const saveBtn = window.locator('div[role="dialog"] button:has-text("저장"), div[role="dialog"] button:has-text("Save")').first(); await expect(saveBtn).toBeDisabled(); // Fill valid data const wordInput = window.locator('div[role="dialog"] input').first(); const pronInput = window.locator('div[role="dialog"] input').nth(1); await wordInput.fill('D3RO_Voice_RedTeam_Keyword'); await pronInput.fill('디쓰리오 보이스 레드팀 키워드'); await expect(saveBtn).toBeEnabled(); // Save await saveBtn.click(); await window.waitForTimeout(600); // Verify new word appears in the list const createdItem = window.locator('text=D3RO_Voice_RedTeam_Keyword').first(); await expect(createdItem).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt02_01_dict_created.png') }); // Test Search Functionality const searchInput = window.locator('input[placeholder*="검색"], input[placeholder*="Search"]').first(); await searchInput.fill('RedTeam'); await window.waitForTimeout(400); await expect(createdItem).toBeVisible(); await searchInput.fill('NonExistentKeywordXYZ999'); await window.waitForTimeout(400); await expect(window.locator('text=D3RO_Voice_RedTeam_Keyword')).not.toBeVisible(); await expect(window.locator('text=/^(검색 결과 없음|검색 결과가 없습니다|일치하는 단어가 없습니다|No results)$/').first()).toBeVisible({ timeout: 5000 }); // Clear search await searchInput.fill(''); await window.waitForTimeout(400); await expect(createdItem).toBeVisible(); // Test Edit const editBtn = window.locator('button[aria-label="편집"], button[aria-label="Edit"]').first(); await expect(editBtn).toBeVisible({ timeout: 5000 }); await editBtn.click(); await window.waitForTimeout(400); // Verify Edit Dialog opens await expect(window.locator('text=/^(단어 편집|단어 수정|Edit Word)$/').first()).toBeVisible({ timeout: 5000 }); const editWordInput = window.locator('div[role="dialog"] input').first(); await editWordInput.fill('D3RO_Voice_RedTeam_Keyword_MOD'); const editSaveBtn = window.locator('div[role="dialog"] button:has-text("저장"), div[role="dialog"] button:has-text("Save")').first(); await editSaveBtn.click(); await window.waitForTimeout(600); // Verify edited word appears const editedItem = window.locator('text=D3RO_Voice_RedTeam_Keyword_MOD').first(); await expect(editedItem).toBeVisible({ timeout: 5000 }); // Test Delete const deleteBtn = window.locator('button[aria-label="삭제"], button[aria-label="Delete"]').first(); await expect(deleteBtn).toBeVisible({ timeout: 5000 }); await deleteBtn.click(); await window.waitForTimeout(700); // Verify deleted item is gone await expect(window.locator('text=D3RO_Voice_RedTeam_Keyword_MOD')).not.toBeVisible(); // Test Adversarial Unicode & Long Text Entry await addBtn.click(); await window.waitForTimeout(500); const complexInput = '🔥⚡ 레드팀 极限 测试 12345 🚀🤖 (Special!@#$%)'; await wordInput.fill(complexInput); await pronInput.fill('특수문자 발음'); await saveBtn.click(); await window.waitForTimeout(600); // Verify complex item rendered safely without unhandled error const complexItem = window.locator(`text=${complexInput}`).first(); await expect(complexItem).toBeVisible({ timeout: 5000 }); // Cleanup complex item const complexDeleteBtn = window.locator('button[aria-label="삭제"], button[aria-label="Delete"]').first(); await complexDeleteBtn.click(); await window.waitForTimeout(700); await expect(complexItem).not.toBeVisible(); expect(uncaughtExceptions).toHaveLength(0); }); test('RT-03: LLM Commands Deep CRUD, Activation Toggle & Interactive Form Verification', async () => { // Navigate to Commands Tab await window.locator('text=/^(Commands|명령어)$/').first().click(); await expect(window.locator('text=/^(LLM Commands|LLM 명령어)$/').first()).toBeVisible(); // Click Add Command button const addCommandBtn = window.locator('header button:has-text("추가"), header button:has-text("Add"), button:has-text("추가"), button:has-text("Add")').first(); await expect(addCommandBtn).toBeVisible({ timeout: 5000 }); await addCommandBtn.click(); await window.waitForTimeout(500); // Verify Dialog opens const dialogTitle = window.locator('text=/^(명령어 추가|Add Command)$/').first(); await expect(dialogTitle).toBeVisible({ timeout: 5000 }); // Fill Command Form const nameInput = window.locator('div[role="dialog"] input').first(); const descInput = window.locator('div[role="dialog"] input').nth(1); const promptInput = window.locator('div[role="dialog"] textarea').first(); await nameInput.fill('RT_BulletPoints_Custom'); await descInput.fill('레드팀 불릿포인트 요약 테스트'); await promptInput.fill('다음 음성 텍스트를 글머리 기호(bullet points)로 간결하게 요약해주세요:\n\n{text}'); const saveBtn = window.locator('div[role="dialog"] button:has-text("저장"), div[role="dialog"] button:has-text("Save")').first(); await saveBtn.click(); await window.waitForTimeout(600); // Verify card is added const createdCmd = window.locator('text=RT_BulletPoints_Custom').first(); await expect(createdCmd).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt03_01_command_created.png') }); // Test Edit Command const editBtn = window.getByRole('button', { name: '편집' }).last(); await expect(editBtn).toBeVisible({ timeout: 5000 }); await editBtn.click(); await window.waitForTimeout(400); await expect(window.getByRole('dialog')).toBeVisible({ timeout: 5000 }); const editNameInput = window.getByRole('dialog').locator('input').first(); await editNameInput.fill('RT_BulletPoints_Custom_MOD'); await window.getByRole('dialog').getByRole('button', { name: '저장' }).click(); await window.waitForTimeout(600); // Verify modified command const editedCmd = window.locator('text=RT_BulletPoints_Custom_MOD').first(); await expect(editedCmd).toBeVisible({ timeout: 5000 }); // Delete Command const deleteBtn = window.getByRole('button', { name: '삭제' }).first(); await expect(deleteBtn).toBeVisible({ timeout: 5000 }); await deleteBtn.click(); await window.waitForTimeout(700); // Verify deleted await expect(window.locator('text=RT_BulletPoints_Custom_MOD')).not.toBeVisible(); expect(uncaughtExceptions).toHaveLength(0); }); test('RT-04: History Search, Filter and Card Interaction Integrity', async () => { // Navigate to History Tab await window.locator('text=/^(History|히스토리|기록)$/').first().click(); await expect(window.locator('text=/^(History|히스토리|변환 기록)$/').first()).toBeVisible(); // Verify Search Input exists and accepts query const searchInput = window.locator('input[placeholder*="검색"], input[placeholder*="Search"]').first(); await expect(searchInput).toBeVisible(); await searchInput.fill('오늘'); await window.waitForTimeout(300); await searchInput.fill(''); await window.waitForTimeout(300); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt04_01_history_view.png') }); expect(uncaughtExceptions).toHaveLength(0); }); test('RT-05: Dashboard Health & Stat Widgets Non-Crashing Check', async () => { // Navigate to Dashboard Tab await window.locator('text=/^(Dashboard|대시보드)$/').first().click(); await expect(window.locator('text=/^(Current Backend|현재 백엔드)$/').first()).toBeVisible({ timeout: 5000 }); // Check stats and status widgets await expect(window.locator('text=/^(전사 기록|최근 변환 기록|최근 전사|Recent Transcriptions)/').first()).toBeVisible({ timeout: 5000 }); await expect(window.locator('text=/^(시스템 정상|System OK|정상)/').first()).toBeVisible({ timeout: 5000 }); await window.screenshot({ path: path.join(SCREENSHOT_DIR, 'rt05_01_dashboard_verified.png') }); expect(uncaughtExceptions).toHaveLength(0); }); });