mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
- restoreSubagentWindowStates now discovers parent sessions before restoring - closeSubagentWindow discovers parent before minimizing to prevent wrong tab - Fixed async handling in subagent:completed event handler chore: bump version to 0.1389 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
446 lines
14 KiB
TypeScript
446 lines
14 KiB
TypeScript
/**
|
|
* Puppeteer/Chrome Test Suite
|
|
*
|
|
* Tests Claudeman web UI using Puppeteer for direct Chrome DevTools Protocol access.
|
|
* Puppeteer provides lower-level Chrome control compared to Playwright.
|
|
*
|
|
* Port: 3156 (see CLAUDE.md test port table)
|
|
*/
|
|
|
|
import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
|
|
import puppeteer, { Browser, Page } from 'puppeteer';
|
|
import { WebServer } from '../src/web/server.js';
|
|
|
|
const TEST_PORT = 3156;
|
|
const BASE_URL = `http://localhost:${TEST_PORT}`;
|
|
|
|
// Metrics collection
|
|
interface TestMetrics {
|
|
testName: string;
|
|
duration: number;
|
|
framework: 'puppeteer';
|
|
success: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const metrics: TestMetrics[] = [];
|
|
|
|
describe('Puppeteer/Chrome Tests', () => {
|
|
let server: WebServer;
|
|
let browser: Browser;
|
|
let page: Page;
|
|
let createdSessions: string[] = [];
|
|
let testStartTime: number;
|
|
|
|
beforeAll(async () => {
|
|
// Start web server
|
|
server = new WebServer(TEST_PORT);
|
|
await server.start();
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
|
|
// Launch Chrome via Puppeteer
|
|
browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--disable-dev-shm-usage',
|
|
'--disable-gpu',
|
|
],
|
|
});
|
|
}, 60000);
|
|
|
|
afterAll(async () => {
|
|
// Cleanup sessions
|
|
for (const sessionId of createdSessions) {
|
|
try {
|
|
await fetch(`${BASE_URL}/api/sessions/${sessionId}`, { method: 'DELETE' });
|
|
} catch { /* ignore */ }
|
|
}
|
|
|
|
// Close browser and server
|
|
if (browser) await browser.close();
|
|
await server.stop();
|
|
|
|
// Output metrics
|
|
console.log('\n📊 Puppeteer Test Metrics:');
|
|
console.table(metrics.map(m => ({
|
|
test: m.testName.substring(0, 40),
|
|
duration: `${m.duration}ms`,
|
|
status: m.success ? '✅' : '❌',
|
|
})));
|
|
}, 60000);
|
|
|
|
beforeEach(async () => {
|
|
page = await browser.newPage();
|
|
await page.setViewport({ width: 1280, height: 720 });
|
|
testStartTime = Date.now();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
const testInfo = expect.getState();
|
|
metrics.push({
|
|
testName: testInfo.currentTestName || 'unknown',
|
|
duration: Date.now() - testStartTime,
|
|
framework: 'puppeteer',
|
|
success: !testInfo.assertionCalls?.some((c: any) => !c.pass),
|
|
});
|
|
|
|
if (page) await page.close();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Core Page Tests
|
|
// ============================================================================
|
|
|
|
it('should load the main page with correct title', async () => {
|
|
await page.goto(BASE_URL);
|
|
const title = await page.title();
|
|
expect(title).toBe('Claudeman');
|
|
});
|
|
|
|
it('should display the logo and branding', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
await page.waitForSelector('.header-brand .logo');
|
|
const logoText = await page.$eval('.header-brand .logo', el => el.textContent);
|
|
expect(logoText).toBe('Claudeman');
|
|
});
|
|
|
|
it('should show version in toolbar', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
await page.waitForSelector('#versionDisplay');
|
|
const version = await page.$eval('#versionDisplay', el => el.textContent);
|
|
expect(version).toMatch(/v\d+\.\d+/);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Button & Control Tests
|
|
// ============================================================================
|
|
|
|
it('should have all main control buttons visible', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
const btnClaude = await page.$('.btn-claude');
|
|
const btnSettings = await page.$('.btn-settings');
|
|
const helpBtn = await page.$('.help-btn');
|
|
const quickStart = await page.$('#quickStartCase');
|
|
|
|
expect(btnClaude).not.toBeNull();
|
|
expect(btnSettings).not.toBeNull();
|
|
expect(helpBtn).not.toBeNull();
|
|
expect(quickStart).not.toBeNull();
|
|
});
|
|
|
|
it('should have font size controls', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
const fontControls = await page.$('.header-font-controls');
|
|
expect(fontControls).not.toBeNull();
|
|
|
|
const fontBtns = await page.$$('.header-font-controls .font-btn');
|
|
expect(fontBtns.length).toBeGreaterThanOrEqual(2);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Modal Tests
|
|
// ============================================================================
|
|
|
|
it('should open and close help modal', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Open help modal
|
|
await page.click('.help-btn');
|
|
await page.waitForSelector('#helpModal .modal-content', { visible: true });
|
|
|
|
// Check content
|
|
const heading = await page.$eval('#helpModal h3', el => el.textContent);
|
|
expect(heading).toContain('Keyboard Shortcuts');
|
|
|
|
// Check modal is visible
|
|
const isVisible = await page.$eval('#helpModal .modal-content', el => {
|
|
const style = window.getComputedStyle(el);
|
|
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
});
|
|
expect(isVisible).toBe(true);
|
|
|
|
// Close modal
|
|
await page.click('#helpModal .modal-close');
|
|
await page.waitForFunction(() => {
|
|
const modal = document.querySelector('#helpModal .modal-content');
|
|
if (!modal) return true;
|
|
const style = window.getComputedStyle(modal);
|
|
return style.display === 'none' || style.visibility === 'hidden';
|
|
}, { timeout: 5000 }).catch(() => {});
|
|
});
|
|
|
|
it('should open settings modal with 4 tabs', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Open settings
|
|
await page.click('.btn-settings');
|
|
await page.waitForSelector('#appSettingsModal .modal-content', { visible: true });
|
|
|
|
// Check tabs
|
|
const tabs = await page.$$('#appSettingsModal .modal-tab-btn');
|
|
expect(tabs.length).toBe(4);
|
|
|
|
// Verify tab names
|
|
const tabTexts = await page.$$eval('#appSettingsModal .modal-tab-btn', els =>
|
|
els.map(el => el.textContent?.trim())
|
|
);
|
|
expect(tabTexts).toContain('Display');
|
|
expect(tabTexts).toContain('Claude CLI');
|
|
|
|
// Close
|
|
await page.click('#appSettingsModal .modal-close');
|
|
});
|
|
|
|
it('should switch between settings tabs', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
await page.click('.btn-settings');
|
|
await page.waitForSelector('#appSettingsModal .modal-content', { visible: true });
|
|
|
|
// Click each tab
|
|
const tabs = await page.$$('#appSettingsModal .modal-tab-btn');
|
|
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
await tabs[i].click();
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
// Check tab is active
|
|
const isActive = await page.$eval(
|
|
`#appSettingsModal .modal-tab-btn:nth-child(${i + 1})`,
|
|
el => el.classList.contains('active')
|
|
);
|
|
expect(isActive).toBe(true);
|
|
}
|
|
|
|
await page.click('#appSettingsModal .modal-close');
|
|
});
|
|
|
|
// ============================================================================
|
|
// Session Management Tests
|
|
// ============================================================================
|
|
|
|
it('should create a new session via button click', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Click create session button
|
|
await page.click('.btn-claude');
|
|
|
|
// Wait for session tab to appear
|
|
await page.waitForSelector('.session-tab', { timeout: 15000 });
|
|
|
|
// Tab should be active
|
|
const hasActive = await page.$('.session-tab.active');
|
|
expect(hasActive).not.toBeNull();
|
|
|
|
// Terminal should be visible
|
|
await page.waitForSelector('.xterm', { timeout: 10000 });
|
|
const terminal = await page.$('.xterm');
|
|
expect(terminal).not.toBeNull();
|
|
|
|
// Track session for cleanup
|
|
const response = await fetch(`${BASE_URL}/api/sessions`);
|
|
const data = await response.json();
|
|
if (data.sessions?.length > 0) {
|
|
const lastSession = data.sessions[data.sessions.length - 1];
|
|
if (lastSession && !createdSessions.includes(lastSession.id)) {
|
|
createdSessions.push(lastSession.id);
|
|
}
|
|
}
|
|
}, 30000);
|
|
|
|
it('should show tab count correctly', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Get initial count
|
|
const initialText = await page.$eval('#tabCount', el => el.textContent);
|
|
const initialCount = parseInt(initialText?.match(/\d+/)?.[0] || '0');
|
|
|
|
// Create a session
|
|
await page.click('.btn-claude');
|
|
await page.waitForSelector('.session-tab', { timeout: 15000 });
|
|
|
|
// Tab count should increase
|
|
await page.waitForFunction(
|
|
(expected: number) => {
|
|
const el = document.querySelector('#tabCount');
|
|
const text = el?.textContent || '';
|
|
const match = text.match(/\d+/);
|
|
return match && parseInt(match[0]) >= expected;
|
|
},
|
|
{ timeout: 5000 },
|
|
initialCount + 1
|
|
);
|
|
|
|
// Track for cleanup
|
|
const response = await fetch(`${BASE_URL}/api/sessions`);
|
|
const data = await response.json();
|
|
data.sessions?.forEach((s: any) => {
|
|
if (!createdSessions.includes(s.id)) createdSessions.push(s.id);
|
|
});
|
|
}, 30000);
|
|
|
|
// ============================================================================
|
|
// Terminal Tests
|
|
// ============================================================================
|
|
|
|
it('should initialize xterm terminal with correct dimensions', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Create session
|
|
await page.click('.btn-claude');
|
|
await page.waitForSelector('.xterm', { timeout: 15000 });
|
|
|
|
// Check terminal container exists
|
|
const terminal = await page.$('.xterm');
|
|
expect(terminal).not.toBeNull();
|
|
|
|
// Check xterm canvas exists (indicates proper initialization)
|
|
const canvas = await page.$('.xterm-screen');
|
|
expect(canvas).not.toBeNull();
|
|
|
|
// Track for cleanup
|
|
const response = await fetch(`${BASE_URL}/api/sessions`);
|
|
const data = await response.json();
|
|
data.sessions?.forEach((s: any) => {
|
|
if (!createdSessions.includes(s.id)) createdSessions.push(s.id);
|
|
});
|
|
}, 30000);
|
|
|
|
// ============================================================================
|
|
// Keyboard Shortcut Tests
|
|
// ============================================================================
|
|
|
|
it('should respond to Ctrl+Enter for quick start', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
const initialSessions = await fetch(`${BASE_URL}/api/sessions`).then(r => r.json());
|
|
const initialCount = initialSessions.sessions?.length || 0;
|
|
|
|
// Press Ctrl+Enter
|
|
await page.keyboard.down('Control');
|
|
await page.keyboard.press('Enter');
|
|
await page.keyboard.up('Control');
|
|
|
|
// Wait for session to be created
|
|
await page.waitForSelector('.session-tab', { timeout: 15000 });
|
|
|
|
// Verify session was created
|
|
const afterSessions = await fetch(`${BASE_URL}/api/sessions`).then(r => r.json());
|
|
expect(afterSessions.sessions?.length).toBeGreaterThan(initialCount);
|
|
|
|
// Track for cleanup
|
|
afterSessions.sessions?.forEach((s: any) => {
|
|
if (!createdSessions.includes(s.id)) createdSessions.push(s.id);
|
|
});
|
|
}, 30000);
|
|
|
|
// ============================================================================
|
|
// Chrome DevTools Protocol Direct Access
|
|
// ============================================================================
|
|
|
|
it('should access CDP for performance metrics', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Get performance metrics via CDP
|
|
const client = await page.target().createCDPSession();
|
|
await client.send('Performance.enable');
|
|
|
|
const { metrics: perfMetrics } = await client.send('Performance.getMetrics');
|
|
|
|
// Should have some metrics
|
|
expect(perfMetrics.length).toBeGreaterThan(0);
|
|
|
|
// Find specific metrics
|
|
const jsHeapSize = perfMetrics.find(m => m.name === 'JSHeapUsedSize');
|
|
expect(jsHeapSize).toBeDefined();
|
|
});
|
|
|
|
it('should capture network activity via CDP', async () => {
|
|
const networkRequests: string[] = [];
|
|
|
|
const client = await page.target().createCDPSession();
|
|
await client.send('Network.enable');
|
|
|
|
client.on('Network.requestWillBeSent', (params) => {
|
|
networkRequests.push(params.request.url);
|
|
});
|
|
|
|
await page.goto(BASE_URL);
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
// Should have captured some requests
|
|
expect(networkRequests.length).toBeGreaterThan(0);
|
|
expect(networkRequests.some(url => url.includes('/api/events'))).toBe(true);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Stress Tests
|
|
// ============================================================================
|
|
|
|
it('should handle rapid button clicks without errors', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Rapidly click help button multiple times
|
|
for (let i = 0; i < 5; i++) {
|
|
await page.click('.help-btn');
|
|
await new Promise(r => setTimeout(r, 100));
|
|
const closeBtn = await page.$('#helpModal .modal-close');
|
|
if (closeBtn) await closeBtn.click().catch(() => {});
|
|
await new Promise(r => setTimeout(r, 100));
|
|
}
|
|
|
|
// Page should still be functional
|
|
const header = await page.$('.header');
|
|
expect(header).not.toBeNull();
|
|
});
|
|
|
|
// ============================================================================
|
|
// Screenshot & Visual Tests
|
|
// ============================================================================
|
|
|
|
it('should capture screenshot for visual inspection', async () => {
|
|
await page.goto(BASE_URL);
|
|
|
|
// Take a screenshot (could be used for visual regression)
|
|
const screenshot = await page.screenshot({ encoding: 'base64' });
|
|
expect(screenshot.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// ============================================================================
|
|
// Console & Error Monitoring
|
|
// ============================================================================
|
|
|
|
it('should not have console errors on page load', async () => {
|
|
const consoleErrors: string[] = [];
|
|
|
|
page.on('console', msg => {
|
|
if (msg.type() === 'error') {
|
|
consoleErrors.push(msg.text());
|
|
}
|
|
});
|
|
|
|
page.on('pageerror', err => {
|
|
consoleErrors.push(err.message);
|
|
});
|
|
|
|
await page.goto(BASE_URL);
|
|
await new Promise(r => setTimeout(r, 2000));
|
|
|
|
// Filter out expected errors (like SSE reconnection)
|
|
const unexpectedErrors = consoleErrors.filter(e =>
|
|
!e.includes('net::ERR') && !e.includes('SSE')
|
|
);
|
|
|
|
expect(unexpectedErrors.length).toBe(0);
|
|
});
|
|
});
|
|
|
|
// Export metrics for comparison
|
|
export { metrics as puppeteerMetrics };
|