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(); }); } const beforeIcons = execFileSync('git', ['show', '79a3c28:frontend/glass-controls.js'], { encoding: 'utf8' }); async function sidebarFixture(page, layout) { await openClassic(page, { populated: true, settings: { autoPreload: false } }); await page.addStyleTag({ content: '*,*::before,*::after{animation:none!important;transition:none!important;caret-color:transparent!important}' }); await page.evaluate(layout => { data.settings.layout = layout; applyAppearance(); data.playlists[0].videos.forEach(v => cachedIds.add(v.id)); view = { type: 'playlist', id: 'studio' }; render(); openSidebar(); }, 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} playlist sidebar icons 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('**/glass-controls.js', route => route.fulfill({ contentType: 'text/javascript', body: beforeIcons })); await sidebarFixture(before, layout); const baseline = await before.screenshot({ path: `/tmp/ytplayer-done8-B-before-${layout}-${width}.png` }); await sidebarFixture(page, layout); const result = await page.screenshot({ path: `/tmp/ytplayer-done8-B-after-${layout}-${width}.png` }); const badge = page.locator('.sidebar .pl-offline-badge.all').first(); if (layout === 'classic') { expect(differentPixels(baseline, result)).toBeLessThanOrEqual(32); await expect(badge).toHaveText('⬇'); } else { await expect(badge.locator('svg')).toHaveCount(1); expect(await badge.locator('svg').evaluate(el => getComputedStyle(el).strokeWidth)).toBe('1.9px'); await expect(page.locator('.home-pl-play svg').first()).toHaveCount(1); await expect(page.locator('#listActions .la-primary svg')).toHaveCount(1); await page.evaluate(() => { data.settings.layout = 'classic'; applyAppearance(); }); await expect(badge).toHaveText('⬇'); await expect(page.locator('.glass-control-icon')).toHaveCount(0); } await before.close(); }); }