6e922fd1cb
Mobile (≤480px): - Hide Side Panel, Floating, Full Screen buttons - Show Split button (50vh vertical split, CV visible above) - Compact mode: 55vh max bottom sheet - Force desktop modes to compact if somehow activated - Disable tooltips on mobile (overflow prevention) - Tighter header padding Desktop (>480px): - Split button hidden, all modes available as before Tests: 85-chat-mobile.test.mjs — 79 assertions across iPhone SE (320x568), iPhone 14 (393x852), iPhone 14 Pro Max (430x932), plus desktop sanity check
228 lines
9.6 KiB
JavaScript
228 lines
9.6 KiB
JavaScript
#!/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);
|
|
});
|