/** * E2E Tests for Chat Error States * Tests error handling and recovery in the chat UI * * Prerequisites: Web server running on http://127.0.0.1:5000 */ const puppeteer = require('puppeteer'); const AuthHelper = require('../auth_helper'); const { getPuppeteerLaunchOptions } = require('../puppeteer_config'); const fs = require('fs'); const path = require('path'); const BASE_URL = process.env.BASE_URL || 'http://127.0.0.1:5000'; const isCI = !!process.env.CI; // Timeouts adjusted for CI environment const TIMEOUTS = { navigation: isCI ? 60000 : 30000, selector: isCI ? 30000 : 10000, element: isCI ? 10000 : 5000, }; async function testChatErrorStates() { console.log(`๐Ÿงช Running chat error states test (CI mode: ${isCI})`); // Create screenshots directory if it doesn't exist const screenshotsDir = path.join(__dirname, '..', 'screenshots'); if (!fs.existsSync(screenshotsDir)) { fs.mkdirSync(screenshotsDir, { recursive: true }); } const browser = await puppeteer.launch(getPuppeteerLaunchOptions()); const page = await browser.newPage(); // Set viewport await page.setViewport({ width: 1280, height: 800 }); // Set timeouts if (isCI) { page.setDefaultTimeout(60000); page.setDefaultNavigationTimeout(60000); } // Enable console logging for debugging page.on('console', msg => { if (msg.type() === 'error') { console.log(`BROWSER ERROR: ${msg.text()}`); } }); page.on('pageerror', error => { console.log('PAGE ERROR:', error.message); }); const authHelper = new AuthHelper(page, BASE_URL); let testsPassed = 0; let testsFailed = 0; try { // Ensure we're logged in console.log('๐Ÿ” Ensuring authentication...'); await authHelper.ensureAuthenticated(); console.log('โœ… Authentication verified\n'); // Test 1: Invalid session ID handling console.log('๐Ÿ“‹ Test 1: Invalid session ID handling'); try { const invalidSessionId = 'nonexistent-session-id-12345'; await page.goto(`${BASE_URL}/chat/${invalidSessionId}`, { waitUntil: 'domcontentloaded', timeout: TIMEOUTS.navigation }); // Wait for the chat container to render (or the timeout // fires, signalling the page never recovered from the // invalid session). Deterministic vs. wall-clock setTimeout. await page.waitForSelector('.ldr-chat-container', { timeout: TIMEOUTS.selector, }).catch(() => {}); // Check that the page loaded without crashing const chatContainer = await page.$('.ldr-chat-container'); if (!chatContainer) { throw new Error('Chat container not found after invalid session navigation'); } console.log(' Page handled invalid session ID gracefully'); console.log('โœ… Test 1 PASSED: Invalid session ID handled\n'); testsPassed++; } catch (error) { console.log(`โŒ Test 1 FAILED: ${error.message}\n`); await page.screenshot({ path: path.join(screenshotsDir, 'chat_invalid_session_failed.png') }); testsFailed++; } // Test 2: API returns 404 for non-existent session console.log('๐Ÿ“‹ Test 2: API returns 404 for non-existent session'); try { await page.goto(`${BASE_URL}/chat/`, { waitUntil: 'domcontentloaded', timeout: TIMEOUTS.navigation }); const response = await page.evaluate(async () => { const res = await fetch('/api/chat/sessions/nonexistent-session-id'); return { status: res.status, data: await res.json() }; }); if (response.status !== 404) { throw new Error(`Expected 404, got ${response.status}`); } console.log(` API returned 404 as expected`); console.log('โœ… Test 2 PASSED: API returns 404 for non-existent session\n'); testsPassed++; } catch (error) { console.log(`โŒ Test 2 FAILED: ${error.message}\n`); await page.screenshot({ path: path.join(screenshotsDir, 'chat_api_404_failed.png') }); testsFailed++; } // Test 3: Empty message rejected by API console.log('๐Ÿ“‹ Test 3: Empty message rejected by API'); try { await page.goto(`${BASE_URL}/chat/`, { waitUntil: 'domcontentloaded', timeout: TIMEOUTS.navigation }); // CSRF is enforced on POST/PATCH/DELETE for chat routes // (Flask-WTF). Pull the token from the page meta tag so // the session-creation POST is not 400'd before we even // reach the empty-content validation we are trying to // exercise. See tests/ui_tests/chat/test_chat_csrf_required.js // for the negative coverage of CSRF itself. const csrf = await page.evaluate(() => { const m = document.querySelector('meta[name="csrf-token"]'); return m ? m.content : ''; }); // Create a session first const sessionResponse = await page.evaluate(async (token) => { const response = await fetch('/api/chat/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': token, }, body: JSON.stringify({ initial_query: 'Error states test' }) }); return response.json(); }, csrf); const sessionId = sessionResponse.session_id; if (!sessionId) { throw new Error(`Failed to create session for test: ${JSON.stringify(sessionResponse)}`); } // Try to send an empty message const emptyMessageResponse = await page.evaluate(async (sid, token) => { const response = await fetch(`/api/chat/sessions/${sid}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': token, }, body: JSON.stringify({ content: '', trigger_research: false }) }); return { status: response.status, data: await response.json() }; }, sessionId, csrf); // Empty message should be rejected with 400 if (emptyMessageResponse.status !== 400) { throw new Error(`Expected 400 for empty message, got ${emptyMessageResponse.status}`); } console.log(` Empty message rejected with status ${emptyMessageResponse.status}`); console.log('โœ… Test 3 PASSED: Empty message rejected\n'); testsPassed++; } catch (error) { console.log(`โŒ Test 3 FAILED: ${error.message}\n`); await page.screenshot({ path: path.join(screenshotsDir, 'chat_empty_message_failed.png') }); testsFailed++; } // Test 4: Chat container accessible after errors console.log('๐Ÿ“‹ Test 4: Chat remains functional after errors'); try { await page.goto(`${BASE_URL}/chat/`, { waitUntil: 'domcontentloaded', timeout: TIMEOUTS.navigation }); await page.waitForSelector('.ldr-chat-container', { timeout: TIMEOUTS.selector }); // Verify chat interface elements are present const hasInput = await page.$('textarea, input[type="text"]'); const hasContainer = await page.$('.ldr-chat-container'); if (!hasContainer) { throw new Error('Chat container not found'); } console.log(` Chat container accessible: ${!!hasContainer}`); console.log(` Input field present: ${!!hasInput}`); console.log('โœ… Test 4 PASSED: Chat functional after errors\n'); testsPassed++; } catch (error) { console.log(`โŒ Test 4 FAILED: ${error.message}\n`); await page.screenshot({ path: path.join(screenshotsDir, 'chat_functional_check_failed.png') }); testsFailed++; } } catch (error) { console.log(`๐Ÿ’ฅ Test suite error: ${error.message}`); testsFailed++; } finally { await browser.close(); } // Print summary console.log('โ”€'.repeat(50)); console.log(`๐Ÿ“Š Chat Error States Tests Summary`); console.log(` โœ… Passed: ${testsPassed}`); console.log(` โŒ Failed: ${testsFailed}`); console.log('โ”€'.repeat(50)); if (testsFailed > 0) { process.exit(1); } } testChatErrorStates().catch(error => { console.error('๐Ÿ’ฅ Test runner error:', error); process.exit(1); });