mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
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>
395 lines
15 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|
|
});
|