@dhis2/app-service-offline 3.17.1 → 3.18.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/.gitignore +0 -5
- package/d2.config.js +0 -9
- package/jest.config.js +0 -14
- package/src/__tests__/integration.test.tsx +0 -341
- package/src/declarations.d.ts +0 -1
- package/src/index.ts +0 -12
- package/src/lib/__tests__/cacheable-section-state.test.tsx +0 -45
- package/src/lib/__tests__/clear-sensitive-caches.test.ts +0 -182
- package/src/lib/__tests__/network-status.test.tsx +0 -496
- package/src/lib/__tests__/offline-provider.test.tsx +0 -116
- package/src/lib/__tests__/use-cacheable-section.test.tsx +0 -280
- package/src/lib/__tests__/use-online-status-message.test.tsx +0 -27
- package/src/lib/cacheable-section-state.tsx +0 -269
- package/src/lib/cacheable-section.tsx +0 -193
- package/src/lib/clear-sensitive-caches.ts +0 -92
- package/src/lib/dhis2-connection-status/dev-debug-log.ts +0 -20
- package/src/lib/dhis2-connection-status/dhis2-connection-status.test.tsx +0 -947
- package/src/lib/dhis2-connection-status/dhis2-connection-status.tsx +0 -241
- package/src/lib/dhis2-connection-status/index.ts +0 -4
- package/src/lib/dhis2-connection-status/is-ping-available.test.ts +0 -32
- package/src/lib/dhis2-connection-status/is-ping-available.ts +0 -31
- package/src/lib/dhis2-connection-status/smart-interval.ts +0 -206
- package/src/lib/dhis2-connection-status/use-ping-query.ts +0 -14
- package/src/lib/global-state-service.tsx +0 -110
- package/src/lib/network-status.ts +0 -80
- package/src/lib/offline-interface.tsx +0 -57
- package/src/lib/offline-provider.tsx +0 -43
- package/src/lib/online-status-message.tsx +0 -47
- package/src/setupRTL.ts +0 -1
- package/src/types.ts +0 -66
- package/src/utils/__tests__/render-counter.test.tsx +0 -45
- package/src/utils/render-counter.tsx +0 -22
- package/src/utils/test-mocks.ts +0 -47
- package/tsconfig.json +0 -10
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhis2/app-service-offline",
|
|
3
3
|
"description": "A runtime service for online/offline detection and offline caching",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.18.0-beta.2",
|
|
5
5
|
"main": "./build/cjs/index.js",
|
|
6
6
|
"module": "./build/es/index.js",
|
|
7
7
|
"types": "./build/types/index.d.ts",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
|
-
"
|
|
24
|
+
"build/**"
|
|
25
25
|
],
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build:types": "tsc --emitDeclarationOnly --outDir ./build/types",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"coverage": "yarn test --coverage"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@dhis2/app-service-config": "3.
|
|
37
|
+
"@dhis2/app-service-config": "3.18.0-beta.2",
|
|
38
38
|
"react": "^16.8.6 || ^18",
|
|
39
39
|
"react-dom": "^16.8.6 || ^18"
|
|
40
40
|
},
|
package/.gitignore
DELETED
package/d2.config.js
DELETED
package/jest.config.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
collectCoverageFrom: [
|
|
3
|
-
'src/**/*.(js|jsx|ts|tsx)',
|
|
4
|
-
'!src/index.ts',
|
|
5
|
-
'!src/**/types/*',
|
|
6
|
-
'!src/**/types.ts',
|
|
7
|
-
],
|
|
8
|
-
|
|
9
|
-
// Setup react-testing-library
|
|
10
|
-
setupFilesAfterEnv: ['<rootDir>/src/setupRTL.ts'],
|
|
11
|
-
// Fix for Jest 27
|
|
12
|
-
// https://github.com/facebook/jest/issues/11404#issuecomment-1003328922
|
|
13
|
-
testRunner: 'jest-jasmine2',
|
|
14
|
-
}
|
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
import { AlertsProvider } from '@dhis2/app-service-alerts'
|
|
2
|
-
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
3
|
-
import React from 'react'
|
|
4
|
-
import {
|
|
5
|
-
useCacheableSection,
|
|
6
|
-
CacheableSection,
|
|
7
|
-
CacheableSectionStartRecording,
|
|
8
|
-
} from '../lib/cacheable-section'
|
|
9
|
-
import { OfflineProvider } from '../lib/offline-provider'
|
|
10
|
-
import { RenderCounter, resetRenderCounts } from '../utils/render-counter'
|
|
11
|
-
import {
|
|
12
|
-
errorRecordingMock,
|
|
13
|
-
failedMessageRecordingMock,
|
|
14
|
-
mockOfflineInterface,
|
|
15
|
-
} from '../utils/test-mocks'
|
|
16
|
-
|
|
17
|
-
const renderCounts = {}
|
|
18
|
-
|
|
19
|
-
const identity = (arg: any) => arg
|
|
20
|
-
|
|
21
|
-
const TestControls = ({
|
|
22
|
-
id,
|
|
23
|
-
makeRecordingHandler = identity,
|
|
24
|
-
}: {
|
|
25
|
-
id: string
|
|
26
|
-
makeRecordingHandler?: (cb?: any) => () => Promise<any>
|
|
27
|
-
}) => {
|
|
28
|
-
const { startRecording, remove, isCached, lastUpdated, recordingState } =
|
|
29
|
-
useCacheableSection(id)
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<>
|
|
33
|
-
<RenderCounter id={`controls-rc-${id}`} countsObj={renderCounts} />
|
|
34
|
-
<button
|
|
35
|
-
data-testid={`start-recording-${id}`}
|
|
36
|
-
onClick={makeRecordingHandler(startRecording)}
|
|
37
|
-
/>
|
|
38
|
-
<button
|
|
39
|
-
data-testid={`remove-${id}`}
|
|
40
|
-
onClick={() => {
|
|
41
|
-
remove()
|
|
42
|
-
}}
|
|
43
|
-
/>
|
|
44
|
-
<div data-testid={`is-cached-${id}`}>{isCached ? 'yes' : 'no'}</div>
|
|
45
|
-
<div data-testid={`last-updated-${id}`}>
|
|
46
|
-
{lastUpdated?.toISOString() ?? 'never'}
|
|
47
|
-
</div>
|
|
48
|
-
<div data-testid={`recording-state-${id}`}>{recordingState}</div>
|
|
49
|
-
</>
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const TestSection = ({
|
|
54
|
-
id,
|
|
55
|
-
children,
|
|
56
|
-
}: {
|
|
57
|
-
id: string
|
|
58
|
-
children?: React.ReactNode
|
|
59
|
-
}) => (
|
|
60
|
-
<CacheableSection
|
|
61
|
-
id={id}
|
|
62
|
-
loadingMask={<div data-testid={`loading-mask-${id}`} />}
|
|
63
|
-
>
|
|
64
|
-
<RenderCounter id={`section-rc-${id}`} countsObj={renderCounts} />
|
|
65
|
-
{children}
|
|
66
|
-
</CacheableSection>
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
const TestSingleSection = (props?: any) => {
|
|
70
|
-
// Props are spread so they can be overwritten
|
|
71
|
-
return (
|
|
72
|
-
<AlertsProvider>
|
|
73
|
-
<OfflineProvider offlineInterface={mockOfflineInterface} {...props}>
|
|
74
|
-
<TestControls id={'1'} {...props} />
|
|
75
|
-
<TestSection id={'1'} {...props} />
|
|
76
|
-
</OfflineProvider>
|
|
77
|
-
</AlertsProvider>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Suppress 'act' warning for these tests
|
|
82
|
-
const originalError = console.error
|
|
83
|
-
beforeEach(() => {
|
|
84
|
-
// This is done before each because the 'recording error' test uses its own
|
|
85
|
-
// spy on console.error
|
|
86
|
-
jest.spyOn(console, 'error').mockImplementation((...args) => {
|
|
87
|
-
const pattern =
|
|
88
|
-
/Warning: An update to .* inside a test was not wrapped in act/
|
|
89
|
-
if (typeof args[0] === 'string' && pattern.test(args[0])) {
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
return originalError.call(console, ...args)
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
afterEach(() => {
|
|
97
|
-
jest.clearAllMocks()
|
|
98
|
-
// This syntax appeases typescript:
|
|
99
|
-
;(console.error as jest.Mock).mockRestore()
|
|
100
|
-
resetRenderCounts(renderCounts)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
describe('Coordination between useCacheableSection and CacheableSection', () => {
|
|
104
|
-
it('renders in the default state initially', async () => {
|
|
105
|
-
render(<TestSingleSection />)
|
|
106
|
-
|
|
107
|
-
const { getByTestId } = screen
|
|
108
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent('default')
|
|
109
|
-
expect(getByTestId(/is-cached/)).toHaveTextContent('no')
|
|
110
|
-
expect(getByTestId(/last-updated/)).toHaveTextContent('never')
|
|
111
|
-
expect(getByTestId(/section-rc/)).toBeInTheDocument()
|
|
112
|
-
expect(getByTestId(/controls-rc/)).toBeInTheDocument()
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('handles a successful recording', async (done) => {
|
|
116
|
-
const { getByTestId, queryByTestId } = screen
|
|
117
|
-
|
|
118
|
-
const onStarted = () => {
|
|
119
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent(
|
|
120
|
-
'recording'
|
|
121
|
-
)
|
|
122
|
-
expect(getByTestId(/loading-mask/)).toBeInTheDocument()
|
|
123
|
-
expect(getByTestId(/section-rc/)).toBeInTheDocument()
|
|
124
|
-
}
|
|
125
|
-
const onCompleted = () => {
|
|
126
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent('default')
|
|
127
|
-
expect(queryByTestId(/loading-mask/)).not.toBeInTheDocument()
|
|
128
|
-
done()
|
|
129
|
-
}
|
|
130
|
-
const recordingOptions = { onStarted, onCompleted }
|
|
131
|
-
const makeRecordingHandler = (
|
|
132
|
-
startRecording: CacheableSectionStartRecording
|
|
133
|
-
) => {
|
|
134
|
-
return () => startRecording(recordingOptions)
|
|
135
|
-
}
|
|
136
|
-
render(
|
|
137
|
-
<TestSingleSection makeRecordingHandler={makeRecordingHandler} />
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
await act(async () => {
|
|
141
|
-
fireEvent.click(getByTestId(/start-recording/))
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
// At this stage, should be pending
|
|
145
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent('pending')
|
|
146
|
-
expect(queryByTestId(/section-rc/)).not.toBeInTheDocument()
|
|
147
|
-
expect.assertions(7)
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('handles a recording that encounters an error', async (done) => {
|
|
151
|
-
// Suppress the expected error from console (in addition to 'act' warning)
|
|
152
|
-
jest.spyOn(console, 'error').mockImplementation((...args) => {
|
|
153
|
-
const actPattern =
|
|
154
|
-
/Warning: An update to .* inside a test was not wrapped in act/
|
|
155
|
-
const errPattern = /Error during recording/
|
|
156
|
-
const matchesPattern =
|
|
157
|
-
actPattern.test(args[0]) || errPattern.test(args[0])
|
|
158
|
-
if (typeof args[0] === 'string' && matchesPattern) {
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
return originalError.call(console, ...args)
|
|
162
|
-
})
|
|
163
|
-
const { getByTestId, queryByTestId } = screen
|
|
164
|
-
|
|
165
|
-
const testOfflineInterface = {
|
|
166
|
-
...mockOfflineInterface,
|
|
167
|
-
startRecording: errorRecordingMock,
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const onError = () => {
|
|
171
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent('error')
|
|
172
|
-
expect(queryByTestId(/loading-mask/)).not.toBeInTheDocument()
|
|
173
|
-
expect(getByTestId(/section-rc/)).toBeInTheDocument()
|
|
174
|
-
done()
|
|
175
|
-
}
|
|
176
|
-
const makeRecordingHandler = (
|
|
177
|
-
startRecording: CacheableSectionStartRecording
|
|
178
|
-
) => {
|
|
179
|
-
return () => startRecording({ onError })
|
|
180
|
-
}
|
|
181
|
-
render(
|
|
182
|
-
<TestSingleSection
|
|
183
|
-
offlineInterface={testOfflineInterface}
|
|
184
|
-
makeRecordingHandler={makeRecordingHandler}
|
|
185
|
-
/>
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
await act(async () => {
|
|
189
|
-
fireEvent.click(getByTestId(/start-recording/))
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
expect.assertions(3)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
// ! After bumping testing-library versions, something about this test
|
|
196
|
-
// ! causes the following ones to mysteriously fail 😤
|
|
197
|
-
it.skip('handles an error starting the recording', async (done) => {
|
|
198
|
-
const { getByTestId } = screen
|
|
199
|
-
const testOfflineInterface = {
|
|
200
|
-
...mockOfflineInterface,
|
|
201
|
-
startRecording: failedMessageRecordingMock,
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const onStarted = jest.fn()
|
|
205
|
-
|
|
206
|
-
const testErrCondition = (err: Error) => {
|
|
207
|
-
expect(err.message).toBe('Failed message') // from the mock
|
|
208
|
-
expect(onStarted).not.toHaveBeenCalled()
|
|
209
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent('default')
|
|
210
|
-
done()
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const makeRecordingHandler = (
|
|
214
|
-
startRecording: CacheableSectionStartRecording
|
|
215
|
-
) => {
|
|
216
|
-
return () => startRecording({ onStarted }).catch(testErrCondition)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
render(
|
|
220
|
-
<TestSingleSection
|
|
221
|
-
offlineInterface={testOfflineInterface}
|
|
222
|
-
makeRecordingHandler={makeRecordingHandler}
|
|
223
|
-
/>
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
await act(async () => {
|
|
227
|
-
fireEvent.click(getByTestId(/start-recording/))
|
|
228
|
-
})
|
|
229
|
-
})
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
const TwoTestSections = (props?: any) => (
|
|
233
|
-
// Props are spread so they can be overwritten (but only on one section)
|
|
234
|
-
<AlertsProvider>
|
|
235
|
-
<OfflineProvider offlineInterface={mockOfflineInterface} {...props}>
|
|
236
|
-
<TestControls id={'1'} {...props} />
|
|
237
|
-
<TestSection id={'1'} {...props} />
|
|
238
|
-
<TestControls id={'2'} />
|
|
239
|
-
<TestSection id={'2'} />
|
|
240
|
-
</OfflineProvider>
|
|
241
|
-
</AlertsProvider>
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
// test that other sections don't rerender when one section does
|
|
245
|
-
describe('Performant state management', () => {
|
|
246
|
-
it('establishes a pre-recording render count', () => {
|
|
247
|
-
render(<TwoTestSections />)
|
|
248
|
-
|
|
249
|
-
const { getByTestId } = screen
|
|
250
|
-
// Two renders for controls: undefined and 'default' states
|
|
251
|
-
expect(getByTestId('controls-rc-1')).toHaveTextContent('2')
|
|
252
|
-
expect(getByTestId('controls-rc-2')).toHaveTextContent('2')
|
|
253
|
-
// Just one render for sections
|
|
254
|
-
expect(getByTestId('section-rc-1')).toHaveTextContent('1')
|
|
255
|
-
expect(getByTestId('section-rc-2')).toHaveTextContent('1')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('isolates rerenders from other consumers', async (done) => {
|
|
259
|
-
const { getByTestId } = screen
|
|
260
|
-
// Make assertions
|
|
261
|
-
const onCompleted = () => {
|
|
262
|
-
// Before refactor: controls components have 6 renders EACH, and
|
|
263
|
-
// sections 1 and 2 have 2 and 1 renders, respectively
|
|
264
|
-
// After refactor, render counts for section that recorded:
|
|
265
|
-
expect(getByTestId('controls-rc-1')).toHaveTextContent('5')
|
|
266
|
-
expect(getByTestId('section-rc-1')).toHaveTextContent('2')
|
|
267
|
-
// Section that did not record (should be same as pre-recording):
|
|
268
|
-
expect(getByTestId('controls-rc-2')).toHaveTextContent('2')
|
|
269
|
-
expect(getByTestId('section-rc-2')).toHaveTextContent('1')
|
|
270
|
-
done()
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const makeRecordingHandler =
|
|
274
|
-
(startRecording: CacheableSectionStartRecording) => () =>
|
|
275
|
-
startRecording({ onCompleted })
|
|
276
|
-
render(<TwoTestSections makeRecordingHandler={makeRecordingHandler} />)
|
|
277
|
-
|
|
278
|
-
await act(async () => {
|
|
279
|
-
fireEvent.click(getByTestId('start-recording-1'))
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
expect.assertions(4)
|
|
283
|
-
})
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
describe('useCacheableSection can be used inside a child of CacheableSection', () => {
|
|
287
|
-
const ChildTest = (props?: any) => {
|
|
288
|
-
// Props are spread so they can be overwritten
|
|
289
|
-
return (
|
|
290
|
-
<AlertsProvider>
|
|
291
|
-
<OfflineProvider
|
|
292
|
-
offlineInterface={mockOfflineInterface}
|
|
293
|
-
{...props}
|
|
294
|
-
>
|
|
295
|
-
<TestSection id={'1'} {...props}>
|
|
296
|
-
<TestControls id={'1'} {...props} />
|
|
297
|
-
</TestSection>
|
|
298
|
-
</OfflineProvider>
|
|
299
|
-
</AlertsProvider>
|
|
300
|
-
)
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
it('handles a successful recording', async (done) => {
|
|
304
|
-
const { getByTestId, queryByTestId } = screen
|
|
305
|
-
|
|
306
|
-
const onStarted = async () => {
|
|
307
|
-
await waitFor(() => {
|
|
308
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent(
|
|
309
|
-
'recording'
|
|
310
|
-
)
|
|
311
|
-
expect(getByTestId(/loading-mask/)).toBeInTheDocument()
|
|
312
|
-
expect(getByTestId(/section-rc/)).toBeInTheDocument()
|
|
313
|
-
})
|
|
314
|
-
}
|
|
315
|
-
const onCompleted = () => {
|
|
316
|
-
expect(getByTestId(/recording-state/)).toHaveTextContent('default')
|
|
317
|
-
expect(queryByTestId(/loading-mask/)).not.toBeInTheDocument()
|
|
318
|
-
done()
|
|
319
|
-
}
|
|
320
|
-
const recordingOptions = { onStarted, onCompleted }
|
|
321
|
-
const makeRecordingHandler = (
|
|
322
|
-
startRecording: CacheableSectionStartRecording
|
|
323
|
-
) => {
|
|
324
|
-
return () => startRecording(recordingOptions)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
render(<ChildTest makeRecordingHandler={makeRecordingHandler} />)
|
|
328
|
-
|
|
329
|
-
await act(async () => {
|
|
330
|
-
await fireEvent.click(getByTestId(/start-recording/))
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
await waitFor(() => {
|
|
334
|
-
// At this stage, should be pending
|
|
335
|
-
// - In this test case, 'controls' should not be rendered
|
|
336
|
-
expect(queryByTestId(/recording-state/)).not.toBeInTheDocument()
|
|
337
|
-
expect(queryByTestId(/section-rc/)).not.toBeInTheDocument()
|
|
338
|
-
expect.assertions(7)
|
|
339
|
-
})
|
|
340
|
-
})
|
|
341
|
-
})
|
package/src/declarations.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module 'fake-indexeddb/lib/FDBFactory'
|
package/src/index.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export { OfflineProvider } from './lib/offline-provider'
|
|
2
|
-
export { CacheableSection, useCacheableSection } from './lib/cacheable-section'
|
|
3
|
-
export { useCachedSections } from './lib/cacheable-section-state'
|
|
4
|
-
// Use "useOnlineStatus" name for backwards compatibility
|
|
5
|
-
export { useNetworkStatus as useOnlineStatus } from './lib/network-status'
|
|
6
|
-
export {
|
|
7
|
-
useOnlineStatusMessage,
|
|
8
|
-
useSetOnlineStatusMessage,
|
|
9
|
-
useOnlineStatusMessageValue,
|
|
10
|
-
} from './lib/online-status-message'
|
|
11
|
-
export { clearSensitiveCaches } from './lib/clear-sensitive-caches'
|
|
12
|
-
export { useDhis2ConnectionStatus } from './lib/dhis2-connection-status'
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { renderHook } from '@testing-library/react'
|
|
2
|
-
import React, { FC, PropsWithChildren } from 'react'
|
|
3
|
-
import { mockOfflineInterface } from '../../utils/test-mocks'
|
|
4
|
-
import { useCachedSection, useRecordingState } from '../cacheable-section-state'
|
|
5
|
-
import { OfflineProvider } from '../offline-provider'
|
|
6
|
-
|
|
7
|
-
const wrapper: FC<PropsWithChildren> = ({ children }) => (
|
|
8
|
-
<OfflineProvider offlineInterface={mockOfflineInterface}>
|
|
9
|
-
{children}
|
|
10
|
-
</OfflineProvider>
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
test('useRecordingState has stable references', () => {
|
|
14
|
-
const { result, rerender } = renderHook(() => useRecordingState('one'), {
|
|
15
|
-
wrapper,
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
const origRecordingState = result.current.recordingState
|
|
19
|
-
const origSetRecordingState = result.current.setRecordingState
|
|
20
|
-
const origRemoveRecordingState = result.current.removeRecordingState
|
|
21
|
-
|
|
22
|
-
rerender()
|
|
23
|
-
|
|
24
|
-
expect(result.current.recordingState).toBe(origRecordingState)
|
|
25
|
-
expect(result.current.setRecordingState).toBe(origSetRecordingState)
|
|
26
|
-
expect(result.current.removeRecordingState).toBe(origRemoveRecordingState)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
test('useCachedSection has stable references', () => {
|
|
30
|
-
const { result, rerender } = renderHook(() => useCachedSection('one'), {
|
|
31
|
-
wrapper,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const origIsCached = result.current.isCached
|
|
35
|
-
const origLastUpdated = result.current.lastUpdated
|
|
36
|
-
const origRemove = result.current.remove
|
|
37
|
-
const origSyncCachedSections = result.current.syncCachedSections
|
|
38
|
-
|
|
39
|
-
rerender()
|
|
40
|
-
|
|
41
|
-
expect(result.current.isCached).toBe(origIsCached)
|
|
42
|
-
expect(result.current.lastUpdated).toBe(origLastUpdated)
|
|
43
|
-
expect(result.current.remove).toBe(origRemove)
|
|
44
|
-
expect(result.current.syncCachedSections).toBe(origSyncCachedSections)
|
|
45
|
-
})
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
import FDBFactory from 'fake-indexeddb/lib/FDBFactory'
|
|
2
|
-
import { openDB } from 'idb'
|
|
3
|
-
import 'fake-indexeddb/auto'
|
|
4
|
-
import {
|
|
5
|
-
clearSensitiveCaches,
|
|
6
|
-
SECTIONS_DB,
|
|
7
|
-
SECTIONS_STORE,
|
|
8
|
-
} from '../clear-sensitive-caches'
|
|
9
|
-
|
|
10
|
-
// Mocks for CacheStorage API
|
|
11
|
-
|
|
12
|
-
// Returns true if an existing cache is deleted
|
|
13
|
-
const makeCachesDeleteMock = (keys: string[]) => {
|
|
14
|
-
return jest
|
|
15
|
-
.fn()
|
|
16
|
-
.mockImplementation((key) => Promise.resolve(keys.includes(key)))
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const keysMockDefault = jest.fn().mockImplementation(async () => [])
|
|
20
|
-
const deleteMockDefault = makeCachesDeleteMock([])
|
|
21
|
-
const cachesDefault = {
|
|
22
|
-
keys: keysMockDefault,
|
|
23
|
-
delete: deleteMockDefault,
|
|
24
|
-
// the following to satisfy types:
|
|
25
|
-
has: () => Promise.resolve(true),
|
|
26
|
-
open: () => Promise.resolve(new Cache()),
|
|
27
|
-
match: () => Promise.resolve(new Response()),
|
|
28
|
-
}
|
|
29
|
-
window.caches = cachesDefault
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
window.caches = cachesDefault
|
|
33
|
-
jest.clearAllMocks()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
// silence debug logs for these tests
|
|
37
|
-
const originalDebug = console.debug
|
|
38
|
-
beforeAll(() => {
|
|
39
|
-
jest.spyOn(console, 'debug').mockImplementation((...args) => {
|
|
40
|
-
const pattern = /Clearing sensitive caches/
|
|
41
|
-
if (typeof args[0] === 'string' && pattern.test(args[0])) {
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
return originalDebug.call(console, ...args)
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
afterAll(() => {
|
|
48
|
-
;(console.debug as jest.Mock).mockRestore()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('does not fail if there are no caches or no sections-db', () => {
|
|
52
|
-
return expect(clearSensitiveCaches()).resolves.toBe(false)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('returns false if caches.keys throws', async () => {
|
|
56
|
-
const spy = jest.fn(() => {
|
|
57
|
-
throw new Error('Security Error')
|
|
58
|
-
})
|
|
59
|
-
window.caches = {
|
|
60
|
-
...cachesDefault,
|
|
61
|
-
keys: spy,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const result = await clearSensitiveCaches()
|
|
65
|
-
|
|
66
|
-
expect(spy).toHaveBeenCalled()
|
|
67
|
-
expect(result).toBe(false)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('clears potentially sensitive caches', async () => {
|
|
71
|
-
const testKeys = ['cache1', 'cache2', 'app-shell', 'other-assets']
|
|
72
|
-
const keysMock = jest
|
|
73
|
-
.fn()
|
|
74
|
-
.mockImplementation(() => Promise.resolve(testKeys))
|
|
75
|
-
const deleteMock = makeCachesDeleteMock(testKeys)
|
|
76
|
-
window.caches = { ...cachesDefault, keys: keysMock, delete: deleteMock }
|
|
77
|
-
|
|
78
|
-
const cachesDeleted = await clearSensitiveCaches()
|
|
79
|
-
expect(cachesDeleted).toBe(true)
|
|
80
|
-
|
|
81
|
-
expect(deleteMock).toHaveBeenCalledTimes(4)
|
|
82
|
-
expect(deleteMock.mock.calls[0][0]).toBe('cache1')
|
|
83
|
-
expect(deleteMock.mock.calls[1][0]).toBe('cache2')
|
|
84
|
-
expect(deleteMock.mock.calls[2][0]).toBe('app-shell')
|
|
85
|
-
expect(deleteMock.mock.calls[3][0]).toBe('other-assets')
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('preserves keepable caches', async () => {
|
|
89
|
-
const keysMock = jest
|
|
90
|
-
.fn()
|
|
91
|
-
.mockImplementation(async () => [
|
|
92
|
-
'cache1',
|
|
93
|
-
'cache2',
|
|
94
|
-
'app-shell',
|
|
95
|
-
'other-assets',
|
|
96
|
-
'workbox-precache-v2-https://hey.howareya.now/',
|
|
97
|
-
])
|
|
98
|
-
window.caches = { ...cachesDefault, keys: keysMock }
|
|
99
|
-
|
|
100
|
-
await clearSensitiveCaches()
|
|
101
|
-
|
|
102
|
-
expect(deleteMockDefault).toHaveBeenCalledTimes(4)
|
|
103
|
-
expect(deleteMockDefault.mock.calls[0][0]).toBe('cache1')
|
|
104
|
-
expect(deleteMockDefault.mock.calls[1][0]).toBe('cache2')
|
|
105
|
-
expect(deleteMockDefault.mock.calls[2][0]).toBe('app-shell')
|
|
106
|
-
expect(deleteMockDefault.mock.calls[3][0]).toBe('other-assets')
|
|
107
|
-
expect(deleteMockDefault).not.toHaveBeenCalledWith(
|
|
108
|
-
'workbox-precache-v2-https://hey.howareya.now/'
|
|
109
|
-
)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
describe('clears sections-db', () => {
|
|
113
|
-
// Test DB
|
|
114
|
-
function openTestDB(dbName: string) {
|
|
115
|
-
// simplified version of app platform openDB logic
|
|
116
|
-
return openDB(dbName, 1, {
|
|
117
|
-
upgrade(db) {
|
|
118
|
-
db.createObjectStore(SECTIONS_STORE, { keyPath: 'sectionId' })
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
afterEach(() => {
|
|
124
|
-
// reset indexedDB state
|
|
125
|
-
window.indexedDB = new FDBFactory()
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('clears sections-db if it exists', async () => {
|
|
129
|
-
// Open and populate test DB
|
|
130
|
-
const db = await openTestDB(SECTIONS_DB)
|
|
131
|
-
await db.put(SECTIONS_STORE, {
|
|
132
|
-
sectionId: 'id-1',
|
|
133
|
-
lastUpdated: new Date(),
|
|
134
|
-
requests: 3,
|
|
135
|
-
})
|
|
136
|
-
await db.put(SECTIONS_STORE, {
|
|
137
|
-
sectionId: 'id-2',
|
|
138
|
-
lastUpdated: new Date(),
|
|
139
|
-
requests: 3,
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
await clearSensitiveCaches()
|
|
143
|
-
|
|
144
|
-
// Sections-db should be cleared
|
|
145
|
-
const allSections = await db.getAll(SECTIONS_STORE)
|
|
146
|
-
expect(allSections).toHaveLength(0)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it("doesn't clear sections-db if it doesn't exist and doesn't open a new one", async () => {
|
|
150
|
-
const openMock = jest.fn()
|
|
151
|
-
window.indexedDB.open = openMock
|
|
152
|
-
|
|
153
|
-
expect(await indexedDB.databases()).not.toContain(SECTIONS_DB)
|
|
154
|
-
|
|
155
|
-
await clearSensitiveCaches()
|
|
156
|
-
|
|
157
|
-
expect(openMock).not.toHaveBeenCalled()
|
|
158
|
-
return expect(await indexedDB.databases()).not.toContain(SECTIONS_DB)
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it("doesn't handle IDB if 'databases' property is not on window.indexedDB", async () => {
|
|
162
|
-
// Open DB -- 'indexedDB.open' _would_ get called in this test
|
|
163
|
-
// if 'databases' property exists
|
|
164
|
-
await openTestDB(SECTIONS_DB)
|
|
165
|
-
const openMock = jest.fn()
|
|
166
|
-
window.indexedDB.open = openMock
|
|
167
|
-
|
|
168
|
-
// Remove 'databases' from indexedDB prototype for this test
|
|
169
|
-
// (simulates Firefox environment)
|
|
170
|
-
const idbProto = Object.getPrototypeOf(window.indexedDB)
|
|
171
|
-
const databases = idbProto.databases
|
|
172
|
-
delete idbProto.databases
|
|
173
|
-
|
|
174
|
-
expect('databases' in window.indexedDB).toBe(false)
|
|
175
|
-
await expect(clearSensitiveCaches()).resolves.toBeDefined()
|
|
176
|
-
expect(openMock).not.toHaveBeenCalled()
|
|
177
|
-
|
|
178
|
-
// Restore indexedDB prototype for later tests
|
|
179
|
-
idbProto.databases = databases
|
|
180
|
-
expect('databases' in window.indexedDB).toBe(true)
|
|
181
|
-
})
|
|
182
|
-
})
|