const { test, expect } = require('@playwright/test'); const { openClassic, showPlayingFixture } = require('./helpers/classic-fixture'); for (const width of [320, 390, 768, 1024, 1280, 1440]) { test(`empty home is unclipped at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await openClassic(page); const result = await page.evaluate(() => { const stage = document.querySelector('.player-stage').getBoundingClientRect(); const hero = document.querySelector('.hero').getBoundingClientRect(); const chips = [...document.querySelectorAll('.hero .chip')].map(el => el.getBoundingClientRect()); return { stageBottom: stage.bottom, heroBottom: hero.bottom, stageTop: stage.top, heroTop: hero.top, chipsFit: chips.every(r => r.right <= innerWidth && r.left >= 0 && r.bottom <= stage.bottom), overflow: document.querySelector('.main').scrollWidth > document.querySelector('.main').clientWidth + 1 }; }); expect(result.heroTop).toBeGreaterThanOrEqual(result.stageTop); expect(result.heroBottom).toBeLessThanOrEqual(result.stageBottom + 1); expect(result.chipsFit).toBe(true); expect(result.overflow).toBe(false); await expect(page.locator('#bottomNav')).toBeVisible({ visible: width <= 860 }); }); } for (const standalone of [false, true]) { test(`navigation during playback and mini-player return (${standalone ? 'PWA' : 'browser'})`, async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await openClassic(page, { populated: true, standalone }); await showPlayingFixture(page); await expect(page.locator('#back10Btn')).toBeVisible(); await expect(page.locator('#fwd10Btn')).toBeVisible(); await page.locator('#bottomNav [data-view="settings"]').click(); await expect(page.locator('.settings')).toBeVisible(); await expect.poll(() => page.locator('.list-pane').evaluate(el => el.getBoundingClientRect().top)).toBeLessThan(130); await expect(page.locator('#miniBar')).toBeVisible(); const boxes = await page.evaluate(() => ({ mini: document.getElementById('miniBar').getBoundingClientRect().bottom, nav: document.getElementById('bottomNav').getBoundingClientRect().top, })); expect(boxes.mini).toBeLessThanOrEqual(boxes.nav + 1); await page.locator('.body').evaluate(el => { el.scrollTop = el.scrollHeight; }); const last = page.locator('.settings .set-group').last(); await expect(last).toBeInViewport(); expect(await last.evaluate(el => el.getBoundingClientRect().bottom)).toBeLessThanOrEqual(boxes.nav); await page.locator('#miniTitle').click(); await expect.poll(() => page.locator('.body').evaluate(el => el.scrollTop)).toBe(0); expect(await page.evaluate(() => window.scrollY)).toBe(0); await page.locator('#ctlMoreBtn').click(); await expect(page.locator('#modal')).toBeVisible(); }); } test('playlist, appearance settings, rotation, and layout switching preserve data', async ({ page }) => { await page.setViewportSize({ width: 1440, height: 960 }); await openClassic(page, { populated: true }); await expect(page.locator('#homePlGrid')).toContainText('Studio sessions'); await showPlayingFixture(page); expect(await page.locator('.list-pane').evaluate(el => el.getBoundingClientRect().width)).toBeGreaterThanOrEqual(340); expect(await page.locator('.card-info').first().evaluate(el => el.getBoundingClientRect().width)).toBeGreaterThan(120); const original = await page.evaluate(() => JSON.stringify(data.playlists)); for (const theme of ['light', 'contrast', 'dark']) { await page.evaluate(theme => { data.settings.theme = theme; applyAppearance(); }, theme); expect(await page.locator('.hero-title').evaluate(el => getComputedStyle(el).webkitTextFillColor)).not.toBe('rgba(0, 0, 0, 0)'); } for (const layout of ['glass-stage', 'sanctuary-stage', 'classic']) { await page.evaluate(layout => { data.settings.layout = layout; applyAppearance(); }, layout); await expect(page.locator('html')).toHaveAttribute('data-layout', layout); expect(await page.evaluate(() => JSON.stringify(data.playlists))).toBe(original); } await page.setViewportSize({ width: 844, height: 390 }); await page.locator('#bottomNav [data-view="history"]').click(); await expect(page.locator('#listTitle')).toHaveText('History'); await page.setViewportSize({ width: 390, height: 844 }); await page.evaluate(() => { data.settings.fontScale = 'xl'; data.settings.density = 'compact'; applyAppearance(); }); await expect(page.locator('#bottomNav [data-view="settings"]')).toBeInViewport(); expect(await page.evaluate(() => document.querySelector('.main').scrollWidth <= document.querySelector('.main').clientWidth + 1)).toBe(true); }); test('320px with extra-large text keeps navigation and controls reachable', async ({ page }) => { await page.setViewportSize({ width: 320, height: 844 }); await openClassic(page, { populated: true, settings: { fontScale: 'xl' } }); await showPlayingFixture(page); await expect(page.locator('#bottomNav [data-view="settings"]')).toBeInViewport(); expect(await page.locator('#controls').evaluate(el => el.scrollWidth <= el.clientWidth + 1)).toBe(true); expect(await page.locator('.app').evaluate(el => Math.abs(el.getBoundingClientRect().height - innerHeight))).toBeLessThan(2); expect(await page.locator('#bottomNav').evaluate(el => el.scrollWidth <= el.clientWidth + 1)).toBe(true); await page.locator('#bottomNav [data-view="settings"]').click(); await expect(page.locator('.settings')).toBeVisible(); }); test.describe('Classic offline shell', () => { test.use({ serviceWorkers: 'allow' }); test('caches the Classic stylesheet and restores it offline', async ({ page, context }) => { await openClassic(page); await page.evaluate(() => navigator.serviceWorker.ready); await page.reload(); await page.waitForFunction(() => !!navigator.serviceWorker.controller); expect(await page.evaluate(async () => !!(await caches.match('/theme-classic.css')))).toBe(true); await context.setOffline(true); await page.reload(); await expect(page.locator('html')).toHaveAttribute('data-layout', 'classic'); expect(await page.locator('html').evaluate(el => getComputedStyle(el).getPropertyValue('--bg').trim())).toBe('#121315'); }); });