Files
cv-site/tests/mjs/74-button-icon-fluid-sizing.test.mjs
juanatsap 976b8ae2e2 fix: Scale floating button icons proportionally on mobile viewports
Remove hardcoded width/height HTML attributes from iconify-icon elements
that were overriding CSS sizing. The iconify-icon component uses HTML
attributes for SVG rendering, ignoring CSS width/height.

- Remove width="28" height="28" from 8 button templates
- Remove conflicting 768px media query from _buttons.css
- Add default desktop icon sizes (24px) in _scroll-behavior.css
- Icons now scale via clamp() from 18px (380px) to 24px (900px)
2025-12-01 12:31:31 +00:00

216 lines
7.7 KiB
JavaScript

#!/usr/bin/env node
/**
* Test: Button and Icon Fluid Sizing
*
* Verifies that on mobile view (max-width: 900px):
* - Buttons scale fluidly from 36px (at 380px viewport) to 50px (at 900px viewport)
* - Icons scale proportionally from 18px to 24px
* - Icons properly override HTML width/height attributes
*/
import { chromium } from 'playwright';
const TEST_URL = 'http://localhost:1999';
// Use height > width to ensure portrait orientation (avoid landscape breakpoint)
const VIEWPORT_HEIGHT = 1000;
// Expected sizes at different viewport widths based on CSS formulas:
// Buttons: clamp(36px, calc(2.7vw + 25.7px), 50px)
// Icons: clamp(18px, calc(1.15vw + 13.6px), 24px)
function expectedSizes(viewportWidth) {
const btnCalc = 0.027 * viewportWidth + 25.7;
const iconCalc = 0.0115 * viewportWidth + 13.6;
return {
button: Math.min(50, Math.max(36, btnCalc)),
icon: Math.min(24, Math.max(18, iconCalc))
};
}
async function testButtonIconFluidSizing() {
console.log('🧪 Testing Button and Icon Fluid Sizing');
console.log('='.repeat(70));
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: { width: 900, height: VIEWPORT_HEIGHT },
deviceScaleFactor: 1,
});
const page = await context.newPage();
// Disable cache
await page.route('**/*', (route) => {
route.continue({
headers: {
...route.request().headers(),
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
});
try {
await page.goto(TEST_URL, { waitUntil: 'networkidle' });
console.log(`✅ Navigated to ${TEST_URL}`);
await page.waitForTimeout(500);
const viewportWidths = [900, 700, 500, 380];
const buttons = [
{ selector: '.cmd-k-btn', name: 'CmdK' },
{ selector: '.download-btn', name: 'Download' },
{ selector: '.print-friendly-btn', name: 'Print' },
{ selector: '.fixed-btn.contact-btn', name: 'Contact' },
{ selector: '.shortcuts-btn', name: 'Shortcuts' },
{ selector: '.color-theme-switcher', name: 'Theme' },
{ selector: '.info-button', name: 'Info' },
{ selector: '.back-to-top', name: 'BackToTop' },
];
let allTestsPassed = true;
const tolerance = 4; // Allow 4px tolerance for rounding
for (const width of viewportWidths) {
await page.setViewportSize({ width, height: VIEWPORT_HEIGHT });
await page.waitForTimeout(300);
const expected = expectedSizes(width);
console.log(`\n📏 Viewport: ${width}px (expected button: ~${Math.round(expected.button)}px, icon: ~${Math.round(expected.icon)}px)`);
console.log('-'.repeat(70));
for (const btn of buttons) {
try {
const buttonElement = await page.$(btn.selector);
if (!buttonElement) {
// Some buttons may be hidden on certain viewports
continue;
}
const isVisible = await buttonElement.isVisible();
if (!isVisible) {
continue;
}
// Get button bounding box
const btnBox = await buttonElement.boundingBox();
if (!btnBox) continue;
const btnWidth = Math.round(btnBox.width);
const btnHeight = Math.round(btnBox.height);
// Get icon inside button
const iconElement = await buttonElement.$('iconify-icon');
let iconWidth = 0;
let iconHeight = 0;
if (iconElement) {
const iconBox = await iconElement.boundingBox();
if (iconBox) {
iconWidth = Math.round(iconBox.width);
iconHeight = Math.round(iconBox.height);
}
}
// Check if sizes are within tolerance of expected
const btnOk = Math.abs(btnWidth - expected.button) <= tolerance;
const iconOk = iconWidth === 0 || Math.abs(iconWidth - expected.icon) <= tolerance;
const status = btnOk && iconOk ? '✅' : '❌';
if (btnOk && iconOk) {
console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight}, icon=${iconWidth}x${iconHeight}`);
} else {
console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight} (expected ~${Math.round(expected.button)}), icon=${iconWidth}x${iconHeight} (expected ~${Math.round(expected.icon)})`);
allTestsPassed = false;
}
} catch (error) {
// Button may not exist
}
}
}
// Test landscape orientation (width > height)
console.log('\n\n📐 LANDSCAPE ORIENTATION TESTS');
console.log('='.repeat(70));
// Landscape viewports: width > height triggers landscape media query
// Note: At 400x400, width == height so CSS treats it as portrait, not landscape
const landscapeViewports = [
{ width: 800, height: 400 }, // Clear landscape
{ width: 600, height: 350 }, // Clear landscape
{ width: 500, height: 300 }, // Clear landscape
];
// Landscape formula: clamp(32px, calc(2.2vw + 19.6px), 40px) for buttons
// clamp(16px, calc(1.1vw + 9.8px), 20px) for icons
function expectedLandscapeSizes(viewportWidth) {
const btnCalc = 0.022 * viewportWidth + 19.6;
const iconCalc = 0.011 * viewportWidth + 9.8;
return {
button: Math.min(40, Math.max(32, btnCalc)),
icon: Math.min(20, Math.max(16, iconCalc))
};
}
for (const viewport of landscapeViewports) {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.waitForTimeout(300);
const expected = expectedLandscapeSizes(viewport.width);
console.log(`\n📏 Landscape ${viewport.width}x${viewport.height} (expected button: ~${Math.round(expected.button)}px, icon: ~${Math.round(expected.icon)}px)`);
console.log('-'.repeat(70));
for (const btn of buttons) {
try {
const buttonElement = await page.$(btn.selector);
if (!buttonElement) continue;
const isVisible = await buttonElement.isVisible();
if (!isVisible) continue;
const btnBox = await buttonElement.boundingBox();
if (!btnBox) continue;
const btnWidth = Math.round(btnBox.width);
const btnHeight = Math.round(btnBox.height);
const iconElement = await buttonElement.$('iconify-icon');
let iconWidth = 0;
if (iconElement) {
const iconBox = await iconElement.boundingBox();
if (iconBox) iconWidth = Math.round(iconBox.width);
}
const btnOk = Math.abs(btnWidth - expected.button) <= tolerance;
const iconOk = iconWidth === 0 || Math.abs(iconWidth - expected.icon) <= tolerance;
const status = btnOk && iconOk ? '✅' : '❌';
if (btnOk && iconOk) {
console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight}, icon=${iconWidth}`);
} else {
console.log(`${status} ${btn.name}: button=${btnWidth}x${btnHeight} (expected ~${Math.round(expected.button)}), icon=${iconWidth} (expected ~${Math.round(expected.icon)})`);
allTestsPassed = false;
}
} catch (error) {}
}
}
console.log('\n' + '='.repeat(70));
if (allTestsPassed) {
console.log('\n✅ ALL TESTS PASSED - Button and icon fluid sizing working correctly!');
console.log(' • Portrait: Buttons 36-50px, Icons 18-24px');
console.log(' • Landscape: Buttons 32-40px, Icons 16-20px');
} else {
console.log('\n❌ SOME TESTS FAILED - Check output above');
}
await browser.close();
process.exit(allTestsPassed ? 0 : 1);
} catch (error) {
console.error('\n❌ Test error:', error);
await browser.close();
process.exit(1);
}
}
testButtonIconFluidSizing();