feat(collections): open a Stalker live favorite inside its portal (#1639)

Live channels in Favorites and Recently viewed now offer "Open in <playlist>"
for Stalker portals, the counterpart of the VOD "View in portal" action. The
chip in the programme panel and the channel's context menu jump to the channel
inside its portal's Live TV, with its genre selected and the channel playing.
Radio stays hidden: it is a separate legacy-paged section with no
open-on-arrival contract.

The handoff refuses to guess. The store records which genre the rendered ITV
list belongs to, so the deferred play waits for a list that can actually serve
the channel instead of inferring it from array identity. An id claimed by one
channel as its `id` and another as its `stream_id` is ambiguous, so neither is
played and the user still lands in the right genre. The handoff is abandoned
when the user changes genre, search, portal or section first.

Two pre-existing defects surfaced during review and are fixed here:

- A blank provider id shadowed a valid one. `a ?? b` keeps an empty string, so
  a channel with a blank `stream_id` was stored with no identity at all and
  could not be selected, played or found again. Six writers had that shape and
  three private copies of the skip-the-blank rule; all now go through one
  `firstNonBlankStalkerId` helper.
- Route-session readiness could publish before the store held the portal's
  row. The constructor starts one sync and the first navigation starts another,
  and the second skipped the bootstrap because the playlist id was claimed
  before the awaits that install it. Arrivals are now serialized, the id is
  claimed only after the store write resolves, and only the newest sync
  publishes readiness.

Both fixes carry regression tests that fail on the old behavior.
This commit is contained in:
4gray authored and GitHub committed 2026-09-20 22:55:29 +02:00
1 parent 6578e4073c
commit f80a21beef
36 files changed
+2211 -121

No files matched your search

+9 -26
View File
@@ -6,6 +6,8 @@ import {
verifyUncachedStalkerSearch,
} from './stalker-category-search.fixture';
import { verifyStalkerSeasonMarkers } from './stalker-season-markers.fixture';
import { verifyStalkerOpenInPlaylist } from './stalker-open-in-playlist.fixture';
import { playFirstItvChannel } from './stalker-itv-playback.fixture';
import { expect, test } from './fixtures';
import {
getRegisteredProviderUrl,
@@ -467,6 +469,13 @@ test('@stalker PWA hides EPG for ITV channel', async ({ page }) => {
expect(shortEpgRequests).toHaveLength(0);
});
test('@stalker ITV favorite opens inside its portal from global favorites', async ({
page,
}) => {
await addStalkerPortal(page);
await verifyStalkerOpenInPlaylist(page, 'Mock Stalker Portal');
});
test('@stalker ITV playback survives a category switch', async ({ page }) => {
await addStalkerPortal(page);
await verifyStalkerPlaybackCategoryReturn(page);
@@ -891,32 +900,6 @@ test('@stalker create_link returns a playable stream URL', async ({
* Play the first channel of the first ITV category and report every
* `create_link` request the page made while doing so.
*/
async function playFirstItvChannel(page: Page): Promise<string[]> {
const createLinkRequests: string[] = [];
page.on('request', (request) => {
if (request.url().includes('action=create_link')) {
createLinkRequests.push(request.url());
}
});
await page.getByRole('link', { name: /live|itv/i }).click();
await page.waitForURL(/stalker.*itv/);
const categories = page.locator('.category-item');
await expect(categories.nth(1)).toBeVisible({ timeout: 10_000 });
await categories.nth(1).click();
const channels = page.locator('[data-test-id="channel-item"]');
await expect(channels.first()).toBeVisible({ timeout: 20_000 });
await channels.first().click();
await expect(channels.first()).toHaveClass(/active/, { timeout: 20_000 });
await expect(page.locator('app-web-player-view')).toBeVisible({
timeout: 20_000,
});
return createLinkRequests;
}
test('@stalker ITV plays an unflagged channel without minting a link', async ({
page,
}) => {