mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-11 01:39:41 +02:00
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:
co-authored by
Claude Opus 5.5
parent
aa4217b8b1
commit
6f87186deb
+23
-19
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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') || '{}');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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, '-')}`;
|
||||
|
||||
Reference in New Issue
Block a user