823030dcf2
- 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
260 lines
11 KiB
JavaScript
260 lines
11 KiB
JavaScript
#!/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);
|
|
});
|