Files
iptvnator/apps/web/src/app/settings/settings-playlist-reset.facade.spec.ts
T
4gray e2300bea11 test(settings): split the settings spec along the facade seams (#1277)
settings.component.spec.ts was 1516 lines and the last settings file in the
max-lines baseline. The behaviour that moved into facades now has its own
specs, driven directly instead of through the rendered page.

- settings-app-update.facade.spec.ts: status polling/retry, bridge actions,
  release notes dialog, version messaging, dispose
- settings-epg.facade.spec.ts: refresh, clear flow, post-save re-fetch
- settings-playlist-reset.facade.spec.ts: summary, dialog, Electron progress,
  browser fallback, failure snackbar
- settings-backup.facade.spec.ts: desktop export, browser download fallback
- settings.component.spec.ts keeps the page shell, the facade lifecycle seam
  and runtime capabilities; settings.component.form.spec.ts takes hydration,
  section outputs, dashboard controls and submit
- settings-section-scroll.directive.spec.ts gives the directive its first spec
- shared TestBed fixtures live in settings/test-stubs/, kept out of both the
  app build (.stub.ts) and the coverage ratchet (test-stubs/)

105 settings tests, up from 94; every file is under the 400-line limit, so
settings.component.spec.ts leaves the baseline.
2026-07-27 08:31:14 +02:00

245 lines
8.7 KiB
TypeScript

import { TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import {
PlaylistActions,
selectAllPlaylistsMeta,
selectIsEpgAvailable,
} from '@iptvnator/m3u-state';
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
import { Store } from '@ngrx/store';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { MockProvider } from 'ng-mocks';
import { of } from 'rxjs';
import { SettingsPlaylistResetFacade } from './settings-playlist-reset.facade';
import { SettingsSnackbarService } from './settings-snackbar.service';
import {
createDialogRef,
createElectronStub,
createPlaylistMeta,
MatSnackBarStub,
} from './test-stubs/settings-test-harness.stub';
describe('SettingsPlaylistResetFacade', () => {
let facade: SettingsPlaylistResetFacade;
let matDialog: MatDialog;
let databaseService: DatabaseService;
let playlistsService: PlaylistsService;
let store: Store;
let mockStore: MockStore;
let snackBar: MatSnackBarStub;
let translate: TranslateService;
const originalElectron = window.electron;
/** The component normally supplies a paint frame; tests skip the wait */
const noWait = () => Promise.resolve();
function setPlaylists(playlists: PlaylistMeta[]): void {
mockStore.overrideSelector(selectAllPlaylistsMeta, playlists);
mockStore.refreshState();
}
beforeEach(() => {
window.electron = createElectronStub();
TestBed.configureTestingModule({
providers: [
SettingsPlaylistResetFacade,
SettingsSnackbarService,
MockProvider(MatDialog, { open: jest.fn() }),
{ provide: MatSnackBar, useClass: MatSnackBarStub },
MockProvider(DatabaseService, {
createOperationId: jest
.fn()
.mockReturnValue('delete-all-op'),
deleteAllPlaylists: jest.fn().mockResolvedValue(true),
}),
MockProvider(PlaylistsService, {
removeAll: jest.fn().mockReturnValue(of(undefined)),
}),
provideMockStore({
selectors: [
{ selector: selectAllPlaylistsMeta, value: [] },
{ selector: selectIsEpgAvailable, value: false },
],
}),
],
imports: [TranslateModule.forRoot()],
});
facade = TestBed.inject(SettingsPlaylistResetFacade);
matDialog = TestBed.inject(MatDialog);
databaseService = TestBed.inject(DatabaseService);
playlistsService = TestBed.inject(PlaylistsService);
store = TestBed.inject(Store);
mockStore = TestBed.inject(MockStore);
snackBar = TestBed.inject(MatSnackBar) as unknown as MatSnackBarStub;
translate = TestBed.inject(TranslateService);
jest.spyOn(translate, 'instant').mockImplementation(
(key: string, params?: Record<string, number>) =>
key === 'SETTINGS.REMOVE_ALL_PROGRESS'
? `${params?.current}/${params?.total}`
: key
);
});
afterEach(() => {
window.electron = originalElectron;
});
it('refuses the global wipe when there are no playlists', () => {
setPlaylists([]);
expect(facade.deleteSummary().total).toBe(0);
expect(facade.canRemoveAll()).toBe(false);
facade.confirmAndRemoveAll(noWait);
expect(matDialog.open).not.toHaveBeenCalled();
});
it('opens the dedicated delete-all dialog with the current playlist type summary', () => {
setPlaylists([
createPlaylistMeta({ _id: 'm3u-1' }),
createPlaylistMeta({
_id: 'xtream-1',
serverUrl: 'http://xtream.example',
}),
createPlaylistMeta({
_id: 'stalker-1',
macAddress: '00:11:22:33:44:55',
}),
]);
const openSpy = jest
.spyOn(matDialog, 'open')
.mockReturnValue(createDialogRef(false));
facade.confirmAndRemoveAll(noWait);
expect(facade.deleteSummary()).toEqual({
total: 3,
m3u: 1,
xtream: 1,
stalker: 1,
});
expect(openSpy).toHaveBeenCalledWith(
expect.any(Function),
expect.objectContaining({
data: {
summary: {
total: 3,
m3u: 1,
xtream: 1,
stalker: 1,
},
},
})
);
});
it('tracks Electron delete-all progress and dispatches store cleanup after success', async () => {
setPlaylists([
createPlaylistMeta({ _id: 'm3u-1' }),
createPlaylistMeta({
_id: 'xtream-1',
serverUrl: 'http://xtream.example',
}),
]);
const dispatchSpy = jest.spyOn(store, 'dispatch');
jest.spyOn(matDialog, 'open').mockReturnValue(createDialogRef(true));
let resolveDelete: (value: boolean) => void = () => undefined;
(databaseService.deleteAllPlaylists as jest.Mock).mockImplementation(
({
onEvent,
}: {
onEvent?: (event: {
operation: string;
status: string;
current?: number;
total?: number;
}) => void;
}) => {
onEvent?.({
operation: 'delete-all-playlists',
status: 'progress',
current: 3,
total: 7,
});
return new Promise<boolean>((resolve) => {
resolveDelete = resolve;
});
}
);
facade.confirmAndRemoveAll(noWait);
await Promise.resolve();
expect(facade.isRemovingAllPlaylists()).toBe(true);
expect(facade.removeAllProgressLabel()).toBe('3/7');
expect(databaseService.deleteAllPlaylists).toHaveBeenCalledWith(
expect.objectContaining({
operationId: 'delete-all-op',
onEvent: expect.any(Function),
})
);
resolveDelete(true);
await new Promise<void>((resolve) => setTimeout(resolve, 0));
expect(facade.isRemovingAllPlaylists()).toBe(false);
expect(facade.removeAllProgress()).toBeNull();
expect(dispatchSpy).toHaveBeenCalledWith(
PlaylistActions.removeAllPlaylists()
);
expect(snackBar.open).toHaveBeenCalledWith(
'SETTINGS.PLAYLISTS_REMOVED',
undefined,
{
duration: 2000,
horizontalPosition: 'center',
panelClass: ['settings-snackbar'],
verticalPosition: 'bottom',
}
);
});
it('falls back to PlaylistsService.removeAll outside Electron', async () => {
window.electron = undefined as unknown as typeof window.electron;
setPlaylists([createPlaylistMeta({ _id: 'browser-m3u' })]);
const dispatchSpy = jest.spyOn(store, 'dispatch');
jest.spyOn(matDialog, 'open').mockReturnValue(createDialogRef(true));
facade.confirmAndRemoveAll(noWait);
await new Promise<void>((resolve) => setTimeout(resolve, 0));
expect(playlistsService.removeAll).toHaveBeenCalled();
expect(databaseService.deleteAllPlaylists).not.toHaveBeenCalled();
expect(dispatchSpy).toHaveBeenCalledWith(
PlaylistActions.removeAllPlaylists()
);
});
it('surfaces a failure snackbar when the wipe reports no success', async () => {
setPlaylists([createPlaylistMeta({ _id: 'm3u-1' })]);
(databaseService.deleteAllPlaylists as jest.Mock).mockResolvedValue(
false
);
jest.spyOn(matDialog, 'open').mockReturnValue(createDialogRef(true));
jest.spyOn(console, 'error').mockImplementation();
facade.confirmAndRemoveAll(noWait);
await new Promise<void>((resolve) => setTimeout(resolve, 0));
expect(facade.isRemovingAllPlaylists()).toBe(false);
expect(snackBar.open).toHaveBeenCalledWith(
'SETTINGS.PLAYLISTS_REMOVE_FAILED',
undefined,
expect.objectContaining({ panelClass: ['settings-snackbar'] })
);
});
});