const { test, expect } = require('@playwright/test'); const { readFileSync } = require('node:fs'); const { openClassic, showPlayingFixture } = require('./helpers/classic-fixture'); const before = process.env.CAPTURE_BEFORE === '1'; const phoneCss = readFileSync('frontend/styles.css', 'utf8').replaceAll('(display-mode: standalone)', '(min-width: 0px)').replaceAll('env(safe-area-inset-bottom)', '34px'); async function setup(page, width, layout = 'glass-stage') { await page.setViewportSize({ width, height: 844 }); if (width < 900) await page.route('**/styles.css', r => r.fulfill({ contentType: 'text/css', body: phoneCss })); await openClassic(page, { populated: true, standalone: width < 900, settings: { autoPreload: false } }); await showPlayingFixture(page); 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(); document.getElementById('notesPanel').classList.remove('hidden'); relatedVideos = data.history.slice(1, 4); renderRelated(); document.getElementById('miniTitle').textContent = current.meta.title; document.getElementById('miniChannel').textContent = current.meta.channel; document.getElementById('miniProgressFill').style.width = '25%'; SectionRail.refresh(); }, layout); await page.evaluate(async () => { await document.fonts.ready; await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); }); } // The rail-in-panel test was removed: the Glass rail floats again (see glass-rail-float.spec.js). for (const width of [390, 1440]) test(`Classic panel layout unchanged at ${width}`, async ({ page, context }, info) => { const baseline = await context.newPage(); await baseline.route('**/theme-glass-panel.css', r => r.fulfill({ contentType: 'text/css', body: '' })); await baseline.route('**/glass-panel-layout.js', r => r.fulfill({ contentType: 'text/javascript', body: '' })); await setup(baseline, width, 'classic'); await setup(page, width, 'classic'); const geometry = p => p.locator('#listPane,#playerPane,#sectionRail,#miniBar,.notes-tabs').evaluateAll(nodes => nodes.map(el => { const r = el.getBoundingClientRect(), s = getComputedStyle(el); return [el.id, r.x,r.y,r.width,r.height,s.padding,s.position,s.overflow]; })); expect(await geometry(page)).toEqual(await geometry(baseline)); const a = await baseline.screenshot({ path: `/tmp/done9-classic-before-${info.project.name}-${width}.png` }); const b = await page.screenshot({ path: `/tmp/done9-classic-after-${info.project.name}-${width}.png` }); const { differentPixels } = require('./helpers/png-diff'); expect(differentPixels(a, b)).toBeLessThanOrEqual(32); await baseline.close(); }); for (const width of [390]) test(`iPhone bottom icons centered above labels with badges at ${width}`, async ({ browser }, info) => { const { devices } = require('@playwright/test'); const { execFileSync } = require('node:child_process'); const context = await browser.newContext({ ...devices['iPhone 13'], viewport: { width, height: 844 }, serviceWorkers: 'block', baseURL: 'http://localhost:8096' }); const baseline = await context.newPage(), page = await context.newPage(); const oldCss = execFileSync('git', ['show', 'f2c9459:frontend/theme-glass-controls.css'], { encoding: 'utf8' }); await baseline.route('**/theme-glass-controls.css', r => r.fulfill({ contentType: 'text/css', body: oldCss })); for (const p of [baseline, page]) { await setup(p, width); await p.evaluate(() => { view = { type: 'history' }; render(); for (const [id, count] of [['bnQueueCount', '12'], ['bnDlCount', '7']]) { const b = document.getElementById(id); b.textContent = count; b.classList.remove('hidden'); } }); } const centers = p => p.locator('.bottom-nav-btn').evaluateAll(buttons => buttons.map(b => { const icon = b.querySelector('svg').getBoundingClientRect(), box = b.querySelector('.bottom-nav-icon').getBoundingClientRect(), label = b.querySelector('.bottom-nav-label').getBoundingClientRect(); const s = getComputedStyle(b.querySelector('.bottom-nav-icon')), badge = b.querySelector('.bottom-nav-badge'), r = badge?.getBoundingClientRect(); return { icon: icon.x + icon.width / 2, box: box.x + box.width / 2, label: label.x + label.width / 2, iconBottom: icon.bottom, labelTop: label.top, padding: s.padding, buttonWidth: b.getBoundingClientRect().width, badge: r?.height ? { left: r.left, top: r.top } : null, iconRight: icon.right, iconTop: icon.top }; })); // Reproduce the uploaded pre-Task-A build's shift before asserting the corrected build. expect((await centers(baseline)).every(x => x.icon - x.label > 8)).toBe(true); for (const x of await centers(page)) { expect(Math.abs(x.icon - x.label)).toBeLessThan(0.6); expect(Math.abs(x.box - x.label)).toBeLessThan(0.6); expect(x.padding).toBe('0px'); expect(x.buttonWidth).toBeGreaterThanOrEqual(44); expect(x.iconBottom).toBeLessThan(x.labelTop); if (x.badge) { expect(Math.abs(x.badge.left - x.iconRight)).toBeLessThanOrEqual(4); expect(x.badge.top).toBeLessThan(x.iconTop); } } await expect(page.locator('.bottom-nav-btn[data-view="history"]')).toHaveClass(/active/); await baseline.locator('#bottomNav').screenshot({ path: `/tmp/done9-nav-before-${info.project.name}-390.png` }); await page.locator('#bottomNav').screenshot({ path: `/tmp/done9-nav-after-${info.project.name}-390.png` }); await context.close(); }); for (const width of [320, 360, 390, 430, 1440]) test(`Now Playing toolbar remains one row at ${width}`, async ({ page }, info) => { await setup(page, width); await page.evaluate(() => { document.getElementById('notesStatus').textContent = 'Verse ยท shared lyrics'; document.getElementById('markersCount').textContent = '12'; }); await page.locator('#notesPanel').scrollIntoViewIfNeeded(); if (!before) { const metrics = await page.locator('.notes-tabs').evaluate(bar => { const b = bar.getBoundingClientRect(), buttons = [...bar.querySelectorAll('button')].map(el => { const r = el.getBoundingClientRect(); return { top: r.top, bottom: r.bottom, left: r.left, right: r.right, width: r.width, height: r.height, label: el.getAttribute('aria-label') || el.getAttribute('title') }; }); return { bar: { left: b.left, right: b.right }, buttons, overflow: bar.scrollWidth - bar.clientWidth }; }); expect(Math.max(...metrics.buttons.map(b => b.top)) - Math.min(...metrics.buttons.map(b => b.top))).toBeLessThan(1); expect(metrics.overflow).toBeLessThanOrEqual(1); for (const b of metrics.buttons) { expect(b.width).toBeGreaterThanOrEqual(44); expect(b.height).toBeGreaterThanOrEqual(44); expect(b.left).toBeGreaterThanOrEqual(metrics.bar.left); expect(b.right).toBeLessThanOrEqual(metrics.bar.right); expect(b.label).toBeTruthy(); } expect(metrics.buttons.at(-1).right).toBeGreaterThan(metrics.bar.right - 12); } await page.locator('#notesPanel .notes-tabs').screenshot({ path: `/tmp/done9-toolbar-${before ? 'before' : 'after'}-${info.project.name}-${width}.png` }); }); test('compact toolbar labels and accessible names restore on leaving Glass Stage', async ({ page }) => { await setup(page, 390, 'classic'); const original = await page.locator('.notes-tab').evaluateAll(nodes => nodes.map(n => ({ html: n.innerHTML, label: n.getAttribute('aria-label'), title: n.getAttribute('title') }))); for (let i = 0; i < 2; i++) { await page.evaluate(() => { data.settings.layout = 'glass-stage'; applyAppearance(); }); await expect(page.locator('.glass-tab-label')).toHaveCount(3); for (const name of ['Lyrics', 'Chapters & bookmarks', 'Search']) await expect(page.getByRole('tab', { name, exact: true })).toHaveCount(1); await page.evaluate(() => { data.settings.layout = 'classic'; applyAppearance(); }); await expect(page.locator('.glass-tab-label')).toHaveCount(0); expect(await page.locator('.notes-tab').evaluateAll(nodes => nodes.map(n => ({ html: n.innerHTML, label: n.getAttribute('aria-label'), title: n.getAttribute('title') })))).toEqual(original); await expect(page.locator('#sectionRail')).not.toHaveJSProperty('parentElement', null); expect(await page.locator('#sectionRail').evaluate(n => n.parentElement.tagName)).toBe('BODY'); } });