From 6f87186debfd6b488cd601b9a4a185cfcc3985f7 Mon Sep 17 00:00:00 2001 From: Randalix Date: Fri, 9 Oct 2026 16:29:01 +0200 Subject: [PATCH] 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) --- test/mobile/README.md | 42 ++++++++++--------- test/mobile/accessibility.test.ts | 26 ++++++------ test/mobile/device-matrix.test.ts | 16 +++---- test/mobile/header-buttons.test.ts | 16 +++---- test/mobile/helpers/constants.ts | 13 ------ test/mobile/helpers/server.ts | 21 ++++------ test/mobile/keyboard.test.ts | 28 ++++++------- test/mobile/layout.test.ts | 60 +++++++++++++-------------- test/mobile/settings.test.ts | 28 ++++++------- test/mobile/subagent-windows.test.ts | 28 ++++++------- test/mobile/tabs.test.ts | 48 ++++++++++----------- test/mobile/visual-regression.test.ts | 18 ++++---- 12 files changed, 164 insertions(+), 180 deletions(-) diff --git a/test/mobile/README.md b/test/mobile/README.md index 8c9098d2..1ff30a73 100644 --- a/test/mobile/README.md +++ b/test/mobile/README.md @@ -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 diff --git a/test/mobile/accessibility.test.ts b/test/mobile/accessibility.test.ts index b50c2c40..b96b10a5 100644 --- a/test/mobile/accessibility.test.ts +++ b/test/mobile/accessibility.test.ts @@ -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; }); diff --git a/test/mobile/device-matrix.test.ts b/test/mobile/device-matrix.test.ts index 3370b7e2..b0f78fa0 100644 --- a/test/mobile/device-matrix.test.ts +++ b/test/mobile/device-matrix.test.ts @@ -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 = {}; 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'); diff --git a/test/mobile/header-buttons.test.ts b/test/mobile/header-buttons.test.ts index c84ba8b5..df69f6c5 100644 --- a/test/mobile/header-buttons.test.ts +++ b/test/mobile/header-buttons.test.ts @@ -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') || '{}'); diff --git a/test/mobile/helpers/constants.ts b/test/mobile/helpers/constants.ts index bdd6198b..7acf316b 100644 --- a/test/mobile/helpers/constants.ts +++ b/test/mobile/helpers/constants.ts @@ -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 diff --git a/test/mobile/helpers/server.ts b/test/mobile/helpers/server.ts index ffa4e58f..2ec0bb3b 100644 --- a/test/mobile/helpers/server.ts +++ b/test/mobile/helpers/server.ts @@ -1,29 +1,22 @@ import { WebServer } from '../../../src/web/server.js'; -let servers: Map = new Map(); +const servers = new Set(); -export async function createTestServer(port: number): Promise { - 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 { + 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 { await server.stop(); - for (const [port, s] of servers) { - if (s === server) { - servers.delete(port); - break; - } - } + servers.delete(server); } export async function stopAllTestServers(): Promise { - for (const server of servers.values()) { + for (const server of servers) { await server.stop(); } servers.clear(); diff --git a/test/mobile/keyboard.test.ts b/test/mobile/keyboard.test.ts index 9df3ca13..91cf4000 100644 --- a/test/mobile/keyboard.test.ts +++ b/test/mobile/keyboard.test.ts @@ -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); diff --git a/test/mobile/layout.test.ts b/test/mobile/layout.test.ts index dcb16951..eb9d325f 100644 --- a/test/mobile/layout.test.ts +++ b/test/mobile/layout.test.ts @@ -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 { diff --git a/test/mobile/settings.test.ts b/test/mobile/settings.test.ts index 8965f0c6..cb6e8b17 100644 --- a/test/mobile/settings.test.ts +++ b/test/mobile/settings.test.ts @@ -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. diff --git a/test/mobile/subagent-windows.test.ts b/test/mobile/subagent-windows.test.ts index 59c5c752..c51a8df2 100644 --- a/test/mobile/subagent-windows.test.ts +++ b/test/mobile/subagent-windows.test.ts @@ -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); diff --git a/test/mobile/tabs.test.ts b/test/mobile/tabs.test.ts index 25270e99..977421d1 100644 --- a/test/mobile/tabs.test.ts +++ b/test/mobile/tabs.test.ts @@ -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); diff --git a/test/mobile/visual-regression.test.ts b/test/mobile/visual-regression.test.ts index 3f41d345..d5d10a49 100644 --- a/test/mobile/visual-regression.test.ts +++ b/test/mobile/visual-regression.test.ts @@ -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, '-')}`;