mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
* refactor(xtream): split the series details page into focused services serial-details.component.ts sat at the 400-line max-lines limit and its playback service and spec were close behind. Move cohesive concerns out without changing behavior: - route params, the provider-only flag and the (re)load of the addressed series go to SerialDetailsRouteService; the component still registers the effect, so effect order is unchanged - season descriptions, posters and the TMDB season enrichment go to SerialDetailsSeasonsService - actor, Similar and Discover navigation go to injectXtreamDetailNavigation, shared with the movie page - the watched toggles and the episode playback payload leave SerialDetailsPlaybackService for SerialDetailsWatchToggles and buildSerialEpisodePlayback - the spec's TestBed moves to a harness and the watched-toggle cases to serial-details.season-watch.spec.ts Counted lines: component 400 -> 318, playback service 388 -> 342, component spec 1196 -> 674. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(xtream): move VOD route selection and position state out of the page vod-details-route.component.ts grew from 618 to 741 counted lines and vod-details-playback.service.ts reached 395. Extract without changing behavior: - the route-derived read model (selected movie, category, catalog item, fallback view, multi-source identity, session and content keys) goes to VodDetailsSelectionService; the component keeps the same member names - trailer state and the Similar-rail click move into the hero presenter, the cancel-download prompt and the progress-ring geometry into the downloads service, navigation into injectXtreamDetailNavigation - stored positions (last seen, route row, guarded load) become VodDetailsPositionState and the external-launch bookkeeping becomes VodExternalLaunchClaim - drop the unused MatTooltip import (NG8113) and eight unused imports Counted lines: route component 741 -> 492, playback service 395 -> 344. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(stalker): extract series-view positions and watched toggles stalker-series-view.component.ts grew from 1601 to 1813 counted lines and its spec reached 1198 of 1200. Move code out verbatim: - saved positions, their reconcile and the persist/clear writes go to the component-provided StalkerSeriesPositionsService; the ordering of reads and writes goes to StalkerSeriesPositionQueue - episode, season and series watched toggles go to StalkerSeriesWatchToggleService, the batch core to runStalkerWatchToggleBatch - the lazy VOD season loads go to StalkerVodSeasonEpisodeLoader Every effect stays registered in the component constructor in its original order; template bindings and public member names are unchanged. The spec setup moves to a shared harness and the spillover-prefetch and TMDB season cases to their own specs; the 184 cases of the directory are unchanged. Counted lines: component 1813 -> 1136, component spec 1198 -> 670. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(stalker): extract catalog-detail position and search paging - stalker-catalog-detail.component.ts (397): the stored VOD position and its runtime updates become StalkerCatalogVodPosition, the Play/Resume start becomes startStalkerCatalogVodPlayback - stalker-search.component.ts (776, baselined): the paging resource, the accumulated results and the parental-lock bookkeeping become StalkerSearchPagingController, with pure helpers in stalker-search-results.util.ts. The spec reaches the moved members through component.paging; its cases and assertions are unchanged. Counted lines: catalog detail 397 -> 317, search 776 -> 482. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * refactor(ui): give season-container and vod-details real line headroom - season-container.component.ts (396): season auto-selection and the seasonSelected emission move to createSeasonAutoSelectState, called at the same place in the constructor so effect order is unchanged; the episode subline becomes buildEpisodeSubline - vod-details.component.ts (393): the cross-portal Similar loader, the provider-only download state and the actor/Similar route helpers move to sibling modules Inputs, outputs, selectors and public members are unchanged. Counted lines: season container 396 -> 341, vod details 393 -> 337. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web-e2e): move Stalker portal helpers into fixtures stalker.e2e.ts was at 1196 of 1200 counted lines. Move the mock endpoints, scenario MACs and page helpers to stalker-portal.fixture.ts and the embedded-series steps three tests repeated to stalker-embedded-series.fixture.ts. Every test stays in stalker.e2e.ts in the same order, with the same serial mode and OWNED_MACS reset. Counted lines: 1196 -> 981. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: follow the detail-page extractions Name the Stalker watched-toggle and position services that now own the batch and reconcile code, point AUTH_REJECTED_MAC and the scenario MACs at stalker-portal.fixture.ts, and drop two stale statements about components sitting at the max-lines cap. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs(xtream): note why the seasons service is injected last Its TMDB enrichment effect keeps the position it had as the first effect of the component constructor only while it is created after the other services' effects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: 4gray <fourgray@proton.me> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
239 lines
7.7 KiB
TypeScript
239 lines
7.7 KiB
TypeScript
import type { Page } from '@playwright/test';
|
|
import { setInputValue } from './e2e-helpers';
|
|
import { expect } from './fixtures';
|
|
import {
|
|
getRegisteredProviderUrl,
|
|
interceptProviderTargetRegistration,
|
|
} from './provider-target-route';
|
|
|
|
/**
|
|
* Mock-portal endpoints, scenario MACs and page helpers of `stalker.e2e.ts`.
|
|
*
|
|
* The scenario MACs declared here belong to that spec alone: it lists them in
|
|
* its `OWNED_MACS` and resets them before every test, so a sibling spec that
|
|
* reused one would have its mock state cleared mid-run. See the isolation
|
|
* notes at the top of `stalker.e2e.ts`.
|
|
*/
|
|
|
|
const MOCK_PORT = process.env['MOCK_PORT'] ?? '3210';
|
|
export const MOCK_SERVER = `http://localhost:${MOCK_PORT}`;
|
|
const PORTAL_URL = `${MOCK_SERVER}/portal.php`;
|
|
/**
|
|
* Canonical Ministra path. `PORTAL_URL` above is classified by the app as a
|
|
* "simple" portal (no handshake, no token, no watchdog); this shape is the
|
|
* authenticated branch, which the mock guards like the real middleware.
|
|
*/
|
|
export const FULL_PORTAL_URL = `${MOCK_SERVER}/stalker_portal/server/load.php`;
|
|
export const BACKEND_PROXY = `${MOCK_SERVER}/stalker`;
|
|
|
|
/** Default scenario MAC — balanced catalog, 8 categories, 40 items */
|
|
export const DEFAULT_MAC = '00:1A:79:00:00:01';
|
|
|
|
/** Minimal scenario MAC — 2 categories, 5 items (edge case testing) */
|
|
export const MINIMAL_MAC = '00:1A:79:00:00:03';
|
|
|
|
/** Embedded-series MAC — 50% of VOD items carry an embedded series[] array */
|
|
export const EMBEDDED_SERIES_MAC = '00:1A:79:00:00:05';
|
|
|
|
/** Legacy pagination MAC — portal without get_all_channels support */
|
|
export const LEGACY_PAGINATION_MAC = '00:1A:79:00:00:06';
|
|
|
|
/**
|
|
* Static-cmd MAC — ITV rows carrying a directly playable `cmd` with
|
|
* `use_http_tmp_link` and `use_load_balancing` both `'0'`, i.e. a portal that
|
|
* expects no `create_link` call at all.
|
|
*/
|
|
export const STATIC_CMD_MAC = '00:1A:79:00:00:0A';
|
|
|
|
/**
|
|
* The full-portal authentication tests assert state transitions within one
|
|
* portal session, so a reset from a concurrent browser project or repeat
|
|
* worker would invalidate the assertion itself. Giving every concurrent
|
|
* worker slot its own MAC range preserves browser parallelism and also keeps
|
|
* `--repeat-each` runs isolated.
|
|
*/
|
|
export interface StatefulAuthMacs {
|
|
authenticatedFlow: string;
|
|
loginRequired: string;
|
|
tokenReuse: string;
|
|
deviceConflict: string;
|
|
reauthentication: string;
|
|
}
|
|
|
|
export function getStatefulAuthMacs({
|
|
parallelIndex,
|
|
}: {
|
|
parallelIndex: number;
|
|
}): StatefulAuthMacs {
|
|
if (
|
|
!Number.isSafeInteger(parallelIndex) ||
|
|
parallelIndex < 0 ||
|
|
parallelIndex > 255
|
|
) {
|
|
throw new Error(
|
|
`Unsupported Playwright parallel index: ${parallelIndex}`
|
|
);
|
|
}
|
|
|
|
const workerOctet = parallelIndex
|
|
.toString(16)
|
|
.padStart(2, '0')
|
|
.toUpperCase();
|
|
const workerPrefix = `00:1A:79:AE:${workerOctet}`;
|
|
|
|
return {
|
|
authenticatedFlow: `${workerPrefix}:01`,
|
|
loginRequired: `${workerPrefix}:02`,
|
|
tokenReuse: `${workerPrefix}:03`,
|
|
deviceConflict: `${workerPrefix}:04`,
|
|
reauthentication: `${workerPrefix}:05`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deliberately NOT an Infomir MAC: the strict endpoint rejects get_profile for
|
|
* it, so no token is ever adopted and content requests fail permanently.
|
|
*/
|
|
export const AUTH_REJECTED_MAC = 'AA:BB:CC:DD:EE:01';
|
|
|
|
/**
|
|
* Intercept calls to the Angular dev backend (/stalker proxy) and redirect
|
|
* them to the mock server. This avoids needing a real backend or changing
|
|
* any app environment configuration.
|
|
*/
|
|
export async function interceptStalkerRequests(page: Page): Promise<void> {
|
|
const providerTargets = await interceptProviderTargetRegistration(page);
|
|
|
|
await page.route('**/localhost:3000/stalker**', async (route) => {
|
|
const originalUrl = new URL(route.request().url());
|
|
const mockUrl = new URL(BACKEND_PROXY);
|
|
const providerUrl = getRegisteredProviderUrl(
|
|
originalUrl,
|
|
providerTargets
|
|
);
|
|
|
|
if (providerUrl) {
|
|
mockUrl.searchParams.set('url', providerUrl);
|
|
}
|
|
|
|
originalUrl.searchParams.forEach((value, key) => {
|
|
if (key === 'targetId') {
|
|
return;
|
|
}
|
|
|
|
mockUrl.searchParams.set(key, value);
|
|
});
|
|
await route.continue({ url: mockUrl.toString() });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Add a Stalker portal via the UI:
|
|
* 1. Click the "add playlist" button to open the unified dialog
|
|
* 2. Select "Stalker" toggle
|
|
* 3. Fill in the form and submit
|
|
*/
|
|
export async function addStalkerPortal(
|
|
page: Page,
|
|
options: { name?: string; mac?: string } = {}
|
|
): Promise<void> {
|
|
const { name = 'Mock Stalker Portal', mac = DEFAULT_MAC } = options;
|
|
|
|
await page.getByRole('button', { name: 'Add playlist' }).click();
|
|
const dialog = page.locator('mat-dialog-container');
|
|
await expect(dialog).toBeVisible();
|
|
// v0.22 redesign: tabs were replaced with a flat 5-card radio picker.
|
|
await dialog.getByRole('radio', { name: /Stalker portal/i }).click();
|
|
|
|
await setInputValue(dialog.locator('input#title'), name);
|
|
await setInputValue(dialog.locator('input#portalUrl'), PORTAL_URL);
|
|
await setInputValue(dialog.locator('input#macAddress'), mac);
|
|
|
|
const addButton = dialog.getByRole('button', {
|
|
name: 'Add playlist',
|
|
exact: true,
|
|
});
|
|
await expect(addButton).toBeEnabled({ timeout: 10_000 });
|
|
await addButton.click();
|
|
await expect(dialog).toBeHidden();
|
|
await page.waitForURL(/stalker.*vod/);
|
|
}
|
|
|
|
/**
|
|
* Add a Stalker portal through the canonical Ministra URL, which the app
|
|
* imports as a FULL portal: handshake, Bearer token and watchdog.
|
|
*/
|
|
export async function addFullStalkerPortal(
|
|
page: Page,
|
|
options: {
|
|
name?: string;
|
|
mac: string;
|
|
expectContent?: boolean;
|
|
username?: string;
|
|
password?: string;
|
|
}
|
|
): Promise<void> {
|
|
const {
|
|
name = 'Full Stalker Portal',
|
|
mac,
|
|
expectContent = true,
|
|
username,
|
|
password,
|
|
} = options;
|
|
|
|
await page.getByRole('button', { name: 'Add playlist' }).click();
|
|
const dialog = page.locator('mat-dialog-container');
|
|
await expect(dialog).toBeVisible();
|
|
await dialog.getByRole('radio', { name: /Stalker portal/i }).click();
|
|
|
|
await setInputValue(dialog.locator('input#title'), name);
|
|
await setInputValue(dialog.locator('input#portalUrl'), FULL_PORTAL_URL);
|
|
await setInputValue(dialog.locator('input#macAddress'), mac);
|
|
if (username !== undefined) {
|
|
await setInputValue(dialog.locator('input#username'), username);
|
|
}
|
|
if (password !== undefined) {
|
|
await setInputValue(dialog.locator('input#password'), password);
|
|
}
|
|
|
|
const addButton = dialog.getByRole('button', {
|
|
name: 'Add playlist',
|
|
exact: true,
|
|
});
|
|
await expect(addButton).toBeEnabled({ timeout: 10_000 });
|
|
await addButton.click();
|
|
await expect(dialog).toBeHidden();
|
|
|
|
if (expectContent) {
|
|
await page.waitForURL(/stalker.*vod/, { timeout: 30_000 });
|
|
}
|
|
}
|
|
|
|
export const CONTENT_ACTIONS = [
|
|
'get_categories',
|
|
'get_genres',
|
|
'get_ordered_list',
|
|
'get_all_channels',
|
|
];
|
|
|
|
/** Every portal request in order, with the token it carried. */
|
|
export function recordPortalRequests(
|
|
page: Page
|
|
): Array<{ action: string; token: string | null }> {
|
|
const requests: Array<{ action: string; token: string | null }> = [];
|
|
|
|
page.on('request', (request) => {
|
|
const url = new URL(request.url());
|
|
if (!url.pathname.endsWith('/stalker')) {
|
|
return;
|
|
}
|
|
const action = url.searchParams.get('action');
|
|
if (!action) {
|
|
return;
|
|
}
|
|
requests.push({ action, token: url.searchParams.get('token') });
|
|
});
|
|
|
|
return requests;
|
|
}
|