refactor(runtime): gate settings backup file save by capability

This commit is contained in:
4gray committed 2026-05-22 13:41:24 +03:00
1 parent c9a0d5210e
commit 582422c512
4 files changed
+81 -2

No files matched your search

@@ -911,6 +911,65 @@ describe('SettingsComponent', () => {
expect(component.isExportingData()).toBe(false);
});
it('falls back to browser backup download when desktop file-save preload is incomplete', async () => {
fixture.destroy();
const saveFileDialog = jest.fn().mockResolvedValue('/tmp/backup.json');
window.electron = {
platform: 'linux',
saveFileDialog,
} as unknown as typeof window.electron;
const createObjectURL = jest.fn().mockReturnValue('blob:backup');
const revokeObjectURL = jest.fn();
const originalCreateObjectURL = window.URL.createObjectURL;
const originalRevokeObjectURL = window.URL.revokeObjectURL;
Object.defineProperty(window.URL, 'createObjectURL', {
configurable: true,
value: createObjectURL,
});
Object.defineProperty(window.URL, 'revokeObjectURL', {
configurable: true,
value: revokeObjectURL,
});
const clickSpy = jest
.spyOn(HTMLAnchorElement.prototype, 'click')
.mockImplementation();
try {
const partialFileSaveFixture =
TestBed.createComponent(SettingsComponent);
const partialFileSaveComponent =
partialFileSaveFixture.componentInstance;
partialFileSaveComponent.checkAppVersion = jest.fn();
partialFileSaveComponent.fetchLocalIpAddresses = jest
.fn()
.mockResolvedValue(undefined);
partialFileSaveFixture.detectChanges();
expect(partialFileSaveComponent.isDesktop).toBe(true);
expect(partialFileSaveComponent.supportsDesktopFileSave).toBe(
false
);
await partialFileSaveComponent.exportData();
expect(saveFileDialog).not.toHaveBeenCalled();
expect(createObjectURL).toHaveBeenCalledWith(expect.any(Blob));
expect(clickSpy).toHaveBeenCalled();
expect(revokeObjectURL).toHaveBeenCalledWith('blob:backup');
} finally {
clickSpy.mockRestore();
Object.defineProperty(window.URL, 'createObjectURL', {
configurable: true,
value: originalCreateObjectURL,
});
Object.defineProperty(window.URL, 'revokeObjectURL', {
configurable: true,
value: originalRevokeObjectURL,
});
}
});
it('shows a failure snackbar and skips refresh when clearing EPG data rejects', async () => {
(window.electron.clearEpgData as jest.Mock).mockRejectedValueOnce(
new Error('boom')
@@ -137,6 +137,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
/** Flag that indicates whether the app runs in electron environment */
readonly isDesktop = this.runtime.isElectron;
readonly supportsDesktopFileSave = this.runtime.supportsDesktopFileSave;
readonly supportsEpg = this.runtime.supportsEpg;
readonly supportsExternalPlayerPathSettings =
this.runtime.supportsExternalPlayerPathSettings;
@@ -699,7 +700,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
try {
const backup = await this.playlistBackupService.exportBackup();
if (this.isDesktop && window.electron?.saveFileDialog) {
if (this.supportsDesktopFileSave && window.electron) {
const savePath = await window.electron.saveFileDialog(
backup.defaultFileName,
[
+2 -1
View File
@@ -79,7 +79,8 @@ panels (`fetchEpg`, `getChannelPrograms`, `checkEpgFreshness`,
`searchEpgPrograms`), `supportsPlaylistRefresh` requires the native playlist
refresh/cancel/progress bridge, `supportsXtreamSectionNavigation` is available
in PWA and in Electron when either the SQLite Xtream data source or the Xtream
API transport is available, and
API transport is available, `supportsDesktopFileSave` requires both
`saveFileDialog` and `writeFile`, and
`supportsManagedExternalPlayers` requires the MPV and VLC preload launch and
path-setting methods (`openInMpv`, `openInVlc`, `setMpvPlayerPath`, and
`setVlcPlayerPath`); a partial Electron bridge must not expose desktop-only
@@ -258,6 +258,24 @@ describe('RuntimeCapabilitiesService', () => {
expect(service.supportsDownloads).toBe(true);
});
it('requires both desktop file-save preload methods', () => {
testWindow.electron = {
saveFileDialog: jest.fn(),
};
const service = new RuntimeCapabilitiesService();
expect(service.isElectron).toBe(true);
expect(service.supportsDesktopFileSave).toBe(false);
testWindow.electron = {
saveFileDialog: jest.fn(),
writeFile: jest.fn(),
};
expect(service.supportsDesktopFileSave).toBe(true);
});
it('requires the complete playlist refresh preload surface', () => {
testWindow.electron = {
refreshPlaylist: jest.fn(),