Wave 2 of the 2026-09-28 design overhaul ("records are the main
character, a quiet instrument"). Decisions: design.md.
- Shell: sidebar with primary Home / Records / Meetings (Ctrl+1..3) and a
secondary "Tools" group; every item is a button with aria-current.
The status bar is removed — its "100% LOCAL PRIVACY · ZERO CLOUD SYNC"
claim contradicted device sync; the sidebar shows only queried state.
- Navigation context (routes, params, back, useRouteRequest) so Home can
open a specific meeting or record.
- Home replaces the dashboard: dictation key, start actions, only real
problems, one-line summary, recent records and meetings, file
transcription, free-tier limits.
- Settings become a full-screen route with vertical tabs (tabs no longer
clip) and a shared row grammar; long-standing bugs fixed: team and
enterprise tiers crashed the license tab (currentTier undefined),
first-run onboarding could not be left, Pro+ saw "upgrade to Pro+".
- Records: date-grouped single list with in-place expansion, search and
tags together, mode/favorite filters, load more past 50, undoable
delete. Meetings: list with search/rename/delete, summary-first detail
(summary | transcript | documents | my notes), notes persisted locally
instead of being lost. Tools pages cleaned up; stale-closure example
chips and a 1550x mouse-distance bug fixed.
- Shared pieces: PageHeader, EmptyStateCard, SearchInput ("/" works),
ConfirmDialog, notify with undo; display-labels maps domain values to
typed i18n keys so missing keys fail to compile.
- Mono font stack falls back to Pretendard for Hangul (Korean labels no
longer render spaced out).
- i18n: new ko/en keys via scripts/i18n-add-keys.mjs (one read-merge-
write so parallel editors cannot clobber locale files).
Some renderer files also carried pre-existing in-flight changes (error
recovery deep links, meeting document hooks); they are included as-is.
130 lines
4.6 KiB
TypeScript
130 lines
4.6 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { test, expect, _electron as electron } from '@playwright/test';
|
|
|
|
const SCREENSHOT_DIR = 'C:/Users/encep/.gemini/antigravity-cli/brain/3ab7ae82-5634-41d5-93a7-3c12c22503e2/screenshots';
|
|
|
|
test.describe('Launch Readiness E2E & Visual Verification', () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
let electronApp: any;
|
|
let window: any;
|
|
|
|
test.beforeAll(async () => {
|
|
if (!fs.existsSync(SCREENSHOT_DIR)) {
|
|
fs.mkdirSync(SCREENSHOT_DIR, { recursive: true });
|
|
}
|
|
|
|
electronApp = await electron.launch({
|
|
args: [
|
|
'out/main/index.js',
|
|
'--disable-gpu',
|
|
'--no-sandbox',
|
|
'--user-data-dir=C:/Users/encep/AppData/Local/Temp/playwright-d3ro-launch-test-' + Date.now(),
|
|
],
|
|
env: {
|
|
...process.env,
|
|
NODE_ENV: 'test',
|
|
},
|
|
});
|
|
|
|
// Poll for main window (excluding popup windows)
|
|
for (let i = 0; i < 40; 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 {
|
|
// ignore
|
|
}
|
|
}
|
|
if (window) break;
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
}
|
|
|
|
if (!window) {
|
|
window = electronApp.windows()[0] || await electronApp.firstWindow();
|
|
}
|
|
await window.waitForLoadState('domcontentloaded');
|
|
|
|
// Bypass onboarding via IPC config set
|
|
await window.evaluate(async () => {
|
|
if (window.electronAPI && window.electronAPI.config) {
|
|
await window.electronAPI.config.set({ key: 'onboardingCompleted', value: true });
|
|
await window.electronAPI.config.set({ key: 'appUsageMode', value: 'online' });
|
|
}
|
|
});
|
|
|
|
await window.waitForTimeout(500);
|
|
await window.reload();
|
|
await window.waitForLoadState('domcontentloaded');
|
|
await window.waitForTimeout(1500);
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
if (electronApp) {
|
|
await electronApp.close();
|
|
}
|
|
});
|
|
|
|
test('01. Should render desktop app with Free Tier AdBanner and Support button', async () => {
|
|
// Check if sidebar has Customer Support button
|
|
const supportBtn = window.locator('text=/^(Customer Support|고객지원)$/').first();
|
|
await expect(supportBtn).toBeVisible({ timeout: 10000 });
|
|
|
|
// Check if Free Tier AdBanner is visible
|
|
const adBanner = window.locator('text=/Cursor AI/').first();
|
|
await expect(adBanner).toBeVisible({ timeout: 10000 });
|
|
|
|
// Take screenshot of main window with AdBanner and sidebar
|
|
await window.screenshot({ path: path.join(SCREENSHOT_DIR, '01_desktop_main_with_adbanner.png') });
|
|
});
|
|
|
|
test('02. Should open SupportModal and interact with AI Customer Assistant', async () => {
|
|
// Click Customer Support in sidebar
|
|
await window.locator('text=/^(Customer Support|고객지원)$/').first().click();
|
|
await window.waitForTimeout(500);
|
|
|
|
// Verify modal header is visible
|
|
await expect(window.locator('text=/D3RO Voice Customer Assistance/').first()).toBeVisible();
|
|
|
|
// Click quick prompt "🎤 마이크 인식 오류"
|
|
const micPrompt = window.locator('text=/마이크 인식/').first();
|
|
if (await micPrompt.isVisible()) {
|
|
await micPrompt.click();
|
|
await window.waitForTimeout(800);
|
|
}
|
|
|
|
// Capture screenshot of SupportModal with AI chat
|
|
await window.screenshot({ path: path.join(SCREENSHOT_DIR, '02_support_modal_ai_chat.png') });
|
|
|
|
// Switch to 7-Day Refund Tab (tab 3)
|
|
const refundTab = window.locator('text=/7일 자동 환불/').first();
|
|
await refundTab.click();
|
|
await window.waitForTimeout(500);
|
|
|
|
// Click "환불 자격 자동 조회하기"
|
|
const checkRefundBtn = window.locator('button:has-text("환불 자격 자동 조회하기")').first();
|
|
if (await checkRefundBtn.isVisible()) {
|
|
await checkRefundBtn.click();
|
|
await window.waitForTimeout(1000);
|
|
}
|
|
|
|
// Capture screenshot of refund eligibility result
|
|
await window.screenshot({ path: path.join(SCREENSHOT_DIR, '03_support_modal_refund_check.png') });
|
|
|
|
// Close Support Modal
|
|
await window.keyboard.press('Escape');
|
|
await window.waitForTimeout(500);
|
|
});
|
|
|
|
test('03. Should expose the web billing upgrade link without an in-app checkout modal', async () => {
|
|
// "Remove ads with Pro" opens billingUrl({ tier }) in the external browser (LICENSE.OPEN_BILLING).
|
|
const removeAdsLink = window.locator('text=/Remove ads with Pro|Pro로 광고 없애기/').first();
|
|
if (await removeAdsLink.isVisible()) {
|
|
await expect(window.locator('text=/D3RO Voice Secure Checkout/')).toHaveCount(0);
|
|
}
|
|
});
|
|
});
|