mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 10:06:15 -08:00
test(e2e): streamline input handling in addStalkerPortal and enhance locator usage
This commit is contained in:
1 parent
83b6604c41
commit
2eb608992a
3 files changed
+116
-84
No files matched your search
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
APIRequestContext,
|
||||
Page,
|
||||
} from '@playwright/test';
|
||||
import { APIRequestContext, Page } from '@playwright/test';
|
||||
import { readFileSync } from 'fs';
|
||||
import {
|
||||
addStalkerPortal,
|
||||
@@ -22,7 +19,7 @@ import {
|
||||
waitForM3uCatalog,
|
||||
waitForPortalDebugEvent,
|
||||
waitForStalkerCatalog,
|
||||
waitForXtreamImportToFinish,
|
||||
waitForXtreamCatalog,
|
||||
xtreamMockServer,
|
||||
} from './electron-test-fixtures';
|
||||
|
||||
@@ -149,13 +146,16 @@ test.describe('Electron Workspace Search', () => {
|
||||
);
|
||||
await expectQueryParam(app.mainWindow, 'q', sample.groupTitle);
|
||||
await expectWorkspaceSearchScope(app.mainWindow, 'Groups');
|
||||
await expect(groupPanelHeader(app.mainWindow, sample.groupTitle)).toHaveCount(
|
||||
1
|
||||
);
|
||||
await expect(
|
||||
groupPanelHeader(app.mainWindow, sample.groupTitle)
|
||||
).toHaveCount(1);
|
||||
await expect(
|
||||
groupPanelHeader(app.mainWindow, sample.groupTitle).first()
|
||||
).toContainText(`${sample.groupTitle} (2)`);
|
||||
await ensureGroupExpanded(app.mainWindow, sample.groupTitle);
|
||||
).toContainText(sample.groupTitle);
|
||||
await expect(
|
||||
groupPanelHeader(app.mainWindow, sample.groupTitle).first()
|
||||
).toContainText('2');
|
||||
await ensureGroupSelected(app.mainWindow, sample.groupTitle);
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.targetTitle).first()
|
||||
).toBeVisible();
|
||||
@@ -171,8 +171,11 @@ test.describe('Electron Workspace Search', () => {
|
||||
await expectQueryParam(app.mainWindow, 'q', sample.targetTitle);
|
||||
await expect(
|
||||
groupPanelHeader(app.mainWindow, sample.groupTitle).first()
|
||||
).toContainText(`${sample.groupTitle} (1)`);
|
||||
await ensureGroupExpanded(app.mainWindow, sample.groupTitle);
|
||||
).toContainText(sample.groupTitle);
|
||||
await expect(
|
||||
groupPanelHeader(app.mainWindow, sample.groupTitle).first()
|
||||
).toContainText('1');
|
||||
await ensureGroupSelected(app.mainWindow, sample.groupTitle);
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.targetTitle).first()
|
||||
).toBeVisible();
|
||||
@@ -237,7 +240,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
await expectPathname(app.mainWindow, /\/workspace\/global-favorites$/);
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/global-favorites$/
|
||||
);
|
||||
await fillWorkspaceSearch(app.mainWindow, sample.targetTitle);
|
||||
|
||||
await expectQueryParam(app.mainWindow, 'q', sample.targetTitle);
|
||||
@@ -278,7 +284,9 @@ test.describe('Electron Workspace Search', () => {
|
||||
|
||||
await clickCategoryByNameExact(app.mainWindow, sample.categoryName);
|
||||
await expect(
|
||||
app.mainWindow.locator('.category-content-layout mat-card').first()
|
||||
app.mainWindow
|
||||
.locator('.category-content-layout mat-card')
|
||||
.first()
|
||||
).toBeVisible({
|
||||
timeout: 20000,
|
||||
});
|
||||
@@ -286,7 +294,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
app.mainWindow,
|
||||
sample
|
||||
);
|
||||
await fillWorkspaceSearch(app.mainWindow, resolvedTitles.targetTitle);
|
||||
await fillWorkspaceSearch(
|
||||
app.mainWindow,
|
||||
resolvedTitles.targetTitle
|
||||
);
|
||||
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
@@ -351,6 +362,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
app.mainWindow,
|
||||
/\/workspace\/xtreams\/[^/]+\/series$/
|
||||
);
|
||||
await clickCategoryByNameExact(
|
||||
app.mainWindow,
|
||||
sample.categoryName
|
||||
);
|
||||
await expect(
|
||||
app.mainWindow
|
||||
.locator('.category-content-layout mat-card')
|
||||
@@ -363,7 +378,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
app.mainWindow,
|
||||
sample
|
||||
);
|
||||
await fillWorkspaceSearch(app.mainWindow, resolvedTitles.targetTitle);
|
||||
await fillWorkspaceSearch(
|
||||
app.mainWindow,
|
||||
resolvedTitles.targetTitle
|
||||
);
|
||||
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
@@ -416,10 +434,7 @@ test.describe('Electron Workspace Search', () => {
|
||||
app.mainWindow,
|
||||
xtreamQueries
|
||||
);
|
||||
await expectWorkspaceSearchScope(
|
||||
app.mainWindow,
|
||||
'Advanced search'
|
||||
);
|
||||
await expectWorkspaceSearchScope(app.mainWindow, 'Advanced search');
|
||||
await expect(
|
||||
app.mainWindow.locator('app-search-layout app-search-form')
|
||||
).toHaveCount(0);
|
||||
@@ -501,7 +516,9 @@ test.describe('Electron Workspace Search', () => {
|
||||
|
||||
await clickCategoryByNameExact(app.mainWindow, sample.categoryName);
|
||||
await expect(
|
||||
app.mainWindow.locator('.category-content-layout mat-card').first()
|
||||
app.mainWindow
|
||||
.locator('.category-content-layout mat-card')
|
||||
.first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
const resolvedTitles = await resolveVisibleXtreamTitles(
|
||||
app.mainWindow,
|
||||
@@ -555,7 +572,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
).first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await fillWorkspaceSearch(app.mainWindow, resolvedTitles.targetTitle);
|
||||
await fillWorkspaceSearch(
|
||||
app.mainWindow,
|
||||
resolvedTitles.targetTitle
|
||||
);
|
||||
|
||||
await expectQueryParam(
|
||||
app.mainWindow,
|
||||
@@ -715,10 +735,7 @@ test.describe('Electron Workspace Search', () => {
|
||||
app.mainWindow,
|
||||
/\/workspace\/stalker\/[^/]+\/search$/
|
||||
);
|
||||
await expectWorkspaceSearchScope(
|
||||
app.mainWindow,
|
||||
'Advanced search'
|
||||
);
|
||||
await expectWorkspaceSearchScope(app.mainWindow, 'Advanced search');
|
||||
await expect(
|
||||
app.mainWindow.locator('app-search-layout app-search-form')
|
||||
).toHaveCount(0);
|
||||
@@ -823,7 +840,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
await app.mainWindow
|
||||
.getByRole('link', { name: 'Live TV', exact: true })
|
||||
.click();
|
||||
await expectPathname(app.mainWindow, /\/workspace\/stalker\/[^/]+\/itv$/);
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/stalker\/[^/]+\/itv$/
|
||||
);
|
||||
|
||||
await clickCategoryById(app.mainWindow, sample.categoryId);
|
||||
await expect(
|
||||
@@ -999,9 +1019,9 @@ async function fetchStalkerCategoryFixture(
|
||||
request: APIRequestContext,
|
||||
type: 'itv' | 'series' | 'vod'
|
||||
): Promise<StalkerCategoryFixture> {
|
||||
const categoriesResponse = await fetchJson<StalkerProxyPayload<
|
||||
StalkerCategory[]
|
||||
>>(
|
||||
const categoriesResponse = await fetchJson<
|
||||
StalkerProxyPayload<StalkerCategory[]>
|
||||
>(
|
||||
request,
|
||||
buildStalkerProxyUrl('get_categories', {
|
||||
type,
|
||||
@@ -1013,9 +1033,9 @@ async function fetchStalkerCategoryFixture(
|
||||
throw new Error(`Stalker mock server returned no ${type} categories.`);
|
||||
}
|
||||
|
||||
const itemsResponse = await fetchJson<StalkerProxyPayload<
|
||||
StalkerOrderedList<StalkerContentItem>
|
||||
>>(
|
||||
const itemsResponse = await fetchJson<
|
||||
StalkerProxyPayload<StalkerOrderedList<StalkerContentItem>>
|
||||
>(
|
||||
request,
|
||||
buildStalkerProxyUrl('get_ordered_list', {
|
||||
category: category.id,
|
||||
@@ -1069,7 +1089,8 @@ function loadM3uSearchFixture(): M3uSearchFixture {
|
||||
}, new Map<string, M3uFixtureItem[]>());
|
||||
|
||||
const groupedSample = [...groups.entries()].find(
|
||||
([groupTitle, channels]) => groupTitle.length > 0 && channels.length >= 2
|
||||
([groupTitle, channels]) =>
|
||||
groupTitle.length > 0 && channels.length >= 2
|
||||
);
|
||||
|
||||
if (!groupedSample) {
|
||||
@@ -1139,7 +1160,10 @@ function parseM3uFixture(filePath: string): M3uFixtureItem[] {
|
||||
return items.filter((item) => item.name.length > 0);
|
||||
}
|
||||
|
||||
async function clickCategoryById(page: Page, categoryId: string): Promise<void> {
|
||||
async function clickCategoryById(
|
||||
page: Page,
|
||||
categoryId: string
|
||||
): Promise<void> {
|
||||
const category = page.locator(
|
||||
`app-workspace-context-panel .category-item[data-category-id="${categoryId}"]:visible`
|
||||
);
|
||||
@@ -1170,20 +1194,16 @@ async function openWorkspaceSection(page: Page, label: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function waitForXtreamWorkspaceReady(page: Page): Promise<void> {
|
||||
await expectPathname(page, /\/workspace\/xtreams\/[^/]+\/vod$/);
|
||||
await expect(
|
||||
page.locator('app-workspace-context-panel .category-item').first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
await waitForXtreamImportToFinish(page);
|
||||
await waitForXtreamCatalog(page);
|
||||
}
|
||||
|
||||
function groupPanelHeader(page: Page, groupTitle: string) {
|
||||
return page.locator('mat-expansion-panel-header').filter({
|
||||
return page.locator('.group-nav-item').filter({
|
||||
hasText: flexibleTextPattern(groupTitle),
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureGroupExpanded(
|
||||
async function ensureGroupSelected(
|
||||
page: Page,
|
||||
groupTitle: string
|
||||
): Promise<void> {
|
||||
@@ -1191,7 +1211,7 @@ async function ensureGroupExpanded(
|
||||
|
||||
await expect(header).toBeVisible();
|
||||
|
||||
if ((await header.getAttribute('aria-expanded')) !== 'true') {
|
||||
if ((await header.getAttribute('aria-current')) !== 'true') {
|
||||
await header.click();
|
||||
}
|
||||
}
|
||||
@@ -1233,9 +1253,11 @@ async function resolveVisibleXtreamTitles(
|
||||
controlTitle: string;
|
||||
targetTitle: string;
|
||||
}> {
|
||||
const visibleTitles = (await page
|
||||
.locator('.category-content-layout mat-card .title')
|
||||
.allTextContents())
|
||||
const visibleTitles = (
|
||||
await page
|
||||
.locator('.category-content-layout mat-card .title')
|
||||
.allTextContents()
|
||||
)
|
||||
.map((title) => title.trim())
|
||||
.filter((title) => title.length > 0);
|
||||
const matchedTitles = fixture.candidateTitles
|
||||
@@ -1282,7 +1304,9 @@ async function addCurrentDetailToFavorites(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
async function goBackFromDetail(page: Page): Promise<void> {
|
||||
const backButton = page.locator('app-content-hero .hero__back-button').first();
|
||||
const backButton = page
|
||||
.locator('app-content-hero .hero__back-button')
|
||||
.first();
|
||||
|
||||
await expect(backButton).toBeVisible({ timeout: 20000 });
|
||||
await backButton.click();
|
||||
@@ -1301,9 +1325,11 @@ async function performXtreamPlaylistSearch(
|
||||
await expectQueryParam(page, 'q', query);
|
||||
|
||||
try {
|
||||
await expect(xtreamSearchResultCards(page).first()).toBeVisible({
|
||||
timeout: 5000,
|
||||
});
|
||||
await expect(xtreamSearchResultCards(page).first()).toBeVisible(
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
return query;
|
||||
} catch {
|
||||
// Try the next candidate token.
|
||||
@@ -1376,9 +1402,7 @@ function pickDistinctItems<T>(
|
||||
|
||||
const control =
|
||||
preferredItems.find((item) => getTitle(item) !== getTitle(target)) ??
|
||||
titledItems.find(
|
||||
(item) => getTitle(item) !== getTitle(target)
|
||||
);
|
||||
titledItems.find((item) => getTitle(item) !== getTitle(target));
|
||||
|
||||
if (!control) {
|
||||
throw new Error('Expected at least two items with distinct titles.');
|
||||
@@ -1488,9 +1512,7 @@ function flexibleTextPattern(value: string): RegExp {
|
||||
}
|
||||
|
||||
async function expectPathname(page: Page, pattern: RegExp): Promise<void> {
|
||||
await expect
|
||||
.poll(() => new URL(page.url()).pathname)
|
||||
.toMatch(pattern);
|
||||
await expect.poll(() => new URL(page.url()).pathname).toMatch(pattern);
|
||||
}
|
||||
|
||||
async function expectQueryParam(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { expect, Page, test } from '@playwright/test';
|
||||
import { expect, Locator, Page, test } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Stalker Portal E2E Tests
|
||||
@@ -47,6 +47,21 @@ 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a Stalker portal via the UI:
|
||||
* 1. Click the "add playlist" button
|
||||
@@ -60,16 +75,19 @@ async function addStalkerPortal(
|
||||
const { name = 'Mock Stalker Portal', mac = DEFAULT_MAC } = options;
|
||||
|
||||
await page.getByRole('button', { name: 'Add playlist' }).click();
|
||||
await page.getByText('Stalker Portal').click();
|
||||
await page.getByRole('menuitem', { name: /Add Stalker Portal/i }).click();
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
|
||||
await dialog.locator('#title').fill(name);
|
||||
await dialog.locator('#portalUrl').fill(PORTAL_URL);
|
||||
await dialog.locator('#macAddress').fill(mac);
|
||||
await setInputValue(dialog.locator('input#title'), name);
|
||||
await setInputValue(dialog.locator('input#portalUrl'), PORTAL_URL);
|
||||
await setInputValue(dialog.locator('input#macAddress'), mac);
|
||||
|
||||
await dialog.getByRole('button', { name: 'Add', exact: true }).click();
|
||||
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 page.waitForURL(/stalker.*vod/);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,29 +145,21 @@ test('@stalker add a Stalker portal and see it in the playlist list', async ({
|
||||
test('@stalker VOD — categories load from mock server', async ({ page }) => {
|
||||
await addStalkerPortal(page);
|
||||
|
||||
// Navigate into the portal — click the portal card
|
||||
await page.getByText('Mock Stalker Portal').click();
|
||||
|
||||
// Wait for VOD route to load
|
||||
await page.waitForURL(/stalker.*vod/);
|
||||
|
||||
// Default scenario has 8 VOD categories (+ 1 "All categories" prepended by the store)
|
||||
const categoryItems = page.locator('.category-item, [class*="category"]');
|
||||
const categoryItems = page.locator('.category-item');
|
||||
await expect(categoryItems.first()).toBeVisible({ timeout: 10_000 });
|
||||
const count = await categoryItems.count();
|
||||
expect(count).toBeGreaterThanOrEqual(8);
|
||||
expect(count).toBeGreaterThanOrEqual(9);
|
||||
});
|
||||
|
||||
test('@stalker VOD — content list loads after selecting a category', async ({
|
||||
page,
|
||||
}) => {
|
||||
await addStalkerPortal(page);
|
||||
await page.getByText('Mock Stalker Portal').click();
|
||||
await page.waitForURL(/stalker.*vod/);
|
||||
|
||||
// Click the first non-"All" category
|
||||
const categories = page.locator('.category-item');
|
||||
await categories.first().click();
|
||||
await categories.nth(1).click();
|
||||
|
||||
// Content grid / list should appear with items
|
||||
const contentItems = page.locator(
|
||||
@@ -165,8 +175,6 @@ test('@stalker minimal scenario — correct item counts', async ({ page }) => {
|
||||
name: 'Minimal Portal',
|
||||
mac: MINIMAL_MAC,
|
||||
});
|
||||
await page.getByText('Minimal Portal').click();
|
||||
await page.waitForURL(/stalker.*vod/);
|
||||
|
||||
// Minimal scenario: 2 categories (+ "All" = 3 visible)
|
||||
const categories = page.locator('.category-item');
|
||||
@@ -178,23 +186,24 @@ test('@stalker minimal scenario — correct item counts', async ({ page }) => {
|
||||
|
||||
test('@stalker EPG data loads for ITV channel', async ({ page }) => {
|
||||
await addStalkerPortal(page);
|
||||
await page.getByText('Mock Stalker Portal').click();
|
||||
await page.waitForURL(/stalker.*vod/);
|
||||
|
||||
// Navigate to ITV tab
|
||||
await page.getByRole('link', { name: /live|itv/i }).click();
|
||||
await page.waitForURL(/stalker.*itv/);
|
||||
|
||||
// ITV view requires an explicit category selection before channels render
|
||||
const categories = page.locator('.category-item');
|
||||
await expect(categories.nth(1)).toBeVisible({ timeout: 10_000 });
|
||||
await categories.nth(1).click();
|
||||
|
||||
// Wait for channels to appear
|
||||
const channels = page.locator(
|
||||
'[data-test-id="channel-item"], [class*="channel"]'
|
||||
);
|
||||
const channels = page.locator('[data-test-id="channel-item"]');
|
||||
await expect(channels.first()).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
// Click a channel — EPG info should appear
|
||||
await channels.first().click();
|
||||
// EPG view should become visible
|
||||
await expect(page.locator('[class*="epg"], app-epg-view')).toBeVisible({
|
||||
await expect(channels.first()).toHaveClass(/active/, { timeout: 10_000 });
|
||||
await expect(page.locator('main app-epg-view')).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ async function addXtreamPortal(
|
||||
} = options;
|
||||
|
||||
await page.getByRole('button', { name: 'Add playlist' }).click();
|
||||
await page.getByText('Xtream').click();
|
||||
await page.getByRole('menuitem', { name: /Add Xtreme Code/i }).click();
|
||||
const dialog = page.locator('mat-dialog-container');
|
||||
|
||||
await dialog.locator('#title').fill(name);
|
||||
@@ -65,6 +65,7 @@ async function addXtreamPortal(
|
||||
|
||||
await dialog.getByRole('button', { name: 'Add', exact: true }).click();
|
||||
await page.waitForSelector('mat-dialog-container', { state: 'detached' });
|
||||
await page.waitForURL(/xtreams.*vod/);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +118,7 @@ test('@xtream get_account_info — active account returns correct fields', async
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.user_info.username).toBe(DEFAULT_USERNAME);
|
||||
expect(body.user_info.status).toBe('active');
|
||||
expect(body.user_info.status).toBe('Active');
|
||||
expect(body.server_info.url).toBeDefined();
|
||||
expect(Array.isArray(body.user_info.allowed_output_formats)).toBeTruthy();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user