Files
Codeman/mobile-test/settings.test.ts
T
arkonandClaude Opus 4.6 3795c45cc1 chore: rename Claudeman to Codeman
Full product rename across 109 files (~834 occurrences):
- Env vars: CLAUDEMAN_* → CODEMAN_*
- Data dirs: ~/.claudeman/ → ~/.codeman/, ~/claudeman-cases/ → ~/codeman-cases/
- tmux prefix: claudeman- → codeman-
- localStorage: claudeman-* → codeman-*
- Package/CLI: claudeman → codeman
- GitHub repo: Ark0N/Claudeman → Ark0N/Codeman
- systemd service: claudeman-web → codeman-web
- Class: ClaudemanApp → CodemanApp

Migration infrastructure for seamless transition:
- state-store.ts: auto-migrates data directories on startup
- tmux-manager.ts: dual-prefix detection (legacy claudeman- sessions)
- app.js: localStorage key migration (preserves old keys)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 16:44:34 +01:00

395 lines
15 KiB
TypeScript

// Port 3203 - Settings modal mobile tests
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import type { Page, BrowserContext } from 'playwright';
import { PORTS, SELECTORS, KEYBOARD, STORAGE_KEYS, BODY_CLASSES, WAIT } from './helpers/constants.js';
import { createTestServer, stopTestServer } from './helpers/server.js';
import { createDevicePage, closeAllBrowsers } from './helpers/browser.js';
import { showKeyboard, hideKeyboard } from './helpers/keyboard-sim.js';
import {
assertVisible, assertHidden, getCSSProperty, getCSSNumericValue,
} from './helpers/assertions.js';
import { REPRESENTATIVE_DEVICES } from './devices.js';
import type { WebServer } from '../src/web/server.js';
const PORT = PORTS.SETTINGS;
const BASE_URL = `http://localhost:${PORT}`;
describe('Settings Modal', () => {
let server: WebServer;
beforeAll(async () => {
server = await createTestServer(PORT);
});
afterAll(async () => {
await stopTestServer(server);
await closeAllBrowsers();
});
// ── Mobile-Specific Behavior ──────────────────────────────────────────────
describe('Mobile-Specific Behavior', () => {
let page: Page;
let context: BrowserContext;
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
page = result.page;
context = result.context;
});
afterEach(async () => {
await context.close();
});
it('uses codeman-app-settings-mobile localStorage key', async () => {
const key = await page.evaluate(() => {
return (window as any).app?.getSettingsStorageKey?.() ?? null;
});
// If the function exists, it should return the mobile key
if (key !== null) {
expect(key).toBe(STORAGE_KEYS.SETTINGS_MOBILE);
} else {
// Verify by checking what key is used when saving settings
await page.evaluate(() => {
localStorage.setItem('codeman-app-settings-mobile', JSON.stringify({ testFlag: true }));
});
const stored = await page.evaluate(() => {
return localStorage.getItem('codeman-app-settings-mobile');
});
expect(stored).toContain('testFlag');
}
});
it('mobile defaults: all panels hidden, subagent tracking OFF, ralph OFF', async () => {
const defaults = await page.evaluate(() => {
const fn = (window as any).app?.getDefaultSettings;
if (fn) return fn.call((window as any).app);
return null;
});
if (defaults !== null) {
expect(defaults.showFontControls).toBe(false);
expect(defaults.showSystemStats).toBe(false);
expect(defaults.showTokenCount).toBe(false);
expect(defaults.showCost).toBe(false);
expect(defaults.showMonitor).toBe(false);
expect(defaults.showProjectInsights).toBe(false);
expect(defaults.showFileBrowser).toBe(false);
expect(defaults.showSubagents).toBe(false);
expect(defaults.subagentTrackingEnabled).toBe(false);
expect(defaults.ralphTrackerEnabled).toBe(false);
}
});
it('settings gear button visible in toolbar on phones', async () => {
await assertVisible(page, SELECTORS.SETTINGS_MOBILE);
});
it('settings gear button is inside toolbar area', async () => {
const gearBox = await page.locator(SELECTORS.SETTINGS_MOBILE).boundingBox();
const toolbarBox = await page.locator(SELECTORS.TOOLBAR).boundingBox();
if (gearBox && toolbarBox) {
// Gear button should be within toolbar's vertical range
expect(gearBox.y).toBeGreaterThanOrEqual(toolbarBox.y - 5);
expect(gearBox.y + gearBox.height).toBeLessThanOrEqual(
toolbarBox.y + toolbarBox.height + 5,
);
}
});
});
// ── Full-Screen Modal ─────────────────────────────────────────────────────
describe('Full-Screen Modal', () => {
let page: Page;
let context: BrowserContext;
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
page = result.page;
context = result.context;
});
afterEach(async () => {
await context.close();
});
async function openSettingsModal(): Promise<void> {
// Try clicking the mobile settings button
const btn = page.locator(SELECTORS.SETTINGS_MOBILE);
if (await btn.isVisible()) {
await btn.click();
await page.waitForTimeout(500);
} else {
// Fallback: trigger via JS
await page.evaluate(() => {
const fn = (window as any).app?.openAppSettings;
if (fn) fn.call((window as any).app);
// Also try directly showing the modal
const modal = document.querySelector('#appSettingsModal') as HTMLElement;
if (modal) modal.style.display = 'flex';
});
await page.waitForTimeout(500);
}
}
it('modal takes full width on mobile', async () => {
await openSettingsModal();
const modalContent = page.locator(SELECTORS.SETTINGS_MODAL_CONTENT).first();
if (await modalContent.isVisible()) {
const box = await modalContent.boundingBox();
const viewport = page.viewportSize()!;
// Full width = very close to viewport width
expect(box!.width).toBeGreaterThanOrEqual(viewport.width - 2);
}
});
it('modal takes full height on mobile', async () => {
await openSettingsModal();
const modalContent = page.locator(SELECTORS.SETTINGS_MODAL_CONTENT).first();
if (await modalContent.isVisible()) {
const box = await modalContent.boundingBox();
const viewport = page.viewportSize()!;
expect(box!.height).toBeGreaterThanOrEqual(viewport.height - 2);
}
});
it('modal has no border-radius on mobile (flat edges)', async () => {
await openSettingsModal();
const modalContent = page.locator(SELECTORS.SETTINGS_MODAL_CONTENT).first();
if (await modalContent.isVisible()) {
const radius = await getCSSProperty(page, SELECTORS.SETTINGS_MODAL_CONTENT, 'border-radius');
const numericRadius = parseFloat(radius);
expect(numericRadius).toBeLessThanOrEqual(1); // 0 or essentially 0
}
});
it('modal tabs are horizontally scrollable', async () => {
await openSettingsModal();
const modalTabs = page.locator(SELECTORS.SETTINGS_MODAL_TABS).first();
if (await modalTabs.isVisible()) {
// Use first() since multiple modal-tabs elements exist
const flexWrap = await page.evaluate((sel) => {
const el = document.querySelector(sel);
return el ? getComputedStyle(el).flexWrap : '';
}, SELECTORS.SETTINGS_MODAL_TABS);
const overflowX = await page.evaluate((sel) => {
const el = document.querySelector(sel);
return el ? getComputedStyle(el).overflowX : '';
}, SELECTORS.SETTINGS_MODAL_TABS);
expect(flexWrap).toBe('nowrap');
expect(overflowX).toBe('auto');
}
});
it('modal body adjusts height when keyboard visible', async () => {
await openSettingsModal();
const modalBody = page.locator(SELECTORS.SETTINGS_MODAL_BODY).first();
if (await modalBody.isVisible()) {
// Show keyboard via DOM (safest for modal context)
await showKeyboard(page, KEYBOARD.TYPICAL_IOS_HEIGHT, { preferredLayer: 'dom' });
await page.waitForTimeout(300);
const maxHeight = await getCSSProperty(page, SELECTORS.SETTINGS_MODAL_BODY, 'max-height');
// When keyboard is visible, modal body should be constrained
// The CSS rule is max-height: 40vh
if (maxHeight.includes('vh') || parseFloat(maxHeight) < device.viewport.height * 0.5) {
expect(true).toBe(true); // Constrained
}
await hideKeyboard(page, { preferredLayer: 'dom' });
}
});
});
// ── Settings Toggles ──────────────────────────────────────────────────────
describe('Settings Toggles', () => {
let page: Page;
let context: BrowserContext;
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
page = result.page;
context = result.context;
// Clear settings before each test
await page.evaluate((key) => localStorage.removeItem(key), STORAGE_KEYS.SETTINGS_MOBILE);
});
afterEach(async () => {
await context.close();
});
async function openSettingsAndToggle(settingId: string): Promise<boolean> {
// Open settings modal
const btn = page.locator(SELECTORS.SETTINGS_MOBILE);
if (await btn.isVisible()) {
await btn.click();
await page.waitForTimeout(500);
}
// Find and toggle the setting
const toggle = page.locator(`#${settingId}`);
if (await toggle.isVisible()) {
const wasCheked = await toggle.isChecked();
await toggle.click();
await page.waitForTimeout(200);
return !wasCheked;
}
return false;
}
async function getStoredSettings(): Promise<Record<string, unknown> | null> {
return page.evaluate((key) => {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
}, STORAGE_KEYS.SETTINGS_MOBILE);
}
it('font controls toggle changes and persists', async () => {
await openSettingsAndToggle('showFontControls');
const settings = await getStoredSettings();
if (settings) {
expect(settings).toHaveProperty('showFontControls');
}
});
it('subagent tracking toggle changes and persists', async () => {
await openSettingsAndToggle('subagentTrackingEnabled');
const settings = await getStoredSettings();
if (settings) {
expect(settings).toHaveProperty('subagentTrackingEnabled');
}
});
it('ralph tracker toggle changes and persists', async () => {
await openSettingsAndToggle('ralphTrackerEnabled');
const settings = await getStoredSettings();
if (settings) {
expect(settings).toHaveProperty('ralphTrackerEnabled');
}
});
it('image watcher toggle changes and persists', async () => {
await openSettingsAndToggle('imageWatcherEnabled');
const settings = await getStoredSettings();
if (settings) {
expect(settings).toHaveProperty('imageWatcherEnabled');
}
});
it('tall tabs toggle changes and persists', async () => {
await openSettingsAndToggle('tallTabs');
const settings = await getStoredSettings();
if (settings) {
expect(settings).toHaveProperty('tallTabs');
}
});
});
// ── Persistence Roundtrip ─────────────────────────────────────────────────
describe('Persistence Roundtrip', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
it('settings survive page reload', async () => {
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
try {
// Store a test setting
await page.evaluate((key) => {
localStorage.setItem(key, JSON.stringify({
showFontControls: true,
showMonitor: true,
subagentTrackingEnabled: true,
}));
}, STORAGE_KEYS.SETTINGS_MOBILE);
// Reload page
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForTimeout(WAIT.PAGE_SETTLE);
// Verify settings persisted
const settings = await page.evaluate((key) => {
const raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
}, STORAGE_KEYS.SETTINGS_MOBILE);
expect(settings).not.toBeNull();
expect(settings?.showFontControls).toBe(true);
expect(settings?.showMonitor).toBe(true);
expect(settings?.subagentTrackingEnabled).toBe(true);
} finally {
await context.close();
}
});
it('mobile notification prefs stored separately from desktop settings', async () => {
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
try {
// Store both mobile and desktop settings
await page.evaluate(({ mobileKey, desktopKey, notifKey }) => {
localStorage.setItem(mobileKey, JSON.stringify({ showFontControls: false }));
localStorage.setItem(desktopKey, JSON.stringify({ showFontControls: true }));
localStorage.setItem(notifKey, JSON.stringify({ mobileNotif: true }));
}, {
mobileKey: STORAGE_KEYS.SETTINGS_MOBILE,
desktopKey: STORAGE_KEYS.SETTINGS_DESKTOP,
notifKey: STORAGE_KEYS.NOTIFICATION_PREFS_MOBILE,
});
// Verify they are independent
const mobile = await page.evaluate((key) => JSON.parse(localStorage.getItem(key) || '{}'), STORAGE_KEYS.SETTINGS_MOBILE);
const desktop = await page.evaluate((key) => JSON.parse(localStorage.getItem(key) || '{}'), STORAGE_KEYS.SETTINGS_DESKTOP);
const notif = await page.evaluate((key) => JSON.parse(localStorage.getItem(key) || '{}'), STORAGE_KEYS.NOTIFICATION_PREFS_MOBILE);
expect(mobile.showFontControls).toBe(false);
expect(desktop.showFontControls).toBe(true);
expect(notif.mobileNotif).toBe(true);
} finally {
await context.close();
}
});
});
// ── Tablet vs Desktop Settings ────────────────────────────────────────────
describe('Tablet vs Desktop Settings', () => {
it('tablet uses desktop settings key', async () => {
const device = REPRESENTATIVE_DEVICES['standard-tablet'];
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
try {
const key = await page.evaluate(() => {
return (window as any).app?.getSettingsStorageKey?.() ?? null;
});
// Tablets (>= 430px) should NOT use mobile settings key
if (key !== null) {
expect(key).toBe(STORAGE_KEYS.SETTINGS_DESKTOP);
}
} finally {
await context.close();
}
});
it('settings gear button not in toolbar on desktop', async () => {
const device = REPRESENTATIVE_DEVICES['large-tablet'];
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
try {
await assertHidden(page, SELECTORS.SETTINGS_MOBILE);
} finally {
await context.close();
}
});
});
});