@dhis2/app-service-offline 3.17.0 → 3.17.1
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/.gitignore +5 -0
- package/build/cjs/declarations.d.js +1 -0
- package/build/es/declarations.d.js +1 -0
- package/d2.config.js +9 -0
- package/jest.config.js +14 -0
- package/package.json +3 -3
- package/src/__tests__/integration.test.tsx +341 -0
- package/src/declarations.d.ts +1 -0
- package/src/index.ts +12 -0
- package/src/lib/__tests__/cacheable-section-state.test.tsx +45 -0
- package/src/lib/__tests__/clear-sensitive-caches.test.ts +182 -0
- package/src/lib/__tests__/network-status.test.tsx +496 -0
- package/src/lib/__tests__/offline-provider.test.tsx +116 -0
- package/src/lib/__tests__/use-cacheable-section.test.tsx +280 -0
- package/src/lib/__tests__/use-online-status-message.test.tsx +27 -0
- package/src/lib/cacheable-section-state.tsx +269 -0
- package/src/lib/cacheable-section.tsx +193 -0
- package/src/lib/clear-sensitive-caches.ts +92 -0
- package/src/lib/dhis2-connection-status/dev-debug-log.ts +20 -0
- package/src/lib/dhis2-connection-status/dhis2-connection-status.test.tsx +947 -0
- package/src/lib/dhis2-connection-status/dhis2-connection-status.tsx +241 -0
- package/src/lib/dhis2-connection-status/index.ts +4 -0
- package/src/lib/dhis2-connection-status/is-ping-available.test.ts +32 -0
- package/src/lib/dhis2-connection-status/is-ping-available.ts +31 -0
- package/src/lib/dhis2-connection-status/smart-interval.ts +206 -0
- package/src/lib/dhis2-connection-status/use-ping-query.ts +14 -0
- package/src/lib/global-state-service.tsx +110 -0
- package/src/lib/network-status.ts +80 -0
- package/src/lib/offline-interface.tsx +57 -0
- package/src/lib/offline-provider.tsx +43 -0
- package/src/lib/online-status-message.tsx +47 -0
- package/src/setupRTL.ts +1 -0
- package/src/types.ts +66 -0
- package/src/utils/__tests__/render-counter.test.tsx +45 -0
- package/src/utils/render-counter.tsx +22 -0
- package/src/utils/test-mocks.ts +47 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import {
|
|
2
|
+
render,
|
|
3
|
+
screen,
|
|
4
|
+
waitFor,
|
|
5
|
+
act,
|
|
6
|
+
renderHook,
|
|
7
|
+
} from '@testing-library/react'
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { useNetworkStatus as useOnlineStatus } from '../network-status'
|
|
10
|
+
|
|
11
|
+
interface CapturedEventListeners {
|
|
12
|
+
[index: string]: EventListener
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function wait(ms: number): Promise<void> {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
setTimeout(() => resolve(), ms)
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
jest.restoreAllMocks()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
localStorage.clear()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
describe('initalizes to navigator.onLine value', () => {
|
|
30
|
+
it('initializes to true', () => {
|
|
31
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
32
|
+
const { result } = renderHook(() => useOnlineStatus())
|
|
33
|
+
|
|
34
|
+
expect(result.current.online).toBe(true)
|
|
35
|
+
expect(result.current.offline).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('initializes to false', () => {
|
|
39
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(false)
|
|
40
|
+
const { result } = renderHook(() => useOnlineStatus())
|
|
41
|
+
|
|
42
|
+
expect(result.current.online).toBe(false)
|
|
43
|
+
expect(result.current.offline).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
describe('state changes in response to browser "online" and "offline" events', () => {
|
|
48
|
+
it('switches from online to offline when the "offline" event triggers', async () => {
|
|
49
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
50
|
+
// Capture callback to trigger later using addEventListener mock
|
|
51
|
+
const events: CapturedEventListeners = {}
|
|
52
|
+
window.addEventListener = jest.fn(
|
|
53
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
54
|
+
)
|
|
55
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
56
|
+
initialProps: { debounceDelay: 50 },
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
act(() => {
|
|
60
|
+
// Trigger callback captured by addEventListener mock
|
|
61
|
+
events.offline(new Event('offline'))
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Wait for debounce
|
|
65
|
+
await waitFor(() => {
|
|
66
|
+
expect(result.current.online).toBe(false)
|
|
67
|
+
expect(result.current.offline).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('switches from offline to online when the "online" event triggers', async () => {
|
|
72
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(false)
|
|
73
|
+
const events: CapturedEventListeners = {}
|
|
74
|
+
window.addEventListener = jest.fn(
|
|
75
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
76
|
+
)
|
|
77
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
78
|
+
initialProps: { debounceDelay: 50 },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
act(() => {
|
|
82
|
+
events.online(new Event('online'))
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Wait for debounce
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
expect(result.current.online).toBe(true)
|
|
88
|
+
expect(result.current.offline).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('debouncing state changes', () => {
|
|
94
|
+
it('debounces with a 1s delay by default', async () => {
|
|
95
|
+
// Start online
|
|
96
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
97
|
+
const events: CapturedEventListeners = {}
|
|
98
|
+
window.addEventListener = jest.fn(
|
|
99
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
100
|
+
)
|
|
101
|
+
const { result } = renderHook(() => useOnlineStatus())
|
|
102
|
+
|
|
103
|
+
await act(async () => {
|
|
104
|
+
// Multiple events in succession
|
|
105
|
+
events.offline(new Event('offline'))
|
|
106
|
+
events.online(new Event('online'))
|
|
107
|
+
events.offline(new Event('offline'))
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// Immediately, nothing should happen
|
|
111
|
+
expect(result.current.online).toBe(true)
|
|
112
|
+
|
|
113
|
+
await wait(1000)
|
|
114
|
+
// 1s later, final 'offline' event should resolve
|
|
115
|
+
await waitFor(() => {
|
|
116
|
+
expect(result.current.online).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('can have debounce delay set to another number', async () => {
|
|
121
|
+
// Start online
|
|
122
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
123
|
+
const events: CapturedEventListeners = {}
|
|
124
|
+
window.addEventListener = jest.fn(
|
|
125
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
126
|
+
)
|
|
127
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
128
|
+
initialProps: { debounceDelay: 50 },
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
await act(async () => {
|
|
132
|
+
// Multiple events in succession
|
|
133
|
+
events.offline(new Event('offline'))
|
|
134
|
+
events.online(new Event('online'))
|
|
135
|
+
events.offline(new Event('offline'))
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
// Immediately, nothing should happen
|
|
139
|
+
expect(result.current.online).toBe(true)
|
|
140
|
+
|
|
141
|
+
// 50ms later, final "offline" event should finally resolve
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(result.current.online).toBe(false)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('can use a debounceDelay of 0 to skip debouncing', async () => {
|
|
148
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
149
|
+
const events: CapturedEventListeners = {}
|
|
150
|
+
window.addEventListener = jest.fn(
|
|
151
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
152
|
+
)
|
|
153
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
154
|
+
initialProps: { debounceDelay: 0 },
|
|
155
|
+
})
|
|
156
|
+
await act(async () => {
|
|
157
|
+
events.offline(new Event('offline'))
|
|
158
|
+
events.online(new Event('online'))
|
|
159
|
+
events.offline(new Event('offline'))
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// await wait(0) didn't work here
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
// There should be no delay before status is offline
|
|
165
|
+
expect(result.current.online).toBe(false)
|
|
166
|
+
expect(result.current.offline).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it('can have the debounce delay changed during its lifecycle', async () => {
|
|
171
|
+
// Start with 150 ms debounce
|
|
172
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
173
|
+
const events: CapturedEventListeners = {}
|
|
174
|
+
window.addEventListener = jest.fn(
|
|
175
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
176
|
+
)
|
|
177
|
+
const { result, rerender } = renderHook(
|
|
178
|
+
(...args) => useOnlineStatus(...args),
|
|
179
|
+
{ initialProps: { debounceDelay: 150 } }
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
await act(async () => {
|
|
183
|
+
// Multiple events in succession
|
|
184
|
+
events.offline(new Event('offline'))
|
|
185
|
+
events.online(new Event('online'))
|
|
186
|
+
events.offline(new Event('offline'))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Immediately, nothing should happen
|
|
190
|
+
expect(result.current.online).toBe(true)
|
|
191
|
+
|
|
192
|
+
// 150ms later, final "offline" event should finally resolve
|
|
193
|
+
await wait(160)
|
|
194
|
+
expect(result.current.online).toBe(false)
|
|
195
|
+
|
|
196
|
+
// Change to 50 ms debounce
|
|
197
|
+
rerender({ debounceDelay: 50 })
|
|
198
|
+
|
|
199
|
+
await act(async () => {
|
|
200
|
+
// Multiple events in succession
|
|
201
|
+
events.online(new Event('online'))
|
|
202
|
+
events.offline(new Event('offline'))
|
|
203
|
+
events.online(new Event('online'))
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Immediately, nothing should happen
|
|
207
|
+
expect(result.current.online).toBe(false)
|
|
208
|
+
|
|
209
|
+
// 50ms later, final "online" event should finally resolve
|
|
210
|
+
await wait(60)
|
|
211
|
+
expect(result.current.online).toBe(true)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('handles debounced state change when parent component rerenders during a debounce delay', async () => {
|
|
215
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
216
|
+
const events: CapturedEventListeners = {}
|
|
217
|
+
window.addEventListener = jest.fn(
|
|
218
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
const TestComponent = () => {
|
|
222
|
+
const { online } = useOnlineStatus({ debounceDelay: 50 })
|
|
223
|
+
return <div data-testid="status">{online ? 'on' : 'off'}</div>
|
|
224
|
+
}
|
|
225
|
+
const { rerender } = render(<TestComponent />)
|
|
226
|
+
|
|
227
|
+
const { getByTestId } = screen
|
|
228
|
+
expect(getByTestId('status')).toHaveTextContent('on')
|
|
229
|
+
|
|
230
|
+
await act(async () => {
|
|
231
|
+
// Multiple events in succession
|
|
232
|
+
events.offline(new Event('offline'))
|
|
233
|
+
events.online(new Event('online'))
|
|
234
|
+
events.offline(new Event('offline'))
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// Immediately, nothing should happen
|
|
238
|
+
expect(getByTestId('status')).toHaveTextContent('on')
|
|
239
|
+
|
|
240
|
+
// Rerender parent component
|
|
241
|
+
rerender(<TestComponent />)
|
|
242
|
+
|
|
243
|
+
// Final "offline" event should still resolve
|
|
244
|
+
await waitFor(() =>
|
|
245
|
+
expect(getByTestId('status')).toHaveTextContent('off')
|
|
246
|
+
)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('handles debounced state change when debounce delay is changed during a delay', async () => {
|
|
250
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
251
|
+
const events: CapturedEventListeners = {}
|
|
252
|
+
window.addEventListener = jest.fn(
|
|
253
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const TestComponent = ({ options }: { options?: any }) => {
|
|
257
|
+
const { online } = useOnlineStatus(options)
|
|
258
|
+
return <div data-testid="status">{online ? 'on' : 'off'}</div>
|
|
259
|
+
}
|
|
260
|
+
const { rerender } = render(
|
|
261
|
+
<TestComponent options={{ debounceDelay: 100 }} />
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const { getByTestId } = screen
|
|
265
|
+
expect(getByTestId('status')).toHaveTextContent('on')
|
|
266
|
+
|
|
267
|
+
await act(async () => {
|
|
268
|
+
// Multiple events in succession
|
|
269
|
+
events.offline(new Event('offline'))
|
|
270
|
+
events.online(new Event('online'))
|
|
271
|
+
events.offline(new Event('offline'))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
// Immediately, nothing should happen
|
|
275
|
+
expect(getByTestId('status')).toHaveTextContent('on')
|
|
276
|
+
|
|
277
|
+
// Change debounce options
|
|
278
|
+
rerender(<TestComponent options={{ debounceDelay: 50 }} />)
|
|
279
|
+
|
|
280
|
+
// Final "offline" event should still resolve
|
|
281
|
+
await waitFor(() =>
|
|
282
|
+
expect(getByTestId('status')).toHaveTextContent('off')
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('debounces consistently across rerenders', async () => {
|
|
287
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
288
|
+
const events: CapturedEventListeners = {}
|
|
289
|
+
window.addEventListener = jest.fn(
|
|
290
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
const TestComponent = () => {
|
|
294
|
+
const { online } = useOnlineStatus({ debounceDelay: 100 })
|
|
295
|
+
return <div data-testid="status">{online ? 'on' : 'off'}</div>
|
|
296
|
+
}
|
|
297
|
+
const { rerender } = render(<TestComponent />)
|
|
298
|
+
|
|
299
|
+
const { getByTestId } = screen
|
|
300
|
+
expect(getByTestId('status')).toHaveTextContent('on')
|
|
301
|
+
|
|
302
|
+
await act(async () => {
|
|
303
|
+
// Multiple events in succession
|
|
304
|
+
events.offline(new Event('offline'))
|
|
305
|
+
events.online(new Event('online'))
|
|
306
|
+
events.offline(new Event('offline'))
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
// wait a little bit - not long enough for debounce to resolve
|
|
310
|
+
await wait(50)
|
|
311
|
+
expect(getByTestId('status')).toHaveTextContent('on')
|
|
312
|
+
|
|
313
|
+
// Rerender parent component
|
|
314
|
+
rerender(<TestComponent />)
|
|
315
|
+
|
|
316
|
+
// Trigger more events
|
|
317
|
+
await act(async () => {
|
|
318
|
+
events.online(new Event('online'))
|
|
319
|
+
events.offline(new Event('offline'))
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// wait a little more - long enough that the first debounced callbacks
|
|
323
|
+
// _would_ have resolved if there weren't the second set of events
|
|
324
|
+
await wait(60)
|
|
325
|
+
expect(getByTestId('status')).toHaveTextContent('on')
|
|
326
|
+
|
|
327
|
+
// wait long enough for second set of callbacks to resolve
|
|
328
|
+
await waitFor(() =>
|
|
329
|
+
expect(getByTestId('status')).toHaveTextContent('off')
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
describe('it updates the lastOnline value in local storage', () => {
|
|
335
|
+
const lastOnlineKey = 'dhis2.lastOnline'
|
|
336
|
+
const testDateString = 'Fri, 27 Aug 2021 19:53:06 GMT'
|
|
337
|
+
|
|
338
|
+
it('sets lastOnline in local storage when it goes offline', async () => {
|
|
339
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
340
|
+
const events: CapturedEventListeners = {}
|
|
341
|
+
window.addEventListener = jest.fn(
|
|
342
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
343
|
+
)
|
|
344
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
345
|
+
initialProps: { debounceDelay: 0 },
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
// Correct initial state
|
|
349
|
+
expect(localStorage.getItem(lastOnlineKey)).toBe(null)
|
|
350
|
+
expect(result.current.lastOnline).toBe(null)
|
|
351
|
+
|
|
352
|
+
act(() => {
|
|
353
|
+
events.offline(new Event('offline'))
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
// Wait for debounce
|
|
357
|
+
await wait(500)
|
|
358
|
+
|
|
359
|
+
expect(result.current.online).toBe(false)
|
|
360
|
+
expect(result.current.offline).toBe(true)
|
|
361
|
+
|
|
362
|
+
// Check localStorage for a stored date
|
|
363
|
+
const parsedDate = new Date(
|
|
364
|
+
localStorage.getItem(lastOnlineKey) as string
|
|
365
|
+
)
|
|
366
|
+
expect(parsedDate.toString()).not.toBe('Invalid Date')
|
|
367
|
+
// Check hook return value
|
|
368
|
+
expect(result.current.lastOnline).toBeInstanceOf(Date)
|
|
369
|
+
expect(result.current.lastOnline?.toUTCString()).toBe(
|
|
370
|
+
localStorage.getItem(lastOnlineKey)
|
|
371
|
+
)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// not necessary
|
|
375
|
+
it.skip("sets lastOnline on mount if it's not set", () => {
|
|
376
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(false)
|
|
377
|
+
const events: CapturedEventListeners = {}
|
|
378
|
+
window.addEventListener = jest.fn(
|
|
379
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
380
|
+
)
|
|
381
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
382
|
+
initialProps: { debounceDelay: 0 },
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
const parsedDate = new Date(
|
|
386
|
+
localStorage.getItem(lastOnlineKey) as string
|
|
387
|
+
)
|
|
388
|
+
expect(parsedDate.toString()).not.toBe('Invalid Date')
|
|
389
|
+
expect(result.current.lastOnline).toBeInstanceOf(Date)
|
|
390
|
+
expect(result.current.lastOnline?.toUTCString()).toBe(
|
|
391
|
+
localStorage.getItem(lastOnlineKey)
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it("doesn't change lastOnline it exists and if it's already offline", async () => {
|
|
396
|
+
// seed localStorage
|
|
397
|
+
localStorage.setItem(lastOnlineKey, testDateString)
|
|
398
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(false)
|
|
399
|
+
const events: CapturedEventListeners = {}
|
|
400
|
+
window.addEventListener = jest.fn(
|
|
401
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
402
|
+
)
|
|
403
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
404
|
+
initialProps: { debounceDelay: 0 },
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
expect(localStorage.getItem(lastOnlineKey)).toBe(testDateString)
|
|
408
|
+
expect(result.current.lastOnline).toEqual(new Date(testDateString))
|
|
409
|
+
|
|
410
|
+
act(() => {
|
|
411
|
+
events.offline(new Event('offline'))
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
await wait(0)
|
|
415
|
+
|
|
416
|
+
expect(result.current.online).toBe(false)
|
|
417
|
+
expect(result.current.offline).toBe(true)
|
|
418
|
+
|
|
419
|
+
expect(localStorage.getItem(lastOnlineKey)).toBe(testDateString)
|
|
420
|
+
expect(result.current.lastOnline).toEqual(new Date(testDateString))
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
it('clears lastOnline when it goes online', async () => {
|
|
424
|
+
// seed localStorage
|
|
425
|
+
localStorage.setItem(lastOnlineKey, testDateString)
|
|
426
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(false)
|
|
427
|
+
const events: CapturedEventListeners = {}
|
|
428
|
+
window.addEventListener = jest.fn(
|
|
429
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
430
|
+
)
|
|
431
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
432
|
+
initialProps: { debounceDelay: 0 },
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
expect(localStorage.getItem(lastOnlineKey)).toBe(testDateString)
|
|
436
|
+
expect(result.current.lastOnline).toEqual(new Date(testDateString))
|
|
437
|
+
|
|
438
|
+
act(() => {
|
|
439
|
+
events.offline(new Event('online'))
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
// Wait for debounce
|
|
443
|
+
await wait(500)
|
|
444
|
+
|
|
445
|
+
expect(result.current.online).toBe(true)
|
|
446
|
+
expect(result.current.offline).toBe(false)
|
|
447
|
+
|
|
448
|
+
// expect(localStorage.getItem(lastOnlineKey)).toBe(null)
|
|
449
|
+
expect(result.current.lastOnline).toBe(null)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('tracks correctly when going offline and online', async () => {
|
|
453
|
+
jest.spyOn(navigator, 'onLine', 'get').mockReturnValueOnce(true)
|
|
454
|
+
const events: CapturedEventListeners = {}
|
|
455
|
+
window.addEventListener = jest.fn(
|
|
456
|
+
(event, cb) => (events[event] = cb as EventListener)
|
|
457
|
+
)
|
|
458
|
+
const { result } = renderHook((...args) => useOnlineStatus(...args), {
|
|
459
|
+
initialProps: { debounceDelay: 0 },
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
// Correct initial state
|
|
463
|
+
expect(localStorage.getItem(lastOnlineKey)).toBe(null)
|
|
464
|
+
expect(result.current.lastOnline).toBe(null)
|
|
465
|
+
|
|
466
|
+
act(() => {
|
|
467
|
+
events.offline(new Event('offline'))
|
|
468
|
+
})
|
|
469
|
+
await waitFor(() => undefined)
|
|
470
|
+
|
|
471
|
+
const firstDate = new Date(
|
|
472
|
+
localStorage.getItem(lastOnlineKey) as string
|
|
473
|
+
)
|
|
474
|
+
const firstValue = result.current.lastOnline?.valueOf()
|
|
475
|
+
|
|
476
|
+
act(() => {
|
|
477
|
+
events.offline(new Event('online'))
|
|
478
|
+
})
|
|
479
|
+
await wait(500)
|
|
480
|
+
|
|
481
|
+
expect(result.current.lastOnline).toBe(null)
|
|
482
|
+
|
|
483
|
+
// todo: this is an error from UTC strings' imprecision
|
|
484
|
+
await wait(500)
|
|
485
|
+
|
|
486
|
+
act(() => {
|
|
487
|
+
events.offline(new Event('offline'))
|
|
488
|
+
})
|
|
489
|
+
await wait(500)
|
|
490
|
+
|
|
491
|
+
expect(
|
|
492
|
+
new Date(localStorage.getItem(lastOnlineKey) as string)
|
|
493
|
+
).not.toEqual(firstDate)
|
|
494
|
+
expect(result.current.lastOnline?.valueOf()).not.toEqual(firstValue)
|
|
495
|
+
})
|
|
496
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { mockOfflineInterface } from '../../utils/test-mocks'
|
|
4
|
+
import { useCacheableSection, CacheableSection } from '../cacheable-section'
|
|
5
|
+
import { useCachedSections } from '../cacheable-section-state'
|
|
6
|
+
import { OfflineProvider } from '../offline-provider'
|
|
7
|
+
|
|
8
|
+
// Suppress 'act' warning for these tests
|
|
9
|
+
const originalError = console.error
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
jest.spyOn(console, 'error').mockImplementation((...args) => {
|
|
12
|
+
const pattern =
|
|
13
|
+
/Warning: An update to .* inside a test was not wrapped in act/
|
|
14
|
+
if (typeof args[0] === 'string' && pattern.test(args[0])) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
return originalError.call(console, ...args)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
jest.clearAllMocks()
|
|
23
|
+
// syntax needed to appease typescript
|
|
24
|
+
;(console.error as jest.Mock).mockRestore()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('Testing offline provider', () => {
|
|
28
|
+
it('Should render without failing', () => {
|
|
29
|
+
render(
|
|
30
|
+
<OfflineProvider offlineInterface={mockOfflineInterface}>
|
|
31
|
+
<div data-testid="test-div" />
|
|
32
|
+
</OfflineProvider>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(screen.getByTestId('test-div')).toBeInTheDocument()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('Should sync cached sections with indexedDB', async () => {
|
|
39
|
+
const testOfflineInterface = {
|
|
40
|
+
...mockOfflineInterface,
|
|
41
|
+
getCachedSections: jest.fn().mockResolvedValue([
|
|
42
|
+
{ sectionId: '1', lastUpdated: 'date1' },
|
|
43
|
+
{ sectionId: '2', lastUpdated: 'date2' },
|
|
44
|
+
]),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CachedSections = () => {
|
|
48
|
+
const { cachedSections } = useCachedSections()
|
|
49
|
+
return (
|
|
50
|
+
<div data-testid="sections">
|
|
51
|
+
{JSON.stringify(cachedSections)}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
render(
|
|
57
|
+
<OfflineProvider offlineInterface={testOfflineInterface}>
|
|
58
|
+
<CachedSections />
|
|
59
|
+
</OfflineProvider>
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const { getByTestId } = screen
|
|
63
|
+
expect(testOfflineInterface.getCachedSections).toHaveBeenCalled()
|
|
64
|
+
await waitFor(() => getByTestId('sections').textContent !== '{}')
|
|
65
|
+
const textContent = JSON.parse(
|
|
66
|
+
getByTestId('sections').textContent || ''
|
|
67
|
+
)
|
|
68
|
+
expect(textContent).toEqual({
|
|
69
|
+
1: { lastUpdated: 'date1' },
|
|
70
|
+
2: { lastUpdated: 'date2' },
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('Should provide the relevant contexts to consumers', () => {
|
|
75
|
+
const TestConsumer = () => {
|
|
76
|
+
useCacheableSection('id')
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<CacheableSection loadingMask={<div />} id={'id'}>
|
|
80
|
+
<div data-testid="test-div" />
|
|
81
|
+
</CacheableSection>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
render(
|
|
86
|
+
<OfflineProvider offlineInterface={mockOfflineInterface}>
|
|
87
|
+
<TestConsumer />
|
|
88
|
+
</OfflineProvider>
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
expect(screen.getByTestId('test-div')).toBeInTheDocument()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('Should render without failing when no offlineInterface is provided', () => {
|
|
95
|
+
render(
|
|
96
|
+
<OfflineProvider>
|
|
97
|
+
<div data-testid="test-div" />
|
|
98
|
+
</OfflineProvider>
|
|
99
|
+
)
|
|
100
|
+
expect(screen.getByTestId('test-div')).toBeInTheDocument()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('Should render without failing if PWA is not enabled', () => {
|
|
104
|
+
const testOfflineInterface = {
|
|
105
|
+
...mockOfflineInterface,
|
|
106
|
+
pwaEnabled: false,
|
|
107
|
+
}
|
|
108
|
+
render(
|
|
109
|
+
<OfflineProvider offlineInterface={testOfflineInterface}>
|
|
110
|
+
<div data-testid="test-div" />
|
|
111
|
+
</OfflineProvider>
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(screen.getByTestId('test-div')).toBeInTheDocument()
|
|
115
|
+
})
|
|
116
|
+
})
|