#!/usr/bin/env node
const puppeteer = require('puppeteer');
async function runHeadlessTerminalTest(testUrl) {
// Default to a test URL if none provided
if (!testUrl) {
console.log('β οΈ No URL provided. Usage: node headless-terminal-test.cjs <terminal-url>');
console.log(' Example: node headless-terminal-test.cjs http://localhost:8081/session/my-session');
process.exit(1);
}
console.log(`π§ͺ Starting headless terminal test for: ${testUrl}`);
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
// Set up console logging to capture any browser errors
page.on('console', msg => {
console.log(`π Browser Console [${msg.type()}]:`, msg.text());
});
page.on('pageerror', error => {
console.error(`β Browser Error:`, error.message);
});
console.log('π Loading terminal page...');
await page.goto(testUrl, { waitUntil: 'networkidle0', timeout: 10000 });
// Wait for terminal to be ready
console.log('β³ Waiting for terminal initialization...');
await page.waitForSelector('.xterm', { timeout: 10000 });
await new Promise(resolve => setTimeout(resolve, 2000)); // Allow WebSocket connection to establish
// Check for any JavaScript errors
console.log('π Checking for JavaScript errors...');
const errors = await page.evaluate(() => {
return window.errors || [];
});
if (errors.length > 0) {
console.error('β JavaScript errors found:', errors);
return false;
}
// Get initial terminal content (history replay) - FIXED: Preserve line structure
console.log('π Checking terminal history replay...');
const initialContent = await page.evaluate(() => {
const terminalElement = document.querySelector('.xterm-screen');
if (!terminalElement) return '';
// CRITICAL FIX: Get terminal content with line structure preserved
const rows = terminalElement.querySelectorAll('.xterm-rows > *');
if (rows.length === 0) {
// Fallback: if no rows structure, try to get inner text which preserves some formatting
return terminalElement.innerText || terminalElement.textContent || '';
}
// Build line-by-line content with explicit line breaks
const lines = [];
rows.forEach(row => {
const lineContent = row.innerText || row.textContent || '';
if (lineContent.trim()) { // Only add non-empty lines
lines.push(lineContent);
}
});
return lines.join('\n'); // Join with explicit newlines
});
console.log('π Initial terminal content:');
console.log('---START TERMINAL CONTENT---');
console.log(initialContent);
console.log('---END TERMINAL CONTENT---');
// Basic validation - look for any actual terminal content vs just CSS
const hasActualContent = initialContent.length > 0 &&
!initialContent.startsWith('WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW') && // Not just CSS
(initialContent.includes('$') || initialContent.includes('localhost') || initialContent.includes('/home'));
// CRITICAL: Check for proper line separation (no concatenation)
const lines = initialContent.split('\n');
const hasProperLineSeparation = lines.length > 1;
const hasNoConcatenation = !initialContent.includes('pwd/home/jsbattig') && !initialContent.includes('whoamijsbattig');
console.log(`π Content Analysis:
- Content length: ${initialContent.length} chars
- Has actual terminal content: ${hasActualContent ? 'β' : 'β'}
- Contains prompt indicators: ${(initialContent.includes('$') || initialContent.includes('@')) ? 'β' : 'β'}
- Has proper line separation: ${hasProperLineSeparation ? 'β' : 'β'} (${lines.length} lines)
- No concatenation detected: ${hasNoConcatenation ? 'β' : 'β'}`);
// Test typing functionality if content looks valid AND properly formatted
if (hasActualContent && hasProperLineSeparation && hasNoConcatenation) {
console.log('β¨οΈ Testing typing functionality...');
const terminalInput = await page.waitForSelector('.xterm textarea', { timeout: 5000 });
// Focus on terminal and type a command
await terminalInput.focus();
const testCommand = 'echo test-command';
console.log(`β¨οΈ Typing command: ${testCommand}`);
await page.type('.xterm textarea', testCommand);
// Wait a moment to see the typed characters
await new Promise(resolve => setTimeout(resolve, 1000));
// Get content after typing (before pressing enter)
const contentAfterTyping = await page.evaluate(() => {
const terminalElement = document.querySelector('.xterm-screen');
return terminalElement ? terminalElement.textContent : '';
});
const showsTypedCommand = contentAfterTyping.includes(testCommand);
console.log(`β¨οΈ Typing verification: ${showsTypedCommand ? 'β' : 'β'} Command appears while typing`);
return hasActualContent && hasProperLineSeparation && hasNoConcatenation && showsTypedCommand;
} else {
if (!hasActualContent) {
console.log('β οΈ Terminal content appears to be CSS or invalid - skipping interaction test');
} else if (!hasProperLineSeparation) {
console.log('β CONCATENATION BUG: Terminal content lacks proper line separation');
} else if (!hasNoConcatenation) {
console.log('β CONCATENATION BUG: Commands and results are concatenated on same line');
}
return false;
}
} catch (error) {
console.error('π₯ Test failed with error:', error.message);
return false;
} finally {
await browser.close();
}
}
// Check if puppeteer is available
async function checkPuppeteer() {
try {
require('puppeteer');
return true;
} catch (e) {
console.log('π¦ Installing puppeteer...');
const { exec } = require('child_process');
return new Promise((resolve) => {
exec('npm install puppeteer', (error) => {
if (error) {
console.error('β Failed to install puppeteer:', error.message);
resolve(false);
} else {
console.log('β
Puppeteer installed successfully');
resolve(true);
}
});
});
}
}
// Main execution
(async () => {
const puppeteerAvailable = await checkPuppeteer();
if (!puppeteerAvailable) {
console.error('β Could not install/access puppeteer');
process.exit(1);
}
// Get URL from command line arguments
const testUrl = process.argv[2];
const success = await runHeadlessTerminalTest(testUrl);
console.log(`\nπ― HEADLESS TEST RESULT: ${success ? 'β
PASSED' : 'β FAILED'}`);
if (success) {
console.log('β
Terminal loads correctly with proper content and typing works');
} else {
console.log('β Terminal has display issues or typing doesn\'t work');
}
process.exit(success ? 0 : 1);
})();