mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +01:00
Found with knip (exports, types, class members) and confirmed by a text search of src, test, scripts and packages before each removal. - Per-CLI get<Cli>NotFoundMessage() (nine of them) with their message constants: missingCliMessage() builds that text from the registry now. - Registry leftovers: renderCliCommand, asCliId, cliIds, isCliRunnable, createCliResolverForTest, profileExists, and two name lists that only repeated schema enums. - A task-description cache in Session that was written on every chunk of terminal output (one regex pass per line) and never read since the subagent watcher takes descriptions from transcripts. The cache module, its pattern and both parse methods go with it. - Session.parentAgentId / childAgentIds: never set by anything, so the two optional SessionState fields were never on the wire. - Unreferenced methods on TmuxManager (pipe-pane pair, syncMouseMode), RespawnController, StateStore.recoverFromBackup (load() already falls back to the .bak file), TeamWatcher, SseStreamManager and the AI checkers' getErrorVerdict. - updateCaseEnvVars: the settings.local.json env path that envOverrides replaced. - Re-exports in the utils, cli-registry, routes and prompts barrels that no importer used, and unused test helpers (MockStateStore, the mobile throttle helpers and others). Eight mobile tests imported the WebServer type from a path that does not exist (../src instead of ../../src); type-only, so nothing failed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
626 lines
23 KiB
TypeScript
626 lines
23 KiB
TypeScript
// Ephemeral port - Mobile subagent window card tests
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import type { Page, BrowserContext } from 'playwright';
|
|
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';
|
|
import { getCSSProperty, getCSSNumericValue } from './helpers/assertions.js';
|
|
import { REPRESENTATIVE_DEVICES } from './devices.js';
|
|
import type { WebServer } from '../../src/web/server.js';
|
|
|
|
let baseUrl: string;
|
|
|
|
const standardPhone = REPRESENTATIVE_DEVICES['standard-phone']; // iPhone 14 Pro
|
|
|
|
/**
|
|
* Inject a mock subagent window into the page DOM.
|
|
* Registers it with app.subagentWindows if the app object is available.
|
|
*/
|
|
async function injectMockSubagentWindow(page: Page, id: string, _index: number): Promise<void> {
|
|
await page.evaluate(({ windowId }) => {
|
|
const el = document.createElement('div');
|
|
el.className = 'subagent-window';
|
|
el.dataset.agentId = windowId;
|
|
el.innerHTML = `
|
|
<div class="subagent-header">
|
|
<span class="agent-icon">A</span>
|
|
<span class="agent-id">${windowId}</span>
|
|
<span class="model-badge">sonnet</span>
|
|
<span class="agent-status">active</span>
|
|
<button class="minimize-btn">-</button>
|
|
</div>
|
|
<div class="subagent-body">
|
|
<div class="activity-content">Working on task...</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(el);
|
|
|
|
// Register with app if available
|
|
if ((window as any).app?.subagentWindows) {
|
|
(window as any).app.subagentWindows.set(windowId, {
|
|
element: el,
|
|
minimized: false,
|
|
hidden: false,
|
|
});
|
|
}
|
|
}, { windowId: id });
|
|
}
|
|
|
|
/**
|
|
* Remove all mock subagent windows from the DOM and app state.
|
|
*/
|
|
async function clearMockSubagentWindows(page: Page): Promise<void> {
|
|
await page.evaluate(() => {
|
|
document.querySelectorAll('.subagent-window').forEach(el => el.remove());
|
|
if ((window as any).app?.subagentWindows) {
|
|
(window as any).app.subagentWindows.clear();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Call relayoutMobileSubagentWindows if available on the app object.
|
|
*/
|
|
async function triggerRelayout(page: Page): Promise<void> {
|
|
await page.evaluate(() => {
|
|
if (typeof (window as any).relayoutMobileSubagentWindows === 'function') {
|
|
(window as any).relayoutMobileSubagentWindows();
|
|
} else if (typeof (window as any).app?.relayoutMobileSubagentWindows === 'function') {
|
|
(window as any).app.relayoutMobileSubagentWindows();
|
|
}
|
|
});
|
|
await page.waitForTimeout(100);
|
|
}
|
|
|
|
describe('Mobile Subagent Windows', () => {
|
|
let server: WebServer;
|
|
|
|
beforeAll(async () => {
|
|
server = await createTestServer();
|
|
baseUrl = `http://localhost:${server.boundPort}`;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await closeAllBrowsers();
|
|
await stopTestServer(server);
|
|
});
|
|
|
|
// ─── Dimensions ──────────────────────────────────────────────────────────
|
|
|
|
describe('Dimensions', () => {
|
|
let context: BrowserContext;
|
|
let page: Page;
|
|
|
|
beforeAll(async () => {
|
|
({ context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium'));
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await context.close();
|
|
});
|
|
|
|
it('width is approximately viewport width minus 8px', async () => {
|
|
await injectMockSubagentWindow(page, 'dim-test-1', 0);
|
|
|
|
// Apply mobile styles via inline style to match CSS: width: calc(100% - 8px)
|
|
await page.evaluate(() => {
|
|
const win = document.querySelector('.subagent-window') as HTMLElement;
|
|
if (win) {
|
|
// Set the expected mobile styles
|
|
win.style.width = 'calc(100% - 8px)';
|
|
win.style.position = 'fixed';
|
|
win.style.left = '4px';
|
|
}
|
|
});
|
|
|
|
const viewport = page.viewportSize()!;
|
|
const box = await page.locator(SELECTORS.SUBAGENT_WINDOW).first().boundingBox();
|
|
expect(box).not.toBeNull();
|
|
|
|
const expectedWidth = viewport.width - 8;
|
|
// Allow 2px tolerance for rounding
|
|
expect(Math.abs(box!.width - expectedWidth)).toBeLessThanOrEqual(2);
|
|
|
|
await clearMockSubagentWindows(page);
|
|
});
|
|
|
|
it('height is 110px', async () => {
|
|
await injectMockSubagentWindow(page, 'dim-test-2', 0);
|
|
|
|
await page.evaluate(() => {
|
|
const win = document.querySelector('.subagent-window') as HTMLElement;
|
|
if (win) {
|
|
win.style.height = `${110}px`;
|
|
win.style.maxHeight = `${110}px`;
|
|
}
|
|
});
|
|
|
|
const box = await page.locator(SELECTORS.SUBAGENT_WINDOW).first().boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box!.height).toBeCloseTo(SUBAGENT.MOBILE_CARD_HEIGHT, 0);
|
|
|
|
await clearMockSubagentWindows(page);
|
|
});
|
|
|
|
it('max-height is 110px', async () => {
|
|
await injectMockSubagentWindow(page, 'dim-test-3', 0);
|
|
|
|
await page.evaluate(() => {
|
|
const win = document.querySelector('.subagent-window') as HTMLElement;
|
|
if (win) {
|
|
win.style.maxHeight = `${110}px`;
|
|
}
|
|
});
|
|
|
|
const maxHeight = await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window') as HTMLElement;
|
|
return el ? el.style.maxHeight : '';
|
|
});
|
|
expect(parseFloat(maxHeight)).toBe(SUBAGENT.MOBILE_CARD_HEIGHT);
|
|
|
|
await clearMockSubagentWindows(page);
|
|
});
|
|
|
|
it('no resize handle visible', async () => {
|
|
await injectMockSubagentWindow(page, 'dim-test-4', 0);
|
|
|
|
await page.evaluate(() => {
|
|
const win = document.querySelector('.subagent-window') as HTMLElement;
|
|
if (win) {
|
|
win.style.resize = 'none';
|
|
}
|
|
});
|
|
|
|
const resize = await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window') as HTMLElement;
|
|
return el ? getComputedStyle(el).resize : '';
|
|
});
|
|
expect(resize).toBe('none');
|
|
|
|
await clearMockSubagentWindows(page);
|
|
});
|
|
});
|
|
|
|
// ─── Stacking ────────────────────────────────────────────────────────────
|
|
|
|
describe('Stacking', () => {
|
|
it('windows stack from top when keyboard hidden', async () => {
|
|
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
|
|
// Inject 3 windows
|
|
for (let i = 0; i < 3; i++) {
|
|
await injectMockSubagentWindow(page, `stack-top-${i}`, i);
|
|
}
|
|
|
|
// Manually position them from top (simulating relayoutMobileSubagentWindows)
|
|
await page.evaluate(({ headerHeight, stride }) => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
windows.forEach((win, idx) => {
|
|
const el = win as HTMLElement;
|
|
el.style.position = 'fixed';
|
|
el.style.top = `${headerHeight + 8 + idx * stride}px`;
|
|
el.style.bottom = 'auto';
|
|
el.style.left = '4px';
|
|
el.style.width = 'calc(100% - 8px)';
|
|
el.style.height = '110px';
|
|
});
|
|
}, { headerHeight: SUBAGENT.DEFAULT_HEADER_HEIGHT, stride: SUBAGENT.MOBILE_CARD_STRIDE });
|
|
|
|
// Also try calling the real relayout function if available
|
|
await triggerRelayout(page);
|
|
|
|
// Verify stacking order: each window's top should increase
|
|
const positions = await page.evaluate(() => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
return Array.from(windows).map(w => {
|
|
const el = w as HTMLElement;
|
|
const rect = el.getBoundingClientRect();
|
|
return { top: rect.top, bottom: rect.bottom };
|
|
});
|
|
});
|
|
|
|
expect(positions.length).toBe(3);
|
|
for (let i = 1; i < positions.length; i++) {
|
|
expect(positions[i].top).toBeGreaterThan(positions[i - 1].top);
|
|
}
|
|
|
|
// Verify stride spacing between windows
|
|
if (positions.length >= 2) {
|
|
const gap = positions[1].top - positions[0].top;
|
|
// Should be approximately MOBILE_CARD_STRIDE (114px)
|
|
expect(gap).toBeCloseTo(SUBAGENT.MOBILE_CARD_STRIDE, -1);
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
it('windows stack from bottom when keyboard visible', async () => {
|
|
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
|
|
// Inject 2 windows
|
|
for (let i = 0; i < 2; i++) {
|
|
await injectMockSubagentWindow(page, `stack-bottom-${i}`, i);
|
|
}
|
|
|
|
// Simulate keyboard show
|
|
await showKeyboard(page, KEYBOARD.TYPICAL_IOS_HEIGHT);
|
|
await page.waitForTimeout(WAIT.KEYBOARD_ANIMATION);
|
|
|
|
// Position windows from bottom (simulating keyboard-visible layout)
|
|
await page.evaluate(({ toolbarOffset, stride }) => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
windows.forEach((win, idx) => {
|
|
const el = win as HTMLElement;
|
|
el.style.position = 'fixed';
|
|
el.style.top = 'auto';
|
|
el.style.bottom = `${toolbarOffset + idx * stride}px`;
|
|
el.style.left = '4px';
|
|
el.style.width = 'calc(100% - 8px)';
|
|
el.style.height = '110px';
|
|
});
|
|
}, { toolbarOffset: SUBAGENT.TOOLBAR_OFFSET, stride: SUBAGENT.MOBILE_CARD_STRIDE });
|
|
|
|
await triggerRelayout(page);
|
|
|
|
// Verify bottom stacking: each window's bottom CSS value should increase
|
|
const bottomValues = await page.evaluate(() => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
return Array.from(windows).map(w => {
|
|
const el = w as HTMLElement;
|
|
return parseFloat(el.style.bottom) || 0;
|
|
});
|
|
});
|
|
|
|
expect(bottomValues.length).toBe(2);
|
|
// Second window should have a larger bottom offset than the first
|
|
expect(bottomValues[1]).toBeGreaterThan(bottomValues[0]);
|
|
|
|
await hideKeyboard(page);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
it('stacking recalculates on keyboard toggle', async () => {
|
|
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
|
|
// Inject 2 windows positioned from top
|
|
for (let i = 0; i < 2; i++) {
|
|
await injectMockSubagentWindow(page, `stack-toggle-${i}`, i);
|
|
}
|
|
|
|
await page.evaluate(({ headerHeight, stride }) => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
windows.forEach((win, idx) => {
|
|
const el = win as HTMLElement;
|
|
el.style.position = 'fixed';
|
|
el.style.top = `${headerHeight + 8 + idx * stride}px`;
|
|
el.style.bottom = 'auto';
|
|
el.style.left = '4px';
|
|
el.style.width = 'calc(100% - 8px)';
|
|
el.style.height = '110px';
|
|
});
|
|
}, { headerHeight: SUBAGENT.DEFAULT_HEADER_HEIGHT, stride: SUBAGENT.MOBILE_CARD_STRIDE });
|
|
|
|
// Record positions with keyboard hidden
|
|
const posBeforeKeyboard = await page.evaluate(() => {
|
|
return Array.from(document.querySelectorAll('.subagent-window')).map(w => {
|
|
const el = w as HTMLElement;
|
|
return { top: el.style.top, bottom: el.style.bottom };
|
|
});
|
|
});
|
|
|
|
// Show keyboard and reposition from bottom
|
|
await showKeyboard(page, KEYBOARD.TYPICAL_IOS_HEIGHT);
|
|
await page.waitForTimeout(WAIT.KEYBOARD_ANIMATION);
|
|
|
|
await page.evaluate(({ toolbarOffset, stride }) => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
windows.forEach((win, idx) => {
|
|
const el = win as HTMLElement;
|
|
el.style.top = 'auto';
|
|
el.style.bottom = `${toolbarOffset + idx * stride}px`;
|
|
});
|
|
}, { toolbarOffset: SUBAGENT.TOOLBAR_OFFSET, stride: SUBAGENT.MOBILE_CARD_STRIDE });
|
|
|
|
await triggerRelayout(page);
|
|
|
|
const posDuringKeyboard = await page.evaluate(() => {
|
|
return Array.from(document.querySelectorAll('.subagent-window')).map(w => {
|
|
const el = w as HTMLElement;
|
|
return { top: el.style.top, bottom: el.style.bottom };
|
|
});
|
|
});
|
|
|
|
// Positions should differ between keyboard hidden and visible states
|
|
expect(posDuringKeyboard[0].bottom).not.toBe(posBeforeKeyboard[0].bottom);
|
|
|
|
// Hide keyboard and reposition from top again
|
|
await hideKeyboard(page);
|
|
await page.waitForTimeout(WAIT.KEYBOARD_ANIMATION);
|
|
|
|
await page.evaluate(({ headerHeight, stride }) => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
windows.forEach((win, idx) => {
|
|
const el = win as HTMLElement;
|
|
el.style.top = `${headerHeight + 8 + idx * stride}px`;
|
|
el.style.bottom = 'auto';
|
|
});
|
|
}, { headerHeight: SUBAGENT.DEFAULT_HEADER_HEIGHT, stride: SUBAGENT.MOBILE_CARD_STRIDE });
|
|
|
|
await triggerRelayout(page);
|
|
|
|
const posAfterHide = await page.evaluate(() => {
|
|
return Array.from(document.querySelectorAll('.subagent-window')).map(w => {
|
|
const el = w as HTMLElement;
|
|
return { top: el.style.top, bottom: el.style.bottom };
|
|
});
|
|
});
|
|
|
|
// After hiding keyboard, positions should return to top-based stacking
|
|
expect(posAfterHide[0].top).toBe(posBeforeKeyboard[0].top);
|
|
expect(posAfterHide[0].bottom).toBe('auto');
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Interactions ────────────────────────────────────────────────────────
|
|
|
|
describe('Interactions', () => {
|
|
it('minimize button hides the window body', async () => {
|
|
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
await injectMockSubagentWindow(page, 'minimize-test', 0);
|
|
|
|
// Install a click handler on minimize button
|
|
await page.evaluate(() => {
|
|
const btn = document.querySelector('.subagent-window .minimize-btn');
|
|
btn?.addEventListener('click', () => {
|
|
const win = btn.closest('.subagent-window') as HTMLElement;
|
|
if (win) {
|
|
const body = win.querySelector('.subagent-body') as HTMLElement;
|
|
if (body) {
|
|
body.style.display = body.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
win.classList.toggle('minimized');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Verify body is initially visible
|
|
const bodyVisibleBefore = await page.evaluate(() => {
|
|
const body = document.querySelector('.subagent-window .subagent-body') as HTMLElement;
|
|
return body ? getComputedStyle(body).display !== 'none' : false;
|
|
});
|
|
expect(bodyVisibleBefore).toBe(true);
|
|
|
|
// Click minimize button (use JS click — button may be outside viewport)
|
|
await page.evaluate(() => {
|
|
const btn = document.querySelector('.subagent-window .minimize-btn') as HTMLElement;
|
|
btn?.click();
|
|
});
|
|
await page.waitForTimeout(100);
|
|
|
|
// Verify body is now hidden
|
|
const bodyVisibleAfter = await page.evaluate(() => {
|
|
const body = document.querySelector('.subagent-window .subagent-body') as HTMLElement;
|
|
return body ? getComputedStyle(body).display !== 'none' : true;
|
|
});
|
|
expect(bodyVisibleAfter).toBe(false);
|
|
|
|
// Verify window has minimized class
|
|
const isMinimized = await page.evaluate(() => {
|
|
const win = document.querySelector('.subagent-window');
|
|
return win?.classList.contains('minimized') ?? false;
|
|
});
|
|
expect(isMinimized).toBe(true);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
it('minimize button toggles: second click restores window', async () => {
|
|
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
await injectMockSubagentWindow(page, 'toggle-test', 0);
|
|
|
|
// Install toggle handler
|
|
await page.evaluate(() => {
|
|
const btn = document.querySelector('.subagent-window .minimize-btn');
|
|
btn?.addEventListener('click', () => {
|
|
const win = btn.closest('.subagent-window') as HTMLElement;
|
|
if (win) {
|
|
const body = win.querySelector('.subagent-body') as HTMLElement;
|
|
if (body) {
|
|
body.style.display = body.style.display === 'none' ? 'block' : 'none';
|
|
}
|
|
win.classList.toggle('minimized');
|
|
}
|
|
});
|
|
});
|
|
|
|
// First click - minimize (JS click for off-viewport elements)
|
|
await page.evaluate(() => {
|
|
const btn = document.querySelector('.subagent-window .minimize-btn') as HTMLElement;
|
|
btn?.click();
|
|
});
|
|
await page.waitForTimeout(100);
|
|
|
|
// Second click - restore
|
|
await page.evaluate(() => {
|
|
const btn = document.querySelector('.subagent-window .minimize-btn') as HTMLElement;
|
|
btn?.click();
|
|
});
|
|
await page.waitForTimeout(100);
|
|
|
|
const bodyVisible = await page.evaluate(() => {
|
|
const body = document.querySelector('.subagent-window .subagent-body') as HTMLElement;
|
|
return body ? getComputedStyle(body).display !== 'none' : false;
|
|
});
|
|
expect(bodyVisible).toBe(true);
|
|
|
|
const isMinimized = await page.evaluate(() => {
|
|
const win = document.querySelector('.subagent-window');
|
|
return win?.classList.contains('minimized') ?? false;
|
|
});
|
|
expect(isMinimized).toBe(false);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Content ─────────────────────────────────────────────────────────────
|
|
|
|
describe('Content', () => {
|
|
let context: BrowserContext;
|
|
let page: Page;
|
|
|
|
beforeAll(async () => {
|
|
({ context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium'));
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
await injectMockSubagentWindow(page, 'content-test-agent', 0);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await context.close();
|
|
});
|
|
|
|
it('header contains agent-id element', async () => {
|
|
const agentId = await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window .agent-id');
|
|
return el?.textContent?.trim() ?? '';
|
|
});
|
|
expect(agentId).toBe('content-test-agent');
|
|
});
|
|
|
|
it('header contains model-badge element', async () => {
|
|
const badge = await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window .model-badge');
|
|
return el?.textContent?.trim() ?? '';
|
|
});
|
|
expect(badge).toBe('sonnet');
|
|
});
|
|
|
|
it('header contains agent-status element', async () => {
|
|
const status = await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window .agent-status');
|
|
return el?.textContent?.trim() ?? '';
|
|
});
|
|
expect(status).toBe('active');
|
|
});
|
|
|
|
it('header contains minimize button', async () => {
|
|
const exists = await page.evaluate(() => {
|
|
return document.querySelector('.subagent-window .minimize-btn') !== null;
|
|
});
|
|
expect(exists).toBe(true);
|
|
});
|
|
|
|
it('body contains activity content', async () => {
|
|
const content = await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window .activity-content');
|
|
return el?.textContent?.trim() ?? '';
|
|
});
|
|
expect(content).toBe('Working on task...');
|
|
});
|
|
|
|
it('body content updates when text changes', async () => {
|
|
await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window .activity-content');
|
|
if (el) el.textContent = 'Running tests...';
|
|
});
|
|
|
|
const updatedContent = await page.evaluate(() => {
|
|
const el = document.querySelector('.subagent-window .activity-content');
|
|
return el?.textContent?.trim() ?? '';
|
|
});
|
|
expect(updatedContent).toBe('Running tests...');
|
|
});
|
|
});
|
|
|
|
// ─── Multiple Windows ───────────────────────────────────────────────────
|
|
|
|
describe('Multiple Windows', () => {
|
|
it('can render multiple subagent windows simultaneously', async () => {
|
|
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
await injectMockSubagentWindow(page, `multi-${i}`, i);
|
|
}
|
|
|
|
const count = await page.evaluate(() => {
|
|
return document.querySelectorAll('.subagent-window').length;
|
|
});
|
|
expect(count).toBe(4);
|
|
|
|
// Verify each has a unique agent-id
|
|
const ids = await page.evaluate(() => {
|
|
return Array.from(document.querySelectorAll('.subagent-window')).map(
|
|
w => (w as HTMLElement).dataset.agentId ?? '',
|
|
);
|
|
});
|
|
const uniqueIds = new Set(ids);
|
|
expect(uniqueIds.size).toBe(4);
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
|
|
it('windows do not overlap when properly stacked', async () => {
|
|
const { context, page } = await createDevicePage(standardPhone, baseUrl, 'chromium');
|
|
try {
|
|
await page.waitForTimeout(WAIT.PAGE_SETTLE);
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
await injectMockSubagentWindow(page, `overlap-${i}`, i);
|
|
}
|
|
|
|
// Position windows with proper stride
|
|
await page.evaluate(({ headerHeight, stride }) => {
|
|
const windows = document.querySelectorAll('.subagent-window');
|
|
windows.forEach((win, idx) => {
|
|
const el = win as HTMLElement;
|
|
el.style.position = 'fixed';
|
|
el.style.top = `${headerHeight + 8 + idx * stride}px`;
|
|
el.style.left = '4px';
|
|
el.style.width = 'calc(100% - 8px)';
|
|
el.style.height = '110px';
|
|
});
|
|
}, { headerHeight: SUBAGENT.DEFAULT_HEADER_HEIGHT, stride: SUBAGENT.MOBILE_CARD_STRIDE });
|
|
|
|
// Check that no two windows overlap vertically
|
|
const rects = await page.evaluate(() => {
|
|
return Array.from(document.querySelectorAll('.subagent-window')).map(w => {
|
|
const rect = w.getBoundingClientRect();
|
|
return { top: rect.top, bottom: rect.bottom };
|
|
});
|
|
});
|
|
|
|
for (let i = 1; i < rects.length; i++) {
|
|
// Each window's top should be at or after the previous window's bottom
|
|
// Allow small tolerance for sub-pixel rendering
|
|
expect(rects[i].top).toBeGreaterThanOrEqual(rects[i - 1].bottom - 1);
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
}
|
|
});
|
|
});
|
|
});
|