mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
fix(xtream): preserve live search playback sidebar
Entire-Checkpoint: f957cd9849e0
This commit is contained in:
1 parent
a3a4c6b5ea
commit
6efa8b9072
4 files changed
+236
-29
No files matched your search
@@ -206,7 +206,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
|
||||
await fillWorkspaceSearch(app.mainWindow, sample.groupTitle);
|
||||
|
||||
await expectPathname(app.mainWindow, /\/workspace\/global-favorites$/);
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/global-favorites$/
|
||||
);
|
||||
await expectQueryParam(app.mainWindow, 'q', sample.groupTitle);
|
||||
await expectWorkspaceSearchScope(
|
||||
app.mainWindow,
|
||||
@@ -411,6 +414,134 @@ test.describe('Electron Workspace Search', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('@search @xtream filters VOD and series root views across categories', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const vodSample = await fetchXtreamVodFixture(request);
|
||||
const seriesSample = await fetchXtreamSeriesFixture(request);
|
||||
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
password: xtreamSearchPassword,
|
||||
username: xtreamSearchUsername,
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
|
||||
await expectXtreamRootCatalogSearch(app.mainWindow, {
|
||||
pathPattern: /\/workspace\/xtreams\/[^/]+\/vod$/,
|
||||
sample: vodSample,
|
||||
sectionLabel: 'Movies',
|
||||
});
|
||||
await expect(
|
||||
app.mainWindow.locator(headerSearchSelector)
|
||||
).toHaveValue(vodSample.targetTitle);
|
||||
|
||||
await fillWorkspaceSearch(app.mainWindow, '');
|
||||
await openWorkspaceSection(app.mainWindow, 'Series');
|
||||
|
||||
await expectXtreamRootCatalogSearch(app.mainWindow, {
|
||||
pathPattern: /\/workspace\/xtreams\/[^/]+\/series$/,
|
||||
sample: seriesSample,
|
||||
sectionLabel: 'Series',
|
||||
});
|
||||
await expect(
|
||||
app.mainWindow.locator(headerSearchSelector)
|
||||
).toHaveValue(seriesSample.targetTitle);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('@search @xtream filters live root and category scopes while preserving playback sidebar', async ({
|
||||
dataDir,
|
||||
request,
|
||||
}) => {
|
||||
await resetMockServers(request, ['xtream']);
|
||||
const sample = await fetchXtreamLiveFixture(request);
|
||||
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await addXtreamPortal(app.mainWindow, {
|
||||
password: xtreamSearchPassword,
|
||||
username: xtreamSearchUsername,
|
||||
});
|
||||
await waitForXtreamWorkspaceReady(app.mainWindow);
|
||||
await openWorkspaceSection(app.mainWindow, 'Live TV');
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/xtreams\/[^/]+\/live$/
|
||||
);
|
||||
|
||||
await fillWorkspaceSearch(app.mainWindow, sample.targetTitle);
|
||||
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/xtreams\/[^/]+\/live$/
|
||||
);
|
||||
await expectQueryParam(app.mainWindow, 'q', sample.targetTitle);
|
||||
await expectWorkspaceSearchScope(
|
||||
app.mainWindow,
|
||||
'Live TV / All Items'
|
||||
);
|
||||
await expect(
|
||||
liveChannelSidebar(app.mainWindow)
|
||||
.locator('[data-test-id="channel-item"]')
|
||||
.first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.targetTitle).first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.controlTitle)
|
||||
).toHaveCount(0);
|
||||
|
||||
await channelItemByTitle(app.mainWindow, sample.targetTitle)
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
app.mainWindow.locator(
|
||||
'app-live-stream-layout .content-container .video-player'
|
||||
)
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
await expect(
|
||||
liveChannelSidebar(app.mainWindow)
|
||||
.locator('[data-test-id="channel-item"]')
|
||||
.first()
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.targetTitle).first()
|
||||
).toHaveClass(/(^|\s)active(\s|$)/);
|
||||
|
||||
await fillWorkspaceSearch(app.mainWindow, '');
|
||||
await clickCategoryByNameExact(app.mainWindow, sample.categoryName);
|
||||
await fillWorkspaceSearch(app.mainWindow, sample.targetTitle);
|
||||
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/xtreams\/[^/]+\/live(?:\/[^/]+)?$/
|
||||
);
|
||||
await expectQueryParam(app.mainWindow, 'q', sample.targetTitle);
|
||||
await expectWorkspaceSearchScope(
|
||||
app.mainWindow,
|
||||
`Live TV / ${sample.categoryName}`
|
||||
);
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.targetTitle).first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.controlTitle)
|
||||
).toHaveCount(0);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('@search @xtream promotes dashboard header search into advanced playlist search', async ({
|
||||
dataDir,
|
||||
request,
|
||||
@@ -561,7 +692,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
);
|
||||
|
||||
await openWorkspaceSection(app.mainWindow, 'Favorites');
|
||||
await expectPathname(app.mainWindow, /\/workspace\/global-favorites$/);
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/global-favorites$/
|
||||
);
|
||||
await expect(
|
||||
contentCardByTitle(
|
||||
app.mainWindow,
|
||||
@@ -801,7 +935,10 @@ test.describe('Electron Workspace Search', () => {
|
||||
await toggleFavoriteForChannel(app.mainWindow, sample.controlTitle);
|
||||
|
||||
await openWorkspaceSection(app.mainWindow, 'Favorites');
|
||||
await expectPathname(app.mainWindow, /\/workspace\/global-favorites$/);
|
||||
await expectPathname(
|
||||
app.mainWindow,
|
||||
/\/workspace\/global-favorites$/
|
||||
);
|
||||
await expect(
|
||||
channelItemByTitle(app.mainWindow, sample.targetTitle).first()
|
||||
).toBeVisible({ timeout: 20000 });
|
||||
@@ -911,6 +1048,15 @@ async function fetchXtreamVodFixture(
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchXtreamLiveFixture(
|
||||
request: APIRequestContext
|
||||
): Promise<XtreamVodFixture> {
|
||||
return fetchXtreamFixture(request, {
|
||||
categoriesAction: 'get_live_categories',
|
||||
itemsAction: 'get_live_streams',
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchXtreamSeriesFixture(
|
||||
request: APIRequestContext
|
||||
): Promise<XtreamVodFixture> {
|
||||
@@ -1262,6 +1408,10 @@ function xtreamSearchResultCards(page: Page) {
|
||||
return page.locator('app-search-results .results-grid .content-card');
|
||||
}
|
||||
|
||||
function liveChannelSidebar(page: Page) {
|
||||
return page.locator('app-live-stream-layout .sidebar');
|
||||
}
|
||||
|
||||
function gridListCardByTitle(page: Page, title: string) {
|
||||
return page.locator('.category-content-layout mat-card').filter({
|
||||
has: page.locator('.title', {
|
||||
@@ -1329,6 +1479,34 @@ async function resolveVisibleXtreamTitles(
|
||||
};
|
||||
}
|
||||
|
||||
async function expectXtreamRootCatalogSearch(
|
||||
page: Page,
|
||||
options: {
|
||||
pathPattern: RegExp;
|
||||
sample: XtreamVodFixture;
|
||||
sectionLabel: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
await openWorkspaceSection(page, options.sectionLabel);
|
||||
await expectPathname(page, options.pathPattern);
|
||||
await fillWorkspaceSearch(page, options.sample.targetTitle);
|
||||
|
||||
await expectPathname(page, options.pathPattern);
|
||||
await expectQueryParam(page, 'q', options.sample.targetTitle);
|
||||
await expectWorkspaceSearchScope(
|
||||
page,
|
||||
`${options.sectionLabel} / All Items`
|
||||
);
|
||||
const contentLayout = page.locator('.category-content-layout');
|
||||
await expect(contentLayout).toContainText(
|
||||
flexibleTextPattern(options.sample.targetTitle),
|
||||
{ timeout: 20000 }
|
||||
);
|
||||
await expect(contentLayout).not.toContainText(
|
||||
flexibleTextPattern(options.sample.controlTitle)
|
||||
);
|
||||
}
|
||||
|
||||
async function addCurrentDetailToFavorites(page: Page): Promise<void> {
|
||||
const addButton = page
|
||||
.locator('button.favorite-btn')
|
||||
|
||||
+18
-18
@@ -1,7 +1,7 @@
|
||||
@let isEmbeddedPlayer = usesEmbeddedPlayer();
|
||||
@let streamUrl = activeStreamUrl();
|
||||
|
||||
@if (selectedCategoryId()) {
|
||||
@if (showLiveChannelSidebar()) {
|
||||
<div
|
||||
class="sidebar"
|
||||
[class.sidebar-collapsed]="isSidebarCollapsed()"
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="sidebar-header">
|
||||
<div class="category-heading">
|
||||
<h2 class="category-title">
|
||||
{{ selectedCategoryInfo()?.name }}
|
||||
{{ selectedCategoryInfo()?.name ?? 'All Items' }}
|
||||
</h2>
|
||||
@if (selectedCategoryInfo()?.count) {
|
||||
<span class="category-count">{{
|
||||
@@ -91,22 +91,7 @@
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (!selectedCategoryId()) {
|
||||
@if (isSelectedTypeContentLoading() || workspaceSearchTerm()) {
|
||||
<div class="live-loading-content">
|
||||
<app-portal-channels-list
|
||||
[sortMode]="liveChannelSortMode()"
|
||||
[searchTermInput]="workspaceSearchTerm()"
|
||||
(playClicked)="playLive($event)"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<app-portal-empty-state
|
||||
icon="live_tv"
|
||||
[message]="'PORTALS.SELECT_CATEGORY' | translate"
|
||||
/>
|
||||
}
|
||||
} @else if (streamUrl) {
|
||||
@if (streamUrl) {
|
||||
@if (isEmbeddedPlayer) {
|
||||
<div class="video-player">
|
||||
<app-web-player-view
|
||||
@@ -148,6 +133,21 @@
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
} @else if (!showLiveChannelSidebar()) {
|
||||
@if (isSelectedTypeContentLoading() || workspaceSearchTerm()) {
|
||||
<div class="live-loading-content">
|
||||
<app-portal-channels-list
|
||||
[sortMode]="liveChannelSortMode()"
|
||||
[searchTermInput]="workspaceSearchTerm()"
|
||||
(playClicked)="playLive($event)"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<app-portal-empty-state
|
||||
icon="live_tv"
|
||||
[message]="'PORTALS.SELECT_CATEGORY' | translate"
|
||||
/>
|
||||
}
|
||||
} @else {
|
||||
<app-portal-empty-state
|
||||
icon="live_tv"
|
||||
|
||||
+33
-3
@@ -355,17 +355,47 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const list = fixture.debugElement.query(
|
||||
const lists = fixture.debugElement.queryAll(
|
||||
By.directive(StubPortalChannelsListComponent)
|
||||
);
|
||||
const list = lists[0];
|
||||
|
||||
expect(list).not.toBeNull();
|
||||
expect(lists).toHaveLength(1);
|
||||
expect(list.componentInstance.searchTermInput() as string).toBe(
|
||||
'world'
|
||||
);
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('app-portal-empty-state')
|
||||
).toBeNull();
|
||||
fixture.nativeElement.querySelector(
|
||||
'.sidebar [data-test-id="portal-channels-list-stub"]'
|
||||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('shows embedded playback after selecting a channel from live root search results', () => {
|
||||
selectedCategoryId.set(null);
|
||||
selectedTypeContentLoading.set(false);
|
||||
routeQueryParamMap.next(convertToParamMap({ q: 'world' }));
|
||||
fixture.detectChanges();
|
||||
|
||||
const list = fixture.debugElement.query(
|
||||
By.directive(StubPortalChannelsListComponent)
|
||||
);
|
||||
|
||||
list.componentInstance.playClicked.emit(sampleChannel);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(xtreamStore.constructStreamUrl).toHaveBeenCalledWith(
|
||||
sampleChannel
|
||||
);
|
||||
expect(
|
||||
fixture.debugElement.query(By.directive(StubWebPlayerViewComponent))
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
fixture.nativeElement.querySelector(
|
||||
'.sidebar [data-test-id="portal-channels-list-stub"]'
|
||||
)
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders the current EPG program in the collapsible panel summary', () => {
|
||||
|
||||
+4
-5
@@ -53,11 +53,7 @@ import {
|
||||
LiveEpgPanelSummary,
|
||||
WebPlayerViewComponent,
|
||||
} from 'shared-portals';
|
||||
import {
|
||||
EpgItem,
|
||||
EpgProgram,
|
||||
ResolvedPortalPlayback,
|
||||
} from 'shared-interfaces';
|
||||
import { EpgItem, EpgProgram, ResolvedPortalPlayback } from 'shared-interfaces';
|
||||
import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@@ -124,6 +120,9 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
|
||||
readonly workspaceSearchTerm = computed(() =>
|
||||
this.isWorkspaceLayout ? this.routeSearchTerm() : ''
|
||||
);
|
||||
readonly showLiveChannelSidebar = computed(
|
||||
() => !!this.selectedCategoryId() || !!this.workspaceSearchTerm()
|
||||
);
|
||||
private readonly pendingAutoOpenLiveItemId = signal<number | null>(null);
|
||||
readonly selectedLiveItem = computed<XtreamLiveChannelItem | null>(() => {
|
||||
if (this.xtreamStore.selectedContentType() !== 'live') {
|
||||
|
||||
Reference in new issue
Block a user