mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
test(e2e): add PWA source details coverage
Add PWA web-e2e source details coverage and shared web-e2e helpers.
This commit is contained in:
1 parent
dfdb5bb8ad
commit
bc7d0fd1b0
5 files changed
+467
-34
No files matched your search
@@ -0,0 +1,20 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { expect } from './fixtures';
|
||||
|
||||
export async function setInputValue(
|
||||
input: Locator,
|
||||
value: string
|
||||
): Promise<void> {
|
||||
await input.fill('');
|
||||
await input.fill(value);
|
||||
|
||||
if ((await input.inputValue()) === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await input.click();
|
||||
await input.press('ControlOrMeta+A');
|
||||
await input.press('Backspace');
|
||||
await input.type(value);
|
||||
await expect(input).toHaveValue(value);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
import type { Page } from '@playwright/test';
|
||||
import { setInputValue } from './e2e-helpers';
|
||||
import { expect, test } from './fixtures';
|
||||
|
||||
const WEB_BACKEND_URL = 'http://localhost:3333';
|
||||
@@ -18,21 +19,6 @@ async function installRuntimeConfig(page: Page): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function setInputValue(input: Locator, value: string): Promise<void> {
|
||||
await input.fill('');
|
||||
await input.fill(value);
|
||||
|
||||
if ((await input.inputValue()) === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await input.click();
|
||||
await input.press('ControlOrMeta+A');
|
||||
await input.press('Backspace');
|
||||
await input.type(value);
|
||||
await expect(input).toHaveValue(value);
|
||||
}
|
||||
|
||||
async function addXtreamPortal(page: Page): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Add playlist' }).click();
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { expect, test } from './fixtures';
|
||||
import {
|
||||
EDITED_MAC,
|
||||
STALKER_PORTAL_URL,
|
||||
XTREAM_MOCK_SERVER,
|
||||
addM3uUrlPlaylist,
|
||||
addStalkerPortal,
|
||||
addXtreamPortal,
|
||||
closeSourceDialog,
|
||||
collectRuntimeErrors,
|
||||
expectElectronBridgeUnavailable,
|
||||
expectNoElectronDbRuntimeErrors,
|
||||
expectSourceDialogValues,
|
||||
interceptPwaProviderRequests,
|
||||
openSourceEditor,
|
||||
openSources,
|
||||
resetPwaMockServers,
|
||||
saveSourceDialog,
|
||||
sourceRowByTitle,
|
||||
updateSourceDialog,
|
||||
} from './sources-pwa.helpers';
|
||||
|
||||
test.beforeEach(async ({ page, request }) => {
|
||||
await resetPwaMockServers(request);
|
||||
await interceptPwaProviderRequests(page);
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('@sources @pwa opens Xtream source details without Electron DB bridge calls', async ({
|
||||
page,
|
||||
}) => {
|
||||
const runtimeErrors = collectRuntimeErrors(page);
|
||||
|
||||
await expectElectronBridgeUnavailable(page);
|
||||
await addXtreamPortal(page, 'PWA Xtream Source');
|
||||
await openSources(page);
|
||||
await expect(sourceRowByTitle(page, 'PWA Xtream Source')).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
const dialog = await openSourceEditor(page, 'PWA Xtream Source');
|
||||
await expectSourceDialogValues(dialog, {
|
||||
password: 'pass1',
|
||||
serverUrl: XTREAM_MOCK_SERVER,
|
||||
title: 'PWA Xtream Source',
|
||||
username: 'user1',
|
||||
});
|
||||
await closeSourceDialog(dialog);
|
||||
|
||||
expectNoElectronDbRuntimeErrors(runtimeErrors);
|
||||
});
|
||||
|
||||
test('@sources @pwa edits M3U URL source details through the browser flow', async ({
|
||||
page,
|
||||
}) => {
|
||||
const runtimeErrors = collectRuntimeErrors(page);
|
||||
|
||||
await expectElectronBridgeUnavailable(page);
|
||||
await addM3uUrlPlaylist(page, 'PWA URL Source');
|
||||
await openSources(page);
|
||||
|
||||
let dialog = await openSourceEditor(page, 'PWA URL Source');
|
||||
await updateSourceDialog(dialog, {
|
||||
title: 'PWA Edited URL Source',
|
||||
});
|
||||
await saveSourceDialog(dialog);
|
||||
|
||||
await expect(sourceRowByTitle(page, 'PWA Edited URL Source')).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
|
||||
dialog = await openSourceEditor(page, 'PWA Edited URL Source');
|
||||
await expectSourceDialogValues(dialog, {
|
||||
title: 'PWA Edited URL Source',
|
||||
});
|
||||
await closeSourceDialog(dialog);
|
||||
|
||||
expectNoElectronDbRuntimeErrors(runtimeErrors);
|
||||
});
|
||||
|
||||
test('@sources @pwa edits Stalker source details through the browser flow', async ({
|
||||
page,
|
||||
}) => {
|
||||
const runtimeErrors = collectRuntimeErrors(page);
|
||||
|
||||
await expectElectronBridgeUnavailable(page);
|
||||
await addStalkerPortal(page, 'PWA Stalker Source');
|
||||
await openSources(page);
|
||||
|
||||
let dialog = await openSourceEditor(page, 'PWA Stalker Source');
|
||||
await updateSourceDialog(dialog, {
|
||||
macAddress: EDITED_MAC,
|
||||
portalUrl: STALKER_PORTAL_URL,
|
||||
title: 'PWA Edited Stalker Source',
|
||||
});
|
||||
await saveSourceDialog(dialog);
|
||||
|
||||
await expect(sourceRowByTitle(page, 'PWA Edited Stalker Source')).toBeVisible(
|
||||
{ timeout: 15_000 }
|
||||
);
|
||||
|
||||
dialog = await openSourceEditor(page, 'PWA Edited Stalker Source');
|
||||
await expectSourceDialogValues(dialog, {
|
||||
macAddress: EDITED_MAC,
|
||||
portalUrl: STALKER_PORTAL_URL,
|
||||
title: 'PWA Edited Stalker Source',
|
||||
});
|
||||
await closeSourceDialog(dialog);
|
||||
|
||||
expectNoElectronDbRuntimeErrors(runtimeErrors);
|
||||
});
|
||||
@@ -0,0 +1,332 @@
|
||||
import type { APIRequestContext, Locator, Page } from '@playwright/test';
|
||||
import { expect } from './fixtures';
|
||||
import { setInputValue } from './e2e-helpers';
|
||||
import {
|
||||
getRegisteredProviderUrl,
|
||||
interceptProviderTargetRegistration,
|
||||
} from './provider-target-route';
|
||||
|
||||
const BACKEND_ORIGIN = 'http://localhost:3000';
|
||||
const XTREAM_MOCK_PORT = process.env['XTREAM_MOCK_PORT'] ?? '3211';
|
||||
const STALKER_MOCK_PORT = process.env['MOCK_PORT'] ?? '3210';
|
||||
|
||||
export const XTREAM_MOCK_SERVER = `http://localhost:${XTREAM_MOCK_PORT}`;
|
||||
export const STALKER_MOCK_SERVER = `http://localhost:${STALKER_MOCK_PORT}`;
|
||||
export const STALKER_PORTAL_URL = `${STALKER_MOCK_SERVER}/portal.php`;
|
||||
export const EDITED_MAC = '00:1A:79:00:00:03';
|
||||
|
||||
const DEFAULT_MAC = '00:1A:79:00:00:01';
|
||||
const M3U_PLAYLIST_URL = `${XTREAM_MOCK_SERVER}/playlist.m3u`;
|
||||
|
||||
type RuntimeErrors = {
|
||||
consoleErrors: string[];
|
||||
pageErrors: string[];
|
||||
};
|
||||
|
||||
type SourceDialogField =
|
||||
| 'macAddress'
|
||||
| 'password'
|
||||
| 'portalUrl'
|
||||
| 'serverUrl'
|
||||
| 'title'
|
||||
| 'username';
|
||||
|
||||
export async function resetPwaMockServers(
|
||||
request: APIRequestContext
|
||||
): Promise<void> {
|
||||
await request.post(`${XTREAM_MOCK_SERVER}/reset`);
|
||||
await request.post(`${STALKER_MOCK_SERVER}/reset`);
|
||||
}
|
||||
|
||||
export async function interceptPwaProviderRequests(
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
const providerTargets = await interceptProviderTargetRegistration(page);
|
||||
|
||||
await page.route(`${BACKEND_ORIGIN}/parse**`, async (route) => {
|
||||
const originalUrl = new URL(route.request().url());
|
||||
const providerUrl = getRegisteredProviderUrl(
|
||||
originalUrl,
|
||||
providerTargets
|
||||
);
|
||||
|
||||
await route.fulfill({
|
||||
body: JSON.stringify(createParsedM3uPlaylist(providerUrl)),
|
||||
contentType: 'application/json',
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
|
||||
await page.route(`${BACKEND_ORIGIN}/xtream**`, async (route) => {
|
||||
const originalUrl = new URL(route.request().url());
|
||||
const mockUrl = new URL(`${XTREAM_MOCK_SERVER}/xtream`);
|
||||
const providerUrl = getRegisteredProviderUrl(
|
||||
originalUrl,
|
||||
providerTargets
|
||||
);
|
||||
|
||||
if (providerUrl) {
|
||||
mockUrl.searchParams.set('url', providerUrl);
|
||||
}
|
||||
|
||||
originalUrl.searchParams.forEach((value, key) => {
|
||||
if (key !== 'targetId') {
|
||||
mockUrl.searchParams.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
await route.continue({ url: mockUrl.toString() });
|
||||
});
|
||||
|
||||
await page.route(`${BACKEND_ORIGIN}/stalker**`, async (route) => {
|
||||
const originalUrl = new URL(route.request().url());
|
||||
const mockUrl = new URL(`${STALKER_MOCK_SERVER}/stalker`);
|
||||
const providerUrl = getRegisteredProviderUrl(
|
||||
originalUrl,
|
||||
providerTargets
|
||||
);
|
||||
|
||||
if (providerUrl) {
|
||||
mockUrl.searchParams.set('url', providerUrl);
|
||||
}
|
||||
|
||||
originalUrl.searchParams.forEach((value, key) => {
|
||||
if (key !== 'targetId') {
|
||||
mockUrl.searchParams.set(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
await route.continue({ url: mockUrl.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
export async function addXtreamPortal(
|
||||
page: Page,
|
||||
title: string
|
||||
): Promise<void> {
|
||||
await openAddPlaylistDialog(page);
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
await dialog.getByRole('radio', { name: /Xtream credentials/i }).click();
|
||||
await setInputValue(dialog.locator('#title'), title);
|
||||
await setInputValue(dialog.locator('#serverUrl'), XTREAM_MOCK_SERVER);
|
||||
await setInputValue(dialog.locator('#username'), 'user1');
|
||||
await setInputValue(dialog.locator('#password'), 'pass1');
|
||||
|
||||
const addButton = dialog.getByRole('button', {
|
||||
name: 'Add',
|
||||
exact: true,
|
||||
});
|
||||
await expect(addButton).toBeEnabled({ timeout: 10_000 });
|
||||
await addButton.click();
|
||||
await expect(dialog).toBeHidden();
|
||||
await page.waitForURL(/xtreams.*vod/);
|
||||
}
|
||||
|
||||
export async function addM3uUrlPlaylist(
|
||||
page: Page,
|
||||
title: string
|
||||
): Promise<void> {
|
||||
await openAddPlaylistDialog(page);
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
await setInputValue(
|
||||
dialog.getByRole('textbox', { name: /Playlist URL/ }),
|
||||
M3U_PLAYLIST_URL
|
||||
);
|
||||
await setInputValue(
|
||||
dialog.getByRole('textbox', { name: 'Playlist title' }),
|
||||
title
|
||||
);
|
||||
|
||||
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(/playlists.*all/);
|
||||
}
|
||||
|
||||
export async function addStalkerPortal(
|
||||
page: Page,
|
||||
title: string
|
||||
): Promise<void> {
|
||||
await openAddPlaylistDialog(page);
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
await dialog.getByRole('radio', { name: /Stalker portal/i }).click();
|
||||
await setInputValue(dialog.locator('input#title'), title);
|
||||
await setInputValue(dialog.locator('input#portalUrl'), STALKER_PORTAL_URL);
|
||||
await setInputValue(dialog.locator('input#macAddress'), DEFAULT_MAC);
|
||||
|
||||
const addButton = dialog.getByRole('button', {
|
||||
name: 'Add',
|
||||
exact: true,
|
||||
});
|
||||
await expect(addButton).toBeEnabled({ timeout: 10_000 });
|
||||
await addButton.click();
|
||||
await expect(dialog).toBeHidden();
|
||||
await page.waitForURL(/stalker.*vod/);
|
||||
}
|
||||
|
||||
export async function openSources(page: Page): Promise<void> {
|
||||
await page.goto('/workspace/sources');
|
||||
await page.waitForURL(/\/workspace\/sources(?:\?.*)?$/);
|
||||
await expect(page.locator('app-workspace-sources')).toBeVisible({
|
||||
timeout: 15_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function sourceRowByTitle(page: Page, title: string): Locator {
|
||||
return page.locator('app-playlist-item').filter({ hasText: title }).first();
|
||||
}
|
||||
|
||||
export async function openSourceEditor(
|
||||
page: Page,
|
||||
title: string
|
||||
): Promise<Locator> {
|
||||
const row = sourceRowByTitle(page, title);
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
await row.hover();
|
||||
await row.locator('.edit-btn').click();
|
||||
const dialog = page.locator('mat-dialog-container').last();
|
||||
await expect(dialog).toBeVisible();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
export async function updateSourceDialog(
|
||||
dialog: Locator,
|
||||
updates: Partial<Record<SourceDialogField, string>>
|
||||
): Promise<void> {
|
||||
for (const [field, value] of Object.entries(updates)) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await setInputValue(
|
||||
dialog.locator(`input[formcontrolname="${field}"]`),
|
||||
value
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveSourceDialog(dialog: Locator): Promise<void> {
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeEnabled();
|
||||
await dialog.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
}
|
||||
|
||||
export async function closeSourceDialog(dialog: Locator): Promise<void> {
|
||||
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
|
||||
await expect(dialog).toBeHidden();
|
||||
}
|
||||
|
||||
export async function expectSourceDialogValues(
|
||||
dialog: Locator,
|
||||
expected: Partial<Record<SourceDialogField, string>>
|
||||
): Promise<void> {
|
||||
for (const [field, value] of Object.entries(expected)) {
|
||||
if (value == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await expect(
|
||||
dialog.locator(`input[formcontrolname="${field}"]`)
|
||||
).toHaveValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
export async function expectElectronBridgeUnavailable(
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
await expect.poll(() => page.evaluate(() => window.electron)).toBeUndefined();
|
||||
}
|
||||
|
||||
export function collectRuntimeErrors(page: Page): RuntimeErrors {
|
||||
const errors: RuntimeErrors = {
|
||||
consoleErrors: [],
|
||||
pageErrors: [],
|
||||
};
|
||||
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
errors.consoleErrors.push(message.text());
|
||||
}
|
||||
});
|
||||
page.on('pageerror', (error) => {
|
||||
errors.pageErrors.push(error.message);
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function expectNoElectronDbRuntimeErrors(
|
||||
errors: RuntimeErrors
|
||||
): void {
|
||||
const electronDbErrorPattern =
|
||||
/window\.electron|Cannot read properties of undefined.*db|db(UpdatePlaylist|SetAppState|GetContentByXtreamId|GetAllCategories|UpdateCategoryVisibility|GlobalSearch)|DB_(UPDATE_PLAYLIST|SET_APP_STATE|GET_CONTENT_BY_XTREAM_ID|GET_ALL_CATEGORIES|UPDATE_CATEGORY_VISIBILITY|GLOBAL_SEARCH)|updateXtreamPlaylistDetails|globalSearchContent/i;
|
||||
const matchingErrors = [...errors.consoleErrors, ...errors.pageErrors].filter(
|
||||
(message) => electronDbErrorPattern.test(message)
|
||||
);
|
||||
|
||||
expect(matchingErrors).toEqual([]);
|
||||
}
|
||||
|
||||
async function openAddPlaylistDialog(page: Page): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Add playlist' }).click();
|
||||
await expect(page.locator('mat-dialog-container')).toBeVisible();
|
||||
}
|
||||
|
||||
function createParsedM3uPlaylist(providerUrl: string | null) {
|
||||
return {
|
||||
_id: 'pwa-url-source-id',
|
||||
autoRefresh: false,
|
||||
count: 2,
|
||||
filename: 'pwa-url-source.m3u',
|
||||
importDate: new Date('2026-01-01T00:00:00.000Z').toISOString(),
|
||||
items: [
|
||||
{
|
||||
group: { title: 'News' },
|
||||
http: {
|
||||
origin: '',
|
||||
referrer: '',
|
||||
'user-agent': '',
|
||||
},
|
||||
id: 'pwa-url-channel-1',
|
||||
name: 'PWA URL Channel One',
|
||||
radio: 'false',
|
||||
tvg: {
|
||||
id: 'pwa-url-1',
|
||||
logo: '',
|
||||
name: 'PWA URL Channel One',
|
||||
rec: '',
|
||||
url: '',
|
||||
},
|
||||
url: 'https://streams.example.test/pwa-url-one.m3u8',
|
||||
},
|
||||
{
|
||||
group: { title: 'News' },
|
||||
http: {
|
||||
origin: '',
|
||||
referrer: '',
|
||||
'user-agent': '',
|
||||
},
|
||||
id: 'pwa-url-channel-2',
|
||||
name: 'PWA URL Channel Two',
|
||||
radio: 'false',
|
||||
tvg: {
|
||||
id: 'pwa-url-2',
|
||||
logo: '',
|
||||
name: 'PWA URL Channel Two',
|
||||
rec: '',
|
||||
url: '',
|
||||
},
|
||||
url: 'https://streams.example.test/pwa-url-two.m3u8',
|
||||
},
|
||||
],
|
||||
lastUsage: '',
|
||||
title: 'pwa-url-source.m3u',
|
||||
url: providerUrl ?? M3U_PLAYLIST_URL,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
type APIRequestContext,
|
||||
type Locator,
|
||||
type Page,
|
||||
} from '@playwright/test';
|
||||
import { setInputValue } from './e2e-helpers';
|
||||
import { expect, test } from './fixtures';
|
||||
import {
|
||||
getRegisteredProviderUrl,
|
||||
@@ -70,21 +70,6 @@ async function interceptStalkerRequests(page: Page): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
async function setInputValue(input: Locator, value: string): Promise<void> {
|
||||
await input.fill('');
|
||||
await input.fill(value);
|
||||
|
||||
if ((await input.inputValue()) === value) {
|
||||
return;
|
||||
}
|
||||
|
||||
await input.click();
|
||||
await input.press('ControlOrMeta+A');
|
||||
await input.press('Backspace');
|
||||
await input.type(value);
|
||||
await expect(input).toHaveValue(value);
|
||||
}
|
||||
|
||||
async function resetMockServer(request: APIRequestContext): Promise<void> {
|
||||
let lastError: unknown;
|
||||
|
||||
@@ -135,8 +120,7 @@ async function addStalkerPortal(
|
||||
const addButton = dialog.getByRole('button', { name: 'Add', exact: true });
|
||||
await expect(addButton).toBeEnabled({ timeout: 10_000 });
|
||||
await addButton.click();
|
||||
// Wait for dialog to close
|
||||
await page.waitForSelector('mat-dialog-container', { state: 'detached' });
|
||||
await expect(dialog).toBeHidden();
|
||||
await page.waitForURL(/stalker.*vod/);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user