mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 10:39:32 +01:00
Found with knip (exports, types, class members) and confirmed by a text search of src, test, scripts and packages before each removal. - Per-CLI get<Cli>NotFoundMessage() (nine of them) with their message constants: missingCliMessage() builds that text from the registry now. - Registry leftovers: renderCliCommand, asCliId, cliIds, isCliRunnable, createCliResolverForTest, profileExists, and two name lists that only repeated schema enums. - A task-description cache in Session that was written on every chunk of terminal output (one regex pass per line) and never read since the subagent watcher takes descriptions from transcripts. The cache module, its pattern and both parse methods go with it. - Session.parentAgentId / childAgentIds: never set by anything, so the two optional SessionState fields were never on the wire. - Unreferenced methods on TmuxManager (pipe-pane pair, syncMouseMode), RespawnController, StateStore.recoverFromBackup (load() already falls back to the .bak file), TeamWatcher, SseStreamManager and the AI checkers' getErrorVerdict. - updateCaseEnvVars: the settings.local.json env path that envOverrides replaced. - Re-exports in the utils, cli-registry, routes and prompts barrels that no importer used, and unused test helpers (MockStateStore, the mobile throttle helpers and others). Eight mobile tests imported the WebServer type from a path that does not exist (../src instead of ../../src); type-only, so nothing failed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
188 lines
7.2 KiB
TypeScript
188 lines
7.2 KiB
TypeScript
// Ephemeral port - Screenshot comparison tests
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import type { BrowserContext, Page } from 'playwright';
|
|
import type { WebServer } from '../../src/web/server.js';
|
|
import { createTestServer, stopTestServer } from './helpers/server.js';
|
|
import { createDevicePage, closeAllBrowsers } from './helpers/browser.js';
|
|
import { SELECTORS, VISUAL, VISUAL_BREAKPOINTS, WAIT } from './helpers/constants.js';
|
|
import { showKeyboard, hideKeyboard } from './helpers/keyboard-sim.js';
|
|
import { compareScreenshot, assertScreenshotMatch } from './helpers/visual.js';
|
|
import { REPRESENTATIVE_DEVICES } from './devices.js';
|
|
|
|
let baseUrl: string;
|
|
|
|
let server: WebServer;
|
|
|
|
// Use a representative subset of breakpoints to keep test time reasonable
|
|
// Covers: small phone, standard phone, large phone/small tablet boundary, tablet, desktop
|
|
const KEY_BREAKPOINTS = [320, 375, 393, 430, 768, 1024] as const;
|
|
|
|
describe('Visual Regression', () => {
|
|
beforeAll(async () => {
|
|
server = await createTestServer();
|
|
baseUrl = `http://localhost:${server.boundPort}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await closeAllBrowsers();
|
|
await stopTestServer(server);
|
|
});
|
|
|
|
// ─── Breakpoint Screenshots ───────────────────────────────────────────────
|
|
|
|
describe.each(
|
|
KEY_BREAKPOINTS.map(w => [w] as [number]),
|
|
)('Width %ipx', (width) => {
|
|
const height = 812; // Standard phone height for consistency
|
|
|
|
it('landing page (no sessions)', async () => {
|
|
const device = {
|
|
name: `custom-${width}`,
|
|
category: 'standard-phone' as const,
|
|
viewport: { width, height },
|
|
deviceScaleFactor: 2,
|
|
isMobile: width < 768,
|
|
hasTouch: width < 768,
|
|
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
|
|
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
|
|
isIOS: true,
|
|
defaultBrowserType: 'chromium' as const,
|
|
};
|
|
|
|
const { context, page } = await createDevicePage(device, baseUrl);
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
const result = await compareScreenshot(page, `landing-${width}w`);
|
|
if (result.isNewBaseline) {
|
|
console.log(`[visual] Created new baseline: landing-${width}w.png`);
|
|
}
|
|
expect(result.passed).toBe(true);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
it('with keyboard visible', async () => {
|
|
const device = {
|
|
name: `custom-${width}`,
|
|
category: 'standard-phone' as const,
|
|
viewport: { width, height },
|
|
deviceScaleFactor: 2,
|
|
isMobile: width < 768,
|
|
hasTouch: width < 768,
|
|
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
|
|
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
|
|
isIOS: true,
|
|
defaultBrowserType: 'chromium' as const,
|
|
};
|
|
|
|
const { context, page } = await createDevicePage(device, baseUrl);
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
// Show keyboard via DOM manipulation (works on all engines)
|
|
await showKeyboard(page, 336, { preferredLayer: 'dom' });
|
|
await page.waitForTimeout(WAIT.KEYBOARD_ANIMATION);
|
|
|
|
const result = await compareScreenshot(page, `keyboard-${width}w`);
|
|
if (result.isNewBaseline) {
|
|
console.log(`[visual] Created new baseline: keyboard-${width}w.png`);
|
|
}
|
|
expect(result.passed).toBe(true);
|
|
|
|
await hideKeyboard(page, { preferredLayer: 'dom' });
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
it('with settings modal open', async () => {
|
|
const device = {
|
|
name: `custom-${width}`,
|
|
category: 'standard-phone' as const,
|
|
viewport: { width, height },
|
|
deviceScaleFactor: 2,
|
|
isMobile: width < 768,
|
|
hasTouch: width < 768,
|
|
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15',
|
|
expectedBreakpoint: (width < 600 ? 'phone' : width < 768 ? 'tablet' : 'desktop') as 'phone' | 'tablet' | 'desktop',
|
|
isIOS: true,
|
|
defaultBrowserType: 'chromium' as const,
|
|
};
|
|
|
|
const { context, page } = await createDevicePage(device, baseUrl);
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
|
|
// Open settings modal - try mobile button first, then desktop
|
|
const mobileBtn = page.locator(SELECTORS.SETTINGS_MOBILE);
|
|
const isPhone = width < 600;
|
|
if (isPhone && await mobileBtn.isVisible()) {
|
|
await mobileBtn.click();
|
|
} else {
|
|
// Try clicking a desktop settings trigger if available
|
|
await page.evaluate(() => {
|
|
const modal = document.getElementById('appSettingsModal');
|
|
if (modal) modal.style.display = 'flex';
|
|
});
|
|
}
|
|
await page.waitForTimeout(500);
|
|
|
|
const result = await compareScreenshot(page, `settings-${width}w`);
|
|
if (result.isNewBaseline) {
|
|
console.log(`[visual] Created new baseline: settings-${width}w.png`);
|
|
}
|
|
expect(result.passed).toBe(true);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Representative Device Screenshots ────────────────────────────────────
|
|
|
|
describe('Representative Devices', () => {
|
|
for (const [category, device] of Object.entries(REPRESENTATIVE_DEVICES)) {
|
|
it(`${device.name} (${category}) landing page`, async () => {
|
|
const { context, page } = await createDevicePage(device, baseUrl);
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
const name = `device-${device.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}`;
|
|
const result = await compareScreenshot(page, name);
|
|
if (result.isNewBaseline) {
|
|
console.log(`[visual] Created new baseline: ${name}.png`);
|
|
}
|
|
expect(result.passed).toBe(true);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── Baseline Management ──────────────────────────────────────────────────
|
|
|
|
describe('Baseline management', () => {
|
|
/*
|
|
* To update visual baselines:
|
|
*
|
|
* 1. Delete the specific snapshot:
|
|
* rm test/mobile/snapshots/<name>.png
|
|
*
|
|
* 2. Re-run the test:
|
|
* npx vitest run test/mobile/visual-regression.test.ts
|
|
*
|
|
* 3. Review the new baseline:
|
|
* The test will create a fresh .png and pass (isNewBaseline = true).
|
|
*
|
|
* To update ALL baselines:
|
|
* rm -rf test/mobile/snapshots/ && npx vitest run test/mobile/visual-regression.test.ts
|
|
*
|
|
* Diff images for failed comparisons are saved as <name>.diff.png
|
|
* alongside the baseline in test/mobile/snapshots/.
|
|
*/
|
|
it('placeholder — see comments above for baseline update instructions', () => {
|
|
expect(true).toBe(true);
|
|
});
|
|
});
|
|
});
|