aboutsummaryrefslogtreecommitdiffstats
path: root/packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts')
-rw-r--r--packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts299
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
23import { jest } from '@jest/globals';
24import { configure } from 'mobx';
25import {
26 destroy,
27 flow,
28 type IJsonPatch,
29 type Instance,
30 unprotect,
31} from 'mobx-state-tree';
32
33import MainStore from '../../../stores/MainStore.js';
34import RendererBridge, { type PatchListener } from '../RendererBridge.js';
35
36const 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*/
63type TestStore = Instance<typeof TestStore>;
64
65function resolveImmediately(): Promise<void> {
66 return new Promise((resolve) => {
67 setImmediate(resolve);
68 });
69}
70
71function rejectImmediately(error: Error): Promise<void> {
72 return new Promise((_resolve, reject) => {
73 setImmediate(() => reject(error));
74 });
75}
76
77const shouldUseDarkColorsPatch: IJsonPatch[] = [
78 {
79 op: 'replace',
80 path: '/shouldUseDarkColors',
81 value: false,
82 },
83];
84
85let store: TestStore;
86let listener: PatchListener;
87let bridge: RendererBridge;
88
89beforeEach(() => {
90 store = TestStore.create({
91 shared: {
92 shouldUseDarkColors: true,
93 },
94 });
95 listener = jest.fn<PatchListener>();
96 bridge = new RendererBridge(store, listener);
97});
98
99afterEach(() => {
100 bridge.dispose();
101 destroy(store);
102});
103
104test('sets the initial snapshot', () => {
105 expect(bridge.snapshot.shouldUseDarkColors).toBe(true);
106});
107
108test('sets the snapshot after an action', () => {
109 store.shared.setShouldUseDarkColors(false);
110 expect(bridge.snapshot.shouldUseDarkColors).toBe(false);
111});
112
113test('generates a patch after an action', () => {
114 store.shared.setShouldUseDarkColors(false);
115 expect(listener).toHaveBeenCalledTimes(1);
116 expect(listener).toHaveBeenCalledWith(shouldUseDarkColorsPatch);
117});
118
119describe('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
138test('does not generate a patch if an action does not modify the store', () => {
139 store.testAction(() => {});
140 expect(listener).not.toHaveBeenCalled();
141});
142
143test('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
150test('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
157test('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
164test('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
171test('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
192test('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
204test('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
219test('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
236test('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
250test('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
263test('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
275test('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
288test('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});