From 823030dcf2b1281f0e95b7e1334db0f5b10dd1d8 Mon Sep 17 00:00:00 2001 From: juanatsap Date: Thu, 9 Apr 2026 11:09:30 +0100 Subject: [PATCH] test: 38 layout mode tests + fix half/full/float CSS positioning - Fix side panel and full screen not covering full viewport - Fix floating mode initial position (near chat button, not top-right) - Reset width/height inline styles when switching modes - Add 84-chat-layout-modes.test.mjs: 38 assertions covering compact, side panel, full screen, floating, drag, rapid switching, and user avatar rendering --- static/css/04-interactive/_chat.css | 15 +- templates/partials/widgets/chat-widget.html | 3 + tests/mjs/84-chat-layout-modes.test.mjs | 259 ++++++++++++++++++++ 3 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 tests/mjs/84-chat-layout-modes.test.mjs diff --git a/static/css/04-interactive/_chat.css b/static/css/04-interactive/_chat.css index 6d49ecd..6aa9ec0 100644 --- a/static/css/04-interactive/_chat.css +++ b/static/css/04-interactive/_chat.css @@ -88,6 +88,7 @@ bottom: 0; left: auto; width: 50vw; + height: auto; max-height: none; border-radius: 0; border-left: 2px solid var(--border-light); @@ -98,14 +99,17 @@ flex: 1; } -/* Size: Floating — draggable, resizable feel */ +/* Size: Floating — draggable, resizable window */ .chat-panel.chat-float { - top: 10vh; - left: auto; + bottom: 10.5rem; right: 2rem; - bottom: auto; + top: auto; + left: auto; width: 420px; + height: 450px; max-height: 70vh; + min-width: 300px; + min-height: 250px; border-radius: 8px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); resize: both; @@ -124,13 +128,14 @@ flex: 1; } -/* Size: Full width */ +/* Size: Full screen */ .chat-panel.chat-full { top: 0; left: 0; right: 0; bottom: 0; width: 100%; + height: auto; max-height: none; border-radius: 0; border: none; diff --git a/templates/partials/widgets/chat-widget.html b/templates/partials/widgets/chat-widget.html index 9fcf20c..bbac6c6 100644 --- a/templates/partials/widgets/chat-widget.html +++ b/templates/partials/widgets/chat-widget.html @@ -210,10 +210,13 @@ document.addEventListener('htmx:afterRequest', function(event) { function setChatSize(size) { var panel = document.getElementById('chat-panel'); panel.classList.remove('chat-half', 'chat-full', 'chat-float'); + // Reset all inline styles from drag/resize panel.style.top = ''; panel.style.left = ''; panel.style.right = ''; panel.style.bottom = ''; + panel.style.width = ''; + panel.style.height = ''; if (size) panel.classList.add(size); // Update active button document.querySelectorAll('.chat-mode-btn[data-mode]').forEach(function(btn) { diff --git a/tests/mjs/84-chat-layout-modes.test.mjs b/tests/mjs/84-chat-layout-modes.test.mjs new file mode 100644 index 0000000..594bbaa --- /dev/null +++ b/tests/mjs/84-chat-layout-modes.test.mjs @@ -0,0 +1,259 @@ +#!/usr/bin/env bun +/** + * CHAT LAYOUT MODES TEST + * ======================= + * Tests the 4 chat panel layout modes: Compact, Side panel, Floating, Full screen. + * Verifies positioning, sizing, drag behavior, resize, and button state. + */ + +import { chromium } from 'playwright'; + +const URL = "http://localhost:1999"; + +async function testChatLayoutModes() { + console.log('📐 CHAT LAYOUT MODES TEST\n'); + console.log('='.repeat(70)); + + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } }); + + let passed = 0; + let failed = 0; + + function record(name, ok, detail = '') { + ok ? passed++ : failed++; + console.log(` ${ok ? '✅' : '❌'} ${name}${detail ? ' — ' + detail : ''}`); + } + + // ====================================================================== + console.log("\n📋 Loading page..."); + await page.goto(`${URL}/?lang=en`); + await page.waitForTimeout(2000); + + // Open chat panel + await page.click('#chat-toggle-btn'); + await page.waitForTimeout(500); + record('Chat panel opens', await page.locator('#chat-panel.chat-open').isVisible()); + + // ====================================================================== + // 1. MODE BUTTONS EXIST + // ====================================================================== + console.log("\n1️⃣ Mode Buttons"); + + const modeButtons = await page.locator('.chat-mode-btn[data-mode]').count(); + record('4 mode buttons exist', modeButtons === 4, `found ${modeButtons}`); + + const helpBtn = await page.locator('.chat-mode-btn[command="show-modal"]').count(); + record('Help button exists', helpBtn === 1); + + const divider = await page.locator('.chat-header-divider').count(); + record('Divider between modes and help', divider === 1); + + // Check compact is active by default + const compactActive = await page.locator('.chat-mode-btn[data-mode=""].active').count(); + record('Compact mode active by default', compactActive === 1); + + // ====================================================================== + // 2. COMPACT MODE (default) + // ====================================================================== + console.log("\n2️⃣ Compact Mode"); + + const compactBox = await page.locator('#chat-panel').boundingBox(); + record('Compact: has fixed width ~360px', compactBox && compactBox.width >= 340 && compactBox.width <= 380, + `width=${compactBox?.width}`); + record('Compact: positioned bottom-right', compactBox && compactBox.x > 1400 && compactBox.y > 300, + `x=${compactBox?.x} y=${compactBox?.y}`); + record('Compact: not full height', compactBox && compactBox.height < 600, + `height=${compactBox?.height}`); + + // No special classes + const hasHalf = await page.locator('#chat-panel.chat-half').count(); + const hasFull = await page.locator('#chat-panel.chat-full').count(); + const hasFloat = await page.locator('#chat-panel.chat-float').count(); + record('Compact: no size classes', hasHalf === 0 && hasFull === 0 && hasFloat === 0); + + // ====================================================================== + // 3. SIDE PANEL MODE + // ====================================================================== + console.log("\n3️⃣ Side Panel Mode"); + + await page.click('.chat-mode-btn[data-mode="chat-half"]'); + await page.waitForTimeout(400); + + const halfActive = await page.locator('.chat-mode-btn[data-mode="chat-half"].active').count(); + record('Side panel: button is active', halfActive === 1); + + const compactNotActive = await page.locator('.chat-mode-btn[data-mode=""].active').count(); + record('Side panel: compact button not active', compactNotActive === 0); + + const halfClass = await page.locator('#chat-panel.chat-half').count(); + record('Side panel: has chat-half class', halfClass === 1); + + const halfBox = await page.locator('#chat-panel').boundingBox(); + record('Side panel: ~50% viewport width', halfBox && halfBox.width >= 900 && halfBox.width <= 1000, + `width=${halfBox?.width}`); + record('Side panel: full height', halfBox && halfBox.height >= 1050, + `height=${halfBox?.height}`); + record('Side panel: docked to right (x ~960)', halfBox && halfBox.x >= 920 && halfBox.x <= 1000, + `x=${halfBox?.x}`); + record('Side panel: starts at top', halfBox && halfBox.y === 0, + `y=${halfBox?.y}`); + + // Messages area should expand + const halfMsgBox = await page.locator('#chat-panel .chat-messages').boundingBox(); + record('Side panel: messages area expanded', halfMsgBox && halfMsgBox.height > 400, + `height=${halfMsgBox?.height}`); + + // ====================================================================== + // 4. FULL SCREEN MODE + // ====================================================================== + console.log("\n4️⃣ Full Screen Mode"); + + await page.click('.chat-mode-btn[data-mode="chat-full"]'); + await page.waitForTimeout(400); + + const fullActive = await page.locator('.chat-mode-btn[data-mode="chat-full"].active').count(); + record('Full screen: button is active', fullActive === 1); + + const fullClass = await page.locator('#chat-panel.chat-full').count(); + record('Full screen: has chat-full class', fullClass === 1); + + const fullBox = await page.locator('#chat-panel').boundingBox(); + record('Full screen: covers full width', fullBox && fullBox.width >= 1900, + `width=${fullBox?.width}`); + record('Full screen: covers full height', fullBox && fullBox.height >= 1050, + `height=${fullBox?.height}`); + record('Full screen: starts at 0,0', fullBox && fullBox.x === 0 && fullBox.y === 0, + `x=${fullBox?.x} y=${fullBox?.y}`); + + // Messages area should be large + const fullMsgBox = await page.locator('#chat-panel .chat-messages').boundingBox(); + record('Full screen: messages area large', fullMsgBox && fullMsgBox.height > 700, + `height=${fullMsgBox?.height}`); + + // ====================================================================== + // 5. FLOATING MODE + // ====================================================================== + console.log("\n5️⃣ Floating Mode"); + + await page.click('.chat-mode-btn[data-mode="chat-float"]'); + await page.waitForTimeout(400); + + const floatActive = await page.locator('.chat-mode-btn[data-mode="chat-float"].active').count(); + record('Floating: button is active', floatActive === 1); + + const floatClass = await page.locator('#chat-panel.chat-float').count(); + record('Floating: has chat-float class', floatClass === 1); + + const floatBox = await page.locator('#chat-panel').boundingBox(); + record('Floating: ~420px wide', floatBox && floatBox.width >= 400 && floatBox.width <= 440, + `width=${floatBox?.width}`); + record('Floating: has reasonable height', floatBox && floatBox.height >= 200 && floatBox.height <= 800, + `height=${floatBox?.height}`); + + // ====================================================================== + // 6. FLOATING DRAG + // ====================================================================== + console.log("\n6️⃣ Floating Drag"); + + const header = await page.locator('#chat-panel .chat-header').boundingBox(); + const startX = header.x + header.width / 2; + const startY = header.y + header.height / 2; + + // Drag 200px left and 100px up + await page.mouse.move(startX, startY); + await page.mouse.down(); + await page.mouse.move(startX - 200, startY - 100, { steps: 10 }); + await page.mouse.up(); + await page.waitForTimeout(200); + + const draggedBox = await page.locator('#chat-panel').boundingBox(); + const movedX = Math.abs((draggedBox.x + draggedBox.width / 2) - (floatBox.x + floatBox.width / 2)); + const movedY = Math.abs((draggedBox.y + draggedBox.height / 2) - (floatBox.y + floatBox.height / 2)); + record('Drag: panel moved horizontally', movedX > 150, `delta=${Math.round(movedX)}px`); + record('Drag: panel moved vertically', movedY > 60, `delta=${Math.round(movedY)}px`); + + // ====================================================================== + // 7. SWITCH BACK TO COMPACT + // ====================================================================== + console.log("\n7️⃣ Return to Compact"); + + await page.click('.chat-mode-btn[data-mode=""]'); + await page.waitForTimeout(400); + + const backCompact = await page.locator('.chat-mode-btn[data-mode=""].active').count(); + record('Back to compact: button active', backCompact === 1); + + const noFloat = await page.locator('#chat-panel.chat-float').count(); + const noHalf = await page.locator('#chat-panel.chat-half').count(); + const noFull = await page.locator('#chat-panel.chat-full').count(); + record('Back to compact: no size classes', noFloat === 0 && noHalf === 0 && noFull === 0); + + const backBox = await page.locator('#chat-panel').boundingBox(); + record('Back to compact: width restored ~360px', backBox && backBox.width >= 340 && backBox.width <= 380, + `width=${backBox?.width}`); + record('Back to compact: position restored (bottom-right)', backBox && backBox.x > 1400, + `x=${backBox?.x}`); + + // ====================================================================== + // 8. RAPID MODE SWITCHING + // ====================================================================== + console.log("\n8️⃣ Rapid Mode Switching"); + + const modes = ['chat-half', 'chat-full', 'chat-float', '', 'chat-full', 'chat-half', '']; + for (const mode of modes) { + await page.click(`.chat-mode-btn[data-mode="${mode}"]`); + await page.waitForTimeout(200); + } + + // Should end in compact + const finalCompact = await page.locator('.chat-mode-btn[data-mode=""].active').count(); + record('Rapid switching: ends in compact', finalCompact === 1); + + const finalBox = await page.locator('#chat-panel').boundingBox(); + record('Rapid switching: panel is valid size', finalBox && finalBox.width > 100 && finalBox.height > 100, + `${finalBox?.width}x${finalBox?.height}`); + + const finalNoClasses = await page.evaluate(() => { + const p = document.getElementById('chat-panel'); + return !p.classList.contains('chat-half') && !p.classList.contains('chat-full') && !p.classList.contains('chat-float'); + }); + record('Rapid switching: no stale classes', finalNoClasses); + + // ====================================================================== + // 9. USER AVATAR + // ====================================================================== + console.log("\n9️⃣ User Avatar in Messages"); + + // Type a message manually + await page.fill('#chat-input', 'Hello'); + await page.click('.chat-send-btn'); + await page.waitForTimeout(500); + + const userBubble = await page.locator('.chat-row-user').last(); + const hasAvatar = await userBubble.locator('.chat-avatar-user').count(); + record('User message has avatar', hasAvatar === 1); + + const avatarIcon = await userBubble.locator('iconify-icon[icon="mdi:account"]').count(); + record('Avatar has user icon', avatarIcon === 1); + + // ====================================================================== + // 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); +} + +testChatLayoutModes().catch(err => { + console.error('Fatal error:', err); + process.exit(1); +});