Files
iptvnator/apps/web-e2e/src/xtream.e2e.ts
T
4gray 81c2aab829 test(e2e): add end-to-end tests for playlist import, providers, and settings persistence
- Implemented tests for importing M3U playlists via native dialog.
- Added smoke tests for loading Xtream and Stalker content through IPC.
- Created tests to verify persistence of remote control settings across app restarts.
- Updated smoke tests to check for main window properties and workspace content rendering.

refactor(electron-backend): enhance store service and database path handling

- Modified store service to use a dynamic configuration directory based on environment.
- Refactored database connection to utilize new path utility functions for better path management.

fix(e2e): improve test selectors and structure for better reliability

- Updated test selectors in web-e2e tests to use role-based queries for better accessibility.
- Refactored settings tests to reduce redundancy and improve clarity.

chore(e2e): update TypeScript configuration for better inclusion of test files

- Adjusted tsconfig.json to ensure proper inclusion of e2e test files.

Entire-Checkpoint: bb9a8e2e351e
2026-03-17 23:30:16 +01:00

359 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test, expect, Page } from '@playwright/test';
/**
* Xtream Codes E2E Tests
*
* These tests use the xtream-mock-server (apps/xtream-mock-server) to simulate
* a real Xtream Codes API portal. The mock server starts automatically alongside
* the Angular dev server when running e2e tests (see playwright.config.ts).
*
* Default scenario (user1:pass1) provides:
* - 8 categories per content type (live / VOD / series)
* - 40 items per category
* - 3 seasons × 8 episodes per series item
*
* Tag: @xtream — run only Xtream tests with: nx e2e web-e2e --grep "@xtream"
*/
const XTREAM_MOCK_PORT = process.env['XTREAM_MOCK_PORT'] ?? '3211';
const MOCK_SERVER = `http://localhost:${XTREAM_MOCK_PORT}`;
/** Default scenario credentials */
const DEFAULT_USERNAME = 'user1';
const DEFAULT_PASSWORD = 'pass1';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Intercept calls from the Angular PWA proxy (/xtream) and redirect them
* to the mock server. This avoids any real backend requirement.
*/
async function interceptXtreamRequests(page: Page): Promise<void> {
await page.route('**/localhost:3000/xtream**', async (route) => {
const originalUrl = new URL(route.request().url());
const mockUrl = new URL(`${MOCK_SERVER}/xtream`);
originalUrl.searchParams.forEach((value, key) => {
mockUrl.searchParams.set(key, value);
});
await route.continue({ url: mockUrl.toString() });
});
}
/**
* Add an Xtream portal via the UI.
*/
async function addXtreamPortal(
page: Page,
options: { name?: string; username?: string; password?: string } = {}
): Promise<void> {
const {
name = 'Mock Xtream Portal',
username = DEFAULT_USERNAME,
password = DEFAULT_PASSWORD,
} = options;
await page.getByRole('button', { name: 'Add playlist' }).click();
await page.getByText('Xtream').click();
const dialog = page.locator('mat-dialog-container');
await dialog.locator('#title').fill(name);
await dialog.locator('#serverUrl').fill(MOCK_SERVER);
await dialog.locator('#username').fill(username);
await dialog.locator('#password').fill(password);
await dialog.getByRole('button', { name: 'Add', exact: true }).click();
await page.waitForSelector('mat-dialog-container', { state: 'detached' });
}
/**
* Clear IndexedDB state between tests so each test starts fresh.
*/
async function clearStorage(page: Page): Promise<void> {
await page.evaluate(async () => {
const dbs = await window.indexedDB.databases();
await Promise.all(
dbs
.filter((db) => db.name != null)
.map((db) => window.indexedDB.deleteDatabase(db.name!))
);
});
}
// ---------------------------------------------------------------------------
// Test setup
// ---------------------------------------------------------------------------
test.beforeEach(async ({ page, request }) => {
await request.post(`${MOCK_SERVER}/reset`);
await page.goto('/');
await clearStorage(page);
await page.reload();
await interceptXtreamRequests(page);
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test('@xtream health check — mock server is running', async ({ request }) => {
const response = await request.get(`${MOCK_SERVER}/health`);
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(body.status).toBe('ok');
expect(body.server).toBe('xtream-mock-server');
});
test('@xtream get_account_info — active account returns correct fields', async ({
request,
}) => {
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}`
);
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(body.user_info.username).toBe(DEFAULT_USERNAME);
expect(body.user_info.status).toBe('active');
expect(body.server_info.url).toBeDefined();
expect(Array.isArray(body.user_info.allowed_output_formats)).toBeTruthy();
});
test('@xtream get_account_info — expired account returns Expired status', async ({
request,
}) => {
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=expired&password=expired`
);
expect(response.ok()).toBeTruthy();
const body = await response.json();
// Expired scenario: still returns 'Active' but exp_date is in the past
// The app's portal-status.service.ts detects expiry via exp_date, not status string
expect(body.user_info.status).toBe('Active');
const expDate = new Date(parseInt(body.user_info.exp_date) * 1000);
expect(expDate.getFullYear()).toBe(2020);
});
test('@xtream get_live_categories — returns expected category count', async ({
request,
}) => {
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_live_categories`
);
expect(response.ok()).toBeTruthy();
const categories = await response.json();
expect(Array.isArray(categories)).toBeTruthy();
// Default scenario: 8 live categories
expect(categories.length).toBe(8);
expect(categories[0]).toHaveProperty('category_id');
expect(categories[0]).toHaveProperty('category_name');
});
test('@xtream get_live_streams — streams have required fields', async ({
request,
}) => {
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_live_streams`
);
expect(response.ok()).toBeTruthy();
const streams = await response.json();
expect(Array.isArray(streams)).toBeTruthy();
expect(streams.length).toBeGreaterThan(0);
const first = streams[0];
expect(first.stream_type).toBe('live');
expect(first.stream_id).toBeGreaterThan(0);
expect(first.epg_channel_id).toMatch(/channel-\d+\.mock/);
});
test('@xtream get_vod_streams — streams have rating and container_extension', async ({
request,
}) => {
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_vod_streams`
);
expect(response.ok()).toBeTruthy();
const streams = await response.json();
expect(Array.isArray(streams)).toBeTruthy();
const first = streams[0];
expect(first.stream_type).toBe('movie');
expect(typeof first.rating).toBe('number');
expect(['mkv', 'mp4', 'avi']).toContain(first.container_extension);
});
test('@xtream get_series — series list has correct structure', async ({
request,
}) => {
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_series`
);
expect(response.ok()).toBeTruthy();
const series = await response.json();
expect(Array.isArray(series)).toBeTruthy();
expect(series.length).toBeGreaterThan(0);
const first = series[0];
expect(first.series_id).toBeGreaterThan(0);
expect(typeof first.name).toBe('string');
expect(typeof first.cover).toBe('string');
expect(Array.isArray(first.backdrop_path)).toBeTruthy();
});
test('@xtream get_series_info — seasons and episodes present', async ({
request,
}) => {
// Get a series ID first
const listResponse = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_series`
);
const series = await listResponse.json();
const firstSeriesId = series[0].series_id;
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_series_info&series_id=${firstSeriesId}`
);
expect(response.ok()).toBeTruthy();
const info = await response.json();
expect(Array.isArray(info.seasons)).toBeTruthy();
// Default scenario: 3 seasons per series
expect(info.seasons.length).toBe(3);
expect(info.seasons[0].name).toBe('Season 1');
// Episodes are keyed by season number string
expect(info.episodes['1']).toBeDefined();
expect(Array.isArray(info.episodes['1'])).toBeTruthy();
// Default scenario: 8 episodes per season
expect(info.episodes['1'].length).toBe(8);
expect(info.episodes['1'][0].episode_num).toBe(1);
});
test('@xtream get_vod_info — returns full movie details', async ({
request,
}) => {
// Get a VOD stream ID first
const listResponse = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_vod_streams`
);
const streams = await listResponse.json();
const firstVodId = streams[0].stream_id;
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_vod_info&vod_id=${firstVodId}`
);
expect(response.ok()).toBeTruthy();
const details = await response.json();
expect(details.info).toBeDefined();
expect(details.movie_data).toBeDefined();
expect(details.info.duration_secs).toBeGreaterThan(0);
expect(details.movie_data.stream_id).toBe(firstVodId);
});
test('@xtream get_short_epg — returns base64-encoded listings', async ({
request,
}) => {
// Get a live stream ID first
const listResponse = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_live_streams`
);
const streams = await listResponse.json();
const streamId = streams[0].stream_id;
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_short_epg&stream_id=${streamId}`
);
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(Array.isArray(body.epg_listings)).toBeTruthy();
expect(body.epg_listings.length).toBeGreaterThan(0);
const listing = body.epg_listings[0];
// Verify base64 encoding — decode and check it's valid text
const decodedTitle = Buffer.from(listing.title, 'base64').toString('utf-8');
expect(decodedTitle.length).toBeGreaterThan(0);
expect(listing.start_timestamp).toBeDefined();
expect(listing.stop_timestamp).toBeDefined();
});
test('@xtream reset — data regenerates identically after reset', async ({
request,
}) => {
const url = `${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_live_categories`;
const before = await (await request.get(url)).json();
await request.post(`${MOCK_SERVER}/reset`);
const after = await (await request.get(url)).json();
// Data is deterministic: same after reset
expect(after).toEqual(before);
});
test('@xtream proxy endpoint — returns payload wrapper', async ({
request,
}) => {
const response = await request.get(
`${MOCK_SERVER}/xtream?url=${MOCK_SERVER}&username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_vod_categories`
);
expect(response.ok()).toBeTruthy();
const body = await response.json();
expect(body.action).toBe('get_vod_categories');
expect(Array.isArray(body.payload)).toBeTruthy();
expect(body.payload.length).toBeGreaterThan(0);
});
test('@xtream category filter — get_live_streams filtered by category_id', async ({
request,
}) => {
// Get categories to find a real ID
const catResponse = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_live_categories`
);
const categories = await catResponse.json();
const categoryId = categories[0].category_id;
// Fetch streams filtered by that category
const streamResponse = await request.get(
`${MOCK_SERVER}/player_api.php?username=${DEFAULT_USERNAME}&password=${DEFAULT_PASSWORD}&action=get_live_streams&category_id=${categoryId}`
);
const streams = await streamResponse.json();
expect(Array.isArray(streams)).toBeTruthy();
expect(streams.length).toBeGreaterThan(0);
// All returned streams should belong to this category
for (const s of streams) {
expect(String(s.category_id)).toBe(String(categoryId));
}
});
test('@xtream add portal and see it in the playlist list', async ({ page }) => {
await addXtreamPortal(page, { name: 'My Xtream Test Portal' });
await expect(
page.getByText('My Xtream Test Portal', { exact: false })
).toBeVisible();
});
test('@xtream minimal scenario — reduced item count', async ({ request }) => {
const response = await request.get(
`${MOCK_SERVER}/player_api.php?username=minimal&password=minimal&action=get_live_categories`
);
const categories = await response.json();
// Minimal scenario: 2 categories
expect(categories.length).toBe(2);
const streams = await (
await request.get(
`${MOCK_SERVER}/player_api.php?username=minimal&password=minimal&action=get_live_streams`
)
).json();
// 2 categories × 5 items = 10
expect(streams.length).toBe(10);
});