diff --git a/frontend/theme-glass-controls.css b/frontend/theme-glass-controls.css index 7f0f788..a84f22b 100644 --- a/frontend/theme-glass-controls.css +++ b/frontend/theme-glass-controls.css @@ -6,8 +6,9 @@ html[data-layout="glass-stage"] :is(#listPane button,.settings-section-row,.note html[data-layout="glass-stage"] :is(.bottom-nav-btn,.nav-item,.playlist-item) { min-height:44px; color:var(--text-2); } html[data-layout="glass-stage"] .bottom-nav { gap:2px; padding-inline:4px; } html[data-layout="glass-stage"] .bottom-nav-btn { border:1px solid transparent; border-radius:18px; gap:6px; min-width:44px; background:transparent; } -html[data-layout="glass-stage"] .bottom-nav-icon { display:grid; place-items:center; width:24px; height:24px; background:none; box-shadow:none; transform:none; } +html[data-layout="glass-stage"] .bottom-nav-icon { display:grid; place-items:center; width:24px; height:24px; padding:0; background:none; box-shadow:none; transform:none; } html[data-layout="glass-stage"] .bottom-nav-label { font-size:11px; } +html[data-layout="glass-stage"] .bottom-nav-badge { top:6px; left:calc(50% + 8px); margin-left:0; } html[data-layout="glass-stage"] .bottom-nav-btn.active { color:var(--accent-bright); background:var(--tile-hi); border-color:var(--glass-line); } html[data-layout="glass-stage"] .bottom-nav-btn.active .bottom-nav-icon { background:none; box-shadow:none; transform:none; } html[data-layout="glass-stage"] :is(.bottom-nav-badge,.nav-badge,.pl-count) { background:var(--tile-hi); border:1px solid var(--glass-line); color:var(--text-2); border-radius:99px; font-size:11px; min-width:18px; font-variant-numeric:tabular-nums; } diff --git a/playwright.glass-navigation.config.js b/playwright.glass-navigation.config.js new file mode 100644 index 0000000..a3c5da7 --- /dev/null +++ b/playwright.glass-navigation.config.js @@ -0,0 +1,2 @@ +const { defineConfig } = require('@playwright/test'); +module.exports = defineConfig({ ...require('./playwright.classic.config'), testMatch: /glass-navigation\.spec\.js/ }); diff --git a/tests/glass-navigation.spec.js b/tests/glass-navigation.spec.js new file mode 100644 index 0000000..1f97f88 --- /dev/null +++ b/tests/glass-navigation.spec.js @@ -0,0 +1,46 @@ +const { test, expect } = require('@playwright/test'); +const { execFileSync } = require('node:child_process'); +const { openClassic } = require('./helpers/classic-fixture'); +const { differentPixels } = require('./helpers/png-diff'); +const { readFileSync } = require('node:fs'); +const phoneCss = readFileSync('frontend/styles.css', 'utf8').replaceAll('(display-mode: standalone)', '(min-width: 0px)'); +const beforeCss = execFileSync('git', ['show', '5e78374:frontend/theme-glass-controls.css'], { encoding: 'utf8' }); +async function setup(page, layout) { + const phone = page.viewportSize().width === 390; + if (phone) await page.route('**/styles.css', route => route.fulfill({ contentType: 'text/css', body: phoneCss })); + await openClassic(page, { populated: true, standalone: phone, settings: { autoPreload: false } }); + await page.addStyleTag({ content: '*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}#bottomNav{display:flex!important}' }); + await page.evaluate(layout => { data.settings.layout = layout; applyAppearance(); view = { type: 'queue' }; render(); }, layout); + await page.evaluate(async () => { await document.fonts.ready; await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); }); +} +for (const layout of ['glass-stage', 'classic']) for (const width of [390, 1440]) { + test(`${layout} bottom tabs at ${width}`, async ({ page, context }) => { + await page.setViewportSize({ width, height: 900 }); + const before = await context.newPage(); await before.setViewportSize({ width, height: 900 }); + await before.route('**/theme-glass-controls.css', route => route.fulfill({ contentType: 'text/css', body: beforeCss })); + await setup(before, layout); + const baseline = await before.screenshot({ path: `/tmp/ytplayer-done8-A-before-${layout}-${width}.png` }); + await setup(page, layout); + const result = await page.screenshot({ path: `/tmp/ytplayer-done8-A-after-${layout}-${width}.png` }); + if (layout === 'classic') { + const geometry = p => p.locator('.bottom-nav-btn,.bottom-nav-icon,.bottom-nav-label').evaluateAll(nodes => nodes.map(el => { const r = el.getBoundingClientRect(), s = getComputedStyle(el); return [r.x,r.y,r.width,r.height,s.padding,s.color,s.display]; })); + expect(await geometry(page)).toEqual(await geometry(before)); + expect(differentPixels(baseline, result)).toBeLessThanOrEqual(32); + } + else { + const positions = await page.locator('.bottom-nav-btn').evaluateAll(buttons => buttons.map(button => { + const icon = button.querySelector('svg').getBoundingClientRect(), label = button.querySelector('.bottom-nav-label').getBoundingClientRect(); + return { offset: Math.abs(icon.x + icon.width / 2 - label.x - label.width / 2), above: icon.bottom <= label.top, target: button.getBoundingClientRect().height }; + })); + expect(positions.every(p => p.offset <= 1 && p.above && p.target >= 44)).toBe(true); + const badge = await page.locator('#bnQueueCount').evaluate(el => { + const icon = el.parentElement.querySelector('svg').getBoundingClientRect(), r = el.getBoundingClientRect(); + return { x: r.left - icon.right, y: r.top - icon.top }; + }); + expect(Math.abs(badge.x)).toBeLessThanOrEqual(5); + expect(Math.abs(badge.y)).toBeLessThanOrEqual(5); + await expect(page.locator('.bottom-nav-btn.active')).toHaveAttribute('aria-label', 'Queue'); + } + await before.close(); + }); +}