@dhis2/app-service-offline 2.10.0 → 2.12.0

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 (41) hide show
  1. package/build/cjs/__tests__/integration.test.js +337 -0
  2. package/build/cjs/index.js +39 -1
  3. package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +131 -0
  4. package/build/cjs/lib/__tests__/offline-provider.test.js +127 -0
  5. package/build/cjs/lib/__tests__/use-cacheable-section.test.js +227 -0
  6. package/build/cjs/lib/cacheable-section-state.js +218 -0
  7. package/build/cjs/lib/cacheable-section.js +156 -0
  8. package/build/cjs/lib/clear-sensitive-caches.js +87 -0
  9. package/build/cjs/lib/global-state-service.js +95 -0
  10. package/build/cjs/lib/offline-interface.js +86 -0
  11. package/build/cjs/lib/offline-provider.js +53 -0
  12. package/build/cjs/types.js +0 -1
  13. package/build/cjs/utils/__tests__/render-counter.test.js +55 -0
  14. package/build/cjs/utils/render-counter.js +26 -0
  15. package/build/cjs/utils/test-mocks.js +40 -0
  16. package/build/es/__tests__/integration.test.js +327 -0
  17. package/build/es/index.js +5 -1
  18. package/build/es/lib/__tests__/clear-sensitive-caches.test.js +123 -0
  19. package/build/es/lib/__tests__/offline-provider.test.js +117 -0
  20. package/build/es/lib/__tests__/use-cacheable-section.test.js +218 -0
  21. package/build/es/lib/cacheable-section-state.js +199 -0
  22. package/build/es/lib/cacheable-section.js +137 -0
  23. package/build/es/lib/clear-sensitive-caches.js +78 -0
  24. package/build/es/lib/global-state-service.js +70 -0
  25. package/build/es/lib/offline-interface.js +65 -0
  26. package/build/es/lib/offline-provider.js +40 -0
  27. package/build/es/types.js +0 -1
  28. package/build/es/utils/__tests__/render-counter.test.js +40 -0
  29. package/build/es/utils/render-counter.js +11 -0
  30. package/build/es/utils/test-mocks.js +30 -0
  31. package/build/types/index.d.ts +4 -0
  32. package/build/types/lib/cacheable-section-state.d.ts +66 -0
  33. package/build/types/lib/cacheable-section.d.ts +52 -0
  34. package/build/types/lib/clear-sensitive-caches.d.ts +16 -0
  35. package/build/types/lib/global-state-service.d.ts +16 -0
  36. package/build/types/lib/offline-interface.d.ts +26 -0
  37. package/build/types/lib/offline-provider.d.ts +19 -0
  38. package/build/types/types.d.ts +50 -0
  39. package/build/types/utils/render-counter.d.ts +10 -0
  40. package/build/types/utils/test-mocks.d.ts +11 -0
  41. package/package.json +2 -2
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+
3
+ var _react = require("@testing-library/react");
4
+
5
+ var _react2 = _interopRequireDefault(require("react"));
6
+
7
+ var _cacheableSection = require("../lib/cacheable-section");
8
+
9
+ var _offlineProvider = require("../lib/offline-provider");
10
+
11
+ var _renderCounter = require("../utils/render-counter");
12
+
13
+ var _testMocks = require("../utils/test-mocks");
14
+
15
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
+
17
+ function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
18
+
19
+ const renderCounts = {};
20
+
21
+ const identity = arg => arg;
22
+
23
+ const TestControls = ({
24
+ id,
25
+ makeRecordingHandler = identity
26
+ }) => {
27
+ const {
28
+ startRecording,
29
+ remove,
30
+ isCached,
31
+ lastUpdated,
32
+ recordingState
33
+ } = (0, _cacheableSection.useCacheableSection)(id);
34
+ return /*#__PURE__*/_react2.default.createElement(_react2.default.Fragment, null, /*#__PURE__*/_react2.default.createElement(_renderCounter.RenderCounter, {
35
+ id: "controls-rc-".concat(id),
36
+ countsObj: renderCounts
37
+ }), /*#__PURE__*/_react2.default.createElement("button", {
38
+ "data-testid": "start-recording-".concat(id),
39
+ onClick: makeRecordingHandler(startRecording)
40
+ }), /*#__PURE__*/_react2.default.createElement("button", {
41
+ "data-testid": "remove-".concat(id),
42
+ onClick: () => {
43
+ remove();
44
+ }
45
+ }), /*#__PURE__*/_react2.default.createElement("div", {
46
+ "data-testid": "is-cached-".concat(id)
47
+ }, isCached ? 'yes' : 'no'), /*#__PURE__*/_react2.default.createElement("div", {
48
+ "data-testid": "last-updated-".concat(id)
49
+ }, lastUpdated || 'never'), /*#__PURE__*/_react2.default.createElement("div", {
50
+ "data-testid": "recording-state-".concat(id)
51
+ }, recordingState));
52
+ };
53
+
54
+ const TestSection = ({
55
+ id,
56
+ children
57
+ }) => /*#__PURE__*/_react2.default.createElement(_cacheableSection.CacheableSection, {
58
+ id: id,
59
+ loadingMask: /*#__PURE__*/_react2.default.createElement("div", {
60
+ "data-testid": "loading-mask-".concat(id)
61
+ })
62
+ }, /*#__PURE__*/_react2.default.createElement(_renderCounter.RenderCounter, {
63
+ id: "section-rc-".concat(id),
64
+ countsObj: renderCounts
65
+ }), children);
66
+
67
+ const TestSingleSection = props => {
68
+ // Props are spread so they can be overwritten
69
+ return /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
70
+ offlineInterface: _testMocks.mockOfflineInterface
71
+ }, props), /*#__PURE__*/_react2.default.createElement(TestControls, _extends({
72
+ id: '1'
73
+ }, props)), /*#__PURE__*/_react2.default.createElement(TestSection, _extends({
74
+ id: '1'
75
+ }, props)));
76
+ }; // Suppress 'act' warning for these tests
77
+
78
+
79
+ const originalError = console.error;
80
+ beforeEach(() => {
81
+ // This is done before each because the 'recording error' test uses its own
82
+ // spy on console.error
83
+ jest.spyOn(console, 'error').mockImplementation((...args) => {
84
+ const pattern = /Warning: An update to .* inside a test was not wrapped in act/;
85
+
86
+ if (typeof args[0] === 'string' && pattern.test(args[0])) {
87
+ return;
88
+ }
89
+
90
+ return originalError.call(console, ...args);
91
+ });
92
+ });
93
+ afterEach(() => {
94
+ jest.clearAllMocks() // This syntax appeases typescript:
95
+ ;
96
+ console.error.mockRestore();
97
+ (0, _renderCounter.resetRenderCounts)(renderCounts);
98
+ });
99
+ describe('Coordination between useCacheableSection and CacheableSection', () => {
100
+ it('renders in the default state initially', async () => {
101
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(TestSingleSection, null));
102
+ const {
103
+ getByTestId
104
+ } = _react.screen;
105
+ expect(getByTestId(/recording-state/)).toHaveTextContent('default');
106
+ expect(getByTestId(/is-cached/)).toHaveTextContent('no');
107
+ expect(getByTestId(/last-updated/)).toHaveTextContent('never');
108
+ expect(getByTestId(/section-rc/)).toBeInTheDocument();
109
+ expect(getByTestId(/controls-rc/)).toBeInTheDocument();
110
+ });
111
+ it('handles a successful recording', async done => {
112
+ const {
113
+ getByTestId,
114
+ queryByTestId
115
+ } = _react.screen;
116
+
117
+ const onStarted = () => {
118
+ expect(getByTestId(/recording-state/)).toHaveTextContent('recording');
119
+ expect(getByTestId(/loading-mask/)).toBeInTheDocument();
120
+ expect(getByTestId(/section-rc/)).toBeInTheDocument();
121
+ };
122
+
123
+ const onCompleted = () => {
124
+ expect(getByTestId(/recording-state/)).toHaveTextContent('default');
125
+ expect(queryByTestId(/loading-mask/)).not.toBeInTheDocument();
126
+ done();
127
+ };
128
+
129
+ const recordingOptions = {
130
+ onStarted,
131
+ onCompleted
132
+ };
133
+
134
+ const makeRecordingHandler = startRecording => {
135
+ return () => startRecording(recordingOptions);
136
+ };
137
+
138
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(TestSingleSection, {
139
+ makeRecordingHandler: makeRecordingHandler
140
+ }));
141
+ await (0, _react.act)(async () => {
142
+ _react.fireEvent.click(getByTestId(/start-recording/));
143
+ }); // At this stage, should be pending
144
+
145
+ expect(getByTestId(/recording-state/)).toHaveTextContent('pending');
146
+ expect(queryByTestId(/section-rc/)).not.toBeInTheDocument();
147
+ expect.assertions(7);
148
+ });
149
+ it('handles a recording that encounters an error', async done => {
150
+ // Suppress the expected error from console (in addition to 'act' warning)
151
+ jest.spyOn(console, 'error').mockImplementation((...args) => {
152
+ const actPattern = /Warning: An update to .* inside a test was not wrapped in act/;
153
+ const errPattern = /Error during recording/;
154
+ const matchesPattern = actPattern.test(args[0]) || errPattern.test(args[0]);
155
+
156
+ if (typeof args[0] === 'string' && matchesPattern) {
157
+ return;
158
+ }
159
+
160
+ return originalError.call(console, ...args);
161
+ });
162
+ const {
163
+ getByTestId,
164
+ queryByTestId
165
+ } = _react.screen;
166
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
167
+ startRecording: _testMocks.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
+
177
+ const makeRecordingHandler = startRecording => {
178
+ return () => startRecording({
179
+ onError
180
+ });
181
+ };
182
+
183
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(TestSingleSection, {
184
+ offlineInterface: testOfflineInterface,
185
+ makeRecordingHandler: makeRecordingHandler
186
+ }));
187
+ await (0, _react.act)(async () => {
188
+ _react.fireEvent.click(getByTestId(/start-recording/));
189
+ });
190
+ expect.assertions(3);
191
+ }); // ! After bumping testing-library versions, something about this test
192
+ // ! causes the following ones to mysteriously fail 😤
193
+
194
+ it.skip('handles an error starting the recording', async done => {
195
+ const {
196
+ getByTestId
197
+ } = _react.screen;
198
+ const testOfflineInterface = { ..._testMocks.mockOfflineInterface,
199
+ startRecording: _testMocks.failedMessageRecordingMock
200
+ };
201
+ const onStarted = jest.fn();
202
+
203
+ const testErrCondition = err => {
204
+ expect(err.message).toBe('Failed message'); // from the mock
205
+
206
+ expect(onStarted).not.toHaveBeenCalled();
207
+ expect(getByTestId(/recording-state/)).toHaveTextContent('default');
208
+ done();
209
+ };
210
+
211
+ const makeRecordingHandler = startRecording => {
212
+ return () => startRecording({
213
+ onStarted
214
+ }).catch(testErrCondition);
215
+ };
216
+
217
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(TestSingleSection, {
218
+ offlineInterface: testOfflineInterface,
219
+ makeRecordingHandler: makeRecordingHandler
220
+ }));
221
+ await (0, _react.act)(async () => {
222
+ _react.fireEvent.click(getByTestId(/start-recording/));
223
+ });
224
+ });
225
+ });
226
+
227
+ const TwoTestSections = props =>
228
+ /*#__PURE__*/
229
+ // Props are spread so they can be overwritten (but only on one section)
230
+ _react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
231
+ offlineInterface: _testMocks.mockOfflineInterface
232
+ }, props), /*#__PURE__*/_react2.default.createElement(TestControls, _extends({
233
+ id: '1'
234
+ }, props)), /*#__PURE__*/_react2.default.createElement(TestSection, _extends({
235
+ id: '1'
236
+ }, props)), /*#__PURE__*/_react2.default.createElement(TestControls, {
237
+ id: '2'
238
+ }), /*#__PURE__*/_react2.default.createElement(TestSection, {
239
+ id: '2'
240
+ })); // test that other sections don't rerender when one section does
241
+
242
+
243
+ describe('Performant state management', () => {
244
+ it('establishes a pre-recording render count', () => {
245
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(TwoTestSections, null));
246
+ const {
247
+ getByTestId
248
+ } = _react.screen; // Two renders for controls: undefined and 'default' states
249
+
250
+ expect(getByTestId('controls-rc-1')).toHaveTextContent('2');
251
+ expect(getByTestId('controls-rc-2')).toHaveTextContent('2'); // Just one render for sections
252
+
253
+ expect(getByTestId('section-rc-1')).toHaveTextContent('1');
254
+ expect(getByTestId('section-rc-2')).toHaveTextContent('1');
255
+ });
256
+ it('isolates rerenders from other consumers', async done => {
257
+ const {
258
+ getByTestId
259
+ } = _react.screen; // Make assertions
260
+
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'); // Section that did not record (should be same as pre-recording):
267
+
268
+ expect(getByTestId('controls-rc-2')).toHaveTextContent('2');
269
+ expect(getByTestId('section-rc-2')).toHaveTextContent('1');
270
+ done();
271
+ };
272
+
273
+ const makeRecordingHandler = startRecording => () => startRecording({
274
+ onCompleted
275
+ });
276
+
277
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(TwoTestSections, {
278
+ makeRecordingHandler: makeRecordingHandler
279
+ }));
280
+ await (0, _react.act)(async () => {
281
+ _react.fireEvent.click(getByTestId('start-recording-1'));
282
+ });
283
+ expect.assertions(4);
284
+ });
285
+ });
286
+ describe('useCacheableSection can be used inside a child of CacheableSection', () => {
287
+ const ChildTest = props => {
288
+ // Props are spread so they can be overwritten
289
+ return /*#__PURE__*/_react2.default.createElement(_offlineProvider.OfflineProvider, _extends({
290
+ offlineInterface: _testMocks.mockOfflineInterface
291
+ }, props), /*#__PURE__*/_react2.default.createElement(TestSection, _extends({
292
+ id: '1'
293
+ }, props), /*#__PURE__*/_react2.default.createElement(TestControls, _extends({
294
+ id: '1'
295
+ }, props))));
296
+ };
297
+
298
+ it('handles a successful recording', async done => {
299
+ const {
300
+ getByTestId,
301
+ queryByTestId
302
+ } = _react.screen;
303
+
304
+ const onStarted = () => {
305
+ expect(getByTestId(/recording-state/)).toHaveTextContent('recording');
306
+ expect(getByTestId(/loading-mask/)).toBeInTheDocument();
307
+ expect(getByTestId(/section-rc/)).toBeInTheDocument();
308
+ };
309
+
310
+ const onCompleted = () => {
311
+ expect(getByTestId(/recording-state/)).toHaveTextContent('default');
312
+ expect(queryByTestId(/loading-mask/)).not.toBeInTheDocument();
313
+ done();
314
+ };
315
+
316
+ const recordingOptions = {
317
+ onStarted,
318
+ onCompleted
319
+ };
320
+
321
+ const makeRecordingHandler = startRecording => {
322
+ return () => startRecording(recordingOptions);
323
+ };
324
+
325
+ (0, _react.render)( /*#__PURE__*/_react2.default.createElement(ChildTest, {
326
+ makeRecordingHandler: makeRecordingHandler
327
+ }));
328
+ await (0, _react.act)(async () => {
329
+ _react.fireEvent.click(getByTestId(/start-recording/));
330
+ }); // At this stage, should be pending
331
+ // - In this test case, 'controls' should not be rendered
332
+
333
+ expect(queryByTestId(/recording-state/)).not.toBeInTheDocument();
334
+ expect(queryByTestId(/section-rc/)).not.toBeInTheDocument();
335
+ expect.assertions(7);
336
+ });
337
+ });
@@ -3,11 +3,49 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ Object.defineProperty(exports, "OfflineProvider", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _offlineProvider.OfflineProvider;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "CacheableSection", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _cacheableSection.CacheableSection;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "useCacheableSection", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _cacheableSection.useCacheableSection;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "useCachedSections", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _cacheableSectionState.useCachedSections;
28
+ }
29
+ });
6
30
  Object.defineProperty(exports, "useOnlineStatus", {
7
31
  enumerable: true,
8
32
  get: function () {
9
33
  return _onlineStatus.useOnlineStatus;
10
34
  }
11
35
  });
36
+ Object.defineProperty(exports, "clearSensitiveCaches", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _clearSensitiveCaches.clearSensitiveCaches;
40
+ }
41
+ });
42
+
43
+ var _offlineProvider = require("./lib/offline-provider");
44
+
45
+ var _cacheableSection = require("./lib/cacheable-section");
46
+
47
+ var _cacheableSectionState = require("./lib/cacheable-section-state");
48
+
49
+ var _onlineStatus = require("./lib/online-status");
12
50
 
13
- var _onlineStatus = require("./lib/online-status");
51
+ var _clearSensitiveCaches = require("./lib/clear-sensitive-caches");
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+
3
+ var _FDBFactory = _interopRequireDefault(require("fake-indexeddb/lib/FDBFactory"));
4
+
5
+ var _idb = require("idb");
6
+
7
+ require("fake-indexeddb/auto");
8
+
9
+ var _clearSensitiveCaches = require("../clear-sensitive-caches");
10
+
11
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
12
+
13
+ // Mocks for CacheStorage API
14
+ const keysMockDefault = jest.fn().mockImplementation(async () => []);
15
+ const deleteMockDefault = jest.fn().mockImplementation(async () => null);
16
+ const cachesDefault = {
17
+ keys: keysMockDefault,
18
+ delete: deleteMockDefault
19
+ };
20
+ window.caches = cachesDefault;
21
+ afterEach(() => {
22
+ window.caches = cachesDefault;
23
+ jest.clearAllMocks();
24
+ }); // silence debug logs for these tests
25
+
26
+ const originalDebug = console.debug;
27
+ beforeAll(() => {
28
+ jest.spyOn(console, 'debug').mockImplementation((...args) => {
29
+ const pattern = /Clearing sensitive caches/;
30
+
31
+ if (typeof args[0] === 'string' && pattern.test(args[0])) {
32
+ return;
33
+ }
34
+
35
+ return originalDebug.call(console, ...args);
36
+ });
37
+ });
38
+ afterAll(() => {
39
+ ;
40
+ console.debug.mockRestore();
41
+ });
42
+ it('does not fail if there are no caches or no sections-db', () => {
43
+ return expect((0, _clearSensitiveCaches.clearSensitiveCaches)()).resolves.toBeDefined();
44
+ });
45
+ it('clears potentially sensitive caches', async () => {
46
+ const keysMock = jest.fn().mockImplementation(async () => ['cache1', 'cache2', 'app-shell']);
47
+ window.caches = { ...cachesDefault,
48
+ keys: keysMock
49
+ };
50
+ await (0, _clearSensitiveCaches.clearSensitiveCaches)();
51
+ expect(deleteMockDefault).toHaveBeenCalledTimes(3);
52
+ expect(deleteMockDefault.mock.calls[0][0]).toBe('cache1');
53
+ expect(deleteMockDefault.mock.calls[1][0]).toBe('cache2');
54
+ expect(deleteMockDefault.mock.calls[2][0]).toBe('app-shell');
55
+ });
56
+ it('preserves keepable caches', async () => {
57
+ const keysMock = jest.fn().mockImplementation(async () => ['cache1', 'cache2', 'app-shell', 'other-assets', 'workbox-precache-v2-https://hey.howareya.now/']);
58
+ window.caches = { ...cachesDefault,
59
+ keys: keysMock
60
+ };
61
+ await (0, _clearSensitiveCaches.clearSensitiveCaches)();
62
+ expect(deleteMockDefault).toHaveBeenCalledTimes(3);
63
+ expect(deleteMockDefault.mock.calls[0][0]).toBe('cache1');
64
+ expect(deleteMockDefault.mock.calls[1][0]).toBe('cache2');
65
+ expect(deleteMockDefault.mock.calls[2][0]).toBe('app-shell');
66
+ expect(deleteMockDefault).not.toHaveBeenCalledWith('other-assets');
67
+ expect(deleteMockDefault).not.toHaveBeenCalledWith('workbox-precache-v2-https://hey.howareya.now/');
68
+ });
69
+ describe('clears sections-db', () => {
70
+ // Test DB
71
+ function openTestDB(dbName) {
72
+ // simplified version of app platform openDB logic
73
+ return (0, _idb.openDB)(dbName, 1, {
74
+ upgrade(db) {
75
+ db.createObjectStore(_clearSensitiveCaches.SECTIONS_STORE, {
76
+ keyPath: 'sectionId'
77
+ });
78
+ }
79
+
80
+ });
81
+ }
82
+
83
+ afterEach(() => {
84
+ // reset indexedDB state
85
+ window.indexedDB = new _FDBFactory.default();
86
+ });
87
+ it('clears sections-db if it exists', async () => {
88
+ // Open and populate test DB
89
+ const db = await openTestDB(_clearSensitiveCaches.SECTIONS_DB);
90
+ await db.put(_clearSensitiveCaches.SECTIONS_STORE, {
91
+ sectionId: 'id-1',
92
+ lastUpdated: new Date(),
93
+ requests: 3
94
+ });
95
+ await db.put(_clearSensitiveCaches.SECTIONS_STORE, {
96
+ sectionId: 'id-2',
97
+ lastUpdated: new Date(),
98
+ requests: 3
99
+ });
100
+ await (0, _clearSensitiveCaches.clearSensitiveCaches)(); // Sections-db should be cleared
101
+
102
+ const allSections = await db.getAll(_clearSensitiveCaches.SECTIONS_STORE);
103
+ expect(allSections).toHaveLength(0);
104
+ });
105
+ it("doesn't clear sections-db if it doesn't exist and doesn't open a new one", async () => {
106
+ const openMock = jest.fn();
107
+ window.indexedDB.open = openMock;
108
+ expect(await indexedDB.databases()).not.toContain(_clearSensitiveCaches.SECTIONS_DB);
109
+ await (0, _clearSensitiveCaches.clearSensitiveCaches)();
110
+ expect(openMock).not.toHaveBeenCalled();
111
+ return expect(await indexedDB.databases()).not.toContain(_clearSensitiveCaches.SECTIONS_DB);
112
+ });
113
+ it("doesn't handle IDB if 'databases' property is not on window.indexedDB", async () => {
114
+ // Open DB -- 'indexedDB.open' _would_ get called in this test
115
+ // if 'databases' property exists
116
+ await openTestDB(_clearSensitiveCaches.SECTIONS_DB);
117
+ const openMock = jest.fn();
118
+ window.indexedDB.open = openMock; // Remove 'databases' from indexedDB prototype for this test
119
+ // (simulates Firefox environment)
120
+
121
+ const idbProto = Object.getPrototypeOf(window.indexedDB);
122
+ const databases = idbProto.databases;
123
+ delete idbProto.databases;
124
+ expect('databases' in window.indexedDB).toBe(false);
125
+ await expect((0, _clearSensitiveCaches.clearSensitiveCaches)()).resolves.toBeDefined();
126
+ expect(openMock).not.toHaveBeenCalled(); // Restore indexedDB prototype for later tests
127
+
128
+ idbProto.databases = databases;
129
+ expect('databases' in window.indexedDB).toBe(true);
130
+ });
131
+ });