#!/usr/bin/env bun /** * CHAT MOBILE LAYOUT TEST * ======================== * Tests that the chat widget behaves correctly on mobile viewports: * - Only Compact and Split modes available * - Desktop-only modes (side panel, floating, full) hidden * - CV always visible (no full takeover) * - Bottom sheet positioning * - Split mode: 50vh with CV visible above * * Tested viewports: iPhone SE (320x568), iPhone 14 (393x852), iPhone 14 Pro Max (430x932) */ import { chromium } from 'playwright'; const URL = "http://localhost:1999"; const VIEWPORTS = [ { name: 'iPhone SE', width: 320, height: 568 }, { name: 'iPhone 14', width: 393, height: 852 }, { name: 'iPhone 14 Pro Max', width: 430, height: 932 }, ]; async function testChatMobile() { console.log('šŸ“± CHAT MOBILE LAYOUT TEST\n'); console.log('='.repeat(70)); const browser = await chromium.launch({ headless: true }); let passed = 0; let failed = 0; function record(name, ok, detail = '') { ok ? passed++ : failed++; console.log(` ${ok ? 'āœ…' : 'āŒ'} ${name}${detail ? ' — ' + detail : ''}`); } for (const vp of VIEWPORTS) { console.log(`\nšŸ“± ${vp.name} (${vp.width}x${vp.height})`); console.log('-'.repeat(50)); const page = await browser.newPage({ viewport: { width: vp.width, height: vp.height } }); await page.goto(`${URL}/?lang=en`); await page.waitForTimeout(1500); // ================================================================ // 1. TOGGLE BUTTON // ================================================================ console.log(`\n 1ļøāƒ£ Toggle Button`); const btnVisible = await page.locator('#chat-toggle-btn').isVisible(); record(`${vp.name}: toggle button visible`, btnVisible); const btnBox = await page.locator('#chat-toggle-btn').boundingBox(); record(`${vp.name}: button within viewport`, btnBox && btnBox.x + btnBox.width <= vp.width, `right=${Math.round(btnBox?.x + btnBox?.width)}`); // ================================================================ // 2. OPEN CHAT — COMPACT MODE // ================================================================ console.log(`\n 2ļøāƒ£ Compact Mode (default)`); await page.evaluate(() => document.getElementById('chat-toggle-btn').click()); await page.waitForTimeout(400); const panelVisible = await page.locator('#chat-panel.chat-open').isVisible(); record(`${vp.name}: panel opens`, panelVisible); const panelBox = await page.locator('#chat-panel').boundingBox(); record(`${vp.name}: full width`, panelBox && panelBox.width >= vp.width - 2, `width=${panelBox?.width}`); record(`${vp.name}: anchored to bottom`, panelBox && (panelBox.y + panelBox.height) >= vp.height - 2, `bottom=${Math.round(panelBox?.y + panelBox?.height)}`); record(`${vp.name}: max 55% viewport height`, panelBox && panelBox.height <= vp.height * 0.56, `height=${Math.round(panelBox?.height)} (max=${Math.round(vp.height * 0.55)})`); // CV is visible above chat const cvVisible = panelBox && panelBox.y > 50; record(`${vp.name}: CV visible above (y > 50px)`, cvVisible, `y=${Math.round(panelBox?.y)}`); // ================================================================ // 3. HEADER BUTTONS — DESKTOP MODES HIDDEN // ================================================================ console.log(`\n 3ļøāƒ£ Header Buttons`); const compactBtn = await page.locator('.chat-mode-btn[data-mode=""]').isVisible(); record(`${vp.name}: compact button visible`, compactBtn); const splitBtn = await page.locator('.chat-mode-btn[data-mode="chat-split"]').isVisible(); record(`${vp.name}: split button visible`, splitBtn); const halfHidden = await page.locator('.chat-mode-btn[data-mode="chat-half"]').isHidden(); record(`${vp.name}: side panel button hidden`, halfHidden); const floatHidden = await page.locator('.chat-mode-btn[data-mode="chat-float"]').isHidden(); record(`${vp.name}: floating button hidden`, floatHidden); const fullHidden = await page.locator('.chat-mode-btn[data-mode="chat-full"]').isHidden(); record(`${vp.name}: full screen button hidden`, fullHidden); const helpVisible = await page.locator('.chat-mode-btn[command="show-modal"]').isVisible(); record(`${vp.name}: help button visible`, helpVisible); // ================================================================ // 4. SPLIT MODE — 50vh // ================================================================ console.log(`\n 4ļøāƒ£ Split Mode`); await page.click('.chat-mode-btn[data-mode="chat-split"]'); await page.waitForTimeout(400); const splitClass = await page.locator('#chat-panel.chat-split').count(); record(`${vp.name}: has chat-split class`, splitClass === 1); const splitBox = await page.locator('#chat-panel').boundingBox(); const expectedSplitH = vp.height * 0.5; record(`${vp.name}: split ~50vh height`, splitBox && Math.abs(splitBox.height - expectedSplitH) < 20, `height=${Math.round(splitBox?.height)} (expected ~${Math.round(expectedSplitH)})`); record(`${vp.name}: split anchored to bottom`, splitBox && (splitBox.y + splitBox.height) >= vp.height - 2, `bottom=${Math.round(splitBox?.y + splitBox?.height)}`); record(`${vp.name}: CV visible above split (top ~50%)`, splitBox && splitBox.y >= expectedSplitH - 20, `y=${Math.round(splitBox?.y)}`); record(`${vp.name}: split full width`, splitBox && splitBox.width >= vp.width - 2, `width=${splitBox?.width}`); // Messages area should expand const splitMsgBox = await page.locator('#chat-panel .chat-messages').boundingBox(); record(`${vp.name}: split messages expanded`, splitMsgBox && splitMsgBox.height > 100, `height=${Math.round(splitMsgBox?.height)}`); // Split active button const splitActive = await page.locator('.chat-mode-btn[data-mode="chat-split"].active').count(); record(`${vp.name}: split button is active`, splitActive === 1); // ================================================================ // 5. BACK TO COMPACT // ================================================================ console.log(`\n 5ļøāƒ£ Back to Compact`); await page.click('.chat-mode-btn[data-mode=""]'); await page.waitForTimeout(400); const backBox = await page.locator('#chat-panel').boundingBox(); record(`${vp.name}: compact restored`, backBox && backBox.height <= vp.height * 0.56, `height=${Math.round(backBox?.height)}`); const noSplit = await page.locator('#chat-panel.chat-split').count(); record(`${vp.name}: no split class`, noSplit === 0); // ================================================================ // 6. INPUT & SEND BUTTON FIT // ================================================================ console.log(`\n 6ļøāƒ£ Input Area`); const inputBox = await page.locator('#chat-input').boundingBox(); record(`${vp.name}: input within viewport`, inputBox && inputBox.x >= 0 && (inputBox.x + inputBox.width) <= vp.width, `x=${Math.round(inputBox?.x)} w=${Math.round(inputBox?.width)}`); const sendBox = await page.locator('.chat-send-btn').boundingBox(); record(`${vp.name}: send button within viewport`, sendBox && (sendBox.x + sendBox.width) <= vp.width, `right=${Math.round(sendBox?.x + sendBox?.width)}`); // ================================================================ // 7. CHIPS NOT OVERFLOWING // ================================================================ console.log(`\n 7ļøāƒ£ Chips`); const chipsBox = await page.locator('.chat-suggestions').boundingBox(); record(`${vp.name}: chips within viewport`, chipsBox && chipsBox.width <= vp.width, `width=${Math.round(chipsBox?.width)}`); await page.close(); } // ====================================================================== // DESKTOP SANITY CHECK — split button hidden, others visible // ====================================================================== console.log(`\nšŸ–„ļø Desktop Sanity Check (1920x1080)`); console.log('-'.repeat(50)); const desktopPage = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); await desktopPage.goto(`${URL}/?lang=en`); await desktopPage.waitForTimeout(1500); await desktopPage.evaluate(() => document.getElementById('chat-toggle-btn').click()); await desktopPage.waitForTimeout(400); const dSplitHidden = await desktopPage.locator('.chat-mode-btn[data-mode="chat-split"]').isHidden(); record('Desktop: split button hidden', dSplitHidden); const dHalfVisible = await desktopPage.locator('.chat-mode-btn[data-mode="chat-half"]').isVisible(); record('Desktop: side panel button visible', dHalfVisible); const dFloatVisible = await desktopPage.locator('.chat-mode-btn[data-mode="chat-float"]').isVisible(); record('Desktop: floating button visible', dFloatVisible); const dFullVisible = await desktopPage.locator('.chat-mode-btn[data-mode="chat-full"]').isVisible(); record('Desktop: full screen button visible', dFullVisible); await desktopPage.close(); // ====================================================================== // SUMMARY // ====================================================================== console.log('\n' + '='.repeat(70)); console.log(`\nšŸ“Š RESULTS: ${passed} passed, ${failed} failed, ${passed + failed} total`); if (failed > 0) { console.log('\nāŒ SOME TESTS FAILED'); } else { console.log('\nāœ… ALL TESTS PASSED'); } await browser.close(); process.exit(failed > 0 ? 1 : 0); } testChatMobile().catch(err => { console.error('Fatal error:', err); process.exit(1); });