test(mobile): createTestServer() binds an ephemeral port (#440)

The helper cached servers by port, but every mobile file starts exactly one, so the cache was never shared: it is now a Set that stopAllTestServers() walks. PORTS leaves helpers/constants.ts, the nine tests set baseUrl after start(), and the README drops the port column and the pick-a-port step.

Committed without the pre-commit hook: accessibility, subagent-windows and visual-regression were not prettier-clean on master already; formatting them would bury the port lines.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Randalix
2026-10-09 16:29:01 +02:00
co-authored by Claude Opus 5.5
parent aa4217b8b1
commit 6f87186deb
12 changed files with 164 additions and 180 deletions
+23 -19
View File
@@ -44,16 +44,18 @@ npm run test:mobile -- test/mobile/visual-regression.test.ts
## Test Files
| File | Port | Description |
|------|------|-------------|
| `keyboard.test.ts` | 3200 | Virtual keyboard simulation (3-layer: CDP, mock, DOM) |
| `tabs.test.ts` | 3201 | Tab switching, swipe navigation, keyboard nav |
| `subagent-windows.test.ts` | 3202 | Mobile subagent card dimensions, stacking, interactions |
| `settings.test.ts` | 3203 | Settings modal, mobile defaults, persistence |
| `layout.test.ts` | 3204 | General mobile layout, fixed elements, device classes |
| `device-matrix.test.ts` | 3205 | Cross-device parametric tests (138 devices) |
| `visual-regression.test.ts` | 3206 | Screenshot comparison at key breakpoints |
| `accessibility.test.ts` | 3207 | WCAG touch targets, zoom, focus, ARIA |
Each file starts its own server on an ephemeral port (`createTestServer()`, then `server.boundPort`), so files never compete for a port.
| File | Description |
|------|-------------|
| `keyboard.test.ts` | Virtual keyboard simulation (3-layer: CDP, mock, DOM) |
| `tabs.test.ts` | Tab switching, swipe navigation, keyboard nav |
| `subagent-windows.test.ts` | Mobile subagent card dimensions, stacking, interactions |
| `settings.test.ts` | Settings modal, mobile defaults, persistence |
| `layout.test.ts` | General mobile layout, fixed elements, device classes |
| `device-matrix.test.ts` | Cross-device parametric tests (138 devices) |
| `visual-regression.test.ts` | Screenshot comparison at key breakpoints |
| `accessibility.test.ts` | WCAG touch targets, zoom, focus, ARIA |
## Device Matrix
@@ -99,7 +101,7 @@ The phone block is `max-width: 599px` and the tablet block starts at `min-width:
```
Test File
├─ helpers/server.ts → WebServer(port, false, testMode=true)
├─ helpers/server.ts → WebServer(0, false, testMode=true), ephemeral port
├─ helpers/browser.ts → Playwright Chromium / WebKit
├─ helpers/cdp.ts → Chrome DevTools Protocol (Chromium only)
├─ helpers/keyboard-sim.ts → 3-layer keyboard simulation
@@ -144,10 +146,10 @@ Snapshots at 10 key breakpoints: 320, 375, 390, 393, 430, 440, 600, 768, 834, 10
## Helpers Reference
### `helpers/constants.ts`
All magic numbers centralized: ports, CSS selectors, breakpoint thresholds, keyboard constants, swipe parameters, touch target minimums, body CSS classes, localStorage keys.
All magic numbers centralized: CSS selectors, breakpoint thresholds, keyboard constants, swipe parameters, touch target minimums, body CSS classes, localStorage keys.
### `helpers/server.ts`
`createTestServer(port)` / `stopTestServer(server)` — wraps `WebServer` with `testMode=true`.
`createTestServer()` / `stopTestServer(server)` — wraps `WebServer` with `testMode=true` on an ephemeral port; read it from `server.boundPort`.
### `helpers/browser.ts`
`createDevicePage(device, url, engine?)` — creates a Playwright browser context with the device's viewport, DPR, UA, touch support, navigates to the URL.
@@ -196,9 +198,7 @@ Unified `swipe()` / `tap()` with auto CDP/synthetic selection. Also exports per-
## Adding New Tests
1. Pick a unique port (next: 3208+) — search `const PORT =` across test files
2. Add the port to `helpers/constants.ts` PORTS object
3. Use the standard test pattern:
1. Use the standard test pattern (no port to pick: the server binds port 0):
```typescript
import { createTestServer, stopTestServer } from './helpers/server.js';
@@ -207,12 +207,16 @@ import { REPRESENTATIVE_DEVICES } from './devices.js';
describe('My Test', () => {
let server;
beforeAll(async () => { server = await createTestServer(MY_PORT); });
let baseUrl: string;
beforeAll(async () => {
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => { await stopTestServer(server); await closeAllBrowsers(); });
it('works on phone', async () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
const { page, context } = await createDevicePage(device, `http://localhost:${MY_PORT}`);
const { page, context } = await createDevicePage(device, baseUrl);
try {
// ... assertions ...
} finally {
@@ -222,7 +226,7 @@ describe('My Test', () => {
});
```
4. For new devices: add to `devices.ts` with correct category, viewport, UA, DPR
2. For new devices: add to `devices.ts` with correct category, viewport, UA, DPR
## Findings Log
+13 -13
View File
@@ -1,7 +1,7 @@
// Port 3207 - Mobile accessibility tests
// Ephemeral port - Mobile accessibility tests
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import type { Page, BrowserContext } from 'playwright';
import { PORTS, SELECTORS, KEYBOARD, MIN_TOUCH_TARGET, WAIT } from './helpers/constants.js';
import { SELECTORS, KEYBOARD, MIN_TOUCH_TARGET, 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';
@@ -12,14 +12,14 @@ import {
import { REPRESENTATIVE_DEVICES } from './devices.js';
import type { WebServer } from '../src/web/server.js';
const PORT = PORTS.ACCESSIBILITY;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('Mobile Accessibility', () => {
let server: WebServer;
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -39,7 +39,7 @@ describe('Mobile Accessibility', () => {
it('all visible interactive elements >= 44x44px on phones', async () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
@@ -64,7 +64,7 @@ describe('Mobile Accessibility', () => {
it('all visible interactive elements >= 44x44px on tablets', async () => {
const device = REPRESENTATIVE_DEVICES['standard-tablet'];
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
@@ -83,7 +83,7 @@ describe('Mobile Accessibility', () => {
it('keyboard accessory bar buttons meet size requirement', async () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
@@ -118,7 +118,7 @@ describe('Mobile Accessibility', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -159,7 +159,7 @@ describe('Mobile Accessibility', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -229,7 +229,7 @@ describe('Mobile Accessibility', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -294,7 +294,7 @@ describe('Mobile Accessibility', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -357,7 +357,7 @@ describe('Mobile Accessibility', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
+8 -8
View File
@@ -1,10 +1,10 @@
// Port 3205 - Cross-device parametric tests
// 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 { PORTS, SELECTORS, BREAKPOINTS, BODY_CLASSES } from './helpers/constants.js';
import { SELECTORS, BREAKPOINTS, BODY_CLASSES } from './helpers/constants.js';
import {
assertFixedPosition,
assertHidden,
@@ -17,8 +17,7 @@ import {
} from './helpers/assertions.js';
import { REPRESENTATIVE_DEVICES, DEVICE_REGISTRY, type DeviceEntry, type DeviceCategory } from './devices.js';
const PORT = PORTS.DEVICE_MATRIX;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
let server: WebServer;
@@ -36,7 +35,8 @@ const PHONE_ONLY_SELECTORS = [SELECTORS.SETTINGS_MOBILE, SELECTORS.CASE_MOBILE];
describe('Device Matrix', () => {
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -53,7 +53,7 @@ describe('Device Matrix', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(device, BASE_URL));
({ context, page } = await createDevicePage(device, baseUrl));
});
afterAll(async () => {
@@ -143,7 +143,7 @@ describe('Device Matrix', () => {
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, BASE_URL);
const { context, page } = await createDevicePage(device, baseUrl);
try {
// Verify viewport was applied
const viewportSize = page.viewportSize();
@@ -178,7 +178,7 @@ describe('Device Matrix', () => {
const results: Record<string, { position: string; top: string; zIndex: number }> = {};
for (const engine of engines) {
const { context, page } = await createDevicePage(device, BASE_URL, engine);
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');
+8 -8
View File
@@ -1,4 +1,4 @@
// Port 3208 - Header button visibility (real-browser E2E).
// Ephemeral port - Header button visibility (real-browser E2E).
//
// Companion to the CI static guard (test/mobile-header-buttons-policy.test.ts).
// Renders the real app and asserts ACTUAL computed visibility, catching CSS/layout
@@ -13,16 +13,16 @@ import { createTestServer, stopTestServer } from './helpers/server.js';
import { createDevicePage, closeAllBrowsers } from './helpers/browser.js';
import { assertHidden, assertVisible } from './helpers/assertions.js';
import { REPRESENTATIVE_DEVICES } from './devices.js';
import { PORTS, WAIT } from './helpers/constants.js';
import { WAIT } from './helpers/constants.js';
const PORT = PORTS.HEADER_BUTTONS;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('Header button visibility (E2E)', () => {
let server: WebServer;
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -31,20 +31,20 @@ describe('Header button visibility (E2E)', () => {
});
it('hides the settings gear + lifecycle log on a standard phone (iPhone 14 Pro)', async () => {
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['standard-phone'], BASE_URL);
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['standard-phone'], baseUrl);
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await assertHidden(page, '.btn-icon-header.btn-settings');
await assertHidden(page, '.btn-icon-header.btn-lifecycle-log');
});
it('keeps the opt-in attachments button HIDDEN by default on a desktop-class viewport', async () => {
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['large-tablet'], BASE_URL);
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['large-tablet'], baseUrl);
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await assertHidden(page, '#attachmentsHistoryBtn');
});
it('shows the attachments button once the setting is enabled', async () => {
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['large-tablet'], BASE_URL);
const { page } = await createDevicePage(REPRESENTATIVE_DEVICES['large-tablet'], baseUrl);
// Desktop-class devices use the non-mobile settings blob.
await page.evaluate(() => {
const cur = JSON.parse(localStorage.getItem('codeman-app-settings') || '{}');
-13
View File
@@ -1,16 +1,3 @@
// Port assignments
export const PORTS = {
KEYBOARD: 3200,
TABS: 3201,
SUBAGENT_WINDOWS: 3202,
SETTINGS: 3203,
LAYOUT: 3204,
DEVICE_MATRIX: 3205,
VISUAL_REGRESSION: 3206,
ACCESSIBILITY: 3207,
HEADER_BUTTONS: 3208,
} as const;
// CSS Selectors
export const SELECTORS = {
// Header
+7 -14
View File
@@ -1,29 +1,22 @@
import { WebServer } from '../../../src/web/server.js';
let servers: Map<number, WebServer> = new Map();
const servers = new Set<WebServer>();
export async function createTestServer(port: number): Promise<WebServer> {
if (servers.has(port)) {
return servers.get(port)!;
}
const server = new WebServer(port, false, true); // testMode = true
/** A test server on an ephemeral port; read the port from `server.boundPort` once this resolves. */
export async function createTestServer(): Promise<WebServer> {
const server = new WebServer(0, false, true); // testMode = true
await server.start();
servers.set(port, server);
servers.add(server);
return server;
}
export async function stopTestServer(server: WebServer): Promise<void> {
await server.stop();
for (const [port, s] of servers) {
if (s === server) {
servers.delete(port);
break;
}
}
servers.delete(server);
}
export async function stopAllTestServers(): Promise<void> {
for (const server of servers.values()) {
for (const server of servers) {
await server.stop();
}
servers.clear();
+14 -14
View File
@@ -1,7 +1,7 @@
// Port 3200 - Virtual keyboard simulation tests
// Ephemeral port - Virtual keyboard simulation tests
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
import type { Page, BrowserContext } from 'playwright';
import { PORTS, KEYBOARD, SELECTORS, BODY_CLASSES, WAIT } from './helpers/constants.js';
import { KEYBOARD, SELECTORS, BODY_CLASSES, WAIT } from './helpers/constants.js';
import { createTestServer, stopTestServer } from './helpers/server.js';
import { createDevicePage, getBrowser, closeAllBrowsers } from './helpers/browser.js';
import {
@@ -26,8 +26,7 @@ import {
import { REPRESENTATIVE_DEVICES } from './devices.js';
import type { WebServer } from '../src/web/server.js';
const PORT = PORTS.KEYBOARD;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
// ─── Page-global access helpers ───
// KeyboardHandler is a `const` in app.js — NOT on `window`.
@@ -59,7 +58,8 @@ describe('Virtual Keyboard', () => {
let server: WebServer;
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -75,7 +75,7 @@ describe('Virtual Keyboard', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone']; // iPhone 14 Pro
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -153,7 +153,7 @@ describe('Virtual Keyboard', () => {
const { context, page } = await createDevicePage(device, 'about:blank', 'chromium');
try {
await setupViewportMock(page);
await page.goto(BASE_URL, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await page.goto(baseUrl, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await page.waitForTimeout(2000);
const success = await showKeyboardViaMock(page, KEYBOARD.TYPICAL_IOS_HEIGHT);
@@ -167,7 +167,7 @@ describe('Virtual Keyboard', () => {
const { context, page } = await createDevicePage(device, 'about:blank', 'chromium');
try {
await setupViewportMock(page);
await page.goto(BASE_URL, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await page.goto(baseUrl, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await page.waitForTimeout(2000);
const success = await showKeyboardViaMock(page, KEYBOARD.TYPICAL_IOS_HEIGHT);
@@ -186,7 +186,7 @@ describe('Virtual Keyboard', () => {
const result = await createDevicePage(device, 'about:blank', 'webkit');
context = result.context;
await setupViewportMock(result.page);
await result.page.goto(BASE_URL, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await result.page.goto(baseUrl, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await result.page.waitForTimeout(2000);
const success = await showKeyboardViaMock(result.page, KEYBOARD.TYPICAL_IOS_HEIGHT);
@@ -213,7 +213,7 @@ describe('Virtual Keyboard', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -1469,7 +1469,7 @@ describe('Virtual Keyboard', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(REPRESENTATIVE_DEVICES['standard-phone'], BASE_URL, 'chromium'));
({ context, page } = await createDevicePage(REPRESENTATIVE_DEVICES['standard-phone'], baseUrl, 'chromium'));
await page.waitForTimeout(WAIT.PAGE_SETTLE);
});
@@ -1638,7 +1638,7 @@ describe('Virtual Keyboard', () => {
describe('Cross-device keyboard behavior', () => {
it('phone: full keyboard handling active', async () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(device, baseUrl, 'chromium');
try {
const state = await getKeyboardState(page);
expect(state.exists).toBe(true);
@@ -1654,7 +1654,7 @@ describe('Virtual Keyboard', () => {
it('tablet: keyboard handling active', async () => {
const device = REPRESENTATIVE_DEVICES['standard-tablet']; // iPad Mini
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(device, baseUrl, 'chromium');
try {
const state = await getKeyboardState(page);
expect(state.exists).toBe(true);
@@ -1674,7 +1674,7 @@ describe('Virtual Keyboard', () => {
});
const page = await context.newPage();
try {
await page.goto(BASE_URL, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await page.goto(baseUrl, { waitUntil: WAIT.DOM_CONTENT_LOADED });
await page.waitForTimeout(1000);
const state = await getKeyboardState(page);
+30 -30
View File
@@ -1,10 +1,10 @@
// Port 3204 - General mobile layout tests
// Ephemeral port - General mobile layout 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 { PORTS, SELECTORS, BREAKPOINTS, BODY_CLASSES, WAIT, MIN_TOUCH_TARGET } from './helpers/constants.js';
import { SELECTORS, BREAKPOINTS, BODY_CLASSES, WAIT, MIN_TOUCH_TARGET } from './helpers/constants.js';
import {
assertFixedPosition,
assertHidden,
@@ -21,8 +21,7 @@ import {
} from './helpers/assertions.js';
import { REPRESENTATIVE_DEVICES, DEVICE_REGISTRY, type DeviceEntry } from './devices.js';
const PORT = PORTS.LAYOUT;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
let server: WebServer;
@@ -33,7 +32,8 @@ const iPadPro = REPRESENTATIVE_DEVICES['large-tablet'];
describe('Mobile Layout', () => {
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -48,7 +48,7 @@ describe('Mobile Layout', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(iPhone14Pro, BASE_URL));
({ context, page } = await createDevicePage(iPhone14Pro, baseUrl));
});
afterAll(async () => {
@@ -98,7 +98,7 @@ describe('Mobile Layout', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(iPhone14Pro, BASE_URL));
({ context, page } = await createDevicePage(iPhone14Pro, baseUrl));
});
afterAll(async () => {
@@ -124,7 +124,7 @@ describe('Mobile Layout', () => {
describe('Mobile-Only Visible Elements', () => {
it('.btn-settings-mobile visible on phones', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertVisible(page, SELECTORS.SETTINGS_MOBILE);
} finally {
@@ -133,7 +133,7 @@ describe('Mobile Layout', () => {
});
it('.btn-case-mobile visible on phones', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertVisible(page, SELECTORS.CASE_MOBILE);
} finally {
@@ -142,7 +142,7 @@ describe('Mobile Layout', () => {
});
it('.btn-settings-mobile hidden on desktop', async () => {
const { context, page } = await createDevicePage(iPadPro, BASE_URL);
const { context, page } = await createDevicePage(iPadPro, baseUrl);
try {
await assertHidden(page, SELECTORS.SETTINGS_MOBILE);
} finally {
@@ -156,7 +156,7 @@ describe('Mobile Layout', () => {
describe('Toolbar Collision Regression', () => {
it('keeps phone toolbar controls inside the viewport', async () => {
const device = DEVICE_REGISTRY.find((d) => d.name === 'iPhone 8')!;
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(device, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
const layout = await page.evaluate(() => {
@@ -189,7 +189,7 @@ describe('Mobile Layout', () => {
it('does not render the desktop voice button on a 430px large phone', async () => {
const device = REPRESENTATIVE_DEVICES['large-phone'];
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(device, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await assertHidden(page, '#voiceInputBtn');
@@ -201,7 +201,7 @@ describe('Mobile Layout', () => {
it('uses phone upload and voice controls without toolbar overlap', async () => {
const device = REPRESENTATIVE_DEVICES['small-phone'];
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(device, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
expect(await page.locator('.toolbar-center .btn-upload').isVisible()).toBe(false);
@@ -246,7 +246,7 @@ describe('Mobile Layout', () => {
it('keeps the mobile recording mic effect inside the button bounds', async () => {
const device = REPRESENTATIVE_DEVICES['small-phone'];
const { context, page } = await createDevicePage(device, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(device, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await page.evaluate(() => {
@@ -266,7 +266,7 @@ describe('Mobile Layout', () => {
});
it('keeps desktop upload and voice controls in one centered row', async () => {
const { context, page } = await createDevicePage(iPadPro, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(iPadPro, baseUrl, 'chromium');
try {
await page.setViewportSize({ width: 1280, height: 800 });
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -294,7 +294,7 @@ describe('Mobile Layout', () => {
describe('Device Classes', () => {
it('iOS user agent adds ios-device class', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertHasClass(page, 'body', BODY_CLASSES.IOS);
} finally {
@@ -304,7 +304,7 @@ describe('Mobile Layout', () => {
it('Android user agent does NOT add ios-device', async () => {
const pixel = DEVICE_REGISTRY.find((d) => d.name === 'Pixel 7')!;
const { context, page } = await createDevicePage(pixel, BASE_URL);
const { context, page } = await createDevicePage(pixel, baseUrl);
try {
await assertNotHasClass(page, 'body', BODY_CLASSES.IOS);
} finally {
@@ -314,7 +314,7 @@ describe('Mobile Layout', () => {
it('Safari user agent adds safari-browser class', async () => {
// iPhone 14 Pro uses Safari UA
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertHasClass(page, 'body', BODY_CLASSES.SAFARI);
} finally {
@@ -323,7 +323,7 @@ describe('Mobile Layout', () => {
});
it('touch device adds touch-device class', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertHasClass(page, 'body', BODY_CLASSES.TOUCH);
} finally {
@@ -332,7 +332,7 @@ describe('Mobile Layout', () => {
});
it('width < 600 adds device-mobile', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertDeviceClasses(page, iPhone14Pro.viewport.width);
} finally {
@@ -342,7 +342,7 @@ describe('Mobile Layout', () => {
it('width 600-768 adds device-tablet', async () => {
const tablet = REPRESENTATIVE_DEVICES['small-tablet'];
const { context, page } = await createDevicePage(tablet, BASE_URL);
const { context, page } = await createDevicePage(tablet, baseUrl);
try {
await assertDeviceClasses(page, tablet.viewport.width);
} finally {
@@ -351,7 +351,7 @@ describe('Mobile Layout', () => {
});
it('width >= 768 adds device-desktop', async () => {
const { context, page } = await createDevicePage(iPadPro, BASE_URL);
const { context, page } = await createDevicePage(iPadPro, baseUrl);
try {
await assertDeviceClasses(page, iPadPro.viewport.width);
} finally {
@@ -364,7 +364,7 @@ describe('Mobile Layout', () => {
describe('iOS Safe Areas', () => {
it('iOS header has extra padding for safe area', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
// On iOS devices, the header should have padding-top that accounts for safe area
const paddingTop = await getCSSNumericValue(page, SELECTORS.HEADER, 'padding-top');
@@ -378,7 +378,7 @@ describe('Mobile Layout', () => {
});
it('iOS modal content has safe area paddings', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
// Open settings modal to test modal safe areas
const settingsBtn = page.locator(SELECTORS.SETTINGS_MOBILE);
@@ -406,7 +406,7 @@ describe('Mobile Layout', () => {
describe('Touch Targets', () => {
it('all interactive elements meet 44px minimum', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
const violations = await assertAccessibleTouchTargets(page, MIN_TOUCH_TARGET);
if (violations.length > 0) {
@@ -428,7 +428,7 @@ describe('Mobile Layout', () => {
describe('Inputs', () => {
it('text inputs use font-size >= 16px', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
// Check visible text inputs (font-size < 16px causes iOS auto-zoom)
const inputs = await page.$$('input[type="text"], input[type="search"], input:not([type]), textarea');
@@ -457,7 +457,7 @@ describe('Mobile Layout', () => {
describe('Terminal', () => {
it('terminal-container has touch-action: pan-y', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
const exists = await page.$(SELECTORS.TERMINAL_CONTAINER);
if (exists) {
@@ -474,7 +474,7 @@ describe('Mobile Layout', () => {
describe('No Horizontal Overflow', () => {
it('no horizontal scroll on phone viewport', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertNoHorizontalOverflow(page);
} finally {
@@ -487,7 +487,7 @@ describe('Mobile Layout', () => {
describe('Main Content', () => {
it('main has margin-top for fixed header', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
const marginTop = await getCSSNumericValue(page, SELECTORS.MAIN, 'margin-top');
// Main content should have enough margin-top to clear the fixed header (~42px)
@@ -502,7 +502,7 @@ describe('Mobile Layout', () => {
describe('Viewport Meta', () => {
it('zoom is not disabled', async () => {
const { context, page } = await createDevicePage(iPhone14Pro, BASE_URL);
const { context, page } = await createDevicePage(iPhone14Pro, baseUrl);
try {
await assertZoomNotDisabled(page);
} finally {
+14 -14
View File
@@ -1,7 +1,7 @@
// Port 3203 - Settings modal mobile tests
// Ephemeral port - 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 { 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';
@@ -9,14 +9,14 @@ import { assertVisible, assertHidden, getCSSProperty, getCSSNumericValue } from
import { DEVICE_REGISTRY, REPRESENTATIVE_DEVICES } from './devices.js';
import type { WebServer } from '../src/web/server.js';
const PORT = PORTS.SETTINGS;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
describe('Settings Modal', () => {
let server: WebServer;
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -32,7 +32,7 @@ describe('Settings Modal', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -105,7 +105,7 @@ describe('Settings Modal', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
});
@@ -210,7 +210,7 @@ describe('Settings Modal', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
beforeEach(async () => {
const result = await createDevicePage(device, BASE_URL, 'chromium');
const result = await createDevicePage(device, baseUrl, 'chromium');
page = result.page;
context = result.context;
@@ -295,7 +295,7 @@ describe('Settings Modal', () => {
const device = REPRESENTATIVE_DEVICES['standard-phone'];
it('settings survive page reload', async () => {
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
const { page, context } = await createDevicePage(device, baseUrl, 'chromium');
try {
// Store a test setting
@@ -330,7 +330,7 @@ describe('Settings Modal', () => {
});
it('mobile notification prefs stored separately from desktop settings', async () => {
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
const { page, context } = await createDevicePage(device, baseUrl, 'chromium');
try {
// Store both mobile and desktop settings
@@ -375,7 +375,7 @@ describe('Settings Modal', () => {
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');
const { page, context } = await createDevicePage(device, baseUrl, 'chromium');
try {
const key = await page.evaluate(() => {
@@ -393,7 +393,7 @@ describe('Settings Modal', () => {
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');
const { page, context } = await createDevicePage(device, baseUrl, 'chromium');
try {
await assertHidden(page, SELECTORS.SETTINGS_MOBILE);
@@ -411,7 +411,7 @@ describe('Settings Modal', () => {
// test/viewport-shape-change.test.ts; this drives the real resize.
const inner = DEVICE_REGISTRY.find((entry) => entry.name === 'iPhone Duo (inner)')!;
const outer = DEVICE_REGISTRY.find((entry) => entry.name === 'iPhone Duo (outer)')!;
const { page, context } = await createDevicePage(inner, BASE_URL, 'chromium');
const { page, context } = await createDevicePage(inner, baseUrl, 'chromium');
try {
await page.evaluate((key) => {
@@ -444,7 +444,7 @@ describe('Settings Modal', () => {
it('keeps handheld settings when a foldable unfolds past the desktop breakpoint', async () => {
const device = DEVICE_REGISTRY.find((entry) => entry.name === 'OPPO Find N5 (unfolded)')!;
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
const { page, context } = await createDevicePage(device, baseUrl, 'chromium');
try {
// Seed the preferences while folded, exactly as a phone user does.
+14 -14
View File
@@ -1,7 +1,7 @@
// Port 3202 - Mobile subagent window card tests
// Ephemeral port - Mobile subagent window card tests
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import type { Page, BrowserContext } from 'playwright';
import { PORTS, SELECTORS, SUBAGENT, KEYBOARD, WAIT } from './helpers/constants.js';
import { SELECTORS, SUBAGENT, KEYBOARD, 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';
@@ -9,8 +9,7 @@ import { getCSSProperty, getCSSNumericValue } from './helpers/assertions.js';
import { REPRESENTATIVE_DEVICES } from './devices.js';
import type { WebServer } from '../src/web/server.js';
const PORT = PORTS.SUBAGENT_WINDOWS;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
const standardPhone = REPRESENTATIVE_DEVICES['standard-phone']; // iPhone 14 Pro
@@ -78,7 +77,8 @@ describe('Mobile Subagent Windows', () => {
let server: WebServer;
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -93,7 +93,7 @@ describe('Mobile Subagent Windows', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium'));
({ context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium'));
await page.waitForTimeout(WAIT.PAGE_SETTLE);
});
@@ -187,7 +187,7 @@ describe('Mobile Subagent Windows', () => {
describe('Stacking', () => {
it('windows stack from top when keyboard hidden', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -240,7 +240,7 @@ describe('Mobile Subagent Windows', () => {
});
it('windows stack from bottom when keyboard visible', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -289,7 +289,7 @@ describe('Mobile Subagent Windows', () => {
});
it('stacking recalculates on keyboard toggle', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -379,7 +379,7 @@ describe('Mobile Subagent Windows', () => {
describe('Interactions', () => {
it('minimize button hides the window body', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockSubagentWindow(page, 'minimize-test', 0);
@@ -432,7 +432,7 @@ describe('Mobile Subagent Windows', () => {
});
it('minimize button toggles: second click restores window', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockSubagentWindow(page, 'toggle-test', 0);
@@ -490,7 +490,7 @@ describe('Mobile Subagent Windows', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium'));
({ context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium'));
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockSubagentWindow(page, 'content-test-agent', 0);
});
@@ -556,7 +556,7 @@ describe('Mobile Subagent Windows', () => {
describe('Multiple Windows', () => {
it('can render multiple subagent windows simultaneously', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -583,7 +583,7 @@ describe('Mobile Subagent Windows', () => {
});
it('windows do not overlap when properly stacked', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
+24 -24
View File
@@ -1,7 +1,7 @@
// Port 3201 - Tab switching and navigation tests
// Ephemeral port - Tab switching and navigation tests
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import type { Page, BrowserContext } from 'playwright';
import { PORTS, SELECTORS, SWIPE, BODY_CLASSES, WAIT } from './helpers/constants.js';
import { SELECTORS, SWIPE, BODY_CLASSES, WAIT } from './helpers/constants.js';
import { createTestServer, stopTestServer } from './helpers/server.js';
import { createDevicePage, closeAllBrowsers } from './helpers/browser.js';
import { swipe, swipeViaCDP, swipeViaSynthetic } from './helpers/touch-sim.js';
@@ -9,8 +9,7 @@ import { assertHidden, assertVisible, getCSSProperty, getCSSNumericValue } from
import { REPRESENTATIVE_DEVICES } from './devices.js';
import type { WebServer } from '../src/web/server.js';
const PORT = PORTS.TABS;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
const standardPhone = REPRESENTATIVE_DEVICES['standard-phone']; // iPhone 14 Pro
@@ -18,7 +17,8 @@ describe('Tab Navigation', () => {
let server: WebServer;
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -33,7 +33,7 @@ describe('Tab Navigation', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(standardPhone, BASE_URL));
({ context, page } = await createDevicePage(standardPhone, baseUrl));
// Wait for SSE and JS to fully initialize
await page.waitForTimeout(WAIT.PAGE_SETTLE);
});
@@ -157,7 +157,7 @@ describe('Tab Navigation', () => {
it('tabs remain visible on large phone and tablet headers', async () => {
for (const device of [REPRESENTATIVE_DEVICES['large-phone'], REPRESENTATIVE_DEVICES['small-tablet']]) {
const { context: deviceContext, page: devicePage } = await createDevicePage(device, BASE_URL, 'chromium');
const { context: deviceContext, page: devicePage } = await createDevicePage(device, baseUrl, 'chromium');
try {
await devicePage.waitForTimeout(WAIT.PAGE_SETTLE);
await devicePage.evaluate(() => {
@@ -224,7 +224,7 @@ describe('Tab Navigation', () => {
}
it('reveals a rightmost tab that selection would otherwise leave off-screen', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await seedTabs(page, 5);
@@ -250,7 +250,7 @@ describe('Tab Navigation', () => {
});
it('scrolls back to reveal a leftmost tab', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await seedTabs(page, 5);
@@ -271,7 +271,7 @@ describe('Tab Navigation', () => {
});
it('keeps the scroll position across an ambient full re-render', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await seedTabs(page, 5);
@@ -296,7 +296,7 @@ describe('Tab Navigation', () => {
});
it('renders tabs in sessionOrder on phones instead of hoisting the active one', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await seedTabs(page, 5, 3); // 4th tab active
@@ -317,7 +317,7 @@ describe('Tab Navigation', () => {
});
it('reaches the last tab with a horizontal touch drag', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await seedTabs(page, 5);
@@ -356,7 +356,7 @@ describe('Tab Navigation', () => {
let page: Page;
beforeAll(async () => {
({ context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium'));
({ context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium'));
await page.waitForTimeout(WAIT.PAGE_SETTLE);
});
@@ -471,7 +471,7 @@ describe('Tab Navigation', () => {
it('synthetic swipe left triggers navigation on WebKit', async () => {
let context: BrowserContext | undefined;
try {
const result = await createDevicePage(standardPhone, BASE_URL, 'webkit');
const result = await createDevicePage(standardPhone, baseUrl, 'webkit');
context = result.context;
await result.page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -510,7 +510,7 @@ describe('Tab Navigation', () => {
it('synthetic swipe right triggers prevSession on WebKit', async () => {
let context: BrowserContext | undefined;
try {
const result = await createDevicePage(standardPhone, BASE_URL, 'webkit');
const result = await createDevicePage(standardPhone, baseUrl, 'webkit');
context = result.context;
await result.page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -578,7 +578,7 @@ describe('Tab Navigation', () => {
}
it('ArrowRight moves focus to next tab', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockTabs(page, 3);
@@ -602,7 +602,7 @@ describe('Tab Navigation', () => {
});
it('ArrowLeft moves focus to previous tab', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockTabs(page, 3);
@@ -627,7 +627,7 @@ describe('Tab Navigation', () => {
});
it('Home key focuses first tab', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockTabs(page, 4);
@@ -651,7 +651,7 @@ describe('Tab Navigation', () => {
});
it('End key focuses last tab', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockTabs(page, 4);
@@ -675,7 +675,7 @@ describe('Tab Navigation', () => {
});
it('Enter activates focused tab', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
await injectMockTabs(page, 3);
@@ -711,7 +711,7 @@ describe('Tab Navigation', () => {
describe('Tab Touch Focus', () => {
it('switching tabs with the keyboard closed does not leave the terminal textarea focused', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -758,7 +758,7 @@ describe('Tab Navigation', () => {
describe('Tab Close Button Visibility', () => {
it('close button hidden on non-active tabs', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -794,7 +794,7 @@ describe('Tab Navigation', () => {
});
it('gear icon visible only on active tab', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -850,7 +850,7 @@ describe('Tab Navigation', () => {
describe('Subagent Badge on Tab', () => {
it('subagent badge has correct dimensions', async () => {
const { context, page } = await createDevicePage(standardPhone, BASE_URL, 'chromium');
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
+9 -9
View File
@@ -1,16 +1,15 @@
// Port 3206 - Screenshot comparison tests
// 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 { PORTS, SELECTORS, VISUAL, VISUAL_BREAKPOINTS, WAIT } from './helpers/constants.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';
const PORT = PORTS.VISUAL_REGRESSION;
const BASE_URL = `http://localhost:${PORT}`;
let baseUrl: string;
let server: WebServer;
@@ -20,7 +19,8 @@ const KEY_BREAKPOINTS = [320, 375, 393, 430, 768, 1024] as const;
describe('Visual Regression', () => {
beforeAll(async () => {
server = await createTestServer(PORT);
server = await createTestServer();
baseUrl = `http://localhost:${server.boundPort}`;
});
afterAll(async () => {
@@ -49,7 +49,7 @@ describe('Visual Regression', () => {
defaultBrowserType: 'chromium' as const,
};
const { context, page } = await createDevicePage(device, BASE_URL);
const { context, page } = await createDevicePage(device, baseUrl);
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
const result = await compareScreenshot(page, `landing-${width}w`);
@@ -76,7 +76,7 @@ describe('Visual Regression', () => {
defaultBrowserType: 'chromium' as const,
};
const { context, page } = await createDevicePage(device, BASE_URL);
const { context, page } = await createDevicePage(device, baseUrl);
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
// Show keyboard via DOM manipulation (works on all engines)
@@ -109,7 +109,7 @@ describe('Visual Regression', () => {
defaultBrowserType: 'chromium' as const,
};
const { context, page } = await createDevicePage(device, BASE_URL);
const { context, page } = await createDevicePage(device, baseUrl);
try {
await page.waitForTimeout(WAIT.PAGE_SETTLE);
@@ -143,7 +143,7 @@ describe('Visual Regression', () => {
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, BASE_URL);
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, '-')}`;