aboutsummaryrefslogtreecommitdiffstats
path: root/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts')
-rw-r--r--packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts211
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
21import { mocked } from 'jest-mock';
22import type { IJsonPatch } from 'mobx-state-tree';
23import type { Action, SharedStoreSnapshotIn, SophieRenderer } from '@sophie/shared';
24
25import { RendererToMainIpcService } from '../../services/RendererToMainIpcService';
26import { createSophieRenderer } from '../SophieRendererImpl';
27
28jest.mock('../../services/RendererToMainIpcService');
29
30const snapshot: SharedStoreSnapshotIn = {
31 shouldUseDarkColors: true,
32};
33
34const invalidSnapshot = {
35 shouldUseDarkColors: -1,
36} as unknown as SharedStoreSnapshotIn;
37
38const patch: IJsonPatch = {
39 op: 'replace',
40 path: 'foo',
41 value: 'bar',
42};
43
44const action: Action = {
45 action: 'set-theme-source',
46 themeSource: 'dark',
47};
48
49const invalidAction = {
50 action: 'not-a-valid-action',
51} as unknown as Action;
52
53describe('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
62describe('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});