← back to Dear Bubbe Nextjs
tests/test-input-modes.js
400 lines
#!/usr/bin/env node
/**
* Comprehensive Test Suite for Talk/Text/Both Input Modes
* Tests all three modes and verifies functionality
*/
const puppeteer = require('puppeteer');
const chalk = require('chalk');
// Test configuration
const TEST_URL = 'http://localhost:3011';
const MOBILE_VIEWPORT = { width: 375, height: 812 }; // iPhone X viewport
const TABLET_VIEWPORT = { width: 768, height: 1024 }; // iPad viewport
const DESKTOP_VIEWPORT = { width: 1920, height: 1080 }; // Desktop viewport
// Test utilities
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const log = {
info: (msg) => console.log(chalk.blue('ℹ'), msg),
success: (msg) => console.log(chalk.green('✓'), msg),
error: (msg) => console.log(chalk.red('✗'), msg),
warning: (msg) => console.log(chalk.yellow('⚠'), msg),
section: (msg) => console.log(chalk.bold.cyan('\n' + '='.repeat(60) + '\n' + msg + '\n' + '='.repeat(60)))
};
// Test results tracker
const results = {
passed: [],
failed: [],
warnings: []
};
async function runTest(name, testFn) {
try {
await testFn();
results.passed.push(name);
log.success(`${name} - PASSED`);
return true;
} catch (error) {
results.failed.push({ name, error: error.message });
log.error(`${name} - FAILED: ${error.message}`);
return false;
}
}
async function testInputModes() {
let browser;
let page;
try {
log.section('STARTING INPUT MODE TESTS');
log.info('Launching browser...');
browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream'
]
});
page = await browser.newPage();
// Grant microphone permissions
const context = browser.defaultBrowserContext();
await context.overridePermissions(TEST_URL, ['microphone']);
// Test 1: Mobile viewport tests
log.section('TEST 1: MOBILE VIEWPORT (375x812)');
await page.setViewport(MOBILE_VIEWPORT);
await page.goto(TEST_URL, { waitUntil: 'networkidle2' });
// Test mode selector visibility
await runTest('Mobile: Mode selector visible', async () => {
const selector = await page.$('.mode-selector-container');
if (!selector) throw new Error('Mode selector not found');
});
// Test all three mode buttons exist
await runTest('Mobile: All three mode buttons exist', async () => {
const buttons = await page.$$('.mode-button');
if (buttons.length !== 3) throw new Error(`Expected 3 mode buttons, found ${buttons.length}`);
});
// Test Talk mode
await runTest('Mobile: Talk mode functionality', async () => {
// Click Talk button
await page.click('.mode-button:nth-child(1)');
await wait(1000);
// Verify Talk mode is active
const talkActive = await page.$eval('.mode-button:nth-child(1)', el => el.classList.contains('active'));
if (!talkActive) throw new Error('Talk button not active');
// Verify text input is hidden
const textInput = await page.$('.text-input-container');
if (textInput) throw new Error('Text input should be hidden in Talk mode');
// Verify voice manager is visible
const voiceManager = await page.$('.voice-manager-mobile');
if (!voiceManager) throw new Error('Voice manager should be visible in Talk mode');
// Verify status shows voice-related message
const statusText = await page.$eval('.mobile-status-bar', el => el.textContent);
if (!statusText.includes('Ready to talk') && !statusText.includes('Waking') && !statusText.includes('Listening')) {
throw new Error(`Unexpected status in Talk mode: ${statusText}`);
}
});
// Test Text mode
await runTest('Mobile: Text mode functionality', async () => {
// Click Text button
await page.click('.mode-button:nth-child(2)');
await wait(1000);
// Verify Text mode is active
const textActive = await page.$eval('.mode-button:nth-child(2)', el => el.classList.contains('active'));
if (!textActive) throw new Error('Text button not active');
// Verify text input is visible
const textInput = await page.$('.text-input-container');
if (!textInput) throw new Error('Text input should be visible in Text mode');
// Verify voice manager is hidden
const voiceSection = await page.$('.mobile-voice-section');
if (voiceSection) throw new Error('Voice section should be hidden in Text mode');
// Verify status shows text-related message
const statusText = await page.$eval('.mobile-status-bar', el => el.textContent);
if (!statusText.includes('Type your message') && !statusText.includes('thinking')) {
throw new Error(`Unexpected status in Text mode: ${statusText}`);
}
});
// Test Both mode
await runTest('Mobile: Both mode functionality', async () => {
// Click Both button
await page.click('.mode-button:nth-child(3)');
await wait(1000);
// Verify Both mode is active
const bothActive = await page.$eval('.mode-button:nth-child(3)', el => el.classList.contains('active'));
if (!bothActive) throw new Error('Both button not active');
// Verify text input is visible
const textInput = await page.$('.text-input-container');
if (!textInput) throw new Error('Text input should be visible in Both mode');
// Verify voice manager is visible
const voiceManager = await page.$('.voice-manager-mobile');
if (!voiceManager) throw new Error('Voice manager should be visible in Both mode');
// Verify status shows combined message
const statusText = await page.$eval('.mobile-status-bar', el => el.textContent);
if (!statusText.includes('Talk or type') && !statusText.includes('Listening') && !statusText.includes('Processing')) {
throw new Error(`Unexpected status in Both mode: ${statusText}`);
}
});
// Test text input functionality
await runTest('Mobile: Text input functionality', async () => {
// Ensure we're in Text mode
await page.click('.mode-button:nth-child(2)');
await wait(1000);
// Type a message
await page.type('.text-input-field', 'Test message from Puppeteer');
// Verify text was entered
const inputValue = await page.$eval('.text-input-field', el => el.value);
if (inputValue !== 'Test message from Puppeteer') {
throw new Error(`Input value mismatch: ${inputValue}`);
}
// Click send button
await page.click('.text-send-button');
await wait(2000);
// Verify message appears in chat
const messages = await page.$$('.mobile-message-bubble-user');
if (messages.length === 0) throw new Error('No user messages found in chat');
const lastMessage = await page.evaluate(
el => el.textContent,
messages[messages.length - 1]
);
if (!lastMessage.includes('Test message')) {
throw new Error(`Message not found in chat: ${lastMessage}`);
}
});
// Test 2: Tablet viewport tests
log.section('TEST 2: TABLET VIEWPORT (768x1024)');
await page.setViewport(TABLET_VIEWPORT);
await page.reload({ waitUntil: 'networkidle2' });
await runTest('Tablet: Mode selector responsive', async () => {
const selector = await page.$('.mode-selector-container');
if (!selector) throw new Error('Mode selector not found on tablet');
// Check button sizing
const buttonWidth = await page.$eval('.mode-button', el =>
window.getComputedStyle(el).width
);
const width = parseInt(buttonWidth);
if (width > 120 || width < 80) {
throw new Error(`Button width out of range on tablet: ${width}px`);
}
});
// Test 3: Desktop viewport tests
log.section('TEST 3: DESKTOP VIEWPORT (1920x1080)');
await page.setViewport(DESKTOP_VIEWPORT);
await page.reload({ waitUntil: 'networkidle2' });
await runTest('Desktop: Debug controls visible', async () => {
const debugControls = await page.$('.mobile-debug-controls');
if (!debugControls) throw new Error('Debug controls should be visible on desktop');
// Check if debug controls are displayed
const display = await page.$eval('.mobile-debug-controls', el =>
window.getComputedStyle(el).display
);
if (display === 'none') {
throw new Error('Debug controls should be displayed on desktop');
}
});
// Test 4: UI overlap checks
log.section('TEST 4: UI OVERLAP CHECKS');
await page.setViewport(MOBILE_VIEWPORT);
await page.reload({ waitUntil: 'networkidle2' });
await runTest('Mobile: No overlapping elements', async () => {
// Get bounding boxes of key elements
const elements = {
header: await page.$('.mobile-header-section'),
modeSelector: await page.$('.mobile-mode-selector-section'),
voiceSection: await page.$('.mobile-voice-section'),
chatContainer: await page.$('.mobile-chat-container'),
textInput: await page.$('.mobile-text-input-section'),
statusBar: await page.$('.mobile-status-bar')
};
const boxes = {};
for (const [name, element] of Object.entries(elements)) {
if (element) {
boxes[name] = await element.boundingBox();
}
}
// Check for overlaps
const checkOverlap = (box1, box2) => {
if (!box1 || !box2) return false;
return !(box1.x + box1.width <= box2.x ||
box2.x + box2.width <= box1.x ||
box1.y + box1.height <= box2.y ||
box2.y + box2.height <= box1.y);
};
const overlaps = [];
const keys = Object.keys(boxes);
for (let i = 0; i < keys.length; i++) {
for (let j = i + 1; j < keys.length; j++) {
if (checkOverlap(boxes[keys[i]], boxes[keys[j]])) {
overlaps.push(`${keys[i]} overlaps with ${keys[j]}`);
}
}
}
if (overlaps.length > 0) {
throw new Error(`Elements overlapping: ${overlaps.join(', ')}`);
}
});
// Test 5: Mode switching transitions
log.section('TEST 5: MODE SWITCHING TRANSITIONS');
await runTest('Mode switching is smooth', async () => {
// Switch between modes rapidly
for (let i = 0; i < 3; i++) {
await page.click('.mode-button:nth-child(1)'); // Talk
await wait(300);
await page.click('.mode-button:nth-child(2)'); // Text
await wait(300);
await page.click('.mode-button:nth-child(3)'); // Both
await wait(300);
}
// Verify final state is Both mode
const bothActive = await page.$eval('.mode-button:nth-child(3)', el => el.classList.contains('active'));
if (!bothActive) throw new Error('Both mode should be active after switching');
});
// Test 6: Accessibility checks
log.section('TEST 6: ACCESSIBILITY CHECKS');
await runTest('All interactive elements have proper ARIA labels', async () => {
const interactiveElements = await page.$$('button, input, textarea');
const missingLabels = [];
for (const element of interactiveElements) {
const hasAriaLabel = await element.evaluate(el =>
el.getAttribute('aria-label') || el.getAttribute('title')
);
if (!hasAriaLabel) {
const className = await element.evaluate(el => el.className);
missingLabels.push(className);
}
}
if (missingLabels.length > 0) {
log.warning(`Elements missing ARIA labels: ${missingLabels.join(', ')}`);
// Don't fail the test, just warn
}
});
await runTest('Touch targets are adequate size', async () => {
await page.setViewport(MOBILE_VIEWPORT);
const buttons = await page.$$('button');
const smallButtons = [];
for (const button of buttons) {
const box = await button.boundingBox();
if (box && (box.width < 44 || box.height < 44)) {
const className = await button.evaluate(el => el.className);
smallButtons.push(`${className}: ${box.width}x${box.height}`);
}
}
if (smallButtons.length > 0) {
throw new Error(`Buttons below 44x44px minimum: ${smallButtons.join(', ')}`);
}
});
} catch (error) {
log.error(`Test suite error: ${error.message}`);
results.failed.push({ name: 'Test Suite', error: error.message });
} finally {
if (browser) {
await browser.close();
}
}
// Print summary
log.section('TEST RESULTS SUMMARY');
log.info(`Total tests: ${results.passed.length + results.failed.length}`);
log.success(`Passed: ${results.passed.length}`);
if (results.failed.length > 0) {
log.error(`Failed: ${results.failed.length}`);
console.log('\nFailed tests:');
results.failed.forEach(({ name, error }) => {
console.log(` - ${name}: ${error}`);
});
}
if (results.warnings.length > 0) {
log.warning(`Warnings: ${results.warnings.length}`);
}
// Exit with appropriate code
process.exit(results.failed.length > 0 ? 1 : 0);
}
// Check if server is running before starting tests
async function checkServer() {
const http = require('http');
return new Promise((resolve) => {
http.get(TEST_URL, (res) => {
resolve(res.statusCode === 200);
}).on('error', () => {
resolve(false);
});
});
}
async function main() {
log.section('INPUT MODE TEST SUITE');
log.info('Testing Talk/Text/Both modes on Dear Bubbe...');
// Check if server is running
const serverRunning = await checkServer();
if (!serverRunning) {
log.error(`Server not running at ${TEST_URL}`);
log.info('Please start the server with: pm2 start bubbe');
process.exit(1);
}
// Run tests
await testInputModes();
}
// Run the test suite
main().catch(console.error);