@xiboplayer/cache 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/cache-analyzer.js +176 -0
- package/src/cache-analyzer.test.js +270 -0
- package/src/cache-proxy.js +26 -0
- package/src/cache.js +8 -659
- package/src/cache.test.js +15 -616
- package/src/index.js +2 -0
- package/src/widget-html.js +181 -0
package/src/cache.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cache Manager Tests
|
|
3
3
|
*
|
|
4
|
-
* Tests for
|
|
5
|
-
*
|
|
4
|
+
* Tests for the slimmed-down CacheManager: dependant tracking, getCacheKey,
|
|
5
|
+
* and clearAll. Download/IndexedDB methods have been removed (handled by SW).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
@@ -11,31 +11,22 @@ import { CacheManager } from './cache.js';
|
|
|
11
11
|
describe('CacheManager', () => {
|
|
12
12
|
let manager;
|
|
13
13
|
let mockCache;
|
|
14
|
-
let mockDB;
|
|
15
|
-
let mockIndexedDB;
|
|
16
14
|
|
|
17
15
|
beforeEach(async () => {
|
|
18
|
-
// Mock Cache API
|
|
16
|
+
// Mock Cache API
|
|
19
17
|
mockCache = {
|
|
20
18
|
_storage: new Map(),
|
|
21
19
|
async match(key) {
|
|
22
20
|
const keyStr = typeof key === 'string' ? key : key.toString();
|
|
23
21
|
const entry = this._storage.get(keyStr);
|
|
24
22
|
if (!entry) return undefined;
|
|
25
|
-
|
|
26
|
-
// Reconstruct a real Response from stored data
|
|
27
|
-
return new Response(entry.body, {
|
|
28
|
-
headers: entry.headers
|
|
29
|
-
});
|
|
23
|
+
return new Response(entry.body, { headers: entry.headers });
|
|
30
24
|
},
|
|
31
25
|
async put(key, response) {
|
|
32
26
|
const keyStr = typeof key === 'string' ? key : key.toString();
|
|
33
|
-
// Read the body as text (preserves strings) and store headers as plain object
|
|
34
27
|
const bodyText = await response.text();
|
|
35
28
|
const headers = {};
|
|
36
|
-
response.headers.forEach((value, name) => {
|
|
37
|
-
headers[name] = value;
|
|
38
|
-
});
|
|
29
|
+
response.headers.forEach((value, name) => { headers[name] = value; });
|
|
39
30
|
this._storage.set(keyStr, { body: bodyText, headers });
|
|
40
31
|
},
|
|
41
32
|
async delete(key) {
|
|
@@ -44,49 +35,9 @@ describe('CacheManager', () => {
|
|
|
44
35
|
}
|
|
45
36
|
};
|
|
46
37
|
|
|
47
|
-
// Setup global mocks
|
|
48
38
|
global.caches = {
|
|
49
|
-
async open() {
|
|
50
|
-
|
|
51
|
-
},
|
|
52
|
-
async delete() {
|
|
53
|
-
mockCache._storage.clear();
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
// Use real fake-indexeddb (provided by vitest.setup.js)
|
|
58
|
-
// Clean the database between tests to avoid data leaking
|
|
59
|
-
const deleteRequest = indexedDB.deleteDatabase('xibo-player');
|
|
60
|
-
await new Promise((resolve) => {
|
|
61
|
-
deleteRequest.onsuccess = resolve;
|
|
62
|
-
deleteRequest.onerror = resolve;
|
|
63
|
-
deleteRequest.onblocked = resolve;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Mock @xiboplayer/utils config
|
|
67
|
-
vi.mock('@xiboplayer/utils', () => ({
|
|
68
|
-
config: {
|
|
69
|
-
cmsAddress: 'https://test.cms.com'
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
// Mock fetch
|
|
74
|
-
global.fetch = vi.fn();
|
|
75
|
-
|
|
76
|
-
// Mock window events
|
|
77
|
-
global.window = {
|
|
78
|
-
dispatchEvent: vi.fn(),
|
|
79
|
-
location: {
|
|
80
|
-
origin: 'https://test.cms.com',
|
|
81
|
-
pathname: '/player/pwa/index.html'
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Mock navigator.serviceWorker
|
|
86
|
-
global.navigator = {
|
|
87
|
-
serviceWorker: {
|
|
88
|
-
controller: null
|
|
89
|
-
}
|
|
39
|
+
async open() { return mockCache; },
|
|
40
|
+
async delete() { mockCache._storage.clear(); }
|
|
90
41
|
};
|
|
91
42
|
|
|
92
43
|
manager = new CacheManager();
|
|
@@ -94,66 +45,6 @@ describe('CacheManager', () => {
|
|
|
94
45
|
|
|
95
46
|
afterEach(() => {
|
|
96
47
|
vi.clearAllMocks();
|
|
97
|
-
// Close any open DB connections to allow deleteDatabase in next beforeEach
|
|
98
|
-
if (manager && manager.db) {
|
|
99
|
-
manager.db.close();
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
describe('Initialization', () => {
|
|
104
|
-
it('should initialize cache and database', async () => {
|
|
105
|
-
await manager.init();
|
|
106
|
-
|
|
107
|
-
expect(manager.cache).toBeDefined();
|
|
108
|
-
expect(manager.db).toBeDefined();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('extractFilename()', () => {
|
|
113
|
-
it('should extract filename from URL query parameter', () => {
|
|
114
|
-
const filename = manager.extractFilename('https://test.com/xmds.php?file=image.jpg&key=123');
|
|
115
|
-
|
|
116
|
-
expect(filename).toBe('image.jpg');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should return "unknown" for invalid URL', () => {
|
|
120
|
-
const filename = manager.extractFilename('not-a-url');
|
|
121
|
-
|
|
122
|
-
expect(filename).toBe('unknown');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should return "unknown" when file parameter missing', () => {
|
|
126
|
-
const filename = manager.extractFilename('https://test.com/xmds.php?other=param');
|
|
127
|
-
|
|
128
|
-
expect(filename).toBe('unknown');
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe('rewriteUrl()', () => {
|
|
133
|
-
it('should rewrite URL to use configured CMS address', () => {
|
|
134
|
-
const rewritten = manager.rewriteUrl('https://different.com/path?file=test.jpg');
|
|
135
|
-
|
|
136
|
-
expect(rewritten).toContain('test.cms.com');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should not rewrite URL if same origin', () => {
|
|
140
|
-
const original = 'https://test.cms.com/path?file=test.jpg';
|
|
141
|
-
const rewritten = manager.rewriteUrl(original);
|
|
142
|
-
|
|
143
|
-
expect(rewritten).toBe(original);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('should return URL as-is if invalid', () => {
|
|
147
|
-
const invalid = 'not-a-url';
|
|
148
|
-
const rewritten = manager.rewriteUrl(invalid);
|
|
149
|
-
|
|
150
|
-
expect(rewritten).toBe(invalid);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should handle null/undefined URLs', () => {
|
|
154
|
-
expect(manager.rewriteUrl(null)).toBeNull();
|
|
155
|
-
expect(manager.rewriteUrl(undefined)).toBeUndefined();
|
|
156
|
-
});
|
|
157
48
|
});
|
|
158
49
|
|
|
159
50
|
describe('getCacheKey()', () => {
|
|
@@ -176,465 +67,6 @@ describe('CacheManager', () => {
|
|
|
176
67
|
});
|
|
177
68
|
});
|
|
178
69
|
|
|
179
|
-
describe('File Record Management', () => {
|
|
180
|
-
beforeEach(async () => {
|
|
181
|
-
await manager.init();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should save file record', async () => {
|
|
185
|
-
const record = {
|
|
186
|
-
id: '1',
|
|
187
|
-
type: 'media',
|
|
188
|
-
path: 'http://test.com/file.mp4',
|
|
189
|
-
md5: 'abc123',
|
|
190
|
-
size: 1024,
|
|
191
|
-
cachedAt: Date.now()
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
await manager.saveFile(record);
|
|
195
|
-
const retrieved = await manager.getFile('1');
|
|
196
|
-
|
|
197
|
-
expect(retrieved).toEqual(record);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should get all files', async () => {
|
|
201
|
-
await manager.saveFile({ id: '1', type: 'media' });
|
|
202
|
-
await manager.saveFile({ id: '2', type: 'layout' });
|
|
203
|
-
|
|
204
|
-
const files = await manager.getAllFiles();
|
|
205
|
-
|
|
206
|
-
expect(files).toHaveLength(2);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('should return undefined for non-existent file', async () => {
|
|
210
|
-
const file = await manager.getFile('non-existent');
|
|
211
|
-
|
|
212
|
-
expect(file).toBeUndefined();
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
describe('downloadFile() - Small Files', () => {
|
|
217
|
-
beforeEach(async () => {
|
|
218
|
-
await manager.init();
|
|
219
|
-
|
|
220
|
-
// Helper: create a mock blob with arrayBuffer()/stream() support
|
|
221
|
-
function createMockBlob(content, type) {
|
|
222
|
-
const blob = new Blob([content], { type });
|
|
223
|
-
// Polyfill arrayBuffer() for jsdom environments that lack it
|
|
224
|
-
if (!blob.arrayBuffer) {
|
|
225
|
-
blob.arrayBuffer = async () => {
|
|
226
|
-
const reader = new FileReader();
|
|
227
|
-
return new Promise((resolve) => {
|
|
228
|
-
reader.onload = () => resolve(reader.result);
|
|
229
|
-
reader.readAsArrayBuffer(blob);
|
|
230
|
-
});
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
// Polyfill stream() for jsdom environments that lack it
|
|
234
|
-
if (!blob.stream) {
|
|
235
|
-
blob.stream = () => new ReadableStream({
|
|
236
|
-
start(controller) {
|
|
237
|
-
controller.enqueue(new TextEncoder().encode(content));
|
|
238
|
-
controller.close();
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
return blob;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Mock successful download
|
|
246
|
-
global.fetch.mockImplementation(async (url, options) => {
|
|
247
|
-
if (options?.method === 'HEAD') {
|
|
248
|
-
return {
|
|
249
|
-
ok: true,
|
|
250
|
-
status: 200,
|
|
251
|
-
headers: {
|
|
252
|
-
get: (name) => name === 'Content-Length' ? '1024' : null
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const blob = createMockBlob('test data', 'image/jpeg');
|
|
258
|
-
return {
|
|
259
|
-
ok: true,
|
|
260
|
-
status: 200,
|
|
261
|
-
headers: {
|
|
262
|
-
get: (name) => name === 'Content-Type' ? 'image/jpeg' : null
|
|
263
|
-
},
|
|
264
|
-
blob: async () => blob
|
|
265
|
-
};
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// Mock SparkMD5
|
|
269
|
-
vi.mock('spark-md5', () => ({
|
|
270
|
-
default: {
|
|
271
|
-
ArrayBuffer: {
|
|
272
|
-
hash: () => 'abc123'
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}));
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('should download and cache file', async () => {
|
|
279
|
-
const fileInfo = {
|
|
280
|
-
id: '1',
|
|
281
|
-
type: 'media',
|
|
282
|
-
path: 'http://test.com/file.jpg',
|
|
283
|
-
md5: 'abc123',
|
|
284
|
-
download: 'http'
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
const result = await manager.downloadFile(fileInfo);
|
|
288
|
-
|
|
289
|
-
expect(result).toMatchObject({
|
|
290
|
-
id: '1',
|
|
291
|
-
type: 'media',
|
|
292
|
-
md5: 'abc123'
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('should skip download if already cached with matching MD5', async () => {
|
|
297
|
-
// Pre-cache file
|
|
298
|
-
await manager.saveFile({
|
|
299
|
-
id: '1',
|
|
300
|
-
type: 'media',
|
|
301
|
-
md5: 'abc123',
|
|
302
|
-
path: 'http://test.com/file.jpg',
|
|
303
|
-
size: 1024,
|
|
304
|
-
cachedAt: Date.now()
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
// Use content >= 100 bytes to avoid "tiny file" corruption check
|
|
308
|
-
const largeContent = 'x'.repeat(200);
|
|
309
|
-
const cacheKey = manager.getCacheKey('media', '1');
|
|
310
|
-
await mockCache.put(cacheKey, new Response(largeContent, {
|
|
311
|
-
headers: { 'Content-Type': 'image/jpeg' }
|
|
312
|
-
}));
|
|
313
|
-
|
|
314
|
-
const fileInfo = {
|
|
315
|
-
id: '1',
|
|
316
|
-
type: 'media',
|
|
317
|
-
path: 'http://test.com/file.jpg',
|
|
318
|
-
md5: 'abc123'
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
const result = await manager.downloadFile(fileInfo);
|
|
322
|
-
|
|
323
|
-
expect(result.md5).toBe('abc123');
|
|
324
|
-
expect(global.fetch).not.toHaveBeenCalled();
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it('should re-download if MD5 mismatch', async () => {
|
|
328
|
-
// Pre-cache file with different MD5
|
|
329
|
-
await manager.saveFile({
|
|
330
|
-
id: '1',
|
|
331
|
-
type: 'media',
|
|
332
|
-
md5: 'old-md5',
|
|
333
|
-
path: 'http://test.com/file.jpg',
|
|
334
|
-
size: 1024,
|
|
335
|
-
cachedAt: Date.now()
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
const fileInfo = {
|
|
339
|
-
id: '1',
|
|
340
|
-
type: 'media',
|
|
341
|
-
path: 'http://test.com/file.jpg',
|
|
342
|
-
md5: 'new-md5'
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
await manager.downloadFile(fileInfo);
|
|
346
|
-
|
|
347
|
-
expect(global.fetch).toHaveBeenCalled();
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('should skip files with no download URL', async () => {
|
|
351
|
-
const fileInfo = {
|
|
352
|
-
id: '1',
|
|
353
|
-
type: 'resource',
|
|
354
|
-
path: null
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
const result = await manager.downloadFile(fileInfo);
|
|
358
|
-
|
|
359
|
-
expect(result).toBeNull();
|
|
360
|
-
expect(global.fetch).not.toHaveBeenCalled();
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('should handle HTTP errors', async () => {
|
|
364
|
-
global.fetch.mockImplementation(async (url, options) => {
|
|
365
|
-
if (options?.method === 'HEAD') {
|
|
366
|
-
return {
|
|
367
|
-
ok: true,
|
|
368
|
-
status: 200,
|
|
369
|
-
headers: { get: (name) => name === 'Content-Length' ? '1024' : null }
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
// GET download returns error
|
|
373
|
-
return {
|
|
374
|
-
ok: false,
|
|
375
|
-
status: 404,
|
|
376
|
-
headers: { get: () => null }
|
|
377
|
-
};
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
const fileInfo = {
|
|
381
|
-
id: '1',
|
|
382
|
-
type: 'media',
|
|
383
|
-
path: 'http://test.com/file.jpg'
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
await expect(manager.downloadFile(fileInfo)).rejects.toThrow('Failed to download');
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('should delete corrupted cache (text/plain responses)', async () => {
|
|
390
|
-
// Pre-cache corrupted file
|
|
391
|
-
await manager.saveFile({
|
|
392
|
-
id: '1',
|
|
393
|
-
type: 'media',
|
|
394
|
-
md5: 'abc123',
|
|
395
|
-
path: 'http://test.com/file.jpg',
|
|
396
|
-
size: 50,
|
|
397
|
-
cachedAt: Date.now()
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const cacheKey = manager.getCacheKey('media', '1');
|
|
401
|
-
await mockCache.put(cacheKey, new Response(new Blob(['error']), {
|
|
402
|
-
headers: { 'Content-Type': 'text/plain' }
|
|
403
|
-
}));
|
|
404
|
-
|
|
405
|
-
const fileInfo = {
|
|
406
|
-
id: '1',
|
|
407
|
-
type: 'media',
|
|
408
|
-
path: 'http://test.com/file.jpg',
|
|
409
|
-
md5: 'abc123'
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
await manager.downloadFile(fileInfo);
|
|
413
|
-
|
|
414
|
-
// Should re-download
|
|
415
|
-
expect(global.fetch).toHaveBeenCalled();
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('should handle Service Worker active (skip download)', async () => {
|
|
419
|
-
global.navigator.serviceWorker.controller = {}; // SW active
|
|
420
|
-
|
|
421
|
-
const fileInfo = {
|
|
422
|
-
id: '1',
|
|
423
|
-
type: 'media',
|
|
424
|
-
path: 'http://test.com/file.jpg',
|
|
425
|
-
md5: 'abc123'
|
|
426
|
-
};
|
|
427
|
-
|
|
428
|
-
const result = await manager.downloadFile(fileInfo);
|
|
429
|
-
|
|
430
|
-
expect(result.isServiceWorkerDownload).toBe(true);
|
|
431
|
-
expect(global.fetch).not.toHaveBeenCalled();
|
|
432
|
-
|
|
433
|
-
global.navigator.serviceWorker.controller = null; // Reset
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
it('should handle HTTP 202 (background download pending)', async () => {
|
|
437
|
-
// HEAD request returns 202 (background download in progress)
|
|
438
|
-
global.fetch.mockImplementation(async (url, options) => {
|
|
439
|
-
return {
|
|
440
|
-
ok: true,
|
|
441
|
-
status: 202,
|
|
442
|
-
headers: { get: () => null }
|
|
443
|
-
};
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
const fileInfo = {
|
|
447
|
-
id: '1',
|
|
448
|
-
type: 'media',
|
|
449
|
-
path: 'http://test.com/file.jpg',
|
|
450
|
-
md5: 'abc123'
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
const result = await manager.downloadFile(fileInfo);
|
|
454
|
-
|
|
455
|
-
expect(result.isPending).toBe(true);
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
describe('downloadFile() - Large Files', () => {
|
|
460
|
-
beforeEach(async () => {
|
|
461
|
-
await manager.init();
|
|
462
|
-
|
|
463
|
-
// Mock large file (>100MB)
|
|
464
|
-
global.fetch.mockImplementation(async (url, options) => {
|
|
465
|
-
if (options?.method === 'HEAD') {
|
|
466
|
-
return {
|
|
467
|
-
ok: true,
|
|
468
|
-
headers: {
|
|
469
|
-
get: (name) => name === 'Content-Length' ? '200000000' : null // 200 MB
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
return {
|
|
475
|
-
ok: true,
|
|
476
|
-
status: 200,
|
|
477
|
-
headers: {
|
|
478
|
-
get: (name) => name === 'Content-Type' ? 'video/mp4' : null
|
|
479
|
-
},
|
|
480
|
-
blob: async () => new Blob(['chunk data'])
|
|
481
|
-
};
|
|
482
|
-
});
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it('should start background download for large files', async () => {
|
|
486
|
-
const fileInfo = {
|
|
487
|
-
id: '1',
|
|
488
|
-
type: 'media',
|
|
489
|
-
path: 'http://test.com/large-video.mp4',
|
|
490
|
-
md5: 'abc123'
|
|
491
|
-
};
|
|
492
|
-
|
|
493
|
-
const result = await manager.downloadFile(fileInfo);
|
|
494
|
-
|
|
495
|
-
expect(result.isBackgroundDownload).toBe(true);
|
|
496
|
-
expect(result.size).toBe(200000000);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it('should return immediately for large files (non-blocking)', async () => {
|
|
500
|
-
const fileInfo = {
|
|
501
|
-
id: '1',
|
|
502
|
-
type: 'media',
|
|
503
|
-
path: 'http://test.com/large-video.mp4',
|
|
504
|
-
md5: 'abc123'
|
|
505
|
-
};
|
|
506
|
-
|
|
507
|
-
const startTime = Date.now();
|
|
508
|
-
await manager.downloadFile(fileInfo);
|
|
509
|
-
const duration = Date.now() - startTime;
|
|
510
|
-
|
|
511
|
-
// Should return in <100ms (not wait for download)
|
|
512
|
-
expect(duration).toBeLessThan(100);
|
|
513
|
-
});
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
describe('getCachedFile()', () => {
|
|
517
|
-
beforeEach(async () => {
|
|
518
|
-
await manager.init();
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it('should retrieve cached file as blob', async () => {
|
|
522
|
-
const cacheKey = manager.getCacheKey('media', '1');
|
|
523
|
-
const blob = new Blob(['test data']);
|
|
524
|
-
await mockCache.put(cacheKey, new Response(blob));
|
|
525
|
-
|
|
526
|
-
const retrieved = await manager.getCachedFile('media', '1');
|
|
527
|
-
|
|
528
|
-
expect(retrieved).toBeTruthy();
|
|
529
|
-
expect(retrieved.size).toBeGreaterThan(0);
|
|
530
|
-
});
|
|
531
|
-
|
|
532
|
-
it('should return null for non-cached file', async () => {
|
|
533
|
-
const retrieved = await manager.getCachedFile('media', 'non-existent');
|
|
534
|
-
|
|
535
|
-
expect(retrieved).toBeNull();
|
|
536
|
-
});
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
describe('getCachedResponse()', () => {
|
|
540
|
-
beforeEach(async () => {
|
|
541
|
-
await manager.init();
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it('should retrieve cached response with headers', async () => {
|
|
545
|
-
const cacheKey = manager.getCacheKey('media', '1');
|
|
546
|
-
await mockCache.put(cacheKey, new Response(new Blob(['data']), {
|
|
547
|
-
headers: { 'Content-Type': 'image/jpeg' }
|
|
548
|
-
}));
|
|
549
|
-
|
|
550
|
-
const response = await manager.getCachedResponse('media', '1');
|
|
551
|
-
|
|
552
|
-
expect(response).toBeInstanceOf(Response);
|
|
553
|
-
expect(response.headers.get('Content-Type')).toBe('image/jpeg');
|
|
554
|
-
});
|
|
555
|
-
});
|
|
556
|
-
|
|
557
|
-
describe('getCachedFileText()', () => {
|
|
558
|
-
beforeEach(async () => {
|
|
559
|
-
await manager.init();
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
it('should retrieve cached file as text', async () => {
|
|
563
|
-
const cacheKey = manager.getCacheKey('layout', '100');
|
|
564
|
-
await mockCache.put(cacheKey, new Response('layout XML'));
|
|
565
|
-
|
|
566
|
-
const text = await manager.getCachedFileText('layout', '100');
|
|
567
|
-
|
|
568
|
-
expect(text).toBe('layout XML');
|
|
569
|
-
});
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
describe('cacheWidgetHtml()', () => {
|
|
573
|
-
beforeEach(async () => {
|
|
574
|
-
await manager.init();
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
it('should cache widget HTML with base tag injection', async () => {
|
|
578
|
-
const html = '<html><head><title>Widget</title></head><body>Content</body></html>';
|
|
579
|
-
|
|
580
|
-
const cacheKey = await manager.cacheWidgetHtml('100', '1', '2', html);
|
|
581
|
-
|
|
582
|
-
expect(cacheKey).toBe('/player/pwa/cache/widget/100/1/2');
|
|
583
|
-
|
|
584
|
-
// Verify base tag was injected
|
|
585
|
-
const cached = await mockCache.match(new URL(cacheKey, 'https://test.cms.com'));
|
|
586
|
-
const cachedHtml = await cached.text();
|
|
587
|
-
|
|
588
|
-
expect(cachedHtml).toContain('<base href="/player/cache/media/">');
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it('should inject base tag after <head> tag', async () => {
|
|
592
|
-
const html = '<html><head><title>Test</title></head></html>';
|
|
593
|
-
|
|
594
|
-
await manager.cacheWidgetHtml('100', '1', '2', html);
|
|
595
|
-
|
|
596
|
-
const cacheKey = '/player/pwa/cache/widget/100/1/2';
|
|
597
|
-
const cached = await mockCache.match(new URL(cacheKey, 'https://test.cms.com'));
|
|
598
|
-
const cachedHtml = await cached.text();
|
|
599
|
-
|
|
600
|
-
expect(cachedHtml).toMatch(/<head><base href="\/player\/cache\/media\/">/);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('should prepend base tag if no <head> tag', async () => {
|
|
604
|
-
const html = '<div>Widget content</div>';
|
|
605
|
-
|
|
606
|
-
await manager.cacheWidgetHtml('100', '1', '2', html);
|
|
607
|
-
|
|
608
|
-
const cacheKey = '/player/pwa/cache/widget/100/1/2';
|
|
609
|
-
const cached = await mockCache.match(new URL(cacheKey, 'https://test.cms.com'));
|
|
610
|
-
const cachedHtml = await cached.text();
|
|
611
|
-
|
|
612
|
-
expect(cachedHtml).toMatch(/^<base href="\/player\/cache\/media\/">/);
|
|
613
|
-
});
|
|
614
|
-
});
|
|
615
|
-
|
|
616
|
-
describe('clearAll()', () => {
|
|
617
|
-
beforeEach(async () => {
|
|
618
|
-
await manager.init();
|
|
619
|
-
|
|
620
|
-
// Add some cached data
|
|
621
|
-
await manager.saveFile({ id: '1', type: 'media' });
|
|
622
|
-
await manager.saveFile({ id: '2', type: 'layout' });
|
|
623
|
-
const cacheKey = manager.getCacheKey('media', '1');
|
|
624
|
-
await mockCache.put(cacheKey, new Response('data'));
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
it('should clear all caches and IndexedDB', async () => {
|
|
628
|
-
await manager.clearAll();
|
|
629
|
-
|
|
630
|
-
const files = await manager.getAllFiles();
|
|
631
|
-
expect(files).toHaveLength(0);
|
|
632
|
-
|
|
633
|
-
const cached = await manager.getCachedFile('media', '1');
|
|
634
|
-
expect(cached).toBeNull();
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
|
|
638
70
|
describe('Dependant Tracking', () => {
|
|
639
71
|
it('should add a dependant mapping from media to layout', () => {
|
|
640
72
|
manager.addDependant('media1', 'layout1');
|
|
@@ -722,49 +154,16 @@ describe('CacheManager', () => {
|
|
|
722
154
|
});
|
|
723
155
|
});
|
|
724
156
|
|
|
725
|
-
describe('
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
it('should dispatch download-progress events', () => {
|
|
731
|
-
manager.notifyDownloadProgress('test.mp4', 512, 1024);
|
|
732
|
-
|
|
733
|
-
expect(window.dispatchEvent).toHaveBeenCalledWith(
|
|
734
|
-
expect.objectContaining({
|
|
735
|
-
type: 'download-progress',
|
|
736
|
-
detail: expect.objectContaining({
|
|
737
|
-
filename: 'test.mp4',
|
|
738
|
-
loaded: 512,
|
|
739
|
-
total: 1024,
|
|
740
|
-
percent: 50
|
|
741
|
-
})
|
|
742
|
-
})
|
|
743
|
-
);
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
it('should mark download as complete', () => {
|
|
747
|
-
manager.notifyDownloadProgress('test.mp4', 1024, 1024, true);
|
|
748
|
-
|
|
749
|
-
expect(window.dispatchEvent).toHaveBeenCalledWith(
|
|
750
|
-
expect.objectContaining({
|
|
751
|
-
detail: expect.objectContaining({
|
|
752
|
-
complete: true
|
|
753
|
-
})
|
|
754
|
-
})
|
|
755
|
-
);
|
|
756
|
-
});
|
|
157
|
+
describe('clearAll()', () => {
|
|
158
|
+
it('should clear caches and dependants', async () => {
|
|
159
|
+
manager.addDependant('media1', 'layout1');
|
|
160
|
+
manager.addDependant('media2', 'layout2');
|
|
757
161
|
|
|
758
|
-
|
|
759
|
-
manager.notifyDownloadProgress('test.mp4', 512, 1024, false, true);
|
|
162
|
+
await manager.clearAll();
|
|
760
163
|
|
|
761
|
-
expect(
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
error: true
|
|
765
|
-
})
|
|
766
|
-
})
|
|
767
|
-
);
|
|
164
|
+
expect(manager.isMediaReferenced('media1')).toBe(false);
|
|
165
|
+
expect(manager.isMediaReferenced('media2')).toBe(false);
|
|
166
|
+
expect(manager.dependants.size).toBe(0);
|
|
768
167
|
});
|
|
769
168
|
});
|
|
770
169
|
});
|
package/src/index.js
CHANGED
|
@@ -4,3 +4,5 @@ export const VERSION = pkg.version;
|
|
|
4
4
|
export { CacheManager, cacheManager } from './cache.js';
|
|
5
5
|
export { CacheProxy } from './cache-proxy.js';
|
|
6
6
|
export { DownloadManager, FileDownload, LayoutTaskBuilder, isUrlExpired } from './download-manager.js';
|
|
7
|
+
export { CacheAnalyzer } from './cache-analyzer.js';
|
|
8
|
+
export { cacheWidgetHtml } from './widget-html.js';
|