diff --git a/apps/electron-backend-e2e/src/category-management.e2e.ts b/apps/electron-backend-e2e/src/category-management.e2e.ts index a7480e4db..8ab3a5b38 100644 --- a/apps/electron-backend-e2e/src/category-management.e2e.ts +++ b/apps/electron-backend-e2e/src/category-management.e2e.ts @@ -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 { - // 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(); } diff --git a/apps/electron-backend-e2e/src/favorites.e2e.ts b/apps/electron-backend-e2e/src/favorites.e2e.ts index 05660ff32..858aaa38b 100644 --- a/apps/electron-backend-e2e/src/favorites.e2e.ts +++ b/apps/electron-backend-e2e/src/favorites.e2e.ts @@ -4,6 +4,7 @@ import { addXtreamPortal, channelItemByTitle, clickCategoryById, + clickGridListCardByTitle, clickCategoryByNameExact, clickFirstGridListCard, closeElectronApp, diff --git a/apps/electron-backend-e2e/src/recent.e2e.ts b/apps/electron-backend-e2e/src/recent.e2e.ts index 31a741bc7..cb996d86e 100644 --- a/apps/electron-backend-e2e/src/recent.e2e.ts +++ b/apps/electron-backend-e2e/src/recent.e2e.ts @@ -4,6 +4,7 @@ import { addXtreamPortal, channelItemByTitle, clickCategoryById, + clickGridListCardByTitle, clickCategoryByNameExact, clickFirstGridListCard, closeElectronApp, diff --git a/apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts b/apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts index c1ab6b8cc..66f452690 100644 --- a/apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts +++ b/apps/electron-backend-e2e/src/xtream-responsiveness.e2e.ts @@ -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', diff --git a/libs/portal/xtream/data-access/src/lib/data-sources/xtream-data-source.interface.ts b/libs/portal/xtream/data-access/src/lib/data-sources/xtream-data-source.interface.ts index abd266093..0e87e9cb8 100644 --- a/libs/portal/xtream/data-access/src/lib/data-sources/xtream-data-source.interface.ts +++ b/libs/portal/xtream/data-access/src/lib/data-sources/xtream-data-source.interface.ts @@ -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; diff --git a/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.html b/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.html index 6ea7fc584..375ef6d6d 100644 --- a/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.html +++ b/libs/portal/xtream/feature/src/lib/category-management-dialog/category-management-dialog.component.html @@ -55,7 +55,11 @@
@for (category of filteredCategories(); track category.id) { -
+
{ 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 { + 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(''); } diff --git a/libs/portal/xtream/feature/src/lib/xtream-workspace-route-session.service.ts b/libs/portal/xtream/feature/src/lib/xtream-workspace-route-session.service.ts index f5a81e1bc..3266fb9a4 100644 --- a/libs/portal/xtream/feature/src/lib/xtream-workspace-route-session.service.ts +++ b/libs/portal/xtream/feature/src/lib/xtream-workspace-route-session.service.ts @@ -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); diff --git a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html index dbc7ea82c..24dfde787 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html +++ b/libs/workspace/shell/feature/src/lib/workspace-context-panel/components/workspace-context-category-view.component.html @@ -3,7 +3,9 @@ @for (item of items(); track $index) {