mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 11:06:16 -08:00
Merge pull request #903 from sapireli/search_all_fix
Fix Xtream section search across categories
This commit is contained in:
8 files changed
+383
-63
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')
|
||||
|
||||
+65
-3
@@ -9,8 +9,37 @@ const TestSelectionStore = signalStore(
|
||||
vod: 'ready',
|
||||
series: 'ready',
|
||||
},
|
||||
liveCategories: [],
|
||||
liveStreams: [],
|
||||
liveCategories: [
|
||||
{
|
||||
id: 50,
|
||||
category_id: '50',
|
||||
category_name: 'News',
|
||||
type: 'live',
|
||||
},
|
||||
{
|
||||
id: 60,
|
||||
category_id: '60',
|
||||
category_name: 'Sports',
|
||||
type: 'live',
|
||||
},
|
||||
],
|
||||
liveStreams: [
|
||||
{
|
||||
xtream_id: 201,
|
||||
category_id: '50',
|
||||
title: 'World News',
|
||||
},
|
||||
{
|
||||
xtream_id: 202,
|
||||
category_id: '60',
|
||||
title: 'World Sports',
|
||||
},
|
||||
{
|
||||
xtream_id: 203,
|
||||
category_id: '60',
|
||||
title: 'Match Day',
|
||||
},
|
||||
],
|
||||
vodCategories: [
|
||||
{
|
||||
id: 10,
|
||||
@@ -175,7 +204,7 @@ describe('withSelection', () => {
|
||||
).toEqual(['Stargate SG-1', 'Stargate Atlantis']);
|
||||
});
|
||||
|
||||
it('keeps VOD search scoped to the selected category', () => {
|
||||
it('keeps VOD category route search scoped to the selected category', () => {
|
||||
store.setSelectedContentType('vod');
|
||||
store.setSelectedCategory(10);
|
||||
|
||||
@@ -185,4 +214,37 @@ describe('withSelection', () => {
|
||||
store.selectItemsFromSelectedCategory().map((item) => item.title)
|
||||
).toEqual(['First']);
|
||||
});
|
||||
|
||||
it('keeps series category route search scoped to the selected category', () => {
|
||||
store.setSelectedContentType('series');
|
||||
store.setSelectedCategory(30);
|
||||
|
||||
store.setCategorySearchTerm('stargate');
|
||||
|
||||
expect(
|
||||
store.selectItemsFromSelectedCategory().map((item) => item.title)
|
||||
).toEqual(['Stargate SG-1']);
|
||||
});
|
||||
|
||||
it('keeps live category route search scoped to the selected category', () => {
|
||||
store.setSelectedContentType('live');
|
||||
store.setSelectedCategory(50);
|
||||
|
||||
store.setCategorySearchTerm('world');
|
||||
|
||||
expect(
|
||||
store.selectItemsFromSelectedCategory().map((item) => item.title)
|
||||
).toEqual(['World News']);
|
||||
});
|
||||
|
||||
it('filters all live items across categories when no category is selected', () => {
|
||||
store.setSelectedContentType('live');
|
||||
store.setSelectedCategory(null);
|
||||
|
||||
store.setCategorySearchTerm('world');
|
||||
|
||||
expect(
|
||||
store.selectItemsFromSelectedCategory().map((item) => item.title)
|
||||
).toEqual(['World News', 'World Sports']);
|
||||
});
|
||||
});
|
||||
@@ -276,19 +276,19 @@ export function withSelection() {
|
||||
: sortedContent();
|
||||
|
||||
filtered = filterBySearchTerm(filtered, searchTerm);
|
||||
return categoryId
|
||||
return categoryId || searchTerm
|
||||
? sortByMode(filtered, sortMode, categoryType)
|
||||
: filtered;
|
||||
}
|
||||
|
||||
if (!categoryId) {
|
||||
return sortedContent();
|
||||
return filterBySearchTerm(sortedContent(), searchTerm);
|
||||
}
|
||||
|
||||
const filtered = content.filter(
|
||||
(item) => Number(item.category_id) === categoryId
|
||||
);
|
||||
return filtered;
|
||||
return filterBySearchTerm(filtered, searchTerm);
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
+19
-19
@@ -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">{{
|
||||
@@ -92,23 +92,7 @@
|
||||
<mat-icon>chevron_right</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (!selectedCategoryId()) {
|
||||
@if (isSelectedTypeContentLoading()) {
|
||||
<div class="live-loading-content">
|
||||
<app-portal-channels-list
|
||||
[sortMode]="liveChannelSortMode()"
|
||||
[searchTermInput]="workspaceSearchTerm()"
|
||||
(playClicked)="playLive($event)"
|
||||
(playbackRequested)="playLive($event, true)"
|
||||
/>
|
||||
</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
|
||||
@@ -150,6 +134,22 @@
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
} @else if (!showLiveChannelSidebar()) {
|
||||
@if (isSelectedTypeContentLoading()) {
|
||||
<div class="live-loading-content">
|
||||
<app-portal-channels-list
|
||||
[sortMode]="liveChannelSortMode()"
|
||||
[searchTermInput]="workspaceSearchTerm()"
|
||||
(playClicked)="playLive($event)"
|
||||
(playbackRequested)="playLive($event, true)"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<app-portal-empty-state
|
||||
icon="live_tv"
|
||||
[message]="'PORTALS.SELECT_CATEGORY' | translate"
|
||||
/>
|
||||
}
|
||||
} @else {
|
||||
<app-portal-empty-state
|
||||
icon="live_tv"
|
||||
|
||||
+58
-3
@@ -1,10 +1,11 @@
|
||||
import { Directive, Component, input, output, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute, convertToParamMap } from '@angular/router';
|
||||
import { MockPipe } from 'ng-mocks';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import {
|
||||
LIVE_EPG_PANEL_STATE_STORAGE_KEY,
|
||||
LIVE_SIDEBAR_STATE_STORAGE_KEY,
|
||||
@@ -105,6 +106,9 @@ class StubResizableDirective {}
|
||||
describe('LiveStreamLayoutComponent', () => {
|
||||
let fixture: ComponentFixture<LiveStreamLayoutComponent>;
|
||||
let component: LiveStreamLayoutComponent;
|
||||
let routeQueryParamMap: BehaviorSubject<
|
||||
ReturnType<typeof convertToParamMap>
|
||||
>;
|
||||
const fixedNow = new Date('2026-04-05T12:00:00.000Z');
|
||||
|
||||
const sampleChannel = {
|
||||
@@ -194,6 +198,7 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
selectedContentType.set('live');
|
||||
selectedItem.set(sampleChannel);
|
||||
currentPlaylist.set(playlist);
|
||||
routeQueryParamMap = new BehaviorSubject(convertToParamMap({}));
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LiveStreamLayoutComponent, NoopAnimationsModule],
|
||||
@@ -205,11 +210,11 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
data: {},
|
||||
queryParamMap: convertToParamMap({}),
|
||||
},
|
||||
queryParamMap: of(convertToParamMap({})),
|
||||
queryParamMap: routeQueryParamMap.asObservable(),
|
||||
pathFromRoot: [
|
||||
{
|
||||
snapshot: {
|
||||
data: {},
|
||||
data: { layout: 'workspace' },
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -350,6 +355,56 @@ describe('LiveStreamLayoutComponent', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('shows the cross-category live channel list while searching from the live root', () => {
|
||||
selectedCategoryId.set(null);
|
||||
selectedTypeContentLoading.set(false);
|
||||
routeQueryParamMap.next(convertToParamMap({ q: 'world' }));
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
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(
|
||||
'.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', () => {
|
||||
epgItems.set([
|
||||
buildEpgItem(
|
||||
|
||||
+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';
|
||||
import { SettingsStore } from 'services';
|
||||
@@ -126,6 +122,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') {
|
||||
|
||||
+44
-21
@@ -17,10 +17,7 @@ import {
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import {
|
||||
PlaylistsService,
|
||||
SettingsStore,
|
||||
} from 'services';
|
||||
import { PlaylistsService, SettingsStore } from 'services';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
import {
|
||||
WorkspaceStartupPreferencesService,
|
||||
@@ -285,7 +282,8 @@ describe('WorkspaceShellFacade', () => {
|
||||
params?: Record<string, string | number>
|
||||
) => {
|
||||
if (
|
||||
key === 'WORKSPACE.SHELL.XTREAM_IMPORT_PROGRESS' &&
|
||||
key ===
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_PROGRESS' &&
|
||||
params
|
||||
) {
|
||||
return `${params.type} imported: ${params.current} / ${params.total}`;
|
||||
@@ -325,7 +323,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
});
|
||||
|
||||
it('routes dashboard search Enter into the active Xtream playlist search', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
const xtreamStore = TestBed.inject(
|
||||
XtreamStore
|
||||
) as unknown as MockXtreamStore;
|
||||
|
||||
facade.currentUrl.set('/workspace/dashboard');
|
||||
router.navigate.mockClear();
|
||||
@@ -342,7 +342,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
});
|
||||
|
||||
it('uses a loading label for remote Xtream fetch phases', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
const xtreamStore = TestBed.inject(
|
||||
XtreamStore
|
||||
) as unknown as MockXtreamStore;
|
||||
const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService);
|
||||
|
||||
xtreamStore.currentImportPhase.set('loading-categories');
|
||||
@@ -446,7 +448,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
});
|
||||
|
||||
it('builds a type-aware xtream import progress label', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
const xtreamStore = TestBed.inject(
|
||||
XtreamStore
|
||||
) as unknown as MockXtreamStore;
|
||||
const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService);
|
||||
|
||||
xtreamStore.activeImportContentType.set('vod');
|
||||
@@ -486,9 +490,12 @@ describe('WorkspaceShellFacade', () => {
|
||||
facade.onSearchEnter('');
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/workspace/sources', {
|
||||
replaceUrl: true,
|
||||
});
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(
|
||||
'/workspace/sources',
|
||||
{
|
||||
replaceUrl: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to the global favorites route', () => {
|
||||
@@ -513,7 +520,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
);
|
||||
expect(storeDispatch).toHaveBeenCalled();
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^\/workspace\/stalker\/pl-1\/recent\?refresh=/),
|
||||
expect.stringMatching(
|
||||
/^\/workspace\/stalker\/pl-1\/recent\?refresh=/
|
||||
),
|
||||
{
|
||||
replaceUrl: true,
|
||||
}
|
||||
@@ -533,7 +542,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
});
|
||||
|
||||
it('applies q to Xtream category search on vod routes', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
const xtreamStore = TestBed.inject(
|
||||
XtreamStore
|
||||
) as unknown as MockXtreamStore;
|
||||
|
||||
facade.currentUrl.set('/workspace/xtreams/pl-1/vod?q=neo');
|
||||
(facade as { syncSearchFromRoute: () => void }).syncSearchFromRoute();
|
||||
@@ -543,6 +554,19 @@ describe('WorkspaceShellFacade', () => {
|
||||
expect(xtreamStore.setSearchTerm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies q to Xtream category search on live routes', () => {
|
||||
const xtreamStore = TestBed.inject(
|
||||
XtreamStore
|
||||
) as unknown as MockXtreamStore;
|
||||
|
||||
facade.currentUrl.set('/workspace/xtreams/pl-1/live?q=world');
|
||||
(facade as { syncSearchFromRoute: () => void }).syncSearchFromRoute();
|
||||
TestBed.flushEffects();
|
||||
|
||||
expect(xtreamStore.setCategorySearchTerm).toHaveBeenCalledWith('world');
|
||||
expect(xtreamStore.setSearchTerm).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('enables local-filter search on playlist favorites routes', () => {
|
||||
facade.currentUrl.set('/workspace/playlists/pl-1/favorites?q=news');
|
||||
(facade as { syncSearchFromRoute: () => void }).syncSearchFromRoute();
|
||||
@@ -586,9 +610,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
});
|
||||
|
||||
it('persists the last restorable route from navigation events', () => {
|
||||
expect(startupPreferences.persistLastRestorablePath).toHaveBeenCalledWith(
|
||||
'/workspace/xtreams/pl-1/vod'
|
||||
);
|
||||
expect(
|
||||
startupPreferences.persistLastRestorablePath
|
||||
).toHaveBeenCalledWith('/workspace/xtreams/pl-1/vod');
|
||||
});
|
||||
|
||||
it('uses the translated recent scope label on the global recent route', () => {
|
||||
@@ -689,9 +713,9 @@ describe('WorkspaceShellFacade', () => {
|
||||
|
||||
expect(commands[0]?.id).toBe('clear-current-favorites');
|
||||
expect(commands[0]?.group).toBe('view');
|
||||
expect(commands.some((command) => command.id === 'playlist-search')).toBe(
|
||||
false
|
||||
);
|
||||
expect(
|
||||
commands.some((command) => command.id === 'playlist-search')
|
||||
).toBe(false);
|
||||
|
||||
unregister();
|
||||
});
|
||||
@@ -701,8 +725,7 @@ describe('WorkspaceShellFacade', () => {
|
||||
open: jest.Mock;
|
||||
};
|
||||
dialog.open.mockReturnValueOnce({
|
||||
afterClosed: () =>
|
||||
of({ commandId: 'open-settings', query: '' }),
|
||||
afterClosed: () => of({ commandId: 'open-settings', query: '' }),
|
||||
});
|
||||
|
||||
facade.openCommandPalette();
|
||||
|
||||
+9
-6
@@ -576,7 +576,11 @@ export class WorkspaceShellFacade {
|
||||
return;
|
||||
}
|
||||
|
||||
if (section === 'vod' || section === 'series') {
|
||||
if (
|
||||
section === 'vod' ||
|
||||
section === 'series' ||
|
||||
section === 'live'
|
||||
) {
|
||||
this.xtreamStore.setCategorySearchTerm(term);
|
||||
}
|
||||
});
|
||||
@@ -617,8 +621,8 @@ export class WorkspaceShellFacade {
|
||||
}
|
||||
|
||||
openActiveExternalSessionTarget(): void {
|
||||
const playlistId = this.externalPlaybackSession()?.contentInfo
|
||||
?.playlistId;
|
||||
const playlistId =
|
||||
this.externalPlaybackSession()?.contentInfo?.playlistId;
|
||||
if (!playlistId) return;
|
||||
|
||||
const playlist = this.playlists().find((p) => p._id === playlistId);
|
||||
@@ -792,7 +796,8 @@ export class WorkspaceShellFacade {
|
||||
}
|
||||
|
||||
private readonly commandBuilderActions: CommandBuilderActions = {
|
||||
openPlaylistSearch: (query) => this.openPlaylistSearchFromPalette(query),
|
||||
openPlaylistSearch: (query) =>
|
||||
this.openPlaylistSearchFromPalette(query),
|
||||
refreshCurrentPlaylist: () => this.refreshCurrentPlaylist(),
|
||||
openPlaylistInfo: () => this.openPlaylistInfo(),
|
||||
openAccountInfo: () => this.openAccountInfo(),
|
||||
@@ -806,7 +811,6 @@ export class WorkspaceShellFacade {
|
||||
: this.workspaceActions.openAddPlaylistDialog(),
|
||||
};
|
||||
|
||||
|
||||
private openPlaylistSearchFromPalette(query: string): void {
|
||||
const effectiveContext =
|
||||
this.dashboardXtreamContext() ?? this.currentContext();
|
||||
@@ -905,5 +909,4 @@ export class WorkspaceShellFacade {
|
||||
): string {
|
||||
return this.translate.instant(key, params);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user