diff options
author | Kristóf Marussy <kristof@marussy.com> | 2022-05-14 23:37:51 +0200 |
---|---|---|
committer | Kristóf Marussy <kristof@marussy.com> | 2022-05-16 00:55:03 +0200 |
commit | 4aabd2d0d6c707eaf0867776482a71f003fd7976 (patch) | |
tree | e7341888cf834a29a80e19ab84e3ed199a2edecc /packages/main/src | |
parent | test: use test instead of it (diff) | |
download | sophie-4aabd2d0d6c707eaf0867776482a71f003fd7976.tar.gz sophie-4aabd2d0d6c707eaf0867776482a71f003fd7976.tar.zst sophie-4aabd2d0d6c707eaf0867776482a71f003fd7976.zip |
test: tests for infrastructure/electron in main
Signed-off-by: Kristóf Marussy <kristof@marussy.com>
Diffstat (limited to 'packages/main/src')
3 files changed, 359 insertions, 23 deletions
diff --git a/packages/main/src/infrastructure/electron/RendererBridge.ts b/packages/main/src/infrastructure/electron/RendererBridge.ts index 3f9b512..097580a 100644 --- a/packages/main/src/infrastructure/electron/RendererBridge.ts +++ b/packages/main/src/infrastructure/electron/RendererBridge.ts | |||
@@ -22,16 +22,12 @@ import type { SharedStoreSnapshotOut } from '@sophie/shared'; | |||
22 | import { | 22 | import { |
23 | addMiddleware, | 23 | addMiddleware, |
24 | getSnapshot, | 24 | getSnapshot, |
25 | IJsonPatch, | 25 | type IJsonPatch, |
26 | IMiddlewareEvent, | ||
27 | onPatch, | 26 | onPatch, |
28 | } from 'mobx-state-tree'; | 27 | } from 'mobx-state-tree'; |
29 | 28 | ||
30 | import type MainStore from '../../stores/MainStore'; | 29 | import type MainStore from '../../stores/MainStore'; |
31 | import Disposer from '../../utils/Disposer'; | 30 | import Disposer from '../../utils/Disposer'; |
32 | import getLogger from '../../utils/getLogger'; | ||
33 | |||
34 | const log = getLogger('RendererBridge'); | ||
35 | 31 | ||
36 | export type PatchListener = (patch: IJsonPatch[]) => void; | 32 | export type PatchListener = (patch: IJsonPatch[]) => void; |
37 | 33 | ||
@@ -45,8 +41,6 @@ export default class RendererBridge { | |||
45 | constructor(store: MainStore, listener: PatchListener) { | 41 | constructor(store: MainStore, listener: PatchListener) { |
46 | this.snapshot = getSnapshot(store.shared); | 42 | this.snapshot = getSnapshot(store.shared); |
47 | 43 | ||
48 | // The call for the currently pending action, if any. | ||
49 | let topLevelCall: IMiddlewareEvent | undefined; | ||
50 | // An array of accumulated patches if we're in an action, `undefined` otherwise. | 44 | // An array of accumulated patches if we're in an action, `undefined` otherwise. |
51 | let patches: IJsonPatch[] | undefined; | 45 | let patches: IJsonPatch[] | undefined; |
52 | 46 | ||
@@ -63,25 +57,12 @@ export default class RendererBridge { | |||
63 | } | 57 | } |
64 | }); | 58 | }); |
65 | 59 | ||
66 | this.disposeMiddleware = addMiddleware(store, (call, next, abort) => { | 60 | this.disposeMiddleware = addMiddleware(store, (call, next) => { |
67 | if (call.parentActionEvent !== undefined) { | 61 | if (patches !== undefined) { |
68 | // We're already in an action, there's no need to enter one. | 62 | // We're already in an action, there's no need to enter one. |
69 | next(call); | 63 | next(call); |
70 | return; | 64 | return; |
71 | } | 65 | } |
72 | if (patches !== undefined) { | ||
73 | log.error( | ||
74 | 'Unexpected call', | ||
75 | call, | ||
76 | 'during dispatching another call', | ||
77 | topLevelCall, | ||
78 | 'with accumulated patches', | ||
79 | patches, | ||
80 | ); | ||
81 | abort(undefined); | ||
82 | return; | ||
83 | } | ||
84 | topLevelCall = call; | ||
85 | patches = []; | 66 | patches = []; |
86 | try { | 67 | try { |
87 | next(call); | 68 | next(call); |
@@ -91,7 +72,6 @@ export default class RendererBridge { | |||
91 | listener(patches); | 72 | listener(patches); |
92 | } | 73 | } |
93 | } finally { | 74 | } finally { |
94 | topLevelCall = undefined; | ||
95 | patches = undefined; | 75 | patches = undefined; |
96 | this.snapshot = getSnapshot(store.shared); | 76 | this.snapshot = getSnapshot(store.shared); |
97 | } | 77 | } |
diff --git a/packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts b/packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts new file mode 100644 index 0000000..b7c8a76 --- /dev/null +++ b/packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts | |||
@@ -0,0 +1,299 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | /* eslint-disable no-param-reassign -- Actions modify the `self` parameter. */ | ||
22 | |||
23 | import { jest } from '@jest/globals'; | ||
24 | import { configure } from 'mobx'; | ||
25 | import { | ||
26 | destroy, | ||
27 | flow, | ||
28 | type IJsonPatch, | ||
29 | type Instance, | ||
30 | unprotect, | ||
31 | } from 'mobx-state-tree'; | ||
32 | |||
33 | import MainStore from '../../../stores/MainStore'; | ||
34 | import RendererBridge, { type PatchListener } from '../RendererBridge'; | ||
35 | |||
36 | const TestStore = MainStore.actions((theSelf) => ({ | ||
37 | /** | ||
38 | * Dispatch `action` as an action in the context of the store. | ||
39 | * | ||
40 | * We cannot use `action` from `'mobx'` here, because it is not bound to | ||
41 | * a `'mobx-state-tree'` store, so it won't trigger our middleware. | ||
42 | * | ||
43 | * @param action The action to dispatch. | ||
44 | */ | ||
45 | testAction(action: (self: MainStore) => void): void { | ||
46 | action(theSelf); | ||
47 | }, | ||
48 | /** | ||
49 | * Executes a flow in the context of the store. | ||
50 | */ | ||
51 | testFlow: flow(function* testFlow( | ||
52 | action: (self: MainStore) => Generator<PromiseLike<void>, void, void>, | ||
53 | ) { | ||
54 | yield* action(theSelf); | ||
55 | }), | ||
56 | })); | ||
57 | |||
58 | /* | ||
59 | eslint-disable-next-line @typescript-eslint/no-empty-interface, | ||
60 | @typescript-eslint/no-redeclare -- | ||
61 | Intentionally naming the type the same as the store definition. | ||
62 | */ | ||
63 | type TestStore = Instance<typeof TestStore>; | ||
64 | |||
65 | function resolveImmediately(): Promise<void> { | ||
66 | return new Promise((resolve) => { | ||
67 | setImmediate(resolve); | ||
68 | }); | ||
69 | } | ||
70 | |||
71 | function rejectImmediately(error: Error): Promise<void> { | ||
72 | return new Promise((_resolve, reject) => { | ||
73 | setImmediate(() => reject(error)); | ||
74 | }); | ||
75 | } | ||
76 | |||
77 | const shouldUseDarkColorsPatch: IJsonPatch[] = [ | ||
78 | { | ||
79 | op: 'replace', | ||
80 | path: '/shouldUseDarkColors', | ||
81 | value: false, | ||
82 | }, | ||
83 | ]; | ||
84 | |||
85 | let store: TestStore; | ||
86 | let listener: PatchListener; | ||
87 | let bridge: RendererBridge; | ||
88 | |||
89 | beforeEach(() => { | ||
90 | store = TestStore.create({ | ||
91 | shared: { | ||
92 | shouldUseDarkColors: true, | ||
93 | }, | ||
94 | }); | ||
95 | listener = jest.fn<PatchListener>(); | ||
96 | bridge = new RendererBridge(store, listener); | ||
97 | }); | ||
98 | |||
99 | afterEach(() => { | ||
100 | bridge.dispose(); | ||
101 | destroy(store); | ||
102 | }); | ||
103 | |||
104 | test('sets the initial snapshot', () => { | ||
105 | expect(bridge.snapshot.shouldUseDarkColors).toBe(true); | ||
106 | }); | ||
107 | |||
108 | test('sets the snapshot after an action', () => { | ||
109 | store.shared.setShouldUseDarkColors(false); | ||
110 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
111 | }); | ||
112 | |||
113 | test('generates a patch after an action', () => { | ||
114 | store.shared.setShouldUseDarkColors(false); | ||
115 | expect(listener).toHaveBeenCalledTimes(1); | ||
116 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
117 | }); | ||
118 | |||
119 | describe('when the store is unprotected', () => { | ||
120 | beforeAll(() => configure({ enforceActions: 'never' })); | ||
121 | |||
122 | beforeEach(() => unprotect(store)); | ||
123 | |||
124 | afterAll(() => configure({ enforceActions: 'observed' })); | ||
125 | |||
126 | test('sets the snapshot after directly setting a value', () => { | ||
127 | store.shared.shouldUseDarkColors = false; | ||
128 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
129 | }); | ||
130 | |||
131 | test('generates a patch after directly setting a value', () => { | ||
132 | store.shared.shouldUseDarkColors = false; | ||
133 | expect(listener).toHaveBeenCalledTimes(1); | ||
134 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
135 | }); | ||
136 | }); | ||
137 | |||
138 | test('does not generate a patch if an action does not modify the store', () => { | ||
139 | store.testAction(() => {}); | ||
140 | expect(listener).not.toHaveBeenCalled(); | ||
141 | }); | ||
142 | |||
143 | test('does not set the snapshot until the end of the action', () => { | ||
144 | store.testAction(({ shared }) => { | ||
145 | shared.shouldUseDarkColors = false; | ||
146 | expect(bridge.snapshot.shouldUseDarkColors).not.toBe(false); | ||
147 | }); | ||
148 | }); | ||
149 | |||
150 | test('does not generate a patch until the end of the action', () => { | ||
151 | store.testAction(({ shared }) => { | ||
152 | shared.shouldUseDarkColors = false; | ||
153 | expect(listener).not.toHaveBeenCalled(); | ||
154 | }); | ||
155 | }); | ||
156 | |||
157 | test('does not set the snapshot until the end of the action with nested action', () => { | ||
158 | store.testAction(({ shared }) => { | ||
159 | shared.setShouldUseDarkColors(false); | ||
160 | expect(bridge.snapshot.shouldUseDarkColors).not.toBe(false); | ||
161 | }); | ||
162 | }); | ||
163 | |||
164 | test('does not generate a patch until the end of the action with nested action', () => { | ||
165 | store.testAction(({ shared }) => { | ||
166 | shared.setShouldUseDarkColors(false); | ||
167 | expect(listener).not.toHaveBeenCalled(); | ||
168 | }); | ||
169 | }); | ||
170 | |||
171 | test('generates a single patch for multiple nested actions', () => { | ||
172 | store.testAction(({ shared }) => { | ||
173 | shared.setShouldUseDarkColors(false); | ||
174 | shared.setLanguage('testLanguage', 'rtl'); | ||
175 | }); | ||
176 | expect(listener).toHaveBeenCalledTimes(1); | ||
177 | expect(listener).toHaveBeenCalledWith([ | ||
178 | ...shouldUseDarkColorsPatch, | ||
179 | { | ||
180 | op: 'replace', | ||
181 | path: '/language', | ||
182 | value: 'testLanguage', | ||
183 | }, | ||
184 | { | ||
185 | op: 'replace', | ||
186 | path: '/writingDirection', | ||
187 | value: 'rtl', | ||
188 | }, | ||
189 | ]); | ||
190 | }); | ||
191 | |||
192 | test('does not abort on a failing action', () => { | ||
193 | expect(() => | ||
194 | store.testAction(({ shared }) => { | ||
195 | shared.setShouldUseDarkColors(false); | ||
196 | throw new Error('test error'); | ||
197 | }), | ||
198 | ).toThrow('test error'); | ||
199 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
200 | expect(listener).toHaveBeenCalledTimes(1); | ||
201 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
202 | }); | ||
203 | |||
204 | test('generates a snapshot and a patch in the beginning of a flow', async () => { | ||
205 | await store.testFlow(function* successfulTestFlowBeginning({ shared }) { | ||
206 | shared.setShouldUseDarkColors(false); | ||
207 | |||
208 | expect(bridge.snapshot.shouldUseDarkColors).not.toBe(false); | ||
209 | expect(listener).not.toHaveBeenCalled(); | ||
210 | |||
211 | yield resolveImmediately(); | ||
212 | |||
213 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
214 | expect(listener).toHaveBeenCalledTimes(1); | ||
215 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
216 | }); | ||
217 | }); | ||
218 | |||
219 | test('generates a snapshot and a patch in the middle of a flow', async () => { | ||
220 | await store.testFlow(function* successfulTestFlowMiddle({ shared }) { | ||
221 | yield resolveImmediately(); | ||
222 | |||
223 | shared.setShouldUseDarkColors(false); | ||
224 | |||
225 | expect(bridge.snapshot.shouldUseDarkColors).not.toBe(false); | ||
226 | expect(listener).not.toHaveBeenCalled(); | ||
227 | |||
228 | yield resolveImmediately(); | ||
229 | |||
230 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
231 | expect(listener).toHaveBeenCalledTimes(1); | ||
232 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
233 | }); | ||
234 | }); | ||
235 | |||
236 | test('generates a snapshot and a patch in the end of a flow', async () => { | ||
237 | await store.testFlow(function* successfulTestFlowEnd({ shared }) { | ||
238 | yield resolveImmediately(); | ||
239 | |||
240 | shared.setShouldUseDarkColors(false); | ||
241 | |||
242 | expect(bridge.snapshot.shouldUseDarkColors).not.toBe(false); | ||
243 | expect(listener).not.toHaveBeenCalled(); | ||
244 | }); | ||
245 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
246 | expect(listener).toHaveBeenCalledTimes(1); | ||
247 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
248 | }); | ||
249 | |||
250 | test('generates a snapshot and a patch when a flow fails synchronously in the beginning', async () => { | ||
251 | await expect( | ||
252 | // eslint-disable-next-line require-yield -- We simulate a failure before `yield`. | ||
253 | store.testFlow(function* synchronousFailureTestFlowBeginning({ shared }) { | ||
254 | shared.setShouldUseDarkColors(false); | ||
255 | throw new Error('test error'); | ||
256 | }), | ||
257 | ).rejects.toBeInstanceOf(Error); | ||
258 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
259 | expect(listener).toHaveBeenCalledTimes(1); | ||
260 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
261 | }); | ||
262 | |||
263 | test('generates a snapshot and a patch when a flow fails asynchronously in the beginning', async () => { | ||
264 | await expect( | ||
265 | store.testFlow(function* asynchronousFailureTestFlowBeginning({ shared }) { | ||
266 | shared.setShouldUseDarkColors(false); | ||
267 | yield rejectImmediately(new Error('test error')); | ||
268 | }), | ||
269 | ).rejects.toBeInstanceOf(Error); | ||
270 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
271 | expect(listener).toHaveBeenCalledTimes(1); | ||
272 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
273 | }); | ||
274 | |||
275 | test('generates a snapshot and a patch when a flow fails synchronously in the middle', async () => { | ||
276 | await expect( | ||
277 | store.testFlow(function* synchronousFailureTestFlowMiddle({ shared }) { | ||
278 | yield resolveImmediately(); | ||
279 | shared.setShouldUseDarkColors(false); | ||
280 | throw new Error('test error'); | ||
281 | }), | ||
282 | ).rejects.toBeInstanceOf(Error); | ||
283 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
284 | expect(listener).toHaveBeenCalledTimes(1); | ||
285 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
286 | }); | ||
287 | |||
288 | test('generates a snapshot and a patch when a flow fails asynchronously in the middle', async () => { | ||
289 | await expect( | ||
290 | store.testFlow(function* asynchronousFailureTestFlowMiddle({ shared }) { | ||
291 | yield resolveImmediately(); | ||
292 | shared.setShouldUseDarkColors(false); | ||
293 | yield rejectImmediately(new Error('test error')); | ||
294 | }), | ||
295 | ).rejects.toBeInstanceOf(Error); | ||
296 | expect(bridge.snapshot.shouldUseDarkColors).toBe(false); | ||
297 | expect(listener).toHaveBeenCalledTimes(1); | ||
298 | expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch); | ||
299 | }); | ||
diff --git a/packages/main/src/infrastructure/electron/__tests__/UserAgents.test.ts b/packages/main/src/infrastructure/electron/__tests__/UserAgents.test.ts new file mode 100644 index 0000000..d963704 --- /dev/null +++ b/packages/main/src/infrastructure/electron/__tests__/UserAgents.test.ts | |||
@@ -0,0 +1,57 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2022 Kristóf Marussy <kristof@marussy.com> | ||
3 | * | ||
4 | * This file is part of Sophie. | ||
5 | * | ||
6 | * Sophie is free software: you can redistribute it and/or modify | ||
7 | * it under the terms of the GNU Affero General Public License as | ||
8 | * published by the Free Software Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
13 | * GNU Affero General Public License for more details. | ||
14 | * | ||
15 | * You should have received a copy of the GNU Affero General Public License | ||
16 | * along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
17 | * | ||
18 | * SPDX-License-Identifier: AGPL-3.0-only | ||
19 | */ | ||
20 | |||
21 | import UserAgents from '../UserAgents'; | ||
22 | |||
23 | let userAgents: UserAgents; | ||
24 | |||
25 | beforeEach(() => { | ||
26 | userAgents = new UserAgents( | ||
27 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) sophie/0.1.0 Chrome/102.0.5005.27 Electron/19.0.0-beta.4 Safari/537.36', | ||
28 | ); | ||
29 | }); | ||
30 | |||
31 | test('removes Electron from the user agent outside dev mode', () => { | ||
32 | expect(userAgents.fallbackUserAgent(false)).toBe( | ||
33 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.27 Safari/537.36', | ||
34 | ); | ||
35 | }); | ||
36 | |||
37 | test('does not remove Electron from the user agent in dev mode', () => { | ||
38 | expect(userAgents.fallbackUserAgent(true)).toBe( | ||
39 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) sophie/0.1.0 Chrome/102.0.5005.27 Electron/19.0.0-beta.4 Safari/537.36', | ||
40 | ); | ||
41 | }); | ||
42 | |||
43 | test('displays the Chrome version for services', () => { | ||
44 | expect(userAgents.serviceUserAgent('https://example.org')).toBe( | ||
45 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.27 Safari/537.36', | ||
46 | ); | ||
47 | }); | ||
48 | |||
49 | test('does not display the Chrome version when loggin in to Google', () => { | ||
50 | expect( | ||
51 | userAgents.serviceUserAgent( | ||
52 | 'https://accounts.google.com/signin/v2/identifier', | ||
53 | ), | ||
54 | ).toBe( | ||
55 | 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Safari/537.36', | ||
56 | ); | ||
57 | }); | ||