mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
test(settings): move the storage-failure coverage to its own spec
Rebase follow-up. #1274 split the settings page into facades and #1277 split its spec along the same seams, so this PR's renderer changes moved: - the failed-read warning is now raised by `SettingsFormFacade.loadSettings()`, which owns the store call - the cover-size and EPG-view-mode writes moved into the facade, so their rejection handling went with them - `showStorageFailure()` became `SettingsSnackbarService.storageFailure()`, because the component and the facade both need it and neither can reach the other's copy - `onSubmit()` still owns the dialog, so the catch and the "close only after the write succeeded" ordering stay in the component The four regression tests live in `settings.component.storage-failure.spec.ts` rather than being appended to `settings.component.form.spec.ts`, which would have pushed that file to 427 lines, past the 400-line lint maximum. They assert against the MatDialog the component actually calls — spying on the instance from `TestBed.inject(MatDialog)` does not observe it — and one of them stubs the post-save EPG fetch, since a throw there would otherwise land in the same catch and look like a failed write. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9f8713bdcb
commit
4779642e6e
3 files changed
+128
-78
No files matched your search
@@ -31,19 +31,6 @@ export class SettingsSnackbarService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Failures need more reading time than the 2s confirmation toast, and a
|
||||
* dismiss action so the message can stay up until it is acknowledged.
|
||||
*/
|
||||
error(message: string, dismissLabel: string): void {
|
||||
this.snackBar.open(message, dismissLabel, {
|
||||
duration: 10000,
|
||||
horizontalPosition: 'center',
|
||||
verticalPosition: 'bottom',
|
||||
panelClass: ['settings-snackbar', 'settings-snackbar--error'],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports a failed settings read/write. Both mean the same thing to the
|
||||
* user — what is on screen will not survive a restart — so they get a
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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 {
|
||||
CoverSize,
|
||||
Language,
|
||||
StartupBehavior,
|
||||
Theme,
|
||||
@@ -23,11 +21,6 @@ import {
|
||||
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'],
|
||||
});
|
||||
|
||||
/**
|
||||
* Everything the rendered settings form does: hydration from the store, the
|
||||
* section outputs that write straight through, and the submit path.
|
||||
@@ -78,18 +71,6 @@ describe('SettingsComponent form', () => {
|
||||
expect(component.settingsForm.value).toEqual(DEFAULT_SETTINGS);
|
||||
});
|
||||
|
||||
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('should get and apply custom settings', () => {
|
||||
settingsStore._setSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
@@ -267,52 +248,6 @@ describe('SettingsComponent form', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('warns and keeps the dialog open when the settings write fails', async () => {
|
||||
settingsStore.updateSettings.mockRejectedValue(
|
||||
new Error('storage unavailable')
|
||||
);
|
||||
const matDialog = TestBed.inject(MatDialog);
|
||||
const closeAll = jest.spyOn(matDialog, '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 matDialog = TestBed.inject(MatDialog);
|
||||
const closeAll = jest.spyOn(matDialog, 'closeAll');
|
||||
component.isDialog = true;
|
||||
|
||||
component.onSubmit();
|
||||
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(CoverSize.Large);
|
||||
await fixture.whenStable();
|
||||
|
||||
expect(snackBar.open).toHaveBeenCalledWith(
|
||||
'SETTINGS.SETTINGS_SAVE_FAILED',
|
||||
'CLOSE',
|
||||
ERROR_SNACKBAR_CONFIG
|
||||
);
|
||||
});
|
||||
|
||||
it('saves the shared web controls setting on submit', async () => {
|
||||
settingsStore.updateSettings.mockResolvedValue(undefined);
|
||||
component.settingsForm
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
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<SettingsComponent>;
|
||||
let settingsStore: MockSettingsStore;
|
||||
let snackBar: MatSnackBarStub;
|
||||
let epgBridge: Partial<EpgRuntimeBridgeService>;
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user