fix(playlist): mount the saved engine for recognized M3U movies

WebPlayerViewComponent resolves the engine from its own asynchronous
settings read and falls back to Video.js until it lands. The engine is part
of the application token, so anyone whose saved player is not Video.js got a
first application on the wrong engine and a swap a tick later. The M3U live
layout never showed it because it passes an explicit playerOverride; the
detail path goes through the shared inline player, which had no way to.

PortalInlinePlayerComponent now accepts an optional playerOverride (null
keeps the portals' behaviour) and the M3U detail chain forwards the value the
live layout already uses.

The E2E guard is a MutationObserver over engine mounts, not a polling
locator: both engines leave the same final DOM, so only the record of what
was ever attached can see the defect. Verified to fail without the fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-13 10:47:43 +02:00
1 parent d0d29567be
commit 5b4079726e
12 files changed
+67 -137

No files matched your search

+33
View File
@@ -145,12 +145,45 @@ test('@web @m3u @tmdb recognized movies open the VOD detail view', async ({
await enableTmdb(page);
await importPlaylist(page);
// Record every engine that is ever attached. A polling assertion cannot
// see the defect this guards: mounting Video.js first and correcting to
// the saved engine a tick later leaves the same final DOM.
await page.evaluate(() => {
const seen = new Set<string>();
(window as unknown as { __enginesSeen: Set<string> }).__enginesSeen =
seen;
const record = () => {
for (const selector of [
'app-vjs-player',
'app-html-video-player',
]) {
if (document.querySelector(selector)) {
seen.add(selector);
}
}
};
record();
new MutationObserver(record).observe(document.body, {
childList: true,
subtree: true,
});
});
// Watch-first: activating the entry plays immediately inside the detail
// shell, with no EPG zone in sight.
await sidebarEntry(page, 'Dune (2021) 1080p').click();
await expect(detail(page)).toBeVisible();
await expect(inlineVideo(page)).toBeVisible();
await expect(page.locator('app-epg-timeline')).toHaveCount(0);
// The engine is part of the application token, so the saved player must
// mount FIRST TIME — a late correction swaps the player mid-playback.
await expect(detail(page).locator('app-html-video-player')).toHaveCount(1);
expect(
await page.evaluate(() => [
...(window as unknown as { __enginesSeen: Set<string> })
.__enginesSeen,
])
).toEqual(['app-html-video-player']);
// Metadata patches the mounted view asynchronously. The shell stamps the
// host templates into BOTH the hero and the watch-state About block, so