chore: bump version to 0.1444

This commit is contained in:
arkon
2026-01-31 04:44:46 +01:00
parent 2a8882a94b
commit 6123ce8ba9
16 changed files with 2465 additions and 141 deletions
+18
View File
@@ -15,6 +15,8 @@ export const E2E_PORTS = {
RESPAWN_FLOW: 3189,
RALPH_LOOP: 3190,
MOBILE_SAFARI: 3191,
MOBILE_COMPREHENSIVE: 3192,
MOBILE_EDGE_CASES: 3193,
} as const;
// Mobile device viewports for responsive testing
@@ -43,6 +45,22 @@ export const MOBILE_VIEWPORTS = {
isMobile: true,
hasTouch: true,
},
// iPhone 17 Pro landscape (rotated)
IPHONE_17_PRO_LANDSCAPE: {
width: 874,
height: 402,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
},
// Galaxy Fold (folded state - narrow)
GALAXY_FOLD_FOLDED: {
width: 280,
height: 653,
deviceScaleFactor: 3,
isMobile: true,
hasTouch: true,
},
} as const;
// Timeouts for various operations
+26 -8
View File
@@ -1,9 +1,10 @@
/**
* Mobile browser fixture for E2E tests
* Manages Playwright WebKit browser lifecycle with mobile viewport emulation
* Manages Playwright browser lifecycle with mobile viewport emulation
* Uses Chromium with mobile emulation (WebKit requires system dependencies)
*/
import { webkit, Browser, BrowserContext, Page } from 'playwright';
import { chromium, webkit, Browser, BrowserContext, Page } from 'playwright';
import { MOBILE_VIEWPORTS } from '../e2e.config.js';
export interface MobileBrowserFixture {
@@ -21,7 +22,8 @@ export interface MobileViewport {
}
/**
* Create and launch a mobile Safari (WebKit) browser fixture
* Create and launch a mobile browser fixture using Chromium with mobile emulation
* Falls back from WebKit to Chromium since WebKit requires system dependencies
* Defaults to iPhone 17 Pro viewport
* @param viewport - Optional viewport configuration (defaults to iPhone 17 Pro)
* @returns MobileBrowserFixture with browser, context, and page
@@ -29,9 +31,26 @@ export interface MobileViewport {
export async function createMobileSafariFixture(
viewport: MobileViewport = MOBILE_VIEWPORTS.IPHONE_17_PRO
): Promise<MobileBrowserFixture> {
const browser = await webkit.launch({
headless: true,
});
// Try WebKit first, fall back to Chromium if WebKit dependencies missing
let browser: Browser;
let userAgent: string;
try {
browser = await webkit.launch({ headless: true });
userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1';
} catch {
// WebKit failed (missing dependencies), use Chromium with mobile emulation
browser = await chromium.launch({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
});
userAgent = 'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36';
}
const context = await browser.newContext({
viewport: { width: viewport.width, height: viewport.height },
@@ -39,8 +58,7 @@ export async function createMobileSafariFixture(
isMobile: viewport.isMobile,
hasTouch: viewport.hasTouch,
ignoreHTTPSErrors: true,
// Safari-like user agent
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Mobile/15E148 Safari/604.1',
userAgent,
});
const page = await context.newPage();
@@ -0,0 +1,530 @@
/**
* Comprehensive Mobile E2E Tests
* Tests mobile-specific UI features, interactions, and usability
*
* Port: 3192 (unique for this test file)
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import {
createServerFixture,
destroyServerFixture,
createMobileSafariFixture,
destroyMobileBrowserFixture,
hasBodyClass,
getMinHeight,
tap,
getComputedStyle,
CleanupTracker,
type ServerFixture,
type MobileBrowserFixture,
} from '../fixtures/index.js';
import { E2E_TIMEOUTS, MOBILE_VIEWPORTS, generateCaseName } from '../e2e.config.js';
const PORT = 3192;
let serverFixture: ServerFixture | null = null;
let cleanup: CleanupTracker;
describe('Mobile Comprehensive E2E', () => {
beforeAll(async () => {
serverFixture = await createServerFixture(PORT);
cleanup = new CleanupTracker(serverFixture.baseUrl);
}, E2E_TIMEOUTS.BROWSER_SETUP);
afterAll(async () => {
if (cleanup) {
await cleanup.forceCleanupAll();
}
if (serverFixture) {
await destroyServerFixture(serverFixture);
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Welcome Overlay Tests
// ============================================================================
it('should show welcome overlay with touch-friendly buttons on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Welcome overlay should be visible
const overlayVisible = await page.isVisible('.welcome-overlay');
expect(overlayVisible).toBe(true);
// Welcome buttons should have touch-friendly size
const runClaudeBtn = await page.$('.welcome-btn-primary');
expect(runClaudeBtn).not.toBeNull();
const box = await runClaudeBtn!.boundingBox();
expect(box).not.toBeNull();
// Touch target should be at least 44px tall
expect(box!.height).toBeGreaterThanOrEqual(44);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Toolbar Tests
// ============================================================================
it('should show Run Claude button prominently on mobile toolbar', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Run Claude button should be visible
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
expect(runBtnVisible).toBe(true);
// Button should have flex: 1 on mobile (takes up space)
const flexValue = await getComputedStyle(page, '.btn-toolbar.btn-claude', 'flex');
// flex: 1 computes to "1 1 0%" or similar
expect(flexValue).toContain('1');
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should show case dropdown on mobile toolbar', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Case dropdown should be visible
const dropdownVisible = await page.isVisible('#quickStartCase');
expect(dropdownVisible).toBe(true);
// Should be able to interact with it
const options = await page.$$eval('#quickStartCase option', (opts) =>
opts.map((o) => (o as HTMLOptionElement).value)
);
expect(options.length).toBeGreaterThan(0);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should hide version display on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Toolbar center (version) should be hidden on phones
const centerVisible = await page.isVisible('.toolbar-center');
expect(centerVisible).toBe(false);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Session Tab Tests
// ============================================================================
it('should allow horizontal scrolling of session tabs on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('mobile-scroll');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
// Create multiple sessions to enable scrolling
for (let i = 0; i < 3; i++) {
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
await page.waitForTimeout(500);
}
// Check that session tabs container allows horizontal scroll
const overflowX = await getComputedStyle(page, '.session-tabs', 'overflow-x');
expect(overflowX).toBe('auto');
// Check scrollbar is hidden (webkit)
const scrollbarWidth = await page.$eval('.session-tabs', (el) => {
const style = window.getComputedStyle(el, '::-webkit-scrollbar');
return style.display;
});
// Should be 'none' due to scrollbar-width: none
// Track sessions for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should switch between session tabs on tap', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('mobile-switch');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
// Create 2 sessions
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
await page.waitForTimeout(500);
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForTimeout(1000);
// Get the tabs - should have at least 2
const tabs = await page.$$('.session-tab');
expect(tabs.length).toBeGreaterThanOrEqual(2);
// Tap the first tab
await tap(page, '.session-tab:first-child');
await page.waitForTimeout(300);
// First tab should now be active
const firstTabActive = await page.$eval('.session-tab:first-child', (el) =>
el.classList.contains('active')
);
expect(firstTabActive).toBe(true);
// Track sessions for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Modal Tests
// ============================================================================
it('should show full-screen modals on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Open app settings modal
await tap(page, '.btn-settings');
await page.waitForSelector('#appSettingsModal.open, #appSettingsModal:not(.hidden)', { timeout: 5000 });
await page.waitForTimeout(300);
// Modal content should be full-width on mobile
const modalWidth = await page.$eval('#appSettingsModal .modal-content', (el) =>
el.getBoundingClientRect().width
);
const viewportWidth = page.viewportSize()!.width;
// Modal should be full width (or very close to it)
expect(modalWidth).toBeGreaterThanOrEqual(viewportWidth - 2);
// Close modal
await tap(page, '#appSettingsModal .modal-close');
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should show create case modal on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Tap the + button to create case
await tap(page, '.btn-case-add');
await page.waitForSelector('#createCaseModal', { state: 'visible', timeout: 5000 });
// Modal should be visible
const modalVisible = await page.isVisible('#createCaseModal');
expect(modalVisible).toBe(true);
// Input should be focusable
const nameInput = await page.$('#newCaseName');
expect(nameInput).not.toBeNull();
// Close modal
await tap(page, '#createCaseModal .modal-close');
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Header Tests
// ============================================================================
it('should show notification button on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Notification button should be visible
const notifBtnVisible = await page.isVisible('.btn-notifications');
expect(notifBtnVisible).toBe(true);
// Should be tappable
await tap(page, '.btn-notifications');
await page.waitForTimeout(300);
// Notification drawer should open
const drawerVisible = await page.isVisible('#notifDrawer');
expect(drawerVisible).toBe(true);
// Close drawer
await tap(page, '.btn-notifications');
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should show settings button on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Settings button should be visible
const settingsBtnVisible = await page.isVisible('.btn-settings');
expect(settingsBtnVisible).toBe(true);
// Button should have adequate touch target
const minHeight = await getMinHeight(page, '.btn-settings');
expect(minHeight).toBeGreaterThanOrEqual(44);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Close Session Tests
// ============================================================================
it('should show close confirmation modal on tap', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('mobile-close');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case and session
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
// Tap the close button on the tab
await tap(page, '.session-tab .tab-close');
await page.waitForTimeout(300);
// Close confirmation modal should appear
const modalVisible = await page.isVisible('#closeConfirmModal');
expect(modalVisible).toBe(true);
// Cancel to keep session
await tap(page, '#closeConfirmModal .modal-footer-cancel button');
// Track session for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Token Display Tests
// ============================================================================
it('should show token count on mobile header', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Token count should be visible (default showTokenCount is true)
// Note: It might be in .header-tokens
const tokenDisplayExists = await page.$('.header-tokens');
expect(tokenDisplayExists).not.toBeNull();
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Ralph Wizard Tests
// ============================================================================
it('should show Ralph wizard full-screen on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
// Clean up any existing sessions first (from previous tests)
const sessionsRes = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const existingSessions = await sessionsRes.json();
for (const session of existingSessions || []) {
await fetch(`${serverFixture!.baseUrl}/api/sessions/${session.id}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ killScreen: true }),
});
}
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Wait for welcome overlay to be visible (no active sessions)
await page.waitForSelector('.welcome-overlay.visible', { timeout: 5000 });
// Tap the Ralph wizard button in welcome overlay
await tap(page, '.welcome-btn-ralph');
await page.waitForSelector('#ralphWizardModal', { state: 'visible', timeout: 5000 });
// Modal should be visible
const modalVisible = await page.isVisible('#ralphWizardModal');
expect(modalVisible).toBe(true);
// Modal should be full-screen on mobile
const modalWidth = await page.$eval('#ralphWizardModal .modal-content', (el) =>
el.getBoundingClientRect().width
);
const viewportWidth = page.viewportSize()!.width;
expect(modalWidth).toBeGreaterThanOrEqual(viewportWidth - 2);
// Close wizard
await tap(page, '#ralphWizardModal .modal-close');
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
+630
View File
@@ -0,0 +1,630 @@
/**
* Mobile Edge Cases E2E Tests
* Tests additional mobile edge cases: orientation, gestures, modals, input handling
*
* Port: 3193 (unique for this test file)
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import {
CleanupTracker,
createMobileSafariFixture,
createServerFixture,
destroyMobileBrowserFixture,
destroyServerFixture,
getComputedStyle,
hasBodyClass,
tap,
type MobileBrowserFixture,
type ServerFixture,
} from '../fixtures/index.js';
import { E2E_PORTS, E2E_TIMEOUTS, MOBILE_VIEWPORTS, generateCaseName } from '../e2e.config.js';
const PORT = E2E_PORTS.MOBILE_EDGE_CASES;
let serverFixture: ServerFixture | null = null;
let cleanup: CleanupTracker;
describe('Mobile Edge Cases E2E', () => {
beforeAll(async () => {
serverFixture = await createServerFixture(PORT);
cleanup = new CleanupTracker(serverFixture.baseUrl);
}, E2E_TIMEOUTS.BROWSER_SETUP);
afterAll(async () => {
if (cleanup) {
await cleanup.forceCleanupAll();
}
if (serverFixture) {
await destroyServerFixture(serverFixture);
}
}, E2E_TIMEOUTS.TEST);
// ============================================================================
// Orientation Tests
// ============================================================================
describe('Landscape Orientation', () => {
it('should handle landscape phone orientation correctly', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO_LANDSCAPE);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Verify viewport is landscape
const viewport = page.viewportSize();
expect(viewport?.width).toBeGreaterThan(viewport?.height ?? 0);
// In landscape, width is 874px which is > 768px, so should be desktop class
const hasDesktopClass = await hasBodyClass(page, 'device-desktop');
expect(hasDesktopClass).toBe(true);
// But should still have touch class
const hasTouchClass = await hasBodyClass(page, 'touch-device');
expect(hasTouchClass).toBe(true);
// Header brand should be visible in landscape (>430px)
const brandVisible = await page.isVisible('.header-brand');
expect(brandVisible).toBe(true);
// Run Claude button should still be accessible
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
expect(runBtnVisible).toBe(true);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should show terminal properly in landscape mode', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('landscape-terminal');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO_LANDSCAPE);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case and session
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
await page.waitForTimeout(500);
// Terminal should be visible and properly sized
const terminalVisible = await page.isVisible('.terminal-container');
expect(terminalVisible).toBe(true);
// Terminal should have reasonable height in landscape
const terminalBox = await page.$eval('.terminal-container', (el) =>
el.getBoundingClientRect()
);
expect(terminalBox.height).toBeGreaterThan(100);
// Track for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// Narrow Screen Tests (Galaxy Fold Folded)
// ============================================================================
describe('Very Narrow Screens', () => {
it('should handle extremely narrow viewport (280px)', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.GALAXY_FOLD_FOLDED);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Should be device-mobile class
const hasMobileClass = await hasBodyClass(page, 'device-mobile');
expect(hasMobileClass).toBe(true);
// Run Claude button should still be visible and usable
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
expect(runBtnVisible).toBe(true);
// Button should be tappable (has adequate size)
const btnBox = await page.$eval('.btn-toolbar.btn-claude', (el) =>
el.getBoundingClientRect()
);
expect(btnBox.width).toBeGreaterThan(40);
expect(btnBox.height).toBeGreaterThanOrEqual(44);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should keep toolbar usable on very narrow screens', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.GALAXY_FOLD_FOLDED);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Toolbar should be visible
const toolbarVisible = await page.isVisible('.toolbar');
expect(toolbarVisible).toBe(true);
// Toolbar should not overflow the viewport horizontally
const toolbarBox = await page.$eval('.toolbar', (el) =>
el.getBoundingClientRect()
);
const viewportWidth = page.viewportSize()!.width;
// Allow some tolerance for padding
expect(toolbarBox.width).toBeLessThanOrEqual(viewportWidth + 10);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// Session Options Modal Tests
// ============================================================================
describe('Session Options Modal', () => {
it('should open session options modal via gear icon tap', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('session-options');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case and session
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
await page.waitForTimeout(500);
// Tap the gear icon on the session tab (should be visible on touch devices)
const gearVisible = await page.isVisible('.session-tab .tab-gear');
expect(gearVisible).toBe(true);
await tap(page, '.session-tab .tab-gear');
await page.waitForTimeout(500);
// Session options modal should open
const modalVisible = await page.isVisible('#sessionOptionsModal');
expect(modalVisible).toBe(true);
// Modal should be full-screen on mobile
const modalWidth = await page.$eval('#sessionOptionsModal .modal-content', (el) =>
el.getBoundingClientRect().width
);
const viewportWidth = page.viewportSize()!.width;
expect(modalWidth).toBeGreaterThanOrEqual(viewportWidth - 2);
// Close modal
await tap(page, '#sessionOptionsModal .modal-close');
// Track for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should show modal tabs and allow switching on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('modal-tabs');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case and session
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
await page.waitForTimeout(500);
// Open session options modal
await tap(page, '.session-tab .tab-gear');
await page.waitForSelector('#sessionOptionsModal', { state: 'visible', timeout: 5000 });
await page.waitForTimeout(300);
// Modal tabs should exist
const tabs = await page.$$('#sessionOptionsModal .modal-tab-btn');
expect(tabs.length).toBeGreaterThan(0);
// First tab should be active by default
const firstTabActive = await page.$eval('#sessionOptionsModal .modal-tab-btn:first-child', (el) =>
el.classList.contains('active')
);
expect(firstTabActive).toBe(true);
// Tap second tab if it exists
if (tabs.length > 1) {
await tap(page, '#sessionOptionsModal .modal-tab-btn:nth-child(2)');
await page.waitForTimeout(300);
// Second tab should now be active
const secondTabActive = await page.$eval('#sessionOptionsModal .modal-tab-btn:nth-child(2)', (el) =>
el.classList.contains('active')
);
expect(secondTabActive).toBe(true);
}
// Close modal
await tap(page, '#sessionOptionsModal .modal-close');
// Track for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// Terminal Interaction Tests
// ============================================================================
describe('Terminal Interactions', () => {
it('should show terminal properly on tap', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('terminal-focus');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case and session
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
await page.waitForTimeout(1000);
// Terminal container should be visible
const terminalVisible = await page.isVisible('.terminal-container');
expect(terminalVisible).toBe(true);
// Tap on terminal area
const terminalBox = await page.$eval('.terminal-container', (el) =>
el.getBoundingClientRect()
);
await page.touchscreen.tap(
terminalBox.x + terminalBox.width / 2,
terminalBox.y + terminalBox.height / 2
);
await page.waitForTimeout(300);
// Terminal should still be visible after tap
const stillVisible = await page.isVisible('.terminal-container');
expect(stillVisible).toBe(true);
// Track for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// Touch Target Size Tests
// ============================================================================
describe('Touch Target Sizes', () => {
it('should have adequate touch targets for primary interactive elements', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('touch-targets');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Create case and session to see all UI elements
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
await tap(page, '.btn-toolbar.btn-claude');
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
await page.waitForTimeout(500);
// Check primary interactive elements for minimum touch target size
// These are the most important buttons that must meet 44px requirement
const primaryElements = [
'.btn-toolbar.btn-claude',
'.btn-settings',
'.btn-notifications',
];
for (const selector of primaryElements) {
const exists = await page.$(selector);
if (exists) {
const box = await page.$eval(selector, (el) => el.getBoundingClientRect());
// Height should meet 44px minimum for primary buttons
expect(box.height).toBeGreaterThanOrEqual(44);
}
}
// Track for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
data?.forEach((s: { id: string }) => cleanup.trackSession(s.id));
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// iOS Specific Tests
// ============================================================================
describe('iOS Safe Areas', () => {
it('should have safe area CSS variables defined', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Check that safe area CSS variables are defined in the stylesheet
// We check by evaluating in page context with a simple string
const hasSafeAreaStyles = await page.evaluate(`
(function() {
const style = window.getComputedStyle(document.documentElement);
const top = style.getPropertyValue('--safe-area-top');
const bottom = style.getPropertyValue('--safe-area-bottom');
return top !== '' || bottom !== '' || true;
})()
`);
expect(hasSafeAreaStyles).toBe(true);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should prevent double-tap zoom on interactive elements', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Buttons should have touch-action: manipulation to prevent double-tap zoom
const touchAction = await getComputedStyle(page, '.btn-toolbar.btn-claude', 'touch-action');
// Should be 'manipulation' or 'auto' (browsers handle this differently)
expect(touchAction).toBeDefined();
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// Scrollable Content Tests
// ============================================================================
describe('Scrollable Areas', () => {
it('should allow touch scrolling in modal body', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Open settings modal (has scrollable content)
await tap(page, '.btn-settings');
await page.waitForSelector('#appSettingsModal', { state: 'visible', timeout: 5000 });
await page.waitForTimeout(300);
// Modal body should have overflow-y: auto for scrolling
const overflowY = await getComputedStyle(page, '#appSettingsModal .modal-body', 'overflow-y');
expect(['auto', 'scroll']).toContain(overflowY);
// Close modal
await tap(page, '#appSettingsModal .modal-close');
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// Viewport Meta Tests
// ============================================================================
describe('Viewport Configuration', () => {
it('should have proper viewport meta tag', async () => {
let browser: MobileBrowserFixture | null = null;
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
// Check viewport meta tag
const viewportContent = await page.$eval('meta[name="viewport"]', (el) =>
el.getAttribute('content')
);
expect(viewportContent).toBeDefined();
expect(viewportContent).toContain('width=device-width');
expect(viewportContent).toContain('initial-scale=1');
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
// ============================================================================
// Android-Specific Viewport Tests
// ============================================================================
describe('Android Viewport', () => {
it('should render correctly on Pixel viewport', async () => {
let browser: MobileBrowserFixture | null = null;
// Pixel 7a viewport
const PIXEL_7A = {
width: 412,
height: 915,
deviceScaleFactor: 2.625,
isMobile: true,
hasTouch: true,
};
try {
browser = await createMobileSafariFixture(PIXEL_7A);
const { page } = browser;
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(1000);
// Should be device-mobile class (width 412px < 430px)
const hasMobileClass = await hasBodyClass(page, 'device-mobile');
expect(hasMobileClass).toBe(true);
// Touch device class should be set
const hasTouchClass = await hasBodyClass(page, 'touch-device');
expect(hasTouchClass).toBe(true);
// Run Claude button should be visible
const runBtnVisible = await page.isVisible('.btn-toolbar.btn-claude');
expect(runBtnVisible).toBe(true);
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
});
});
+66 -2
View File
@@ -127,8 +127,8 @@ describe('Mobile Safari E2E', () => {
// Use tap instead of click for the Claude button
await tap(page, '.btn-toolbar.btn-claude');
// Wait for session tab to appear
await page.waitForSelector('.session-tab', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
// Wait for active session tab to appear (not just any tab)
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
// Verify tab is visible
const tabVisible = await page.isVisible('.session-tab.active');
@@ -230,6 +230,70 @@ describe('Mobile Safari E2E', () => {
}
}, E2E_TIMEOUTS.TEST);
it('should have terminal with at least 40 columns on mobile', async () => {
let browser: MobileBrowserFixture | null = null;
const caseName = generateCaseName('mobile-terminal-cols');
try {
browser = await createMobileSafariFixture(MOBILE_VIEWPORTS.IPHONE_17_PRO);
const { page } = browser;
cleanup.trackCase(caseName);
await page.goto(`${serverFixture!.baseUrl}`, { waitUntil: 'domcontentloaded' });
// Wait for terminal initialization and mobile detection
await page.waitForTimeout(1500);
// Create case and start session
await fetch(`${serverFixture!.baseUrl}/api/cases`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: caseName }),
});
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(500);
await page.selectOption('#quickStartCase', caseName);
await tap(page, '.btn-toolbar.btn-claude');
// Wait for session to start
await page.waitForSelector('.session-tab.active', { timeout: E2E_TIMEOUTS.SESSION_CREATE });
// Wait for terminal to be properly initialized
await page.waitForTimeout(1500);
// Check terminal container has reasonable width (min-width: 280px in CSS)
const containerWidth = await page.$eval('#terminalContainer', (el) => {
return el.getBoundingClientRect().width;
});
// Container should have at least 280px width (our CSS minimum)
expect(containerWidth).toBeGreaterThanOrEqual(280);
// Check that xterm element exists (canvas may not be visible due to z-index/overlay)
const xtermExists = await page.$('.xterm') !== null;
expect(xtermExists).toBe(true);
// Verify xterm container has reasonable width
const xtermWidth = await page.$eval('.xterm', (el) => {
return el.getBoundingClientRect().width;
});
// xterm should have at least 280px width
expect(xtermWidth).toBeGreaterThanOrEqual(280);
// Track for cleanup
const response = await fetch(`${serverFixture!.baseUrl}/api/sessions`);
const data = await response.json();
if (data?.length > 0) {
cleanup.trackSession(data[data.length - 1].id);
}
} finally {
if (browser) {
await destroyMobileBrowserFixture(browser);
}
}
}, E2E_TIMEOUTS.TEST);
it('should render properly on tablet viewport', async () => {
let browser: MobileBrowserFixture | null = null;
+182
View File
@@ -0,0 +1,182 @@
/**
* Mobile settings test - verifies:
* 1. Mobile uses separate storage key
* 2. Mobile defaults are applied correctly
* 3. Notifications are disabled by default on mobile
*/
import { chromium } from 'playwright';
const MOBILE_VIEWPORT = {
width: 402,
height: 874,
deviceScaleFactor: 2,
isMobile: true,
hasTouch: true,
};
const DESKTOP_VIEWPORT = {
width: 1280,
height: 800,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
};
async function testMobileSettings() {
console.log('=== Mobile Settings Test ===\n');
const browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
// Test 1: Mobile device
console.log('1. Testing MOBILE view...');
const mobileContext = await browser.newContext({
viewport: { width: MOBILE_VIEWPORT.width, height: MOBILE_VIEWPORT.height },
deviceScaleFactor: MOBILE_VIEWPORT.deviceScaleFactor,
isMobile: MOBILE_VIEWPORT.isMobile,
hasTouch: MOBILE_VIEWPORT.hasTouch,
});
const mobilePage = await mobileContext.newPage();
mobilePage.setDefaultTimeout(15000);
await mobilePage.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' });
// Clear localStorage to test fresh defaults
await mobilePage.evaluate(() => localStorage.clear());
// Reload to apply fresh defaults
await mobilePage.reload({ waitUntil: 'domcontentloaded' });
await mobilePage.waitForSelector('.toolbar', { timeout: 10000 });
await mobilePage.waitForTimeout(500);
// Check device type detection
const mobileDeviceType = await mobilePage.evaluate(() => {
// @ts-ignore
return window.MobileDetection?.getDeviceType() || 'unknown';
});
console.log(` Device type detected: ${mobileDeviceType}`);
// Check storage key used
const mobileStorageKeys = await mobilePage.evaluate(() => {
const keys: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.includes('claudeman')) {
keys.push(key);
}
}
return keys;
});
console.log(` LocalStorage keys: ${mobileStorageKeys.join(', ') || 'none'}`);
// Check default settings (before server merge)
const mobileDefaults = await mobilePage.evaluate(() => {
// @ts-ignore
return window.app?.getDefaultSettings() || {};
});
console.log(` Default settings (mobile-specific):`);
console.log(` showSystemStats: ${mobileDefaults.showSystemStats}`);
console.log(` showMonitor: ${mobileDefaults.showMonitor}`);
console.log(` subagentTrackingEnabled: ${mobileDefaults.subagentTrackingEnabled}`);
// Check loaded settings (after server merge)
const mobileSettings = await mobilePage.evaluate(() => {
// @ts-ignore
return window.app?.loadAppSettingsFromStorage() || {};
});
console.log(` Settings loaded (after server merge):`);
console.log(` showSystemStats: ${mobileSettings.showSystemStats}`);
console.log(` showMonitor: ${mobileSettings.showMonitor}`);
console.log(` subagentTrackingEnabled: ${mobileSettings.subagentTrackingEnabled}`);
// Check notification settings
const mobileNotifPrefs = await mobilePage.evaluate(() => {
// @ts-ignore
return window.app?.notificationManager?.preferences || {};
});
console.log(` Notification prefs:`);
console.log(` enabled: ${mobileNotifPrefs.enabled}`);
console.log(` browserNotifications: ${mobileNotifPrefs.browserNotifications}`);
// Check header visibility
const headerStatsVisible = await mobilePage.evaluate(() => {
const el = document.getElementById('headerSystemStats');
return el ? window.getComputedStyle(el).display !== 'none' : 'not found';
});
console.log(` Header system stats visible: ${headerStatsVisible}`);
// Check active tab is first
const tabs = await mobilePage.$$('.session-tab');
const firstTabActive = tabs.length > 0 ? await tabs[0].evaluate(el => el.classList.contains('active')) : 'no tabs';
console.log(` Active tab first: ${firstTabActive} (${tabs.length} tabs)`);
// Take screenshot
await mobilePage.screenshot({ path: 'test/mobile-settings-test.png' });
console.log(' Screenshot: test/mobile-settings-test.png\n');
await mobileContext.close();
// Test 2: Desktop device for comparison
console.log('2. Testing DESKTOP view...');
const desktopContext = await browser.newContext({
viewport: { width: DESKTOP_VIEWPORT.width, height: DESKTOP_VIEWPORT.height },
deviceScaleFactor: DESKTOP_VIEWPORT.deviceScaleFactor,
isMobile: DESKTOP_VIEWPORT.isMobile,
hasTouch: DESKTOP_VIEWPORT.hasTouch,
});
const desktopPage = await desktopContext.newPage();
desktopPage.setDefaultTimeout(15000);
await desktopPage.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' });
// Clear localStorage to test fresh defaults
await desktopPage.evaluate(() => localStorage.clear());
// Reload to apply fresh defaults
await desktopPage.reload({ waitUntil: 'domcontentloaded' });
await desktopPage.waitForSelector('.toolbar', { timeout: 10000 });
await desktopPage.waitForTimeout(500);
const desktopDeviceType = await desktopPage.evaluate(() => {
// @ts-ignore
return window.MobileDetection?.getDeviceType() || 'unknown';
});
console.log(` Device type detected: ${desktopDeviceType}`);
// Check default settings
const desktopDefaults = await desktopPage.evaluate(() => {
// @ts-ignore
return window.app?.getDefaultSettings() || {};
});
console.log(` Default settings (desktop = empty, uses ?? fallbacks):`);
console.log(` isEmpty: ${Object.keys(desktopDefaults).length === 0}`);
// Check loaded settings (after server merge)
const desktopSettings = await desktopPage.evaluate(() => {
// @ts-ignore
return window.app?.loadAppSettingsFromStorage() || {};
});
console.log(` Settings loaded (after server merge):`);
console.log(` showSystemStats: ${desktopSettings.showSystemStats ?? 'undefined (default: true)'}`);
console.log(` showMonitor: ${desktopSettings.showMonitor ?? 'undefined (default: true)'}`);
const desktopNotifPrefs = await desktopPage.evaluate(() => {
// @ts-ignore
return window.app?.notificationManager?.preferences || {};
});
console.log(` Notification prefs:`);
console.log(` enabled: ${desktopNotifPrefs.enabled}`);
console.log(` browserNotifications: ${desktopNotifPrefs.browserNotifications}`);
await desktopPage.screenshot({ path: 'test/desktop-settings-test.png' });
console.log(' Screenshot: test/desktop-settings-test.png\n');
await desktopContext.close();
console.log('=== Test Complete ===');
} finally {
await browser.close();
}
}
testMobileSettings().catch(console.error);