diff options
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/__tests__/lockWebContentsToFile.spec.ts | 194 | ||||
-rw-r--r-- | packages/main/src/infrastructure/electron/impl/lockWebContentsToFile.ts | 14 |
2 files changed, 141 insertions, 67 deletions
diff --git a/packages/main/src/infrastructure/electron/impl/__tests__/lockWebContentsToFile.spec.ts b/packages/main/src/infrastructure/electron/impl/__tests__/lockWebContentsToFile.spec.ts index f7bad0a..c9aeaab 100644 --- a/packages/main/src/infrastructure/electron/impl/__tests__/lockWebContentsToFile.spec.ts +++ b/packages/main/src/infrastructure/electron/impl/__tests__/lockWebContentsToFile.spec.ts | |||
@@ -30,80 +30,144 @@ import lockWebContentsToFile from '../lockWebContentsToFile'; | |||
30 | 30 | ||
31 | type WillNavigateHandler = (event: Event, url: string) => void; | 31 | type WillNavigateHandler = (event: Event, url: string) => void; |
32 | 32 | ||
33 | let willNavigate: WillNavigateHandler | undefined; | 33 | const filePrefix = |
34 | 34 | 'file:///opt/sophie/resources/app.asar/packages/renderer/dist/'; | |
35 | let windowOpenHandler: | ||
36 | | ((details: HandlerDetails) => { action: 'allow' | 'deny' }) | ||
37 | | undefined; | ||
38 | |||
39 | const urlToLoad = | ||
40 | 'file:///opt/sophie/resources/app.asar/packages/renderer/dist/index.html'; | ||
41 | |||
42 | const fakeResources = fake<Resources>({ | ||
43 | getRendererURL(path: string) { | ||
44 | return new URL( | ||
45 | path, | ||
46 | 'file:///opt/sophie/resources/app.asar/packages/renderer/dist/', | ||
47 | ).toString(); | ||
48 | }, | ||
49 | }); | ||
50 | 35 | ||
51 | const fakeWebContents = fake<WebContents>({ | 36 | function createFakeResources(prefix: string): Resources { |
52 | setWindowOpenHandler(handler) { | 37 | return fake<Resources>({ |
53 | windowOpenHandler = handler; | 38 | getRendererURL(path: string) { |
54 | }, | 39 | return new URL(path, prefix).toString(); |
55 | on(event, listener) { | 40 | }, |
56 | if (event === 'will-navigate') { | 41 | }); |
57 | willNavigate = listener as WillNavigateHandler; | 42 | } |
58 | } | ||
59 | return this as WebContents; | ||
60 | }, | ||
61 | loadURL: jest.fn<WebContents['loadURL']>(), | ||
62 | }); | ||
63 | 43 | ||
64 | const event: Event = { | 44 | function createAbortedError(): Error { |
65 | preventDefault: jest.fn(), | 45 | const error = new Error('Aborted error'); |
66 | }; | 46 | Object.assign(error, { |
47 | errno: -3, | ||
48 | code: 'ERR_ABORTED', | ||
49 | }); | ||
50 | return error; | ||
51 | } | ||
67 | 52 | ||
68 | beforeEach(async () => { | 53 | describe('when loadURL does not throw', () => { |
69 | windowOpenHandler = undefined; | 54 | let willNavigate: WillNavigateHandler | undefined; |
70 | willNavigate = undefined; | ||
71 | mocked(fakeWebContents.loadURL).mockResolvedValueOnce(); | ||
72 | await lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents); | ||
73 | }); | ||
74 | 55 | ||
75 | it('should load the specified file', () => { | 56 | let windowOpenHandler: |
76 | expect(fakeWebContents.loadURL).toHaveBeenCalledWith(urlToLoad); | 57 | | ((details: HandlerDetails) => { action: 'allow' | 'deny' }) |
77 | }); | 58 | | undefined; |
78 | 59 | ||
79 | it('should set up will navigate and window open listeners', () => { | 60 | const urlToLoad = `${filePrefix}index.html`; |
80 | expect(willNavigate).toBeDefined(); | 61 | |
81 | expect(windowOpenHandler).toBeDefined(); | 62 | const fakeResources = createFakeResources(filePrefix); |
82 | }); | ||
83 | 63 | ||
84 | it('should prevent opening a window', () => { | 64 | const fakeWebContents = fake<WebContents>({ |
85 | const { action } = windowOpenHandler!({ | 65 | setWindowOpenHandler(handler) { |
86 | url: 'https://example.com', | 66 | windowOpenHandler = handler; |
87 | frameName: 'newWindow', | ||
88 | features: '', | ||
89 | disposition: 'default', | ||
90 | referrer: { | ||
91 | url: urlToLoad, | ||
92 | policy: 'default', | ||
93 | }, | 67 | }, |
68 | on(event, listener) { | ||
69 | if (event === 'will-navigate') { | ||
70 | willNavigate = listener as WillNavigateHandler; | ||
71 | } | ||
72 | return this as WebContents; | ||
73 | }, | ||
74 | loadURL: jest.fn<WebContents['loadURL']>(), | ||
75 | }); | ||
76 | |||
77 | const event: Event = { | ||
78 | preventDefault: jest.fn(), | ||
79 | }; | ||
80 | |||
81 | beforeEach(async () => { | ||
82 | windowOpenHandler = undefined; | ||
83 | willNavigate = undefined; | ||
84 | mocked(fakeWebContents.loadURL).mockResolvedValueOnce(); | ||
85 | await lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents); | ||
94 | }); | 86 | }); |
95 | expect(action).toBe('deny'); | ||
96 | }); | ||
97 | 87 | ||
98 | it('should allow navigation to the loaded URL', () => { | 88 | it('should load the specified file', () => { |
99 | willNavigate!(event, urlToLoad); | 89 | expect(fakeWebContents.loadURL).toHaveBeenCalledWith(urlToLoad); |
100 | expect(event.preventDefault).not.toHaveBeenCalled(); | 90 | }); |
91 | |||
92 | it('should set up will navigate and window open listeners', () => { | ||
93 | expect(willNavigate).toBeDefined(); | ||
94 | expect(windowOpenHandler).toBeDefined(); | ||
95 | }); | ||
96 | |||
97 | it('should prevent opening a window', () => { | ||
98 | const { action } = windowOpenHandler!({ | ||
99 | url: 'https://example.com', | ||
100 | frameName: 'newWindow', | ||
101 | features: '', | ||
102 | disposition: 'default', | ||
103 | referrer: { | ||
104 | url: urlToLoad, | ||
105 | policy: 'default', | ||
106 | }, | ||
107 | }); | ||
108 | expect(action).toBe('deny'); | ||
109 | }); | ||
110 | |||
111 | it('should allow navigation to the loaded URL', () => { | ||
112 | willNavigate!(event, urlToLoad); | ||
113 | expect(event.preventDefault).not.toHaveBeenCalled(); | ||
114 | }); | ||
115 | |||
116 | it('should not allow navigation to another URL', () => { | ||
117 | willNavigate!( | ||
118 | event, | ||
119 | 'file:///opt/sophie/resources/app.asar/packages/renderer/not-allowed.html', | ||
120 | ); | ||
121 | expect(event.preventDefault).toHaveBeenCalled(); | ||
122 | }); | ||
101 | }); | 123 | }); |
102 | 124 | ||
103 | it('should not allow navigation to another URL', () => { | 125 | describe('when loadURL throws', () => { |
104 | willNavigate!( | 126 | const fakeWebContents = fake<WebContents>({ |
105 | event, | 127 | setWindowOpenHandler: jest.fn<WebContents['setWindowOpenHandler']>(), |
106 | 'file:///opt/sophie/resources/app.asar/packages/renderer/not-allowed.html', | 128 | on: jest.fn<() => WebContents>(), |
107 | ); | 129 | loadURL: jest.fn<WebContents['loadURL']>(), |
108 | expect(event.preventDefault).toHaveBeenCalled(); | 130 | }); |
131 | |||
132 | describe('when the URL points at a file', () => { | ||
133 | const fakeResources = createFakeResources('http://localhost:3000'); | ||
134 | |||
135 | it('should swallow ERR_ABORTED errors', async () => { | ||
136 | const error = createAbortedError(); | ||
137 | mocked(fakeWebContents.loadURL).mockRejectedValueOnce(error); | ||
138 | await expect( | ||
139 | lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), | ||
140 | ).resolves.not.toThrow(); | ||
141 | }); | ||
142 | |||
143 | it('should pass through other errors', async () => { | ||
144 | mocked(fakeWebContents.loadURL).mockRejectedValueOnce( | ||
145 | new Error('other error'), | ||
146 | ); | ||
147 | await expect( | ||
148 | lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), | ||
149 | ).rejects.toBeInstanceOf(Error); | ||
150 | }); | ||
151 | }); | ||
152 | |||
153 | describe('when the URL points at a local server', () => { | ||
154 | const fakeResources = createFakeResources(filePrefix); | ||
155 | |||
156 | it('should pass through ERR_ABORTED errors', async () => { | ||
157 | const error = createAbortedError(); | ||
158 | mocked(fakeWebContents.loadURL).mockRejectedValueOnce(error); | ||
159 | await expect( | ||
160 | lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), | ||
161 | ).rejects.toBeInstanceOf(Error); | ||
162 | }); | ||
163 | |||
164 | it('should pass through other errors', async () => { | ||
165 | mocked(fakeWebContents.loadURL).mockRejectedValueOnce( | ||
166 | new Error('other error'), | ||
167 | ); | ||
168 | await expect( | ||
169 | lockWebContentsToFile(fakeResources, 'index.html', fakeWebContents), | ||
170 | ).rejects.toBeInstanceOf(Error); | ||
171 | }); | ||
172 | }); | ||
109 | }); | 173 | }); |
diff --git a/packages/main/src/infrastructure/electron/impl/lockWebContentsToFile.ts b/packages/main/src/infrastructure/electron/impl/lockWebContentsToFile.ts index 8d557c4..da40a56 100644 --- a/packages/main/src/infrastructure/electron/impl/lockWebContentsToFile.ts +++ b/packages/main/src/infrastructure/electron/impl/lockWebContentsToFile.ts | |||
@@ -21,6 +21,7 @@ | |||
21 | import type { WebContents } from 'electron'; | 21 | import type { WebContents } from 'electron'; |
22 | 22 | ||
23 | import getLogger from '../../../utils/getLogger'; | 23 | import getLogger from '../../../utils/getLogger'; |
24 | import isErrno from '../../../utils/isErrno'; | ||
24 | import type Resources from '../../resources/Resources'; | 25 | import type Resources from '../../resources/Resources'; |
25 | 26 | ||
26 | const log = getLogger('lockWebContentsToFile'); | 27 | const log = getLogger('lockWebContentsToFile'); |
@@ -33,8 +34,9 @@ const log = getLogger('lockWebContentsToFile'); | |||
33 | * @param resources The resource handle associated with the paths and URL of the application. | 34 | * @param resources The resource handle associated with the paths and URL of the application. |
34 | * @param filePath The path to the file in the render package to load. | 35 | * @param filePath The path to the file in the render package to load. |
35 | * @param webContents The webContents to lock. | 36 | * @param webContents The webContents to lock. |
37 | * @returns A promise that resolves when the webpage is loaded. | ||
36 | */ | 38 | */ |
37 | export default function lockWebContentsToFile( | 39 | export default async function lockWebContentsToFile( |
38 | resources: Resources, | 40 | resources: Resources, |
39 | filePath: string, | 41 | filePath: string, |
40 | webContents: WebContents, | 42 | webContents: WebContents, |
@@ -55,5 +57,13 @@ export default function lockWebContentsToFile( | |||
55 | } | 57 | } |
56 | }); | 58 | }); |
57 | 59 | ||
58 | return webContents.loadURL(pageURL); | 60 | try { |
61 | await webContents.loadURL(pageURL); | ||
62 | } catch (error) { | ||
63 | // Chromium will throw `ERR_ABORTED` when the vite dev server is still initializing, | ||
64 | // but will load the page nevertheless. | ||
65 | if (!isErrno(error, 'ERR_ABORTED') || !pageURL.startsWith('http:')) { | ||
66 | throw error; | ||
67 | } | ||
68 | } | ||
59 | } | 69 | } |