Proyecto App Windows
This commit is contained in:
@@ -0,0 +1,706 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { selectAllPlaylistsMeta, selectPlaylistsLoadingFlag } from 'm3u-state';
|
||||
import { of } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from 'services';
|
||||
import { Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
import { DashboardDataService } from './dashboard-data.service';
|
||||
|
||||
describe('DashboardDataService', () => {
|
||||
let service: DashboardDataService;
|
||||
const playlistsLoadedSignal = signal(true);
|
||||
|
||||
const createDefaultPlaylists = (): PlaylistMeta[] => [
|
||||
{
|
||||
_id: 'm3u-1',
|
||||
title: 'M3U Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
favorites: ['channel-1', 'https://example.com/stream-2.m3u8'],
|
||||
recentlyViewed: [
|
||||
{
|
||||
source: 'm3u',
|
||||
id: 'https://example.com/stream-1.m3u8',
|
||||
url: 'https://example.com/stream-1.m3u8',
|
||||
title: 'Channel One',
|
||||
channel_id: 'channel-1',
|
||||
poster_url: 'https://example.com/logo-1.png',
|
||||
tvg_id: 'tvg-1',
|
||||
tvg_name: 'Channel One',
|
||||
group_title: 'News',
|
||||
category_id: 'live',
|
||||
added_at: '2026-02-01T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: 'xtream-1',
|
||||
title: 'Xtream Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
serverUrl: 'https://example.com',
|
||||
},
|
||||
];
|
||||
|
||||
const playlistsSignal = signal<PlaylistMeta[]>(createDefaultPlaylists());
|
||||
|
||||
const storeMock = {
|
||||
selectSignal: jest.fn((selector: unknown) => {
|
||||
if (selector === selectAllPlaylistsMeta) {
|
||||
return playlistsSignal;
|
||||
}
|
||||
if (selector === selectPlaylistsLoadingFlag) {
|
||||
return playlistsLoadedSignal;
|
||||
}
|
||||
return signal(null);
|
||||
}),
|
||||
dispatch: jest.fn(),
|
||||
};
|
||||
|
||||
const dbServiceMock = {
|
||||
getGlobalRecentlyAdded: jest.fn().mockResolvedValue([]),
|
||||
getGlobalRecentlyViewed: jest.fn().mockResolvedValue([]),
|
||||
getAllGlobalFavorites: jest.fn().mockResolvedValue([]),
|
||||
removeFromFavorites: jest.fn().mockResolvedValue(undefined),
|
||||
removeRecentItem: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
const playlistMock: Playlist = {
|
||||
_id: 'm3u-1',
|
||||
title: 'M3U Playlist',
|
||||
count: 2,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
lastUsage: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
favorites: ['channel-1', 'https://example.com/stream-2.m3u8'],
|
||||
playlist: {
|
||||
items: [
|
||||
{
|
||||
id: 'channel-1',
|
||||
name: 'Channel One',
|
||||
url: 'https://example.com/stream-1.m3u8',
|
||||
tvg: {
|
||||
logo: 'https://example.com/logo-1.png',
|
||||
id: 'tvg-1',
|
||||
name: 'Channel One',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'channel-2',
|
||||
name: 'Channel Two',
|
||||
url: 'https://example.com/stream-2.m3u8',
|
||||
tvg: {
|
||||
logo: 'https://example.com/logo-2.png',
|
||||
id: 'tvg-2',
|
||||
name: 'Channel Two',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as Playlist;
|
||||
const playlistsServiceMock = {
|
||||
getPlaylistById: jest.fn().mockReturnValue(of(playlistMock)),
|
||||
setFavorites: jest.fn().mockReturnValue(of(undefined)),
|
||||
removeFromM3uRecentlyViewed: jest.fn().mockReturnValue(
|
||||
of({
|
||||
...playlistMock,
|
||||
recentlyViewed: [],
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, 'electron', {
|
||||
value: {} as Window['electron'],
|
||||
configurable: true,
|
||||
});
|
||||
playlistsLoadedSignal.set(true);
|
||||
playlistsSignal.set(createDefaultPlaylists());
|
||||
playlistsServiceMock.getPlaylistById.mockClear();
|
||||
playlistsServiceMock.getPlaylistById.mockReturnValue(of(playlistMock));
|
||||
playlistsServiceMock.setFavorites.mockClear();
|
||||
playlistsServiceMock.setFavorites.mockReturnValue(of(undefined));
|
||||
playlistsServiceMock.removeFromM3uRecentlyViewed.mockClear();
|
||||
playlistsServiceMock.removeFromM3uRecentlyViewed.mockReturnValue(
|
||||
of({
|
||||
...playlistMock,
|
||||
recentlyViewed: [],
|
||||
})
|
||||
);
|
||||
dbServiceMock.getAllGlobalFavorites.mockClear();
|
||||
dbServiceMock.getAllGlobalFavorites.mockResolvedValue([]);
|
||||
dbServiceMock.getGlobalRecentlyAdded.mockClear();
|
||||
dbServiceMock.getGlobalRecentlyAdded.mockResolvedValue([]);
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockClear();
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([]);
|
||||
dbServiceMock.removeFromFavorites.mockClear();
|
||||
dbServiceMock.removeRecentItem.mockClear();
|
||||
storeMock.dispatch.mockClear();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
DashboardDataService,
|
||||
{ provide: Store, useValue: storeMock },
|
||||
{ provide: DatabaseService, useValue: dbServiceMock },
|
||||
{
|
||||
provide: PlaylistsService,
|
||||
useValue: playlistsServiceMock,
|
||||
},
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: {
|
||||
instant: (key: string) => key,
|
||||
onLangChange: of(null),
|
||||
currentLang: 'en',
|
||||
defaultLang: 'en',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(DashboardDataService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('does not eagerly load dashboard datasets on construction', () => {
|
||||
expect(dbServiceMock.getGlobalRecentlyViewed).not.toHaveBeenCalled();
|
||||
expect(dbServiceMock.getAllGlobalFavorites).not.toHaveBeenCalled();
|
||||
expect(dbServiceMock.getGlobalRecentlyAdded).not.toHaveBeenCalled();
|
||||
expect(playlistsServiceMock.getPlaylistById).not.toHaveBeenCalled();
|
||||
expect(service.dashboardReady()).toBe(false);
|
||||
});
|
||||
|
||||
it('keeps dashboardReady false until xtream recently added finishes its first load', async () => {
|
||||
expect(service.dashboardReady()).toBe(false);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
expect(service.dashboardReady()).toBe(false);
|
||||
|
||||
await service.reloadGlobalFavorites();
|
||||
expect(service.dashboardReady()).toBe(false);
|
||||
|
||||
await service.reloadXtreamRecentlyAddedItems();
|
||||
expect(service.dashboardReady()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not wait on xtream recently added when no xtream playlists exist', async () => {
|
||||
playlistsSignal.set([
|
||||
{
|
||||
_id: 'm3u-1',
|
||||
title: 'M3U Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
expect(service.dashboardReady()).toBe(false);
|
||||
|
||||
await service.reloadGlobalFavorites();
|
||||
expect(service.dashboardReady()).toBe(true);
|
||||
});
|
||||
|
||||
it('includes M3U favorites in global favorite items', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
|
||||
expect(service.globalFavoriteItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'channel-1',
|
||||
title: 'Channel One',
|
||||
type: 'live',
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
poster_url: 'https://example.com/logo-1.png',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 'https://example.com/stream-2.m3u8',
|
||||
title: 'Channel Two',
|
||||
type: 'live',
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
poster_url: 'https://example.com/logo-2.png',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('includes Xtream movies and series in global favorite items', async () => {
|
||||
dbServiceMock.getAllGlobalFavorites.mockResolvedValue([
|
||||
{
|
||||
id: 51,
|
||||
category_id: 12,
|
||||
title: 'Action Movie',
|
||||
rating: '8.0',
|
||||
added_at: '2026-02-02T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/movie.png',
|
||||
xtream_id: 5001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
category_id: 13,
|
||||
title: 'Sci-Fi Series',
|
||||
rating: '8.7',
|
||||
added_at: '2026-02-03T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/series.png',
|
||||
xtream_id: 5002,
|
||||
type: 'series',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalFavorites();
|
||||
|
||||
expect(service.globalFavoriteItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 51,
|
||||
title: 'Action Movie',
|
||||
type: 'movie',
|
||||
source: 'xtream',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: 52,
|
||||
title: 'Sci-Fi Series',
|
||||
type: 'series',
|
||||
source: 'xtream',
|
||||
}),
|
||||
])
|
||||
);
|
||||
expect(
|
||||
service
|
||||
.globalFavoriteItems()
|
||||
.filter((item) => item.type === 'movie')
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
service
|
||||
.globalFavoriteItems()
|
||||
.filter((item) => item.type === 'series')
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('builds the M3U favorites route', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
const m3uItem = service
|
||||
.globalFavoriteItems()
|
||||
.find((item) => item.source === 'm3u');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
expect(service.getGlobalFavoriteLink(m3uItem!)).toEqual([
|
||||
'/workspace',
|
||||
'playlists',
|
||||
'm3u-1',
|
||||
'favorites',
|
||||
]);
|
||||
expect(service.getGlobalFavoriteNavigationState(m3uItem!)).toEqual({
|
||||
openLiveCollectionItem: {
|
||||
contentType: 'live',
|
||||
sourceType: 'm3u',
|
||||
playlistId: 'm3u-1',
|
||||
itemId: 'channel-1',
|
||||
title: 'Channel One',
|
||||
imageUrl: 'https://example.com/logo-1.png',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('routes Xtream non-live favorites through the global favorites page with inline detail state', async () => {
|
||||
dbServiceMock.getAllGlobalFavorites.mockResolvedValue([
|
||||
{
|
||||
id: 51,
|
||||
category_id: 12,
|
||||
title: 'Action Movie',
|
||||
rating: '8.0',
|
||||
added_at: '2026-02-02T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/movie.png',
|
||||
xtream_id: 5001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalFavorites();
|
||||
const movieItem = service
|
||||
.globalFavoriteItems()
|
||||
.find((item) => item.type === 'movie');
|
||||
|
||||
expect(movieItem).toBeDefined();
|
||||
expect(service.getGlobalFavoriteLink(movieItem!)).toEqual([
|
||||
'/workspace',
|
||||
'global-favorites',
|
||||
]);
|
||||
expect(service.getGlobalFavoriteNavigationState(movieItem!)).toEqual({
|
||||
openCollectionDetailItem: {
|
||||
item: expect.objectContaining({
|
||||
uid: 'xtream::xtream-1::movie:5001',
|
||||
name: 'Action Movie',
|
||||
contentType: 'movie',
|
||||
sourceType: 'xtream',
|
||||
playlistId: 'xtream-1',
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('removes M3U favorites via PlaylistsService', async () => {
|
||||
await service.reloadGlobalFavorites();
|
||||
const m3uItem = service
|
||||
.globalFavoriteItems()
|
||||
.find((item) => item.id === 'channel-1');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
|
||||
await service.removeGlobalFavorite(m3uItem!);
|
||||
|
||||
expect(playlistsServiceMock.setFavorites).toHaveBeenCalledWith(
|
||||
'm3u-1',
|
||||
['https://example.com/stream-2.m3u8']
|
||||
);
|
||||
});
|
||||
|
||||
it('includes M3U recently viewed items in global recent items', () => {
|
||||
expect(service.globalRecentItems()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: 'https://example.com/stream-1.m3u8',
|
||||
title: 'Channel One',
|
||||
type: 'live',
|
||||
playlist_id: 'm3u-1',
|
||||
source: 'm3u',
|
||||
xtream_id: 'https://example.com/stream-1.m3u8',
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('prioritizes recently used Xtream sources over newer imported M3U sources', async () => {
|
||||
playlistsSignal.set([
|
||||
{
|
||||
_id: 'm3u-fresh',
|
||||
title: 'Fresh Import',
|
||||
count: 1,
|
||||
importDate: '2026-04-20T12:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
},
|
||||
{
|
||||
_id: 'xtream-1',
|
||||
title: 'Xtream Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
serverUrl: 'https://example.com',
|
||||
},
|
||||
]);
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([
|
||||
{
|
||||
id: 91,
|
||||
category_id: 18,
|
||||
title: 'Recent Movie',
|
||||
rating: '7.8',
|
||||
viewed_at: '2026-04-21T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/recent-movie.png',
|
||||
xtream_id: 7001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
|
||||
expect(service.recentPlaylists().map((playlist) => playlist._id)).toEqual(
|
||||
['xtream-1', 'm3u-fresh']
|
||||
);
|
||||
});
|
||||
|
||||
it('includes M3U, Xtream, and Stalker sources when all have recent activity', async () => {
|
||||
playlistsSignal.set([
|
||||
{
|
||||
_id: 'm3u-1',
|
||||
title: 'M3U Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
favorites: [],
|
||||
recentlyViewed: [
|
||||
{
|
||||
source: 'm3u',
|
||||
id: 'https://example.com/stream-1.m3u8',
|
||||
url: 'https://example.com/stream-1.m3u8',
|
||||
title: 'Channel One',
|
||||
category_id: 'live',
|
||||
added_at: '2026-04-19T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: 'stalker-1',
|
||||
title: 'Stalker Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
macAddress: '00:11:22:33:44:55',
|
||||
recentlyViewed: [
|
||||
{
|
||||
id: 'stalker-item-1',
|
||||
name: 'Stalker Movie',
|
||||
category_id: 'vod',
|
||||
added_at: '2026-04-20T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
_id: 'xtream-1',
|
||||
title: 'Xtream Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
serverUrl: 'https://example.com',
|
||||
},
|
||||
]);
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([
|
||||
{
|
||||
id: 91,
|
||||
category_id: 18,
|
||||
title: 'Recent Movie',
|
||||
rating: '7.8',
|
||||
viewed_at: '2026-04-21T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/recent-movie.png',
|
||||
xtream_id: 7001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
|
||||
expect(service.recentPlaylists().map((playlist) => playlist._id)).toEqual(
|
||||
['xtream-1', 'stalker-1', 'm3u-1']
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to playlist metadata ordering when sources have no recent activity', async () => {
|
||||
playlistsSignal.set([
|
||||
{
|
||||
_id: 'xtream-1',
|
||||
title: 'Xtream Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-01-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
serverUrl: 'https://example.com',
|
||||
},
|
||||
{
|
||||
_id: 'stalker-1',
|
||||
title: 'Stalker Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-03-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
macAddress: '00:11:22:33:44:55',
|
||||
recentlyViewed: [],
|
||||
},
|
||||
{
|
||||
_id: 'm3u-1',
|
||||
title: 'M3U Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-02-01T00:00:00.000Z',
|
||||
updateDate: Date.parse('2026-04-01T00:00:00.000Z'),
|
||||
autoRefresh: false,
|
||||
favorites: [],
|
||||
recentlyViewed: [],
|
||||
},
|
||||
]);
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([]);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
|
||||
expect(service.recentPlaylists().map((playlist) => playlist._id)).toEqual(
|
||||
['m3u-1', 'stalker-1', 'xtream-1']
|
||||
);
|
||||
});
|
||||
|
||||
it('maps recently added Xtream rows for the widget', async () => {
|
||||
dbServiceMock.getGlobalRecentlyAdded.mockResolvedValue([
|
||||
{
|
||||
id: 11,
|
||||
category_id: 77,
|
||||
title: 'Fresh Movie',
|
||||
rating: '8.1',
|
||||
added: '1774298340',
|
||||
added_at: '1774298340',
|
||||
poster_url: 'https://example.com/poster.png',
|
||||
xtream_id: 501,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await expect(
|
||||
service.getGlobalRecentlyAddedItems('all', 25)
|
||||
).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
id: 11,
|
||||
title: 'Fresh Movie',
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
source: 'xtream',
|
||||
added_at: expect.any(String),
|
||||
}),
|
||||
]);
|
||||
expect(dbServiceMock.getGlobalRecentlyAdded).toHaveBeenCalledWith(
|
||||
'all',
|
||||
25
|
||||
);
|
||||
});
|
||||
|
||||
it('builds the Xtream recently added route', () => {
|
||||
const item = {
|
||||
id: 11,
|
||||
title: 'Fresh Series',
|
||||
type: 'series',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
added_at: '2026-03-27T14:39:00.000Z',
|
||||
category_id: 77,
|
||||
xtream_id: 501,
|
||||
poster_url: 'https://example.com/poster.png',
|
||||
source: 'xtream',
|
||||
} as const;
|
||||
|
||||
expect(service.getRecentlyAddedLink(item)).toEqual([
|
||||
'/workspace',
|
||||
'xtreams',
|
||||
'xtream-1',
|
||||
'series',
|
||||
'77',
|
||||
'501',
|
||||
]);
|
||||
expect(service.getRecentlyAddedNavigationState(item)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('builds the M3U recent route with collection auto-open state', () => {
|
||||
const m3uItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'm3u');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
expect(service.getRecentItemLink(m3uItem!)).toEqual([
|
||||
'/workspace',
|
||||
'playlists',
|
||||
'm3u-1',
|
||||
'recent',
|
||||
]);
|
||||
expect(service.getRecentItemNavigationState(m3uItem!)).toEqual({
|
||||
openLiveCollectionItem: {
|
||||
contentType: 'live',
|
||||
sourceType: 'm3u',
|
||||
playlistId: 'm3u-1',
|
||||
itemId: 'https://example.com/stream-1.m3u8',
|
||||
title: 'Channel One',
|
||||
imageUrl: 'https://example.com/logo-1.png',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('routes Xtream non-live recents through the global recent page with inline detail state', async () => {
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([
|
||||
{
|
||||
id: 91,
|
||||
category_id: 18,
|
||||
title: 'Recent Movie',
|
||||
rating: '7.8',
|
||||
viewed_at: '2026-02-04T10:00:00.000Z',
|
||||
poster_url: 'https://example.com/recent-movie.png',
|
||||
backdrop_url: 'https://example.com/recent-movie-backdrop.png',
|
||||
xtream_id: 7001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
const movieItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'xtream');
|
||||
|
||||
expect(movieItem).toEqual(
|
||||
expect.objectContaining({
|
||||
backdrop_url: 'https://example.com/recent-movie-backdrop.png',
|
||||
})
|
||||
);
|
||||
expect(service.getRecentItemLink(movieItem!)).toEqual([
|
||||
'/workspace',
|
||||
'global-recent',
|
||||
]);
|
||||
expect(service.getRecentItemNavigationState(movieItem!)).toEqual({
|
||||
openCollectionDetailItem: {
|
||||
item: expect.objectContaining({
|
||||
uid: 'xtream::xtream-1::movie:7001',
|
||||
name: 'Recent Movie',
|
||||
contentType: 'movie',
|
||||
sourceType: 'xtream',
|
||||
playlistId: 'xtream-1',
|
||||
}),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes SQLite-style Xtream recent timestamps before sorting and rendering', async () => {
|
||||
dbServiceMock.getGlobalRecentlyViewed.mockResolvedValue([
|
||||
{
|
||||
id: 91,
|
||||
category_id: 18,
|
||||
title: 'Recent Movie',
|
||||
rating: '7.8',
|
||||
viewed_at: '2026-02-04 10:00:00',
|
||||
poster_url: 'https://example.com/recent-movie.png',
|
||||
xtream_id: 7001,
|
||||
type: 'movie',
|
||||
playlist_id: 'xtream-1',
|
||||
playlist_name: 'Xtream Playlist',
|
||||
},
|
||||
]);
|
||||
|
||||
await service.reloadGlobalRecentItems();
|
||||
const movieItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'xtream');
|
||||
|
||||
expect(movieItem).toEqual(
|
||||
expect.objectContaining({
|
||||
viewed_at: '2026-02-04T10:00:00.000Z',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('removes M3U recently viewed via PlaylistsService', async () => {
|
||||
const m3uItem = service
|
||||
.globalRecentItems()
|
||||
.find((item) => item.source === 'm3u');
|
||||
|
||||
expect(m3uItem).toBeDefined();
|
||||
|
||||
await service.removeGlobalRecentItem(m3uItem!);
|
||||
|
||||
expect(
|
||||
playlistsServiceMock.removeFromM3uRecentlyViewed
|
||||
).toHaveBeenCalledWith('m3u-1', 'https://example.com/stream-1.m3u8');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user