Full-featured desktop application built with Next.js + Electron: - Streaming chat with Claude Code via Agent SDK - Session management with SQLite persistence - Project file tree and file preview - MCP server and custom skills management - Permission controls with Code/Plan/Ask modes - Dark/light theme, token tracking - macOS DMG packaging with electron-builder Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
468 lines
22 KiB
TypeScript
468 lines
22 KiB
TypeScript
/**
|
|
* Functional test script -- comprehensive interactive testing.
|
|
* Run with: npx tsx src/__tests__/functional-test.ts
|
|
*/
|
|
import { chromium, type Page, type BrowserContext } from 'playwright';
|
|
|
|
interface TestResult {
|
|
category: string;
|
|
name: string;
|
|
status: 'PASS' | 'FAIL' | 'SKIP';
|
|
details: string;
|
|
}
|
|
|
|
const results: TestResult[] = [];
|
|
|
|
function record(category: string, name: string, status: 'PASS' | 'FAIL' | 'SKIP', details: string) {
|
|
results.push({ category, name, status, details });
|
|
const icon = status === 'PASS' ? '[PASS]' : status === 'FAIL' ? '[FAIL]' : '[SKIP]';
|
|
console.log(`${icon} [${category}] ${name} -- ${details}`);
|
|
}
|
|
|
|
async function testLayoutAndNav(context: BrowserContext) {
|
|
const page = await context.newPage();
|
|
await page.goto('http://localhost:3000/chat', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
|
|
// --- Sidebar exists ---
|
|
const sidebarEl = page.locator('aside, [class*="sidebar"], nav').first();
|
|
const sidebarVisible = await sidebarEl.isVisible();
|
|
record('Layout', 'Sidebar visible on desktop', sidebarVisible ? 'PASS' : 'FAIL', sidebarVisible ? 'Sidebar is visible' : 'Sidebar not found');
|
|
|
|
// --- Nav items ---
|
|
const navTexts = ['Chat', 'Plugins', 'MCP Servers', 'Settings'];
|
|
for (const text of navTexts) {
|
|
const navItem = page.locator(`text="${text}"`).first();
|
|
const visible = await navItem.isVisible().catch(() => false);
|
|
record('Layout', `Nav item "${text}" visible`, visible ? 'PASS' : 'FAIL', visible ? 'Found' : 'Not found');
|
|
}
|
|
|
|
// --- Nav highlight (Chat should be active on /chat) ---
|
|
const chatNavLink = page.locator('a[href="/chat"], a[href*="chat"]').first();
|
|
const chatNavClasses = await chatNavLink.getAttribute('class') || '';
|
|
const isActiveChat = chatNavClasses.includes('active') || chatNavClasses.includes('bg-') || chatNavClasses.includes('font-medium') || chatNavClasses.includes('font-semibold');
|
|
record('Layout', 'Chat nav highlighted on /chat', isActiveChat ? 'PASS' : 'FAIL', `Classes: ${chatNavClasses.substring(0, 100)}`);
|
|
|
|
// --- Navigate to Plugins and check highlight ---
|
|
await page.click('text="Plugins"');
|
|
await page.waitForTimeout(500);
|
|
const pluginsUrl = page.url();
|
|
record('Layout', 'Navigate to Plugins via nav', pluginsUrl.includes('/plugins') ? 'PASS' : 'FAIL', `URL: ${pluginsUrl}`);
|
|
|
|
// --- Navigate to Settings ---
|
|
await page.click('text="Settings"');
|
|
await page.waitForTimeout(500);
|
|
const settingsUrl = page.url();
|
|
record('Layout', 'Navigate to Settings via nav', settingsUrl.includes('/settings') ? 'PASS' : 'FAIL', `URL: ${settingsUrl}`);
|
|
|
|
// --- Theme toggle ---
|
|
const themeBtn = page.locator('button:has(svg)').filter({ hasText: '' }).last();
|
|
const allButtons = await page.locator('button').all();
|
|
let themeToggleBtn = null;
|
|
for (const btn of allButtons) {
|
|
const ariaLabel = await btn.getAttribute('aria-label') || '';
|
|
const title = await btn.getAttribute('title') || '';
|
|
if (ariaLabel.toLowerCase().includes('theme') || title.toLowerCase().includes('theme')) {
|
|
themeToggleBtn = btn;
|
|
break;
|
|
}
|
|
}
|
|
// Try finding the moon/sun icon button in the header
|
|
if (!themeToggleBtn) {
|
|
themeToggleBtn = page.locator('header button, [class*="header"] button').last();
|
|
}
|
|
|
|
if (themeToggleBtn) {
|
|
const htmlBefore = await page.locator('html').getAttribute('class') || '';
|
|
const styleBefore = await page.locator('html').getAttribute('style') || '';
|
|
await themeToggleBtn.click();
|
|
await page.waitForTimeout(500);
|
|
const htmlAfter = await page.locator('html').getAttribute('class') || '';
|
|
const styleAfter = await page.locator('html').getAttribute('style') || '';
|
|
const themeChanged = htmlBefore !== htmlAfter || styleBefore !== styleAfter;
|
|
record('Layout', 'Theme toggle changes theme', themeChanged ? 'PASS' : 'FAIL', `Before: class="${htmlBefore}" style="${styleBefore}" | After: class="${htmlAfter}" style="${styleAfter}"`);
|
|
|
|
// Take dark mode screenshot
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/dark-mode.png', fullPage: true });
|
|
|
|
// Toggle back
|
|
await themeToggleBtn.click();
|
|
await page.waitForTimeout(300);
|
|
} else {
|
|
record('Layout', 'Theme toggle changes theme', 'SKIP', 'Theme toggle button not found');
|
|
}
|
|
|
|
// --- Sidebar collapse ---
|
|
const collapseBtn = page.locator('[data-testid="sidebar-toggle"], button[aria-label*="sidebar"], button[aria-label*="collapse"], button[title*="sidebar"]');
|
|
const collapseCount = await collapseBtn.count();
|
|
if (collapseCount > 0) {
|
|
await collapseBtn.first().click();
|
|
await page.waitForTimeout(500);
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/sidebar-collapsed.png', fullPage: true });
|
|
record('Layout', 'Sidebar collapse button works', 'PASS', 'Clicked collapse button');
|
|
// Expand again
|
|
await collapseBtn.first().click();
|
|
await page.waitForTimeout(300);
|
|
} else {
|
|
record('Layout', 'Sidebar collapse button', 'SKIP', 'No collapse button found with common selectors');
|
|
}
|
|
|
|
await page.close();
|
|
}
|
|
|
|
async function testMobileResponsive(context: BrowserContext) {
|
|
const page = await context.newPage();
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
|
await page.goto('http://localhost:3000/chat', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/mobile-chat.png', fullPage: true });
|
|
|
|
// Check if sidebar is hidden on mobile
|
|
const sidebarEl = page.locator('aside, [class*="sidebar"]').first();
|
|
const sidebarBox = await sidebarEl.boundingBox().catch(() => null);
|
|
const sidebarHidden = !sidebarBox || sidebarBox.width === 0 || sidebarBox.x < -100;
|
|
record('Responsive', 'Sidebar hidden on mobile', sidebarHidden ? 'PASS' : 'FAIL', sidebarBox ? `Box: x=${sidebarBox.x} w=${sidebarBox.width}` : 'No bounding box');
|
|
|
|
// Check for hamburger menu
|
|
const hamburger = page.locator('button[aria-label*="menu"], button[aria-label*="sidebar"], [data-testid="mobile-menu"]');
|
|
const hamburgerCount = await hamburger.count();
|
|
record('Responsive', 'Hamburger menu button exists', hamburgerCount > 0 ? 'PASS' : 'FAIL', `Found ${hamburgerCount} potential hamburger buttons`);
|
|
|
|
// Test plugins page on mobile
|
|
await page.goto('http://localhost:3000/plugins', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/mobile-plugins.png', fullPage: true });
|
|
record('Responsive', 'Plugins page renders on mobile', 'PASS', 'Screenshot taken');
|
|
|
|
// Test settings page on mobile
|
|
await page.goto('http://localhost:3000/settings', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/mobile-settings.png', fullPage: true });
|
|
record('Responsive', 'Settings page renders on mobile', 'PASS', 'Screenshot taken');
|
|
|
|
await page.close();
|
|
}
|
|
|
|
async function testChatFlow(context: BrowserContext) {
|
|
const page = await context.newPage();
|
|
await page.goto('http://localhost:3000/chat', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
|
|
// --- New Chat button ---
|
|
const newChatBtn = page.locator('text="New Chat"').first();
|
|
const newChatVisible = await newChatBtn.isVisible().catch(() => false);
|
|
record('Chat', 'New Chat button visible', newChatVisible ? 'PASS' : 'FAIL', newChatVisible ? 'Found' : 'Not found');
|
|
|
|
// --- Chat input ---
|
|
const chatInput = page.locator('textarea, input[type="text"]').first();
|
|
const inputVisible = await chatInput.isVisible().catch(() => false);
|
|
record('Chat', 'Chat input visible', inputVisible ? 'PASS' : 'FAIL', inputVisible ? 'Found' : 'Not found');
|
|
|
|
// --- Send button ---
|
|
const sendBtn = page.locator('button[type="submit"], button:has(svg[class*="send"]), button[aria-label*="send"]');
|
|
// Also try locating by the paper plane icon area
|
|
const sendBtnCount = await sendBtn.count();
|
|
record('Chat', 'Send button present', sendBtnCount > 0 ? 'PASS' : 'FAIL', `Found ${sendBtnCount} potential send buttons`);
|
|
|
|
// --- Placeholder text ---
|
|
if (inputVisible) {
|
|
const placeholder = await chatInput.getAttribute('placeholder') || '';
|
|
record('Chat', 'Input has placeholder text', placeholder.length > 0 ? 'PASS' : 'FAIL', `Placeholder: "${placeholder}"`);
|
|
}
|
|
|
|
// --- Empty state ---
|
|
const emptyStateText = page.locator('text="Claude Chat"').first();
|
|
const hasEmptyState = await emptyStateText.isVisible().catch(() => false);
|
|
record('Chat', 'Empty state shown when no messages', hasEmptyState ? 'PASS' : 'FAIL', hasEmptyState ? 'Claude Chat welcome shown' : 'Empty state not found');
|
|
|
|
// --- Try sending a message (this likely won't get a real response without API key, but we can test the UI) ---
|
|
if (inputVisible) {
|
|
await chatInput.fill('Hello, this is a test message');
|
|
await page.waitForTimeout(200);
|
|
const inputValue = await chatInput.inputValue();
|
|
record('Chat', 'Can type in chat input', inputValue.includes('test message') ? 'PASS' : 'FAIL', `Input value: "${inputValue.substring(0, 50)}"`);
|
|
|
|
// Press Enter to send
|
|
await chatInput.press('Enter');
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Check if the message appeared in the chat
|
|
const userMessage = page.locator('[class*="message"], [data-role="user"], [class*="user"]');
|
|
const messageCount = await userMessage.count();
|
|
record('Chat', 'Message appears after sending', messageCount > 0 ? 'PASS' : 'FAIL', `Found ${messageCount} message elements`);
|
|
|
|
// Take screenshot of the chat after sending
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/chat-after-send.png', fullPage: true });
|
|
|
|
// Check if URL changed (new conversation created)
|
|
const urlAfterSend = page.url();
|
|
record('Chat', 'URL updates after sending message', urlAfterSend !== 'http://localhost:3000/chat' ? 'PASS' : 'FAIL', `URL: ${urlAfterSend}`);
|
|
|
|
// Check sidebar for new conversation
|
|
await page.waitForTimeout(1000);
|
|
const recentChats = page.locator('[class*="sidebar"] a, aside a').filter({ hasNotText: 'New Chat' }).filter({ hasNotText: 'Chat' });
|
|
const chatLinks = await page.locator('aside a[href*="/chat/"]').count();
|
|
record('Chat', 'Conversation appears in sidebar', chatLinks > 0 ? 'PASS' : 'FAIL', `Found ${chatLinks} chat links in sidebar`);
|
|
}
|
|
|
|
await page.close();
|
|
}
|
|
|
|
async function testPlugins(context: BrowserContext) {
|
|
const page = await context.newPage();
|
|
await page.goto('http://localhost:3000/plugins', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
|
|
// --- Page title ---
|
|
const title = page.locator('text="Plugins & Skills"').first();
|
|
const hasTitle = await title.isVisible().catch(() => false);
|
|
record('Plugins', 'Page title visible', hasTitle ? 'PASS' : 'FAIL', hasTitle ? 'Found' : 'Not found');
|
|
|
|
// --- Search input ---
|
|
const searchInput = page.locator('input[placeholder*="Search"], input[type="search"]').first();
|
|
const hasSearch = await searchInput.isVisible().catch(() => false);
|
|
record('Plugins', 'Search input visible', hasSearch ? 'PASS' : 'FAIL', hasSearch ? 'Found' : 'Not found');
|
|
|
|
// --- Filter tabs ---
|
|
const allTab = page.locator('text="All"').first();
|
|
const globalTab = page.locator('text="Global"').first();
|
|
const projectTab = page.locator('text="Project"').first();
|
|
const hasFilters = (await allTab.isVisible().catch(() => false)) && (await globalTab.isVisible().catch(() => false));
|
|
record('Plugins', 'Filter tabs visible (All/Global/Project)', hasFilters ? 'PASS' : 'FAIL', hasFilters ? 'Found' : 'Not found');
|
|
|
|
// --- Empty state ---
|
|
const emptyState = page.locator('text="No skills found"').first();
|
|
const hasEmptyState = await emptyState.isVisible().catch(() => false);
|
|
record('Plugins', 'Empty state shown when no skills', hasEmptyState ? 'PASS' : 'FAIL', hasEmptyState ? 'Shown' : 'Not shown');
|
|
|
|
// --- MCP Servers link ---
|
|
const mcpLink = page.locator('text="MCP Servers"').first();
|
|
const hasMcpLink = await mcpLink.isVisible().catch(() => false);
|
|
record('Plugins', 'MCP Servers button/link visible', hasMcpLink ? 'PASS' : 'FAIL', hasMcpLink ? 'Found' : 'Not found');
|
|
|
|
// --- Navigate to MCP page ---
|
|
if (hasMcpLink) {
|
|
await mcpLink.click();
|
|
await page.waitForTimeout(500);
|
|
const mcpUrl = page.url();
|
|
record('Plugins', 'Navigate to MCP Servers page', mcpUrl.includes('/plugins/mcp') ? 'PASS' : 'FAIL', `URL: ${mcpUrl}`);
|
|
}
|
|
|
|
await page.close();
|
|
}
|
|
|
|
async function testMCPPage(context: BrowserContext) {
|
|
const page = await context.newPage();
|
|
await page.goto('http://localhost:3000/plugins/mcp', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
|
|
// --- Page title ---
|
|
const title = page.locator('text="MCP Servers"').first();
|
|
const hasTitle = await title.isVisible().catch(() => false);
|
|
record('MCP', 'Page title visible', hasTitle ? 'PASS' : 'FAIL', hasTitle ? 'Found' : 'Not found');
|
|
|
|
// --- Add Server button ---
|
|
const addBtn = page.locator('text="Add Server"').first();
|
|
const hasAddBtn = await addBtn.isVisible().catch(() => false);
|
|
record('MCP', 'Add Server button visible', hasAddBtn ? 'PASS' : 'FAIL', hasAddBtn ? 'Found' : 'Not found');
|
|
|
|
// --- Tabs (Servers / JSON Config) ---
|
|
const serversTab = page.locator('text="Servers"').first();
|
|
const jsonTab = page.locator('text="JSON Config"').first();
|
|
const hasTabs = (await serversTab.isVisible().catch(() => false)) && (await jsonTab.isVisible().catch(() => false));
|
|
record('MCP', 'Tabs visible (Servers/JSON Config)', hasTabs ? 'PASS' : 'FAIL', hasTabs ? 'Found' : 'Not found');
|
|
|
|
// --- Empty state ---
|
|
const emptyState = page.locator('text="No MCP servers configured"').first();
|
|
const hasEmptyState = await emptyState.isVisible().catch(() => false);
|
|
record('MCP', 'Empty state shown', hasEmptyState ? 'PASS' : 'FAIL', hasEmptyState ? 'Shown' : 'Not shown');
|
|
|
|
// --- Back button ---
|
|
const backBtn = page.locator('button:has(svg), a[href="/plugins"]').first();
|
|
const hasBackBtn = await backBtn.isVisible().catch(() => false);
|
|
record('MCP', 'Back navigation available', hasBackBtn ? 'PASS' : 'FAIL', hasBackBtn ? 'Found' : 'Not found');
|
|
|
|
// --- Test Add Server dialog ---
|
|
if (hasAddBtn) {
|
|
await addBtn.click();
|
|
await page.waitForTimeout(500);
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/mcp-add-dialog.png', fullPage: true });
|
|
|
|
const dialog = page.locator('[role="dialog"], [class*="dialog"], [class*="modal"]').first();
|
|
const hasDialog = await dialog.isVisible().catch(() => false);
|
|
record('MCP', 'Add Server dialog opens', hasDialog ? 'PASS' : 'FAIL', hasDialog ? 'Dialog visible' : 'Dialog not found');
|
|
|
|
if (hasDialog) {
|
|
// Check form fields
|
|
const nameInput = page.locator('input[placeholder*="name"], input[name="name"], label:has-text("Name") + input, label:has-text("Name") ~ input').first();
|
|
const hasName = await nameInput.isVisible().catch(() => false);
|
|
record('MCP', 'Add dialog has name field', hasName ? 'PASS' : 'FAIL', hasName ? 'Found' : 'Not found');
|
|
|
|
// Close dialog
|
|
const cancelBtn = page.locator('text="Cancel"').first();
|
|
if (await cancelBtn.isVisible().catch(() => false)) {
|
|
await cancelBtn.click();
|
|
await page.waitForTimeout(300);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Test JSON Config tab ---
|
|
if (hasTabs) {
|
|
await jsonTab.click();
|
|
await page.waitForTimeout(500);
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/mcp-json-config.png', fullPage: true });
|
|
|
|
const jsonEditor = page.locator('textarea, [class*="editor"], [class*="json"], pre, code');
|
|
const jsonEditorCount = await jsonEditor.count();
|
|
record('MCP', 'JSON Config tab shows editor', jsonEditorCount > 0 ? 'PASS' : 'FAIL', `Found ${jsonEditorCount} editor elements`);
|
|
}
|
|
|
|
await page.close();
|
|
}
|
|
|
|
async function testSettings(context: BrowserContext) {
|
|
const page = await context.newPage();
|
|
await page.goto('http://localhost:3000/settings', { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(500);
|
|
|
|
// --- Page title ---
|
|
const title = page.locator('h1:has-text("Settings"), h2:has-text("Settings")').first();
|
|
const hasTitle = await title.isVisible().catch(() => false);
|
|
record('Settings', 'Page title visible', hasTitle ? 'PASS' : 'FAIL', hasTitle ? 'Found' : 'Not found');
|
|
|
|
// --- Visual/JSON Editor toggle ---
|
|
const visualBtn = page.locator('text="Visual Editor"').first();
|
|
const jsonBtn = page.locator('text="JSON Editor"').first();
|
|
const hasEditorToggle = (await visualBtn.isVisible().catch(() => false)) && (await jsonBtn.isVisible().catch(() => false));
|
|
record('Settings', 'Editor mode toggle visible', hasEditorToggle ? 'PASS' : 'FAIL', hasEditorToggle ? 'Found' : 'Not found');
|
|
|
|
// --- Permissions section ---
|
|
const permissionsSection = page.locator('text="Permissions"').first();
|
|
const hasPermissions = await permissionsSection.isVisible().catch(() => false);
|
|
record('Settings', 'Permissions section visible', hasPermissions ? 'PASS' : 'FAIL', hasPermissions ? 'Found' : 'Not found');
|
|
|
|
// --- Environment Variables section ---
|
|
const envSection = page.locator('text="Environment Variables"').first();
|
|
const hasEnv = await envSection.isVisible().catch(() => false);
|
|
record('Settings', 'Environment Variables section visible', hasEnv ? 'PASS' : 'FAIL', hasEnv ? 'Found' : 'Not found');
|
|
|
|
// --- Save button ---
|
|
const saveBtn = page.locator('text="Save Changes"').first();
|
|
const hasSave = await saveBtn.isVisible().catch(() => false);
|
|
record('Settings', 'Save Changes button visible', hasSave ? 'PASS' : 'FAIL', hasSave ? 'Found' : 'Not found');
|
|
|
|
// --- Reset button ---
|
|
const resetBtn = page.locator('text="Reset"').first();
|
|
const hasReset = await resetBtn.isVisible().catch(() => false);
|
|
record('Settings', 'Reset button visible', hasReset ? 'PASS' : 'FAIL', hasReset ? 'Found' : 'Not found');
|
|
|
|
// --- Switch to JSON Editor ---
|
|
if (hasEditorToggle) {
|
|
await jsonBtn.click();
|
|
await page.waitForTimeout(500);
|
|
await page.screenshot({ path: '/Users/op7418/Documents/code/opus-4.6-test/src/__tests__/screenshots/settings-json-mode.png', fullPage: true });
|
|
|
|
const jsonTextarea = page.locator('textarea').first();
|
|
const jsonVisible = await jsonTextarea.isVisible().catch(() => false);
|
|
record('Settings', 'JSON editor textarea visible', jsonVisible ? 'PASS' : 'FAIL', jsonVisible ? 'Found' : 'Not found');
|
|
|
|
if (jsonVisible) {
|
|
const jsonContent = await jsonTextarea.inputValue();
|
|
let isValidJson = false;
|
|
try {
|
|
JSON.parse(jsonContent);
|
|
isValidJson = true;
|
|
} catch {}
|
|
record('Settings', 'JSON editor contains valid JSON', isValidJson ? 'PASS' : 'FAIL', `Content length: ${jsonContent.length}, preview: ${jsonContent.substring(0, 100)}`);
|
|
}
|
|
|
|
// Switch back to Visual Editor
|
|
await visualBtn.click();
|
|
await page.waitForTimeout(500);
|
|
record('Settings', 'Switch back to Visual Editor', 'PASS', 'Switched');
|
|
}
|
|
|
|
await page.close();
|
|
}
|
|
|
|
async function testConsoleErrors(context: BrowserContext) {
|
|
const routes = ['/chat', '/plugins', '/plugins/mcp', '/settings'];
|
|
|
|
for (const route of routes) {
|
|
const page = await context.newPage();
|
|
const errors: string[] = [];
|
|
page.on('console', (msg) => {
|
|
if (msg.type() === 'error') {
|
|
errors.push(msg.text());
|
|
}
|
|
});
|
|
|
|
await page.goto(`http://localhost:3000${route}`, { waitUntil: 'networkidle' });
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Filter out known non-critical errors (like favicon 404, hydration warnings in dev)
|
|
const criticalErrors = errors.filter(e =>
|
|
!e.includes('favicon') &&
|
|
!e.includes('hydrat') &&
|
|
!e.includes('Warning:') &&
|
|
!e.includes('DevTools')
|
|
);
|
|
|
|
record('Console', `No JS errors on ${route}`, criticalErrors.length === 0 ? 'PASS' : 'FAIL',
|
|
criticalErrors.length === 0 ? 'No critical errors' : `Errors: ${criticalErrors.join(' | ').substring(0, 200)}`);
|
|
|
|
await page.close();
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log('Starting comprehensive functional tests...\n');
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
|
|
|
|
await testConsoleErrors(context);
|
|
await testLayoutAndNav(context);
|
|
await testMobileResponsive(context);
|
|
await testChatFlow(context);
|
|
await testPlugins(context);
|
|
await testMCPPage(context);
|
|
await testSettings(context);
|
|
|
|
await browser.close();
|
|
|
|
// Summary
|
|
console.log('\n========================================');
|
|
console.log(' FUNCTIONAL TEST SUMMARY');
|
|
console.log('========================================\n');
|
|
|
|
const categories = [...new Set(results.map(r => r.category))];
|
|
let totalPass = 0, totalFail = 0, totalSkip = 0;
|
|
|
|
for (const cat of categories) {
|
|
const catResults = results.filter(r => r.category === cat);
|
|
const pass = catResults.filter(r => r.status === 'PASS').length;
|
|
const fail = catResults.filter(r => r.status === 'FAIL').length;
|
|
const skip = catResults.filter(r => r.status === 'SKIP').length;
|
|
console.log(` ${cat}: ${pass} passed, ${fail} failed, ${skip} skipped`);
|
|
totalPass += pass;
|
|
totalFail += fail;
|
|
totalSkip += skip;
|
|
}
|
|
|
|
console.log(`\n TOTAL: ${totalPass} passed, ${totalFail} failed, ${totalSkip} skipped`);
|
|
console.log('========================================\n');
|
|
|
|
if (totalFail > 0) {
|
|
console.log('FAILURES:');
|
|
for (const r of results.filter(r => r.status === 'FAIL')) {
|
|
console.log(` [${r.category}] ${r.name}: ${r.details}`);
|
|
}
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
main().catch(console.error);
|