@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.
Files changed (35) hide show
  1. package/package.json +3 -3
  2. package/.gitignore +0 -5
  3. package/d2.config.js +0 -9
  4. package/jest.config.js +0 -14
  5. package/src/__tests__/integration.test.tsx +0 -341
  6. package/src/declarations.d.ts +0 -1
  7. package/src/index.ts +0 -12
  8. package/src/lib/__tests__/cacheable-section-state.test.tsx +0 -45
  9. package/src/lib/__tests__/clear-sensitive-caches.test.ts +0 -182
  10. package/src/lib/__tests__/network-status.test.tsx +0 -496
  11. package/src/lib/__tests__/offline-provider.test.tsx +0 -116
  12. package/src/lib/__tests__/use-cacheable-section.test.tsx +0 -280
  13. package/src/lib/__tests__/use-online-status-message.test.tsx +0 -27
  14. package/src/lib/cacheable-section-state.tsx +0 -269
  15. package/src/lib/cacheable-section.tsx +0 -193
  16. package/src/lib/clear-sensitive-caches.ts +0 -92
  17. package/src/lib/dhis2-connection-status/dev-debug-log.ts +0 -20
  18. package/src/lib/dhis2-connection-status/dhis2-connection-status.test.tsx +0 -947
  19. package/src/lib/dhis2-connection-status/dhis2-connection-status.tsx +0 -241
  20. package/src/lib/dhis2-connection-status/index.ts +0 -4
  21. package/src/lib/dhis2-connection-status/is-ping-available.test.ts +0 -32
  22. package/src/lib/dhis2-connection-status/is-ping-available.ts +0 -31
  23. package/src/lib/dhis2-connection-status/smart-interval.ts +0 -206
  24. package/src/lib/dhis2-connection-status/use-ping-query.ts +0 -14
  25. package/src/lib/global-state-service.tsx +0 -110
  26. package/src/lib/network-status.ts +0 -80
  27. package/src/lib/offline-interface.tsx +0 -57
  28. package/src/lib/offline-provider.tsx +0 -43
  29. package/src/lib/online-status-message.tsx +0 -47
  30. package/src/setupRTL.ts +0 -1
  31. package/src/types.ts +0 -66
  32. package/src/utils/__tests__/render-counter.test.tsx +0 -45
  33. package/src/utils/render-counter.tsx +0 -22
  34. package/src/utils/test-mocks.ts +0 -47
  35. package/tsconfig.json +0 -10
@@ -1,496 +0,0 @@
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
- })
@@ -1,116 +0,0 @@
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
- })