diff --git a/libs/playlist/shared/util/jest.config.ts b/libs/playlist/shared/util/jest.config.ts new file mode 100644 index 000000000..264b8c141 --- /dev/null +++ b/libs/playlist/shared/util/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'playlist-shared-util', + preset: '../../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../../coverage/libs/playlist/shared/util', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/libs/playlist/shared/util/project.json b/libs/playlist/shared/util/project.json index 9b55122b0..5c3b6a5c1 100644 --- a/libs/playlist/shared/util/project.json +++ b/libs/playlist/shared/util/project.json @@ -6,6 +6,14 @@ "projectType": "library", "tags": ["scope:playlist", "domain:m3u", "type:util"], "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/playlist/shared/util/jest.config.ts", + "tsConfig": "libs/playlist/shared/util/tsconfig.spec.json" + } + }, "lint": { "executor": "@nx/eslint:lint" } diff --git a/libs/playlist/shared/util/src/lib/playlist-refresh-action.service.spec.ts b/libs/playlist/shared/util/src/lib/playlist-refresh-action.service.spec.ts new file mode 100644 index 000000000..2678e3547 --- /dev/null +++ b/libs/playlist/shared/util/src/lib/playlist-refresh-action.service.spec.ts @@ -0,0 +1,182 @@ +import { TestBed } from '@angular/core/testing'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; +import { DialogService } from 'components'; +import { DatabaseService, PlaylistRefreshService } from 'services'; +import { PlaylistActions } from 'm3u-state'; +import { PlaylistMeta } from 'shared-interfaces'; +import { PlaylistRefreshActionService } from './playlist-refresh-action.service'; + +function createPlaylistMeta( + overrides: Partial = {} +): PlaylistMeta { + return { + _id: 'playlist-1', + title: 'Xtream Playlist', + serverUrl: 'http://localhost:8080', + username: 'demo', + password: 'secret', + ...overrides, + } as PlaylistMeta; +} + +describe('PlaylistRefreshActionService', () => { + let service: PlaylistRefreshActionService; + let databaseService: { + createOperationId: jest.Mock; + deleteXtreamPlaylistContent: jest.Mock; + updateXtreamPlaylistDetails: jest.Mock; + }; + let dialogService: { + openConfirmDialog: jest.Mock; + }; + let router: { + navigate: jest.Mock; + }; + let snackBar: { + open: jest.Mock; + }; + let store: { + dispatch: jest.Mock; + }; + + beforeEach(() => { + localStorage.clear(); + + databaseService = { + createOperationId: jest.fn((prefix: string) => `${prefix}-op`), + deleteXtreamPlaylistContent: jest.fn().mockResolvedValue({ + success: true, + favoritedXtreamIds: [101, 202], + recentlyViewedXtreamIds: [ + { + xtreamId: 303, + viewedAt: '2026-04-04T08:00:00.000Z', + }, + ], + hiddenCategories: [{ xtreamId: 404, type: 'live' }], + }), + updateXtreamPlaylistDetails: jest.fn().mockResolvedValue(true), + }; + dialogService = { + openConfirmDialog: jest.fn(), + }; + router = { + navigate: jest.fn().mockResolvedValue(true), + }; + snackBar = { + open: jest.fn(), + }; + store = { + dispatch: jest.fn(), + }; + + TestBed.configureTestingModule({ + providers: [ + PlaylistRefreshActionService, + { + provide: Router, + useValue: router, + }, + { + provide: Store, + useValue: store, + }, + { + provide: TranslateService, + useValue: { + instant: jest.fn((key: string) => key), + }, + }, + { + provide: MatSnackBar, + useValue: snackBar, + }, + { + provide: DialogService, + useValue: dialogService, + }, + { + provide: DatabaseService, + useValue: databaseService, + }, + { + provide: PlaylistRefreshService, + useValue: { + refreshPlaylist: jest.fn(), + }, + }, + ], + }); + + service = TestBed.inject(PlaylistRefreshActionService); + }); + + afterEach(() => { + jest.restoreAllMocks(); + localStorage.clear(); + }); + + it('stores Xtream restore data before updating playlist meta and navigating', async () => { + const item = createPlaylistMeta(); + const executionOrder: string[] = []; + let confirmPromise: Promise | undefined; + const dateNowSpy = jest + .spyOn(Date, 'now') + .mockReturnValue(1712217600000); + const originalSetItem = Storage.prototype.setItem; + const setItemSpy = jest + .spyOn(Storage.prototype, 'setItem') + .mockImplementation(function (key: string, value: string) { + executionOrder.push('setItem'); + return originalSetItem.call(this, key, value); + }); + + store.dispatch.mockImplementation((action: unknown) => { + executionOrder.push('dispatch'); + return action; + }); + router.navigate.mockImplementation((commands: unknown[]) => { + executionOrder.push('navigate'); + return Promise.resolve(Boolean(commands)); + }); + dialogService.openConfirmDialog.mockImplementation( + ({ onConfirm }: { onConfirm?: () => Promise }) => { + confirmPromise = onConfirm?.(); + } + ); + + service.refresh(item); + await confirmPromise; + + expect(setItemSpy).toHaveBeenCalledWith( + `xtream-restore-${item._id}`, + JSON.stringify({ + favoritedXtreamIds: [101, 202], + recentlyViewedXtreamIds: [ + { + xtreamId: 303, + viewedAt: '2026-04-04T08:00:00.000Z', + }, + ], + hiddenCategories: [{ xtreamId: 404, type: 'live' }], + }) + ); + expect(store.dispatch).toHaveBeenCalledWith( + PlaylistActions.updatePlaylistMeta({ + playlist: { ...item, updateDate: 1712217600000 }, + }) + ); + expect(router.navigate).toHaveBeenCalledWith([ + '/workspace', + 'xtreams', + item._id, + ]); + expect(executionOrder).toEqual(['setItem', 'dispatch', 'navigate']); + + setItemSpy.mockRestore(); + dateNowSpy.mockRestore(); + }); +}); diff --git a/libs/playlist/shared/util/src/lib/playlist-refresh-action.service.ts b/libs/playlist/shared/util/src/lib/playlist-refresh-action.service.ts index d47b42b31..6e895c4a0 100644 --- a/libs/playlist/shared/util/src/lib/playlist-refresh-action.service.ts +++ b/libs/playlist/shared/util/src/lib/playlist-refresh-action.service.ts @@ -90,22 +90,23 @@ export class PlaylistRefreshActionService { }), ]); + const restoreKey = `xtream-restore-${item._id}`; + const restorePayload = { + favoritedXtreamIds, + recentlyViewedXtreamIds, + hiddenCategories, + }; + localStorage.setItem( + restoreKey, + JSON.stringify(restorePayload) + ); + this.store.dispatch( PlaylistActions.updatePlaylistMeta({ playlist: { ...item, updateDate }, }) ); - const restoreKey = `xtream-restore-${item._id}`; - localStorage.setItem( - restoreKey, - JSON.stringify({ - favoritedXtreamIds, - recentlyViewedXtreamIds, - hiddenCategories, - }) - ); - await this.router.navigate([ '/workspace', 'xtreams', diff --git a/libs/playlist/shared/util/src/test-setup.ts b/libs/playlist/shared/util/src/test-setup.ts new file mode 100644 index 000000000..0d88648e3 --- /dev/null +++ b/libs/playlist/shared/util/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/playlist/shared/util/tsconfig.spec.json b/libs/playlist/shared/util/tsconfig.spec.json new file mode 100644 index 000000000..c0bb248be --- /dev/null +++ b/libs/playlist/shared/util/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"], + "moduleResolution": "node10" + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +}