/* * Copyright (C) 2022 Kristóf Marussy * * This file is part of Sophie. * * Sophie is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * SPDX-License-Identifier: AGPL-3.0-only */ import { URL } from 'node:url'; import { jest } from '@jest/globals'; import { fake } from '@sophie/test-utils'; import type { Event, HandlerDetails, WebContents } from 'electron'; import { mocked } from 'jest-mock'; import type Resources from '../../../resources/Resources'; import lockWebContentsToFile from '../lockWebContentsToFile'; type WillNavigateHandler = (event: Event, url: string) => void; const filePrefix = 'file:///opt/sophie/resources/app.asar/packages/renderer/dist/'; function createFakeResources(prefix: string): Resources { return fake({ getRendererURL(path: string) { return new URL(path, prefix).toString(); }, }); } function createAbortedError(): Error { const error = new Error('Aborted error'); Object.assign(error, { errno: -3, code: 'ERR_ABORTED', }); return error; } describe('when loadURL does not throw', () => { let willNavigate: WillNavigateHandler | undefined; let windowOpenHandler: | ((details: HandlerDetails) => { action: 'allow' | 'deny' }) | undefined; const urlToLoad = `${filePrefix}index.html`; const fakeResources = createFakeResources(filePrefix); const fakeWebContents = fake({ setWindowOpenHandler(handler) { windowOpenHandler = handler; }, on(event, listener) { if (event === 'will-navigate') { willNavigate = listener as WillNavigateHandler; } return this as WebContents; }, loadURL: jest.fn(), }); const event: Event = { preventDefault: jest.fn(), }; beforeEach(async () => { windowOpenHandler = undefined; willNavigate = undefined; mocked(fakeWebContents.loadURL).mockResolvedValueOnce(); await lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents); }); it('should load the specified file', () => { expect(fakeWebContents.loadURL).toHaveBeenCalledWith(urlToLoad); }); it('should set up will navigate and window open listeners', () => { expect(willNavigate).toBeDefined(); expect(windowOpenHandler).toBeDefined(); }); it('should prevent opening a window', () => { const { action } = windowOpenHandler!({ url: 'https://example.com', frameName: 'newWindow', features: '', disposition: 'default', referrer: { url: urlToLoad, policy: 'default', }, }); expect(action).toBe('deny'); }); it('should allow navigation to the loaded URL', () => { willNavigate!(event, urlToLoad); expect(event.preventDefault).not.toHaveBeenCalled(); }); it('should not allow navigation to another URL', () => { willNavigate!( event, 'file:///opt/sophie/resources/app.asar/packages/renderer/not-allowed.html', ); expect(event.preventDefault).toHaveBeenCalled(); }); }); describe('when loadURL throws', () => { const fakeWebContents = fake({ setWindowOpenHandler: jest.fn(), on: jest.fn<() => WebContents>(), loadURL: jest.fn(), }); describe('when the URL points at a file', () => { const fakeResources = createFakeResources('http://localhost:3000'); it('should swallow ERR_ABORTED errors', async () => { const error = createAbortedError(); mocked(fakeWebContents.loadURL).mockRejectedValueOnce(error); await expect( lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), ).resolves.not.toThrow(); }); it('should pass through other errors', async () => { mocked(fakeWebContents.loadURL).mockRejectedValueOnce( new Error('other error'), ); await expect( lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), ).rejects.toBeInstanceOf(Error); }); }); describe('when the URL points at a local server', () => { const fakeResources = createFakeResources(filePrefix); it('should pass through ERR_ABORTED errors', async () => { const error = createAbortedError(); mocked(fakeWebContents.loadURL).mockRejectedValueOnce(error); await expect( lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), ).rejects.toBeInstanceOf(Error); }); it('should pass through other errors', async () => { mocked(fakeWebContents.loadURL).mockRejectedValueOnce( new Error('other error'), ); await expect( lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), ).rejects.toBeInstanceOf(Error); }); }); });