test(web-e2e): mock provider target registration

This commit is contained in:
4gray committed 2026-05-15 19:22:37 +02:00
1 parent 90c5c30eae
commit db19d7d9f4
3 files changed
+129 -13

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
import { type Page } from '@playwright/test';
interface ProviderTargetPayload {
readonly url: string;
}
const BACKEND_ORIGIN = 'http://localhost:3000';
const CORS_HEADERS = {
'access-control-allow-headers': 'content-type',
'access-control-allow-methods': 'POST, OPTIONS',
'access-control-allow-origin': 'http://localhost:4200',
} as const;
export async function interceptProviderTargetRegistration(
page: Page
): Promise<Map<string, string>> {
const providerTargets = new Map<string, string>();
await page.route(`${BACKEND_ORIGIN}/provider-targets**`, async (route) => {
if (route.request().method() === 'OPTIONS') {
await route.fulfill({
body: '',
headers: CORS_HEADERS,
status: 200,
});
return;
}
const providerUrl = getProviderUrl(route.request().postDataJSON());
if (!providerUrl) {
await route.fulfill({
body: JSON.stringify({
message: 'Missing url',
status: 400,
}),
contentType: 'application/json',
headers: CORS_HEADERS,
status: 400,
});
return;
}
const targetId = Buffer.from(providerUrl).toString('base64url');
providerTargets.set(targetId, providerUrl);
await route.fulfill({
body: JSON.stringify({ targetId }),
contentType: 'application/json',
headers: CORS_HEADERS,
status: 200,
});
});
return providerTargets;
}
export function getRegisteredProviderUrl(
url: URL,
providerTargets: ReadonlyMap<string, string>
): string | null {
const targetId = url.searchParams.get('targetId');
if (!targetId) {
return url.searchParams.get('url');
}
return providerTargets.get(targetId) ?? null;
}
function getProviderUrl(value: unknown): string | null {
if (
value &&
typeof value === 'object' &&
'url' in value &&
typeof (value as Partial<ProviderTargetPayload>).url === 'string'
) {
return (value as ProviderTargetPayload).url;
}
return null;
}
+20 -3
View File
@@ -5,6 +5,10 @@ import {
type Page,
test,
} from '@playwright/test';
import {
getRegisteredProviderUrl,
interceptProviderTargetRegistration,
} from './provider-target-route';
/**
* Stalker Portal E2E Tests
@@ -42,11 +46,25 @@ const MINIMAL_MAC = '00:1A:79:00:00:03';
* any app environment configuration.
*/
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);
// Forward all query params unchanged
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() });
@@ -183,9 +201,8 @@ test('@stalker VOD — content list loads after selecting a category', async ({
const contentItems = page.locator(
'.content-card, [data-test-id="channel-item"], mat-card'
);
await expect(contentItems).not.toHaveCount(0, { timeout: 10_000 });
await expect(contentItems.first()).toBeVisible({ timeout: 10_000 });
const itemCount = await contentItems.count();
expect(itemCount).toBeGreaterThan(0);
});
test('@stalker minimal scenario — correct item counts', async ({ page }) => {
+29 -10
View File
@@ -1,4 +1,8 @@
import { APIRequestContext, test, expect, Page } from '@playwright/test';
import {
getRegisteredProviderUrl,
interceptProviderTargetRegistration,
} from './provider-target-route';
/**
* Xtream Codes E2E Tests
@@ -33,10 +37,25 @@ const EPG_PASSWORD = 'epg';
* to the mock server. This avoids any real backend requirement.
*/
async function interceptXtreamRequests(page: Page): Promise<void> {
const providerTargets = await interceptProviderTargetRegistration(page);
await page.route('**/localhost:3000/xtream**', async (route) => {
const originalUrl = new URL(route.request().url());
const mockUrl = new URL(`${MOCK_SERVER}/xtream`);
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() });
@@ -289,21 +308,19 @@ test('@xtream epg fixture — short epg starts at the current program, respects
expect(decodeXtreamText(nextListing.title)).toBe('Market Wrap');
const now = Math.floor(Date.now() / 1000);
expect(Number.parseInt(currentListing.start_timestamp, 10)).toBeLessThanOrEqual(
now
);
expect(Number.parseInt(currentListing.stop_timestamp, 10)).toBeGreaterThanOrEqual(
now
);
expect(
Number.parseInt(currentListing.start_timestamp, 10)
).toBeLessThanOrEqual(now);
expect(
Number.parseInt(currentListing.stop_timestamp, 10)
).toBeGreaterThanOrEqual(now);
expect(currentListing.start).not.toBe(
formatXtreamDateTime(
Number.parseInt(currentListing.start_timestamp, 10)
)
);
expect(currentListing.end).not.toBe(
formatXtreamDateTime(
Number.parseInt(currentListing.stop_timestamp, 10)
)
formatXtreamDateTime(Number.parseInt(currentListing.stop_timestamp, 10))
);
});
@@ -344,7 +361,9 @@ test('@xtream epg fixture — full epg and legacy alias return the same ordered
decodeXtreamText(listing.title) === 'Late Edition'
);
if (!boundaryListing) {
throw new Error('Expected the full EPG fixture to include Late Edition.');
throw new Error(
'Expected the full EPG fixture to include Late Edition.'
);
}
const boundaryStart = new Date(