@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.
- package/build/cjs/__tests__/integration.test.js +337 -0
- package/build/cjs/index.js +39 -1
- package/build/cjs/lib/__tests__/clear-sensitive-caches.test.js +131 -0
- package/build/cjs/lib/__tests__/offline-provider.test.js +127 -0
- package/build/cjs/lib/__tests__/use-cacheable-section.test.js +227 -0
- package/build/cjs/lib/cacheable-section-state.js +218 -0
- package/build/cjs/lib/cacheable-section.js +156 -0
- package/build/cjs/lib/clear-sensitive-caches.js +87 -0
- package/build/cjs/lib/global-state-service.js +95 -0
- package/build/cjs/lib/offline-interface.js +86 -0
- package/build/cjs/lib/offline-provider.js +53 -0
- package/build/cjs/types.js +0 -1
- package/build/cjs/utils/__tests__/render-counter.test.js +55 -0
- package/build/cjs/utils/render-counter.js +26 -0
- package/build/cjs/utils/test-mocks.js +40 -0
- package/build/es/__tests__/integration.test.js +327 -0
- package/build/es/index.js +5 -1
- package/build/es/lib/__tests__/clear-sensitive-caches.test.js +123 -0
- package/build/es/lib/__tests__/offline-provider.test.js +117 -0
- package/build/es/lib/__tests__/use-cacheable-section.test.js +218 -0
- package/build/es/lib/cacheable-section-state.js +199 -0
- package/build/es/lib/cacheable-section.js +137 -0
- package/build/es/lib/clear-sensitive-caches.js +78 -0
- package/build/es/lib/global-state-service.js +70 -0
- package/build/es/lib/offline-interface.js +65 -0
- package/build/es/lib/offline-provider.js +40 -0
- package/build/es/types.js +0 -1
- package/build/es/utils/__tests__/render-counter.test.js +40 -0
- package/build/es/utils/render-counter.js +11 -0
- package/build/es/utils/test-mocks.js +30 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/lib/cacheable-section-state.d.ts +66 -0
- package/build/types/lib/cacheable-section.d.ts +52 -0
- package/build/types/lib/clear-sensitive-caches.d.ts +16 -0
- package/build/types/lib/global-state-service.d.ts +16 -0
- package/build/types/lib/offline-interface.d.ts +26 -0
- package/build/types/lib/offline-provider.d.ts +19 -0
- package/build/types/types.d.ts +50 -0
- package/build/types/utils/render-counter.d.ts +10 -0
- package/build/types/utils/test-mocks.d.ts +11 -0
- 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
|
+
});
|
package/build/cjs/index.js
CHANGED
|
@@ -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
|
|
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
|
+
});
|