aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-08 22:21:40 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:03 +0200
commit056beb732b7456881a0461a6ed7452e203b495aa (patch)
tree27adfc816f5e4d833e48f69f72a9b617b2b483d9
parentbuild: fix git lab ci configuration (diff)
downloadsophie-056beb732b7456881a0461a6ed7452e203b495aa.tar.gz
sophie-056beb732b7456881a0461a6ed7452e203b495aa.tar.zst
sophie-056beb732b7456881a0461a6ed7452e203b495aa.zip
fix: vite dev server race condition
Handle the ERR_ABORTED error emitted by chromium when it tries to load the UI before the vite dev server is fully initialized. Signed-off-by: Kristóf Marussy <kristof@marussy.com>
-rw-r--r--packages/main/src/infrastructure/electron/impl/__tests__/lockWebContentsToFile.spec.ts194
-rw-r--r--packages/main/src/infrastructure/electron/impl/lockWebContentsToFile.ts14
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
31type WillNavigateHandler = (event: Event, url: string) => void; 31type WillNavigateHandler = (event: Event, url: string) => void;
32 32
33let willNavigate: WillNavigateHandler | undefined; 33const filePrefix =
34 34 'file:///opt/sophie/resources/app.asar/packages/renderer/dist/';
35let windowOpenHandler:
36 | ((details: HandlerDetails) => { action: 'allow' | 'deny' })
37 | undefined;
38
39const urlToLoad =
40 'file:///opt/sophie/resources/app.asar/packages/renderer/dist/index.html';
41
42const 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
51const fakeWebContents = fake<WebContents>({ 36function 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
64const event: Event = { 44function 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
68beforeEach(async () => { 53describe('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
75it('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
79it('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
84it('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
98it('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
103it('should not allow navigation to another URL', () => { 125describe('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 @@
21import type { WebContents } from 'electron'; 21import type { WebContents } from 'electron';
22 22
23import getLogger from '../../../utils/getLogger'; 23import getLogger from '../../../utils/getLogger';
24import isErrno from '../../../utils/isErrno';
24import type Resources from '../../resources/Resources'; 25import type Resources from '../../resources/Resources';
25 26
26const log = getLogger('lockWebContentsToFile'); 27const 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 */
37export default function lockWebContentsToFile( 39export 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}