diff options
Diffstat (limited to 'packages/preload')
-rw-r--r-- | packages/preload/src/contextBridge/SophieRendererImpl.ts | 8 | ||||
-rw-r--r-- | packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts | 51 | ||||
-rw-r--r-- | packages/preload/src/index.ts | 4 | ||||
-rw-r--r-- | packages/preload/tsconfig.json | 3 |
4 files changed, 45 insertions, 21 deletions
diff --git a/packages/preload/src/contextBridge/SophieRendererImpl.ts b/packages/preload/src/contextBridge/SophieRendererImpl.ts index 18ab07e..4c24b74 100644 --- a/packages/preload/src/contextBridge/SophieRendererImpl.ts +++ b/packages/preload/src/contextBridge/SophieRendererImpl.ts | |||
@@ -35,7 +35,7 @@ class SophieRendererImpl implements SophieRenderer { | |||
35 | 35 | ||
36 | private listener: SharedStoreListener | null = null; | 36 | private listener: SharedStoreListener | null = null; |
37 | 37 | ||
38 | constructor() { | 38 | constructor(private readonly allowReplaceListener: boolean) { |
39 | ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => { | 39 | ipcRenderer.on(MainToRendererIpcMessage.SharedStorePatch, (_event, patch) => { |
40 | try { | 40 | try { |
41 | // `mobx-state-tree` will validate the patch, so we can safely cast here. | 41 | // `mobx-state-tree` will validate the patch, so we can safely cast here. |
@@ -48,7 +48,7 @@ class SophieRendererImpl implements SophieRenderer { | |||
48 | } | 48 | } |
49 | 49 | ||
50 | async onSharedStoreChange(listener: SharedStoreListener): Promise<void> { | 50 | async onSharedStoreChange(listener: SharedStoreListener): Promise<void> { |
51 | if (this.onSharedStoreChangeCalled) { | 51 | if (this.onSharedStoreChangeCalled && !this.allowReplaceListener) { |
52 | throw new Error('Shared store change listener was already set'); | 52 | throw new Error('Shared store change listener was already set'); |
53 | } | 53 | } |
54 | this.onSharedStoreChangeCalled = true; | 54 | this.onSharedStoreChangeCalled = true; |
@@ -86,8 +86,8 @@ class SophieRendererImpl implements SophieRenderer { | |||
86 | } | 86 | } |
87 | } | 87 | } |
88 | 88 | ||
89 | export function createSophieRenderer(): SophieRenderer { | 89 | export function createSophieRenderer(allowReplaceListener: boolean): SophieRenderer { |
90 | const impl = new SophieRendererImpl(); | 90 | const impl = new SophieRendererImpl(allowReplaceListener); |
91 | return { | 91 | return { |
92 | onSharedStoreChange: impl.onSharedStoreChange.bind(impl), | 92 | onSharedStoreChange: impl.onSharedStoreChange.bind(impl), |
93 | dispatchAction: impl.dispatchAction.bind(impl), | 93 | dispatchAction: impl.dispatchAction.bind(impl), |
diff --git a/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts b/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts index 2f295e6..71ac2a1 100644 --- a/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts +++ b/packages/preload/src/contextBridge/__tests__/SophieRendererImpl.spec.ts | |||
@@ -60,7 +60,7 @@ const invalidAction = { | |||
60 | 60 | ||
61 | describe('createSophieRenderer', () => { | 61 | describe('createSophieRenderer', () => { |
62 | it('registers a shared store patch listener', () => { | 62 | it('registers a shared store patch listener', () => { |
63 | createSophieRenderer(); | 63 | createSophieRenderer(false); |
64 | expect(ipcRenderer.on).toHaveBeenCalledWith( | 64 | expect(ipcRenderer.on).toHaveBeenCalledWith( |
65 | MainToRendererIpcMessage.SharedStorePatch, | 65 | MainToRendererIpcMessage.SharedStorePatch, |
66 | expect.anything(), | 66 | expect.anything(), |
@@ -77,26 +77,21 @@ describe('SophieRendererImpl', () => { | |||
77 | }; | 77 | }; |
78 | 78 | ||
79 | beforeEach(() => { | 79 | beforeEach(() => { |
80 | sut = createSophieRenderer(); | 80 | sut = createSophieRenderer(false); |
81 | onSharedStorePatch = mocked(ipcRenderer.on).mock.calls.find(([channel]) => { | 81 | onSharedStorePatch = mocked(ipcRenderer.on).mock.calls.find(([channel]) => { |
82 | return channel === MainToRendererIpcMessage.SharedStorePatch; | 82 | return channel === MainToRendererIpcMessage.SharedStorePatch; |
83 | })?.[1]!; | 83 | })?.[1]!; |
84 | }); | 84 | }); |
85 | 85 | ||
86 | describe('onSharedStoreChange', () => { | 86 | describe('onSharedStoreChange', () => { |
87 | it('requests a snapshot from the service', async () => { | 87 | it('requests a snapshot from the main process', async () => { |
88 | mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); | 88 | mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); |
89 | await sut.onSharedStoreChange(listener); | 89 | await sut.onSharedStoreChange(listener); |
90 | expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); | 90 | expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); |
91 | }); | ||
92 | |||
93 | it('passes the snapshot to the listener', async () => { | ||
94 | mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot); | ||
95 | await sut.onSharedStoreChange(listener); | ||
96 | expect(listener.onSnapshot).toBeCalledWith(snapshot); | 91 | expect(listener.onSnapshot).toBeCalledWith(snapshot); |
97 | }); | 92 | }); |
98 | 93 | ||
99 | it('catches service errors without exposing them', async () => { | 94 | it('catches IPC errors without exposing them', async () => { |
100 | mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t')); | 95 | mocked(ipcRenderer.invoke).mockRejectedValue(new Error('s3cr3t')); |
101 | await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty( | 96 | await expect(sut.onSharedStoreChange(listener)).rejects.not.toHaveProperty( |
102 | 'message', | 97 | 'message', |
@@ -125,7 +120,7 @@ describe('SophieRendererImpl', () => { | |||
125 | }); | 120 | }); |
126 | 121 | ||
127 | describe('when no listener is registered', () => { | 122 | describe('when no listener is registered', () => { |
128 | it('discards the received patch', () => { | 123 | it('discards the received patch without any error', () => { |
129 | onSharedStorePatch(event, patch); | 124 | onSharedStorePatch(event, patch); |
130 | }); | 125 | }); |
131 | }); | 126 | }); |
@@ -136,8 +131,10 @@ describe('SophieRendererImpl', () => { | |||
136 | }); | 131 | }); |
137 | } | 132 | } |
138 | 133 | ||
139 | function itDoesNotPassPatchesToTheListener() { | 134 | function itDoesNotPassPatchesToTheListener( |
140 | it('does not pass patches to the listener', () => { | 135 | name: string = 'does not pass patches to the listener', |
136 | ) { | ||
137 | it(name, () => { | ||
141 | onSharedStorePatch(event, patch); | 138 | onSharedStorePatch(event, patch); |
142 | expect(listener.onPatch).toBeCalledTimes(0); | 139 | expect(listener.onPatch).toBeCalledTimes(0); |
143 | }); | 140 | }); |
@@ -161,18 +158,18 @@ describe('SophieRendererImpl', () => { | |||
161 | 158 | ||
162 | itRefusesToRegisterAnotherListener(); | 159 | itRefusesToRegisterAnotherListener(); |
163 | 160 | ||
164 | describe('that later threw an error', () => { | 161 | describe('after the listener threw in onPatch', () => { |
165 | beforeEach(() => { | 162 | beforeEach(() => { |
166 | mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); | 163 | mocked(listener.onPatch).mockImplementation(() => { throw new Error(); }); |
167 | onSharedStorePatch(event, patch); | 164 | onSharedStorePatch(event, patch); |
168 | listener.onPatch.mockRestore(); | 165 | listener.onPatch.mockRestore(); |
169 | }); | 166 | }); |
170 | 167 | ||
171 | itDoesNotPassPatchesToTheListener(); | 168 | itDoesNotPassPatchesToTheListener('does not pass on patches any more'); |
172 | }); | 169 | }); |
173 | }); | 170 | }); |
174 | 171 | ||
175 | describe('when a listener failed to register due to service error', () => { | 172 | describe('when a listener failed to register due to IPC error', () => { |
176 | beforeEach(async () => { | 173 | beforeEach(async () => { |
177 | mocked(ipcRenderer.invoke).mockRejectedValue(new Error()); | 174 | mocked(ipcRenderer.invoke).mockRejectedValue(new Error()); |
178 | try { | 175 | try { |
@@ -217,4 +214,28 @@ describe('SophieRendererImpl', () => { | |||
217 | 214 | ||
218 | itDoesNotPassPatchesToTheListener(); | 215 | itDoesNotPassPatchesToTheListener(); |
219 | }); | 216 | }); |
217 | |||
218 | describe('when it is allowed to replace listeners', () => { | ||
219 | const snapshot2 = { | ||
220 | shouldUseDarkColors: false, | ||
221 | } | ||
222 | const listener2 = { | ||
223 | onSnapshot: jest.fn((_snapshot: SharedStoreSnapshotIn) => { }), | ||
224 | onPatch: jest.fn((_patch: IJsonPatch) => { }), | ||
225 | }; | ||
226 | |||
227 | it('should fetch a second snapshot', async () => { | ||
228 | mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2); | ||
229 | await sut.onSharedStoreChange(listener2); | ||
230 | expect(ipcRenderer.invoke).toBeCalledWith(RendererToMainIpcMessage.GetSharedStoreSnapshot); | ||
231 | expect(listener2.onSnapshot).toBeCalledWith(snapshot2); | ||
232 | }); | ||
233 | |||
234 | it('should pass the second snapshot to the new listener', async () => { | ||
235 | mocked(ipcRenderer.invoke).mockResolvedValueOnce(snapshot2); | ||
236 | await sut.onSharedStoreChange(listener2); | ||
237 | onSharedStorePatch(event, patch); | ||
238 | expect(listener2.onPatch).toBeCalledWith(patch); | ||
239 | }); | ||
240 | }); | ||
220 | }); | 241 | }); |
diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index ef466b4..de91742 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts | |||
@@ -22,6 +22,8 @@ import { contextBridge } from 'electron'; | |||
22 | 22 | ||
23 | import { createSophieRenderer } from './contextBridge/SophieRendererImpl'; | 23 | import { createSophieRenderer } from './contextBridge/SophieRendererImpl'; |
24 | 24 | ||
25 | const sophieRenderer = createSophieRenderer(); | 25 | const isDevelopment = import.meta.env.MODE === 'development'; |
26 | |||
27 | const sophieRenderer = createSophieRenderer(isDevelopment); | ||
26 | 28 | ||
27 | contextBridge.exposeInMainWorld('sophieRenderer', sophieRenderer); | 29 | contextBridge.exposeInMainWorld('sophieRenderer', sophieRenderer); |
diff --git a/packages/preload/tsconfig.json b/packages/preload/tsconfig.json index ab274a1..49f223a 100644 --- a/packages/preload/tsconfig.json +++ b/packages/preload/tsconfig.json | |||
@@ -11,7 +11,8 @@ | |||
11 | "esnext" | 11 | "esnext" |
12 | ], | 12 | ], |
13 | "types": [ | 13 | "types": [ |
14 | "@types/jest" | 14 | "@types/jest", |
15 | "vite/client" | ||
15 | ] | 16 | ] |
16 | }, | 17 | }, |
17 | "references": [ | 18 | "references": [ |