mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
test(e2e): enhance category management dialog and update playlist handling
This commit is contained in:
1 parent
0f3a75f2b2
commit
a15ac7f7c1
9 files changed
+80
-25
No files matched your search
@@ -9,6 +9,7 @@ import {
|
||||
openWorkspaceSection,
|
||||
refreshSource,
|
||||
resetMockServers,
|
||||
restartElectronApp,
|
||||
sourceRowByTitle,
|
||||
test,
|
||||
waitForSourceRowIdle,
|
||||
@@ -62,7 +63,7 @@ test.describe('Electron Xtream Category Management', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await dialog.locator('.search-field input').fill(targetCategory.name);
|
||||
await toggleManagedCategory(dialog, targetCategory);
|
||||
await toggleManagedCategory(dialog, targetCategory, false);
|
||||
await dialog.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await app.mainWindow.waitForSelector('mat-dialog-container', {
|
||||
state: 'detached',
|
||||
@@ -76,6 +77,12 @@ test.describe('Electron Xtream Category Management', () => {
|
||||
await refreshSource(app.mainWindow, portalName, { confirm: true });
|
||||
await openSources(app.mainWindow);
|
||||
await waitForSourceRowIdle(app.mainWindow, portalName);
|
||||
|
||||
const restarted = await restartElectronApp(app, dataDir);
|
||||
app.electronApp = restarted.electronApp;
|
||||
app.mainWindow = restarted.mainWindow;
|
||||
|
||||
await openSources(app.mainWindow);
|
||||
await sourceRowByTitle(app.mainWindow, portalName).first().click();
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
await openWorkspaceSection(app.mainWindow, 'Live TV');
|
||||
@@ -88,16 +95,23 @@ test.describe('Electron Xtream Category Management', () => {
|
||||
|
||||
dialog = await openManageCategoriesDialog(app.mainWindow);
|
||||
await dialog.locator('.search-field input').fill(targetCategory.name);
|
||||
await toggleManagedCategory(dialog, targetCategory);
|
||||
await toggleManagedCategory(dialog, targetCategory, true);
|
||||
await dialog.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await app.mainWindow.waitForSelector('mat-dialog-container', {
|
||||
state: 'detached',
|
||||
});
|
||||
|
||||
await expect(sidebarCategoryById(app.mainWindow, targetCategory.id)).toHaveCount(
|
||||
1
|
||||
);
|
||||
await clickCategoryById(app.mainWindow, targetCategory.id);
|
||||
dialog = await openManageCategoriesDialog(app.mainWindow);
|
||||
await dialog.locator('.search-field input').fill(targetCategory.name);
|
||||
const restoredRows = dialog.locator('.category-item');
|
||||
await expect(restoredRows).toHaveCount(1, { timeout: 15000 });
|
||||
await expect(
|
||||
restoredRows.first().locator('mat-checkbox input')
|
||||
).toBeChecked();
|
||||
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
|
||||
await app.mainWindow.waitForSelector('mat-dialog-container', {
|
||||
state: 'detached',
|
||||
});
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
@@ -168,19 +182,25 @@ async function pickSidebarCategory(
|
||||
async function toggleManagedCategory(
|
||||
dialog: Locator,
|
||||
targetCategory: {
|
||||
id: string;
|
||||
itemCount: number;
|
||||
name: string;
|
||||
}
|
||||
},
|
||||
shouldBeSelected: boolean
|
||||
): Promise<void> {
|
||||
// Use hasText on the whole row so Playwright's retry loop waits for Angular
|
||||
// to re-render filteredCategories() after the search field is populated.
|
||||
// Avoid nesting dialog.locator() inside filter({ has: }) — Playwright
|
||||
// cannot scope an absolute locator to each candidate element.
|
||||
const categoryRow = dialog
|
||||
.locator('.category-item')
|
||||
.filter({ hasText: targetCategory.name })
|
||||
.first();
|
||||
void targetCategory.id;
|
||||
const categoryRows = dialog.locator('.category-item');
|
||||
await expect(categoryRows).toHaveCount(1, { timeout: 15000 });
|
||||
const categoryRow = categoryRows.first();
|
||||
const checkbox = categoryRow.locator('mat-checkbox input');
|
||||
|
||||
await expect(categoryRow).toBeVisible({ timeout: 15000 });
|
||||
await categoryRow.click();
|
||||
if (shouldBeSelected) {
|
||||
await checkbox.check();
|
||||
await expect(checkbox).toBeChecked();
|
||||
return;
|
||||
}
|
||||
|
||||
await checkbox.uncheck();
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
addXtreamPortal,
|
||||
channelItemByTitle,
|
||||
clickCategoryById,
|
||||
clickGridListCardByTitle,
|
||||
clickCategoryByNameExact,
|
||||
clickFirstGridListCard,
|
||||
closeElectronApp,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
addXtreamPortal,
|
||||
channelItemByTitle,
|
||||
clickCategoryById,
|
||||
clickGridListCardByTitle,
|
||||
clickCategoryByNameExact,
|
||||
clickFirstGridListCard,
|
||||
closeElectronApp,
|
||||
|
||||
@@ -52,7 +52,7 @@ test.describe('Electron Xtream Responsiveness', () => {
|
||||
await expect(overlay).toBeVisible({ timeout: 15000 });
|
||||
await expect(
|
||||
overlay.getByRole('button', { name: /cancel import/i })
|
||||
).toBeVisible({ timeout: 10000 });
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
|
||||
await waitForDbOperationEvent(app.mainWindow, {
|
||||
operation: 'save-content',
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface XtreamPlaylistData {
|
||||
id: string;
|
||||
name: string;
|
||||
title?: string; // Alias for name, used in some templates
|
||||
updateDate?: number;
|
||||
serverUrl: string;
|
||||
username: string;
|
||||
password: string;
|
||||
|
||||
+5
-1
@@ -55,7 +55,11 @@
|
||||
|
||||
<div class="categories-list">
|
||||
@for (category of filteredCategories(); track category.id) {
|
||||
<div class="category-item" (click)="toggleCategory(category)">
|
||||
<div
|
||||
class="category-item"
|
||||
[attr.data-category-id]="category.xtream_id"
|
||||
(click)="toggleCategory(category)"
|
||||
>
|
||||
<mat-checkbox
|
||||
[checked]="category.selected"
|
||||
(click)="$event.stopPropagation()"
|
||||
|
||||
+25
-4
@@ -86,10 +86,7 @@ export class CategoryManagementDialogComponent implements OnInit {
|
||||
private async loadCategories(): Promise<void> {
|
||||
try {
|
||||
const type = this.getDbType();
|
||||
const allCategories = await this.dbService.getAllXtreamCategories(
|
||||
this.data.playlistId,
|
||||
type
|
||||
);
|
||||
const allCategories = await this.waitForCategories(type);
|
||||
this.categories.set(
|
||||
allCategories.map((c) => ({
|
||||
...c,
|
||||
@@ -103,6 +100,30 @@ export class CategoryManagementDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForCategories(
|
||||
type: 'live' | 'movies' | 'series'
|
||||
): Promise<XCategoryFromDb[]> {
|
||||
const retryDeadlineMs = Date.now() + 10000;
|
||||
const expectedCategoryCount = this.data.itemCounts.size;
|
||||
|
||||
while (true) {
|
||||
const categories = await this.dbService.getAllXtreamCategories(
|
||||
this.data.playlistId,
|
||||
type
|
||||
);
|
||||
|
||||
if (
|
||||
categories.length > 0 ||
|
||||
expectedCategoryCount === 0 ||
|
||||
Date.now() >= retryDeadlineMs
|
||||
) {
|
||||
return categories;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
|
||||
clearSearch(): void {
|
||||
this.searchTerm.set('');
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ function toXtreamPlaylistData(
|
||||
id: playlist._id,
|
||||
name: playlist.title || playlist.filename || 'Untitled playlist',
|
||||
title: playlist.title,
|
||||
updateDate: playlist.updateDate,
|
||||
serverUrl: playlist.serverUrl,
|
||||
username: playlist.username,
|
||||
password: playlist.password,
|
||||
@@ -51,6 +52,7 @@ export class XtreamWorkspaceRouteSession {
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
|
||||
private currentPlaylistId: string | null = null;
|
||||
private currentPlaylistUpdateDate: number | null = null;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
@@ -95,19 +97,22 @@ export class XtreamWorkspaceRouteSession {
|
||||
? toXtreamPlaylistData(this.playlistContext.activePlaylist())
|
||||
: null;
|
||||
const currentPlaylist = this.xtreamStore.currentPlaylist();
|
||||
const routePlaylistUpdateDate = routePlaylist?.updateDate ?? null;
|
||||
const needsPlaylistBootstrap = Boolean(
|
||||
playlistId &&
|
||||
routePlaylist &&
|
||||
currentPlaylist?.id !== playlistId
|
||||
(currentPlaylist?.id !== playlistId ||
|
||||
this.currentPlaylistUpdateDate !== routePlaylistUpdateDate)
|
||||
);
|
||||
|
||||
if (
|
||||
playlistId &&
|
||||
(this.currentPlaylistId !== playlistId || needsPlaylistBootstrap)
|
||||
) {
|
||||
if (this.currentPlaylistId !== playlistId) {
|
||||
if (this.currentPlaylistId !== playlistId || needsPlaylistBootstrap) {
|
||||
this.xtreamStore.resetStore(playlistId);
|
||||
this.currentPlaylistId = playlistId;
|
||||
this.currentPlaylistUpdateDate = routePlaylistUpdateDate;
|
||||
}
|
||||
|
||||
this.xtreamStore.setCurrentPlaylist(routePlaylist);
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@
|
||||
@for (item of items(); track $index) {
|
||||
<a
|
||||
class="nav-item category-item"
|
||||
[attr.data-category-id]="$any(item).category_id ?? item.id"
|
||||
[attr.data-category-id]="
|
||||
$any(item).category_id ?? $any(item).xtream_id ?? item.id
|
||||
"
|
||||
[class.selected]="isSelected(item)"
|
||||
[class.category-item--selected]="isSelected(item)"
|
||||
[attr.aria-current]="isSelected(item) ? 'true' : null"
|
||||
|
||||
Reference in new issue
Block a user