mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
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:
1 parent
6578e4073c
commit
f80a21beef
36 files changed
+2211
-121
No files matched your search
@@ -0,0 +1,32 @@
|
||||
import { expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Enters the ITV section, selects the second category (the first row is
|
||||
* "All") and plays its first channel, returning every `create_link` request
|
||||
* observed so callers can assert whether a link was minted.
|
||||
*/
|
||||
export 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;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, Page } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Favorites → "Open in <portal>" → the same channel selected and playing in
|
||||
* the portal's ITV section (the live counterpart of "View in portal").
|
||||
*
|
||||
* Runs against the PWA, where the collection tab renders no EPG panel and
|
||||
* therefore no toolbar chip, so the row context menu is the entry point. The
|
||||
* mock portal answers `get_all_channels`, so the layout locates the channel
|
||||
* through the full ITV list and lands in its genre.
|
||||
*
|
||||
* Expects the portal to be imported already (`addStalkerPortal`).
|
||||
*/
|
||||
export async function verifyStalkerOpenInPlaylist(
|
||||
page: Page,
|
||||
portalName: string
|
||||
): Promise<void> {
|
||||
await page.getByRole('link', { name: /live|itv/i }).click();
|
||||
await page.waitForURL(/stalker.*itv/);
|
||||
const portalUrl = new URL(page.url());
|
||||
const itvPath = portalUrl.pathname;
|
||||
|
||||
// Favorite the first channel of the second category (the first row is
|
||||
// "All"), remembering its name for the round-trip assertion.
|
||||
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 });
|
||||
const channelName = (
|
||||
await channels.first().locator('.channel-name').innerText()
|
||||
).trim();
|
||||
expect(channelName).not.toBe('');
|
||||
await channels.first().locator('.favorite-button').click();
|
||||
await expect(
|
||||
channels.first().locator('.favorite-button mat-icon')
|
||||
).toHaveText('star', { timeout: 10_000 });
|
||||
|
||||
// Global favorites, all playlists: the row offers the jump.
|
||||
await page.goto('/workspace/global-favorites');
|
||||
const allPlaylists = page
|
||||
.locator('.scope-toggle')
|
||||
.getByRole('radio', { name: 'All playlists' });
|
||||
if (await allPlaylists.count()) {
|
||||
await allPlaylists.click();
|
||||
}
|
||||
const favoriteRow = page
|
||||
.locator('app-global-favorites-list [data-test-id="channel-item"]')
|
||||
.filter({ hasText: channelName })
|
||||
.first();
|
||||
await expect(favoriteRow).toBeVisible({ timeout: 20_000 });
|
||||
await favoriteRow.click({ button: 'right' });
|
||||
const openInPlaylist = page.locator(
|
||||
'[data-testid="channel-open-in-playlist"]'
|
||||
);
|
||||
await expect(openInPlaylist).toBeVisible({ timeout: 10_000 });
|
||||
await expect(openInPlaylist).toContainText(portalName);
|
||||
await openInPlaylist.click();
|
||||
|
||||
// Lands inside the portal's ITV section with that channel selected and
|
||||
// playing, in its own genre.
|
||||
await page.waitForURL((url) => url.pathname === itvPath, {
|
||||
timeout: 20_000,
|
||||
});
|
||||
const activeRow = page.locator('[data-test-id="channel-item"].active');
|
||||
await expect(activeRow).toContainText(channelName, { timeout: 30_000 });
|
||||
await expect(page.locator('app-web-player-view')).toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
}) => {
|
||||
|
||||
Reference in new issue
Block a user