Files
iptvnator/apps/web/src/app/app.routes.spec.ts
T
4gray beb62db314 feat(settings): add shared web player controls toggle (#1198)
* docs(playback): design shared controls setting

* docs(playback): plan shared controls setting

* feat(settings): persist shared web controls preference

* test(settings): harden shared controls normalization coverage

* feat(settings): expose shared web controls toggle

* fix(settings): label shared controls toggle

* feat(playback): resolve shared controls from settings

* test(playback): cover shared controls setting

* docs(playback): document shared controls preference

* fix(playback): await settings before host creation

* fix(settings): normalize shared controls updates
2026-07-17 13:38:06 +02:00

195 lines
6.5 KiB
TypeScript

import { TestBed } from '@angular/core/testing';
import { SettingsStore } from '@iptvnator/services';
describe('app routes', () => {
let workspaceRoute: import('@angular/router').Route | undefined;
let workspaceChildren: Array<{
canActivate?: unknown[];
data?: Record<string, unknown>;
loadChildren?: unknown;
loadComponent?: unknown;
path?: string;
redirectTo?: unknown;
}> = [];
let resolveElectronOnlyGlobalSearchRoute: (
runtime: { isElectron: boolean },
router: { parseUrl: (url: string) => unknown }
) => unknown;
beforeAll(async () => {
jest.unstable_mockModule(
'@iptvnator/playlist/m3u/feature-player',
() => ({
createM3uWorkspaceRoutes: () => [
{
path: '',
pathMatch: 'full',
redirectTo: 'all',
},
{
path: 'favorites',
data: {
mode: 'favorites',
portalType: 'm3u',
defaultScope: 'playlist',
},
},
{
path: 'recent',
data: {
mode: 'recent',
portalType: 'm3u',
defaultScope: 'playlist',
},
},
{
path: ':view',
},
],
})
);
const appRoutes = await import('./app.routes');
const { routes } = appRoutes;
workspaceRoute = routes.find((route) => route.path === 'workspace');
workspaceChildren = workspaceRoute?.children ?? [];
resolveElectronOnlyGlobalSearchRoute =
appRoutes.resolveElectronOnlyGlobalSearchRoute;
});
afterEach(() => {
TestBed.resetTestingModule();
});
it('waits for settings before activating workspace children', async () => {
let releaseSettings!: () => void;
const settingsPending = new Promise<void>((resolve) => {
releaseSettings = resolve;
});
const loadSettings = jest.fn(() => settingsPending);
TestBed.configureTestingModule({
providers: [
{
provide: SettingsStore,
useValue: { loadSettings },
},
],
});
const settingsReadyResolver =
workspaceRoute?.resolve?.['settingsReady'];
expect(typeof settingsReadyResolver).toBe('function');
if (typeof settingsReadyResolver !== 'function') {
return;
}
const resolution = TestBed.runInInjectionContext(() =>
Promise.resolve(
(
settingsReadyResolver as import('@angular/router').ResolveFn<void>
)(
{} as import('@angular/router').ActivatedRouteSnapshot,
{} as import('@angular/router').RouterStateSnapshot
)
)
);
let resolved = false;
void resolution.then(() => {
resolved = true;
});
await Promise.resolve();
expect(loadSettings).toHaveBeenCalledTimes(1);
expect(resolved).toBe(false);
releaseSettings();
await expect(resolution).resolves.toBeUndefined();
expect(resolved).toBe(true);
});
it('lazy-loads M3U workspace routes through the feature boundary', async () => {
const playlistRoute = workspaceChildren.find(
(route) => route.path === 'playlists/:id'
);
const loadChildren = playlistRoute?.loadChildren as
| (() => Promise<typeof workspaceChildren>)
| undefined;
const m3uRoutes = (await loadChildren?.()) ?? [];
const defaultRoute = m3uRoutes.find((route) => route.path === '');
const favoritesRoute = m3uRoutes.find(
(route) => route.path === 'favorites'
);
const recentRoute = m3uRoutes.find((route) => route.path === 'recent');
const playerRoute = m3uRoutes.find((route) => route.path === ':view');
expect(defaultRoute?.redirectTo).toBe('all');
expect(favoritesRoute?.data).toEqual({
mode: 'favorites',
portalType: 'm3u',
defaultScope: 'playlist',
});
expect(recentRoute?.data).toEqual({
mode: 'recent',
portalType: 'm3u',
defaultScope: 'playlist',
});
expect(playerRoute).toBeDefined();
});
it('adds a shared /workspace/global-recent route', async () => {
const globalRecentRoute = workspaceChildren.find(
(route) => route.path === 'global-recent'
);
expect(globalRecentRoute?.data).toEqual({
mode: 'recent',
defaultScope: 'all',
});
expect(typeof globalRecentRoute?.loadComponent).toBe('function');
});
it('adds a routed /workspace/search global search view', async () => {
const globalSearchRoute = workspaceChildren.find(
(route) => route.path === 'search'
);
expect(globalSearchRoute?.data).toEqual({
isGlobalSearch: true,
});
expect(globalSearchRoute?.canActivate).toHaveLength(1);
expect(typeof globalSearchRoute?.loadComponent).toBe('function');
});
it('redirects the Electron-only global search route in the web runtime', async () => {
const redirectTree = { url: '/workspace/sources' };
const parseUrl = jest.fn().mockReturnValue(redirectTree);
const result = resolveElectronOnlyGlobalSearchRoute(
{ isElectron: false },
{ parseUrl }
);
expect(parseUrl).toHaveBeenCalledWith('/workspace/sources');
expect(result).toBe(redirectTree);
});
it('uses a dynamic redirect for the default /workspace child route', async () => {
const workspaceDefaultRoute = workspaceChildren.find(
(route) => route.path === ''
);
expect(typeof workspaceDefaultRoute?.redirectTo).toBe('function');
});
it('protects the dashboard route behind a visibility guard', async () => {
const dashboardRoute = workspaceChildren.find(
(route) => route.path === 'dashboard'
);
expect(dashboardRoute?.canActivate).toHaveLength(1);
});
});