aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-14 23:37:51 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-05-16 00:55:03 +0200
commit4aabd2d0d6c707eaf0867776482a71f003fd7976 (patch)
treee7341888cf834a29a80e19ab84e3ed199a2edecc
parenttest: use test instead of it (diff)
downloadsophie-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>
-rw-r--r--.eslintrc.cjs7
-rw-r--r--packages/main/src/infrastructure/electron/RendererBridge.ts26
-rw-r--r--packages/main/src/infrastructure/electron/__tests__/RendererBridge.test.ts299
-rw-r--r--packages/main/src/infrastructure/electron/__tests__/UserAgents.test.ts57
4 files changed, 365 insertions, 24 deletions
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 289d374..3d944e4 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -85,7 +85,12 @@ module.exports = {
85 files: ['**/stores/**/*.ts'], 85 files: ['**/stores/**/*.ts'],
86 rules: { 86 rules: {
87 // In a mobx-state-tree action, we assign to the properties of `self` to update the store. 87 // In a mobx-state-tree action, we assign to the properties of `self` to update the store.
88 'no-param-reassign': 'off', 88 'no-param-reassign': [
89 'error',
90 {
91 props: false,
92 },
93 ],
89 // mobx-state-tree uses empty interfaces to speed up typechecking. 94 // mobx-state-tree uses empty interfaces to speed up typechecking.
90 '@typescript-eslint/no-empty-interface': 'off', 95 '@typescript-eslint/no-empty-interface': 'off',
91 }, 96 },
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';
22import { 22import {
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
30import type MainStore from '../../stores/MainStore'; 29import type MainStore from '../../stores/MainStore';
31import Disposer from '../../utils/Disposer'; 30import Disposer from '../../utils/Disposer';
32import getLogger from '../../utils/getLogger';
33
34const log = getLogger('RendererBridge');
35 31
36export type PatchListener = (patch: IJsonPatch[]) => void; 32export 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
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';
34import RendererBridge, { type PatchListener } from '../RendererBridge';
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});
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
21import UserAgents from '../UserAgents';
22
23let userAgents: UserAgents;
24
25beforeEach(() => {
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
31test('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
37test('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
43test('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
49test('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});