diff --git a/docs/now-playing-return-regression.md b/docs/now-playing-return-regression.md new file mode 100644 index 0000000..521dbe5 --- /dev/null +++ b/docs/now-playing-return-regression.md @@ -0,0 +1,36 @@ +# Now Playing list return regression + +The shared browsing list retains the last page independently of the live playback +queue. Search home hides `#listPane`; the mini-player previously only removed +`home-over` and scrolled. It did not reset that hidden flag or replace the previous +page with the playing playlist. At 390 px the original code fails in both Classic +and Glass Stage with `#listPane` still hidden after tapping the mini-player. +The separate `#upnext` card remained intact in this reproduction. + +Returning now rebuilds the source playlist and Up next. If its source playlist +was removed (or playback came from search/channel/history), the list displays the +live queue, without replacing it with the unrelated editable saved queue. +Mini-player, close, Player jump, user-picked track/deep-link reveal and browser +history returns share the restoration. Auto-advance still skips the reveal hook. +History entries retain browsing views, and Settings section entries remain owned +by Settings. Repeated restoration does not add duplicate player history entries. + +Run `npx playwright test --config playwright.now-playing.config.js`. +The 20 cases cover both themes at 390/1440 px, return controls, Back/Forward, +missing source playlists, stale Up next DOM, repeated returns and an exhausted +queue. Synthetic tracks avoid dependence on YouTube decoding/network; the +track-reveal case exercises the shared post-load hook, rather than real media +loading. Headless Chromium exposes the mini-player via test CSS because it is +not an installed phone PWA. The Player jump is tested only at phone width, +where that navigation exists. + +Validation: 115 frontend unit tests, app syntax check, server build and 20 browser +cases pass. Replaying the 390 px mini-player tests with the original app script +fails in both themes at the hidden list assertion; the patched script passes. +No server implementation or tests changed. + +On a real iPhone, confirm the same browse/return sequence in Safari and the +installed PWA, plus Back/Forward and an actual shared video deep link. Confirm +playback keeps its position and the page scrolls to the player without shifting +the document viewport. Desktop checks cover both themes; no playback/background, +playlist action layout or device-export implementation was changed. diff --git a/playwright.now-playing.config.js b/playwright.now-playing.config.js new file mode 100644 index 0000000..7e0154e --- /dev/null +++ b/playwright.now-playing.config.js @@ -0,0 +1,2 @@ +const { defineConfig } = require('@playwright/test'); +module.exports = defineConfig({ ...require('./playwright.classic.config'), testMatch: /now-playing-return\.spec\.js/ }); diff --git a/tests/now-playing-return.spec.js b/tests/now-playing-return.spec.js new file mode 100644 index 0000000..fd7cd7b --- /dev/null +++ b/tests/now-playing-return.spec.js @@ -0,0 +1,75 @@ +const { test, expect } = require('@playwright/test'); +const { openClassic, showPlayingFixture } = require('./helpers/classic-fixture'); + +for (const layout of ['classic', 'glass-stage']) { + for (const width of [390, 1440]) { + for (const route of ['mini player', 'close', 'player jump', 'browser back', 'track reveal'].filter(route => width === 390 || route !== 'player jump')) { + test(`${layout} ${width}: ${route} restores the playing list`, async ({ page }) => { + await page.setViewportSize({ width, height: 844 }); + await openClassic(page, { populated: true, settings: { autoPreload: false } }); + await showPlayingFixture(page); + await page.evaluate(layout => { + document.documentElement.dataset.layout = layout; + for (const type of ['search', 'history', 'saved', 'downloads', 'channel', 'settings', 'search']) { + view = { type }; + if (type === 'search') setHomeOverPlayer(true); + render(); + } + showMiniBar(); + }, layout); + // Chromium is not an installed phone PWA. Expose its mini-player + // chrome while exercising the actual click listeners (no forced click). + await page.addStyleTag({ content: '#miniBar { display:flex !important; }' }); + if (route === 'close') await page.locator('#miniCloseBtn').click(); + else if (route === 'track reveal') { + // User-picked tracks/deep links share this reveal hook after loading; + // avoid testing network/media decoding with synthetic fixture ids. + await page.evaluate(() => scrollPlayerIntoViewPortrait()); + } else { + await page.locator('#miniTitle').click(); + if (route === 'player jump') { + await page.evaluate(() => { view = { type: 'history' }; render(); }); + await page.locator('#sectionRail [data-sec="player"]').click(); + } else if (route === 'browser back') { + await page.evaluate(() => { view = { type: 'history' }; render(); }); + await page.goBack(); + await expect(page.locator('#listTitle')).toHaveText('Studio sessions'); + await page.goBack(); + await expect(page.locator('#playerPane')).toHaveClass(/home-over/); + await page.goForward(); + } + } + await expect(page.locator('#upnext')).toBeVisible(); + await expect(page.locator('#upnextList .upnext-item')).toHaveCount(11); + await expect(page.locator('#listPane')).toBeVisible(); + await expect(page.locator('#listTitle')).toHaveText('Studio sessions'); + await expect(page.locator('#cards .card')).toHaveCount(12); + expect(await page.evaluate(() => ({ source: queueSource, index: queueIndex, size: queue.length, id: current.meta.id }))) + .toEqual({ source: 'playlist:studio', index: 0, size: 12, id: 'preview0000' }); + }); + } + } + test(`${layout}: missing source playlist falls back to the live queue`, async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await openClassic(page, { populated: true, settings: { autoPreload: false } }); + await showPlayingFixture(page); + await page.evaluate(layout => { + document.documentElement.dataset.layout = layout; + data.playlists = []; + data.queue = []; + view = { type: 'downloads' }; render(); + scrollPlayerIntoViewPortrait(); + }, layout); + await expect(page.locator('#listTitle')).toHaveText('Now playing'); + await expect(page.locator('#cards .card')).toHaveCount(12); + await expect(page.locator('#upnextList .upnext-item')).toHaveCount(11); + const historyLength = await page.evaluate(() => history.length); + // Re-entry must repair stale DOM even when home-over is already false. + await page.evaluate(() => { document.getElementById('upnextList').innerHTML = ''; scrollPlayerIntoViewPortrait(); }); + await expect(page.locator('#upnextList .upnext-item')).toHaveCount(11); + expect(await page.evaluate(() => history.length)).toBe(historyLength); + await page.evaluate(() => { queueIndex = queue.length - 1; scrollPlayerIntoViewPortrait(); }); + await expect(page.locator('#upnext')).toBeHidden(); + await expect(page.locator('#cards .card')).toHaveCount(12); + }); +}