diff options
Diffstat (limited to 'packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts')
-rw-r--r-- | packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts | 299 |
1 files changed, 299 insertions, 0 deletions
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..e29429d --- /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.js'; | ||
34 | import RendererBridge, { type PatchListener } from '../RendererBridge.js'; | ||
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 | }); | ||