import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access'; import { SettingsStore } from '../services/settings-store.service'; import { SettingsComponent } from './settings.component'; import { configureSettingsComponentTestBed, createElectronStub, createEpgBridgeStub, MatSnackBarStub, MockSettingsStore, stubSettingsSideEffects, } from './test-stubs/settings-test-harness.stub'; /** Matches the snackbar config used by `SettingsSnackbarService.error`. */ const ERROR_SNACKBAR_CONFIG = expect.objectContaining({ panelClass: ['settings-snackbar', 'settings-snackbar--error'], }); /** * Assertions target the MatDialog the component actually calls: spying on the * one returned by `TestBed.inject(MatDialog)` does not observe it. */ interface SettingsComponentPrivateTestApi { matDialog: MatDialog; } /** * Settings live in the renderer's IndexedDB, and both halves of the round trip * can fail while the UI keeps looking healthy: a failed read shows defaults as * if they were saved, and a failed write is applied in memory so it survives * until the next restart. These cover the user-visible reporting of both. */ describe('SettingsComponent storage failures', () => { let component: SettingsComponent; let fixture: ComponentFixture; let settingsStore: MockSettingsStore; let snackBar: MatSnackBarStub; let epgBridge: Partial; const originalElectron = window.electron; const componentMatDialog = (): MatDialog => (component as unknown as SettingsComponentPrivateTestApi).matDialog; beforeEach(waitForAsync(() => { epgBridge = createEpgBridgeStub(); configureSettingsComponentTestBed(epgBridge); })); beforeEach(() => { window.electron = createElectronStub(); fixture = TestBed.createComponent(SettingsComponent); settingsStore = TestBed.inject( SettingsStore ) as unknown as MockSettingsStore; snackBar = TestBed.inject(MatSnackBar) as unknown as MatSnackBarStub; component = fixture.componentInstance; stubSettingsSideEffects(component); fixture.detectChanges(); }); afterEach(() => { window.electron = originalElectron; }); it('warns when the persisted settings could not be read', async () => { settingsStore.storageFailure.set('load'); await component.ngOnInit(); expect(snackBar.open).toHaveBeenCalledWith( 'SETTINGS.SETTINGS_LOAD_FAILED', 'CLOSE', ERROR_SNACKBAR_CONFIG ); }); it('warns and keeps the dialog open when the settings write fails', async () => { settingsStore.updateSettings.mockRejectedValue( new Error('storage unavailable') ); const closeAll = jest.spyOn(componentMatDialog(), 'closeAll'); component.isDialog = true; component.onSubmit(); await fixture.whenStable(); expect(snackBar.open).toHaveBeenCalledWith( 'SETTINGS.SETTINGS_SAVE_FAILED', 'CLOSE', ERROR_SNACKBAR_CONFIG ); expect(closeAll).not.toHaveBeenCalled(); }); it('closes the dialog only after the settings write succeeded', async () => { settingsStore.updateSettings.mockResolvedValue(undefined); const closeAll = jest.spyOn(componentMatDialog(), 'closeAll'); jest.spyOn(component.epg, 'fetchConfiguredEpg').mockImplementation(); component.isDialog = true; component.onSubmit(); // save() resolves a tick after the store write: the callback and // the Electron mirror run first, so the close lands on the next // turn of the microtask queue. await fixture.whenStable(); await fixture.whenStable(); expect(closeAll).toHaveBeenCalledTimes(1); }); it('warns when a section write fails without leaving an unhandled rejection', async () => { settingsStore.updateSettings.mockRejectedValue( new Error('storage unavailable') ); component.form.selectCoverSize('large'); await fixture.whenStable(); expect(snackBar.open).toHaveBeenCalledWith( 'SETTINGS.SETTINGS_SAVE_FAILED', 'CLOSE', ERROR_SNACKBAR_CONFIG ); }); });