/** * Portrait PWA layout tests. * * Verifies that the Settings panel (and other list-pane views) are fully * accessible in portrait orientation when the app is running as a PWA * (standalone display mode). * * Because Playwright cannot set display-mode:standalone natively, we inject * a small CSS override that activates the portrait media query rule-set via * a data attribute, and also add the .portrait-pwa class that the JS watcher * would normally add. This exercises the same CSS paths that fire in the real * installed PWA. */ const { test, expect } = require('@playwright/test'); // Portrait dimensions (iPhone 12 / similar) const PORTRAIT_WIDTH = 390; const PORTRAIT_HEIGHT = 844; const LANDSCAPE_WIDTH = 844; const LANDSCAPE_HEIGHT = 390; /** * Inject an override so the portrait media-query CSS fires without needing * an actual installed PWA. We piggyback on a custom data attribute. */ async function enablePortraitPwaMode(page) { await page.addStyleTag({ content: ` /* Mirror (display-mode:standalone) and (orientation:portrait) rules for testing */ [data-portrait-pwa-test] .app { grid-template-columns: 1fr; } [data-portrait-pwa-test] .sidebar-toggle { display: inline-flex; } [data-portrait-pwa-test] .sidebar { position: fixed; top:0; left:0; bottom:0; width: min(86vw, 300px); max-width:300px; z-index:100; border-right:1px solid var(--line-soft); box-shadow:var(--shadow); transform:translateX(-100%); transition:transform 0.28s var(--ease); } [data-portrait-pwa-test] .app.sidebar-open .sidebar { transform:translateX(0); } [data-portrait-pwa-test] .app.sidebar-open .sidebar-backdrop { display:block; opacity:1; } [data-portrait-pwa-test] .topbar { position:sticky; top:0; z-index:50; } [data-portrait-pwa-test] .body { flex-direction:column; overflow:hidden; } [data-portrait-pwa-test] .player-pane { flex:none; width:100%; overflow-y:visible; } [data-portrait-pwa-test] .player-stage { width:100%; aspect-ratio:16/9; } [data-portrait-pwa-test] .btn-row { flex-wrap:wrap; gap:7px; } [data-portrait-pwa-test] .now-meta { flex-direction:column; gap:12px; margin-top:14px; } [data-portrait-pwa-test] .np-actions { flex-wrap:wrap; gap:6px; } [data-portrait-pwa-test] .list-pane { flex:1; min-height:0; width:100%; border-left:none; border-top:1px solid var(--line-soft); overflow-y:auto; overflow-x:hidden; padding-bottom: 60px; } [data-portrait-pwa-test] .cards { max-height:none; overflow-y:visible; padding-bottom:0; } [data-portrait-pwa-test] .settings { padding-bottom:20px; } `, }); await page.evaluate(() => { document.documentElement.setAttribute('data-portrait-pwa-test', '1'); // Also add the JS class that setupPortraitPwaWatcher would add const app = document.querySelector('.app'); if (app) app.classList.add('portrait-pwa'); }); } test.describe('Portrait PWA layout', () => { test.beforeEach(async ({ page }) => { await page.setViewportSize({ width: PORTRAIT_WIDTH, height: PORTRAIT_HEIGHT }); await page.goto('/'); // Wait for app JS to initialise await page.waitForSelector('.app', { state: 'attached' }); await enablePortraitPwaMode(page); }); test('app starts in portrait without horizontal scroll', async ({ page }) => { const bodyScrollWidth = await page.evaluate(() => document.body.scrollWidth); const viewportWidth = await page.evaluate(() => window.innerWidth); expect(bodyScrollWidth).toBeLessThanOrEqual(viewportWidth + 2); // allow 2px rounding }); test('Settings panel is accessible in portrait', async ({ page }) => { // Open the sidebar drawer (hamburger) await page.click('#sidebarToggle'); await page.waitForTimeout(350); // drawer animation // Navigate to Settings await page.click('[data-view="settings"]'); await page.waitForTimeout(300); // Settings content must exist inside the list pane const settingsEl = await page.locator('.settings'); await expect(settingsEl).toBeVisible(); // The settings element must not be clipped — its bounding box bottom // should be reachable (list-pane scrolls to accommodate it) const listPane = page.locator('.list-pane'); await expect(listPane).toBeVisible(); const listPaneBox = await listPane.boundingBox(); expect(listPaneBox).not.toBeNull(); expect(listPaneBox.height).toBeGreaterThan(50); // must have meaningful height }); test('Settings panel scrolls to bottom without overlap', async ({ page }) => { await page.click('#sidebarToggle'); await page.waitForTimeout(350); await page.click('[data-view="settings"]'); await page.waitForTimeout(300); // Scroll list-pane to the bottom await page.evaluate(() => { const lp = document.querySelector('.list-pane'); if (lp) lp.scrollTop = lp.scrollHeight; }); await page.waitForTimeout(100); // After scrolling, the last setting group should be visible const lastGroup = page.locator('.set-group').last(); await expect(lastGroup).toBeVisible(); }); test('switching landscape to portrait restores layout', async ({ page }) => { // Start in landscape await page.setViewportSize({ width: LANDSCAPE_WIDTH, height: LANDSCAPE_HEIGHT }); await page.waitForTimeout(200); // Switch back to portrait await page.setViewportSize({ width: PORTRAIT_WIDTH, height: PORTRAIT_HEIGHT }); await page.waitForTimeout(300); // App should not have horizontal overflow const bodyScrollWidth = await page.evaluate(() => document.body.scrollWidth); const viewportWidth = await page.evaluate(() => window.innerWidth); expect(bodyScrollWidth).toBeLessThanOrEqual(viewportWidth + 2); // List pane should be visible and have height const listPane = page.locator('.list-pane'); await expect(listPane).toBeVisible(); const box = await listPane.boundingBox(); expect(box.height).toBeGreaterThan(50); }); test('landscape mode is unaffected — sidebar column visible at wide viewport', async ({ page }) => { // Remove the portrait-pwa test overrides await page.evaluate(() => { document.documentElement.removeAttribute('data-portrait-pwa-test'); const app = document.querySelector('.app'); if (app) app.classList.remove('portrait-pwa'); }); // Switch to a wide desktop viewport await page.setViewportSize({ width: 1280, height: 800 }); await page.waitForTimeout(200); // In landscape desktop, the sidebar should be visible (grid column) const sidebar = page.locator('.sidebar'); await expect(sidebar).toBeVisible(); // list-pane should have a fixed width (452px desktop) const listPaneBox = await page.locator('.list-pane').boundingBox(); expect(listPaneBox.width).toBeGreaterThan(200); }); });