diff options
Diffstat (limited to 'packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts')
-rw-r--r-- | packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts | 211 |
1 files changed, 211 insertions, 0 deletions
diff --git a/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts b/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts new file mode 100644 index 0000000..c0e5ec2 --- /dev/null +++ b/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts | |||
@@ -0,0 +1,211 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2021-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 { mocked } from 'jest-mock'; | ||
22 | import type { IJsonPatch } from 'mobx-state-tree'; | ||
23 | import type { Action, SharedStoreSnapshotIn, SophieRenderer } from '@sophie/shared'; | ||
24 | |||
25 | import { RendererToMainIpcService } from '../../services/RendererToMainIpcService'; | ||
26 | import { createSophieRenderer } from '../SophieRendererImpl'; | ||
27 | |||
28 | jest.mock('../../services/RendererToMainIpcService'); | ||
29 | |||
30 | const snapshot: SharedStoreSnapshotIn = { | ||
31 | shouldUseDarkColors: true, | ||
32 | }; | ||
33 | |||
34 | const invalidSnapshot = { | ||
35 | shouldUseDarkColors: -1, | ||
36 | } as unknown as SharedStoreSnapshotIn; | ||
37 | |||
38 | const patch: IJsonPatch = { | ||
39 | op: 'replace', | ||
40 | path: 'foo', | ||
41 | value: 'bar', | ||
42 | }; | ||
43 | |||
44 | const action: Action = { | ||
45 | action: 'set-theme-source', | ||
46 | themeSource: 'dark', | ||
47 | }; | ||
48 | |||
49 | const invalidAction = { | ||
50 | action: 'not-a-valid-action', | ||
51 | } as unknown as Action; | ||
52 | |||
53 | describe('constructor', () => { | ||
54 | it('registers a shared store patch listener', () => { | ||
55 | createSophieRenderer(); | ||
56 | expect(RendererToMainIpcService).toHaveBeenCalledTimes(1); | ||
57 | const service = mocked(RendererToMainIpcService).mock.instances[0]; | ||
58 | expect(service.onSharedStorePatch).toHaveBeenCalledTimes(1); | ||
59 | }); | ||
60 | }); | ||
61 | |||
62 | describe('instance', () => { | ||
63 | let sut: SophieRenderer; | ||
64 | let service: RendererToMainIpcService; | ||
65 | let onSharedStorePatch: (patch: unknown) => void; | ||
66 | let listener = { | ||
67 | onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => {}), | ||
68 | onPatch: jest.fn((_patch: IJsonPatch) => {}), | ||
69 | }; | ||
70 | |||
71 | beforeEach(() => { | ||
72 | sut = createSophieRenderer(); | ||
73 | service = mocked(RendererToMainIpcService).mock.instances[0]; | ||
74 | onSharedStorePatch = mocked(service.onSharedStorePatch).mock.calls[0][0]; | ||
75 | }); | ||
76 | |||
77 | describe('onSharedStoreChange', () => { | ||
78 | it('requests a snapshot from the service', async () => { | ||
79 | mocked(service.getSharedStoreSnapshot).mockResolvedValueOnce(snapshot); | ||
80 | await sut.onSharedStoreChange(listener); | ||
81 | expect(service.getSharedStoreSnapshot).toBeCalledTimes(1); | ||
82 | }); | ||
83 | |||
84 | it('passes the snapshot to the listener', async () => { | ||
85 | mocked(service.getSharedStoreSnapshot).mockResolvedValueOnce(snapshot); | ||
86 | await sut.onSharedStoreChange(listener); | ||
87 | expect(listener.onSnapshot).toBeCalledWith(snapshot); | ||
88 | }); | ||
89 | |||
90 | it('catches service errors without exposing them', async () => { | ||
91 | mocked(service.getSharedStoreSnapshot).mockRejectedValue(new Error('s3cr3t')); | ||
92 | await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty( | ||
93 | 'message', | ||
94 | expect.stringMatching(/s3cr3t/), | ||
95 | ); | ||
96 | expect(listener.onSnapshot).toBeCalledTimes(0); | ||
97 | }); | ||
98 | |||
99 | it('does not pass on invalid snapshots', async () => { | ||
100 | mocked(service.getSharedStoreSnapshot).mockResolvedValueOnce(invalidSnapshot); | ||
101 | await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); | ||
102 | expect(listener.onSnapshot).toBeCalledTimes(0); | ||
103 | }); | ||
104 | }); | ||
105 | |||
106 | describe('dispatchAction', () => { | ||
107 | it('dispatched valid actions', () => { | ||
108 | sut.dispatchAction(action); | ||
109 | expect(service.dispatchAction).toBeCalledWith(action); | ||
110 | }); | ||
111 | |||
112 | it('does not dispatch invalid actions', () => { | ||
113 | expect(() => sut.dispatchAction(invalidAction)).toThrowError(); | ||
114 | expect(service.dispatchAction).toBeCalledTimes(0); | ||
115 | }); | ||
116 | }); | ||
117 | |||
118 | describe('when no listener is registered', () => { | ||
119 | it('discards the received patch', () => { | ||
120 | onSharedStorePatch(patch); | ||
121 | }); | ||
122 | }); | ||
123 | |||
124 | function itRefusesToRegisterAnotherListener() { | ||
125 | it('refuses to register another listener', async () => { | ||
126 | await expect(sut.onSharedStoreChange(listener)).rejects.toBeInstanceOf(Error); | ||
127 | }); | ||
128 | } | ||
129 | |||
130 | function itDoesNotPassPatchesToTheListener() { | ||
131 | it('does not pass patches to the listener', () => { | ||
132 | onSharedStorePatch(patch); | ||
133 | expect(listener.onPatch).toBeCalledTimes(0); | ||
134 | }); | ||
135 | } | ||
136 | |||
137 | describe('when a listener registered successfully', () => { | ||
138 | beforeEach(async () => { | ||
139 | mocked(service.getSharedStoreSnapshot).mockResolvedValueOnce(snapshot); | ||
140 | await sut.onSharedStoreChange(listener); | ||
141 | }); | ||
142 | |||
143 | it('passes patches to the listener', () => { | ||
144 | onSharedStorePatch(patch); | ||
145 | expect(listener.onPatch).toBeCalledWith(patch); | ||
146 | }); | ||
147 | |||
148 | it('catches listener errors', () => { | ||
149 | mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); | ||
150 | onSharedStorePatch(patch); | ||
151 | }); | ||
152 | |||
153 | itRefusesToRegisterAnotherListener(); | ||
154 | |||
155 | describe('that later threw an error', () => { | ||
156 | beforeEach(() => { | ||
157 | mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); | ||
158 | onSharedStorePatch(patch); | ||
159 | listener.onPatch.mockRestore(); | ||
160 | }); | ||
161 | |||
162 | itDoesNotPassPatchesToTheListener(); | ||
163 | }); | ||
164 | }); | ||
165 | |||
166 | describe('when a listener failed to register due to service error', () => { | ||
167 | beforeEach(async () => { | ||
168 | mocked(service.getSharedStoreSnapshot).mockRejectedValue(new Error()); | ||
169 | try { | ||
170 | await sut.onSharedStoreChange(listener); | ||
171 | } catch { | ||
172 | // Ignore error. | ||
173 | } | ||
174 | }); | ||
175 | |||
176 | itRefusesToRegisterAnotherListener(); | ||
177 | |||
178 | itDoesNotPassPatchesToTheListener(); | ||
179 | }); | ||
180 | |||
181 | describe('when a listener failed to register due to an invalid snapshot', () => { | ||
182 | beforeEach(async () => { | ||
183 | mocked(service.getSharedStoreSnapshot).mockResolvedValueOnce(invalidSnapshot); | ||
184 | try { | ||
185 | await sut.onSharedStoreChange(listener); | ||
186 | } catch { | ||
187 | // Ignore error. | ||
188 | } | ||
189 | }); | ||
190 | |||
191 | itRefusesToRegisterAnotherListener(); | ||
192 | |||
193 | itDoesNotPassPatchesToTheListener(); | ||
194 | }); | ||
195 | |||
196 | describe('when a listener failed to register due to listener error', () => { | ||
197 | beforeEach(async () => { | ||
198 | mocked(service.getSharedStoreSnapshot).mockResolvedValueOnce(snapshot); | ||
199 | mocked(listener.onSnapshot).mockImplementation(() => { throw new Error(); }); | ||
200 | try { | ||
201 | await sut.onSharedStoreChange(listener); | ||
202 | } catch { | ||
203 | // Ignore error. | ||
204 | } | ||
205 | }); | ||
206 | |||
207 | itRefusesToRegisterAnotherListener(); | ||
208 | |||
209 | itDoesNotPassPatchesToTheListener(); | ||
210 | }); | ||
211 | }); | ||