#!/usr/bin/env bun import { chromium } from 'playwright'; (async () => { const browser = await chromium.launch({ headless: false }); console.log('========================================'); console.log(' MOBILE TOOLTIP POSITION TEST'); console.log(' Testing tooltip appears ABOVE buttons'); console.log('========================================\n'); // Test with actual mobile viewport const page = await browser.newPage({ viewport: { width: 375, height: 667 }, hasTouch: true, isMobile: true }); await page.goto('http://localhost:1999/'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); const buttonsToTest = [ { id: 'color-theme-switcher', name: 'Theme Switcher' }, { id: 'info-button', name: 'Info Button' } ]; for (const btn of buttonsToTest) { console.log(`\nšŸ” Testing: ${btn.name}`); console.log('─────────────────────────────'); const button = page.locator(`#${btn.id}`); const exists = await button.count() > 0; if (!exists) { console.log(`āŒ Button NOT FOUND in DOM`); continue; } const isVisible = await button.isVisible(); console.log(`Button visible: ${isVisible ? 'āœ…' : 'āŒ'}`); if (!isVisible) continue; // Get button position const buttonBox = await button.boundingBox(); console.log(`Button position: y=${Math.round(buttonBox.y)}, height=${buttonBox.height}`); // Tap (touch) the button to trigger hover state await button.tap(); await page.waitForTimeout(500); // Get computed tooltip styles const tooltipInfo = await page.evaluate((id) => { const btn = document.getElementById(id); if (!btn) return null; const btnRect = btn.getBoundingClientRect(); const computed = window.getComputedStyle(btn, '::before'); // Parse transform to get actual Y offset const transform = computed.transform; let transformY = 0; if (transform && transform !== 'none') { const matrix = transform.match(/matrix\((.+)\)/); if (matrix) { const values = matrix[1].split(', '); transformY = parseFloat(values[5] || 0); } } return { buttonTop: btnRect.top, buttonBottom: btnRect.bottom, opacity: computed.opacity, visibility: computed.visibility, display: computed.display, bottom: computed.bottom, top: computed.top, left: computed.left, position: computed.position, content: computed.content, transform: computed.transform }; }, btn.id); if (!tooltipInfo) { console.log('āŒ Could not get tooltip info'); continue; } console.log(`\nTooltip CSS:`); console.log(` - Visibility: ${tooltipInfo.visibility}`); console.log(` - Display: ${tooltipInfo.display}`); console.log(` - Opacity: ${tooltipInfo.opacity}`); console.log(` - Position: ${tooltipInfo.position}`); console.log(` - Bottom: ${tooltipInfo.bottom}`); console.log(` - Top: ${tooltipInfo.top}`); console.log(` - Left: ${tooltipInfo.left}`); console.log(` - Transform: ${tooltipInfo.transform}`); // Check if tooltip is positioned ABOVE the button // On mobile with our CSS, bottom should be calc(100% + 8px) which means // the tooltip bottom edge is 8px above the button top const bottomValue = parseFloat(tooltipInfo.bottom); if (tooltipInfo.display === 'none') { console.log('\nāš ļø Tooltip is HIDDEN (display: none) - Expected for touch devices'); } else if (tooltipInfo.bottom && tooltipInfo.bottom.includes('100%')) { console.log('\nāœ… Tooltip positioned ABOVE button (bottom: calc(100% + 8px))'); } else if (tooltipInfo.left && tooltipInfo.left.includes('100%')) { console.log('\nāŒ Tooltip positioned to RIGHT (should be ABOVE on mobile!)'); } else { console.log('\nāš ļø Unclear positioning'); } } // Take screenshot await page.screenshot({ path: '/Users/txeo/Git/yo/cv/tests/mjs/screenshots/mobile-tooltip-positions.png', fullPage: true }); console.log('\nšŸ“ø Screenshot saved\n'); console.log('========================================'); console.log(' TEST COMPLETE'); console.log('========================================\n'); await page.waitForTimeout(3000); await browser.close(); })();