mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +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>
208 lines
8.0 KiB
TypeScript
208 lines
8.0 KiB
TypeScript
// Ephemeral port - Cross-device parametric 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, BREAKPOINTS, BODY_CLASSES } from './helpers/constants.js';
|
|
import {
|
|
assertFixedPosition,
|
|
assertHidden,
|
|
assertVisible,
|
|
assertNoHorizontalOverflow,
|
|
assertDeviceClasses,
|
|
assertAccessibleTouchTargets,
|
|
getCSSProperty,
|
|
getCSSNumericValue,
|
|
} from './helpers/assertions.js';
|
|
import { REPRESENTATIVE_DEVICES, DEVICE_REGISTRY, type DeviceEntry, type DeviceCategory } from './devices.js';
|
|
|
|
let baseUrl: string;
|
|
|
|
let server: WebServer;
|
|
|
|
// Hidden on phones (< 600px width)
|
|
const PHONE_HIDDEN_SELECTORS = [
|
|
SELECTORS.HEADER_BRAND,
|
|
SELECTORS.CASE_SELECT_GROUP,
|
|
SELECTORS.VERSION_DISPLAY,
|
|
SELECTORS.TOOLBAR_RIGHT,
|
|
SELECTORS.TAB_COUNT_GROUP,
|
|
];
|
|
|
|
// Visible only on phones
|
|
const PHONE_ONLY_SELECTORS = [SELECTORS.SETTINGS_MOBILE, SELECTORS.CASE_MOBILE];
|
|
|
|
describe('Device Matrix', () => {
|
|
beforeAll(async () => {
|
|
server = await createTestServer();
|
|
baseUrl = `http://localhost:${server.boundPort}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await closeAllBrowsers();
|
|
await stopTestServer(server);
|
|
});
|
|
|
|
// ─── Representative Devices ───────────────────────────────────────────────
|
|
|
|
describe.each(Object.entries(REPRESENTATIVE_DEVICES) as [DeviceCategory, DeviceEntry][])(
|
|
'Representative: %s',
|
|
(category, device) => {
|
|
let context: BrowserContext;
|
|
let page: Page;
|
|
|
|
beforeAll(async () => {
|
|
({ context, page } = await createDevicePage(device, baseUrl));
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await context.close();
|
|
});
|
|
|
|
it(`has correct device class for ${device.name} (${device.viewport.width}px)`, async () => {
|
|
await assertDeviceClasses(page, device.viewport.width);
|
|
});
|
|
|
|
it('no horizontal overflow', async () => {
|
|
await assertNoHorizontalOverflow(page);
|
|
});
|
|
|
|
it('header positioning matches breakpoint', async () => {
|
|
const { width } = device.viewport;
|
|
const position = await getCSSProperty(page, SELECTORS.HEADER, 'position');
|
|
if (width <= BREAKPOINTS.TABLET_MAX) {
|
|
// Phone + tablet (max-width: 768px includes 768): fixed header
|
|
expect(position).toBe('fixed');
|
|
const top = await getCSSProperty(page, SELECTORS.HEADER, 'top');
|
|
expect(parseFloat(top)).toBe(0);
|
|
} else {
|
|
// Desktop: relative header (not fixed)
|
|
expect(position).toBe('relative');
|
|
}
|
|
});
|
|
|
|
it('toolbar positioning matches breakpoint', async () => {
|
|
const { width } = device.viewport;
|
|
const position = await getCSSProperty(page, SELECTORS.TOOLBAR, 'position');
|
|
if (width < BREAKPOINTS.PHONE_MAX) {
|
|
// Phone (max-width: 599px): fixed toolbar
|
|
expect(position).toBe('fixed');
|
|
} else {
|
|
// Tablet/desktop: relative toolbar
|
|
expect(position).toBe('relative');
|
|
}
|
|
});
|
|
|
|
it('correct elements hidden/visible for breakpoint', async () => {
|
|
const { width } = device.viewport;
|
|
const isPhone = width < BREAKPOINTS.PHONE_MAX;
|
|
// Skip strict assertions for devices at the phone/tablet boundary (±10px)
|
|
const atBoundary =
|
|
Math.abs(width - BREAKPOINTS.PHONE_MAX) <= 10 || Math.abs(width - BREAKPOINTS.TABLET_MAX) <= 10;
|
|
|
|
if (atBoundary) return;
|
|
|
|
if (isPhone) {
|
|
// Phone: certain elements hidden, mobile buttons visible
|
|
for (const sel of PHONE_HIDDEN_SELECTORS) {
|
|
await assertHidden(page, sel);
|
|
}
|
|
for (const sel of PHONE_ONLY_SELECTORS) {
|
|
await assertVisible(page, sel);
|
|
}
|
|
} else {
|
|
// Tablet/desktop: phone-hidden elements should be visible, mobile buttons hidden
|
|
for (const sel of PHONE_ONLY_SELECTORS) {
|
|
await assertHidden(page, sel);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('touch targets pass minimum size', async () => {
|
|
const violations = await assertAccessibleTouchTargets(page);
|
|
// Log violations for debugging but allow a small number
|
|
if (violations.length > 0) {
|
|
console.warn(
|
|
`[${device.name}] Touch target violations (${violations.length}):\n` +
|
|
violations.map((v) => ` ${v.selector}: ${v.width}x${v.height}px`).join('\n')
|
|
);
|
|
}
|
|
// Larger viewports show more UI elements, so allow more violations.
|
|
// Known violators: notification action buttons (26x26), some icon buttons.
|
|
const { width } = device.viewport;
|
|
// Larger viewports show more elements; scale threshold accordingly
|
|
const maxViolations = width >= BREAKPOINTS.TABLET_MAX ? 25 : width >= BREAKPOINTS.PHONE_MAX ? 20 : 15;
|
|
expect(violations.length).toBeLessThanOrEqual(maxViolations);
|
|
});
|
|
}
|
|
);
|
|
|
|
// ─── Full Device Matrix (skip with CI_QUICK=1) ───────────────────────────
|
|
|
|
describe.skipIf(process.env.CI_QUICK === '1')('Full Device Matrix', () => {
|
|
for (const device of DEVICE_REGISTRY) {
|
|
it(`${device.name} (${device.viewport.width}x${device.viewport.height}): correct layout`, async () => {
|
|
const { context, page } = await createDevicePage(device, baseUrl);
|
|
try {
|
|
// Verify viewport was applied
|
|
const viewportSize = page.viewportSize();
|
|
expect(viewportSize?.width).toBe(device.viewport.width);
|
|
expect(viewportSize?.height).toBe(device.viewport.height);
|
|
|
|
// No horizontal overflow
|
|
await assertNoHorizontalOverflow(page);
|
|
|
|
// Correct breakpoint class
|
|
await assertDeviceClasses(page, device.viewport.width);
|
|
|
|
// Header fixed at top (only on phone/tablet breakpoints, max-width: 768px)
|
|
if (device.viewport.width <= BREAKPOINTS.TABLET_MAX) {
|
|
await assertFixedPosition(page, SELECTORS.HEADER);
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// ─── Dual Engine Matrix (representatives only) ────────────────────────────
|
|
|
|
describe('Dual Engine Matrix', () => {
|
|
const engines = ['chromium', 'webkit'] as const;
|
|
|
|
for (const [category, device] of Object.entries(REPRESENTATIVE_DEVICES) as [DeviceCategory, DeviceEntry][]) {
|
|
describe(`${device.name} (${category})`, () => {
|
|
it('layout matches across engines', async () => {
|
|
const results: Record<string, { position: string; top: string; zIndex: number }> = {};
|
|
|
|
for (const engine of engines) {
|
|
const { context, page } = await createDevicePage(device, baseUrl, engine);
|
|
try {
|
|
const position = await getCSSProperty(page, SELECTORS.HEADER, 'position');
|
|
const top = await getCSSProperty(page, SELECTORS.HEADER, 'top');
|
|
const zIndex = await getCSSNumericValue(page, SELECTORS.HEADER, 'z-index');
|
|
results[engine] = { position, top, zIndex };
|
|
|
|
// Each engine should have no overflow
|
|
await assertNoHorizontalOverflow(page);
|
|
|
|
// Each engine should have correct device classes
|
|
await assertDeviceClasses(page, device.viewport.width);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
}
|
|
|
|
// Cross-engine comparison: key CSS properties should match
|
|
expect(results.chromium.position).toBe(results.webkit.position);
|
|
expect(parseFloat(results.chromium.top)).toBe(parseFloat(results.webkit.top));
|
|
// z-index should be the same
|
|
expect(results.chromium.zIndex).toBe(results.webkit.zIndex);
|
|
});
|
|
});
|
|
}
|
|
});
|
|
});
|