@xiboplayer/cache 0.4.0 → 0.4.3
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/README.md +2 -0
- package/docs/CACHE_PROXY_ARCHITECTURE.md +12 -0
- package/package.json +2 -2
- package/src/cache-analyzer.js +8 -0
- package/src/cache-analyzer.test.js +14 -0
- package/src/download-manager.js +16 -2
- package/src/widget-html.js +19 -15
- package/src/widget-html.test.js +297 -0
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ Manages media downloads and offline storage for Xibo players:
|
|
|
11
11
|
- **MD5 verification** — integrity checking with CRC32-based skip optimization
|
|
12
12
|
- **Download queue** — flat queue with barriers for layout-ordered downloading
|
|
13
13
|
- **CacheProxy** — browser-side proxy that communicates with a Service Worker backend
|
|
14
|
+
- **Widget data via enriched RequiredFiles** — RSS/dataset widget data is fetched through server-side enriched RequiredFiles paths (CMS adds download URLs), not via client-side pre-fetching
|
|
15
|
+
- **Dynamic BASE path** — widget HTML `<base>` tag uses a dynamic path within the Service Worker scope for correct relative URL resolution
|
|
14
16
|
|
|
15
17
|
## Installation
|
|
16
18
|
|
|
@@ -363,6 +363,18 @@ it('should download and cache files', async () => {
|
|
|
363
363
|
const blob = await cacheProxy.getFile('media', id);
|
|
364
364
|
```
|
|
365
365
|
|
|
366
|
+
## Widget Data Download Flow
|
|
367
|
+
|
|
368
|
+
Widget data for RSS feeds and dataset widgets is handled server-side. The CMS enriches
|
|
369
|
+
the RequiredFiles response with absolute download URLs for widget data files. These are
|
|
370
|
+
downloaded through the normal CacheProxy/Service Worker pipeline alongside regular media,
|
|
371
|
+
rather than being fetched client-side by the player. This ensures widget data is available
|
|
372
|
+
offline and benefits from the same parallel chunk download and caching infrastructure.
|
|
373
|
+
|
|
374
|
+
Widget HTML served from cache uses a dynamic `<base>` tag pointing to the Service Worker
|
|
375
|
+
scope path, ensuring relative URLs within widget HTML resolve correctly regardless of the
|
|
376
|
+
player's deployment path.
|
|
377
|
+
|
|
366
378
|
### For New Platforms
|
|
367
379
|
|
|
368
380
|
Simply use CacheProxy from the start:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiboplayer/cache",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Offline caching and download management with parallel chunk downloads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"spark-md5": "^3.0.2",
|
|
15
|
-
"@xiboplayer/utils": "0.4.
|
|
15
|
+
"@xiboplayer/utils": "0.4.3"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"vitest": "^2.0.0",
|
package/src/cache-analyzer.js
CHANGED
|
@@ -56,6 +56,14 @@ export class CacheAnalyzer {
|
|
|
56
56
|
for (const file of cachedFiles) {
|
|
57
57
|
if (requiredIds.has(String(file.id))) {
|
|
58
58
|
required.push(file);
|
|
59
|
+
} else if (file.type === 'widget') {
|
|
60
|
+
// Widget HTML IDs are "layoutId/regionId/widgetId" — check parent layout
|
|
61
|
+
const parentLayoutId = String(file.id).split('/')[0];
|
|
62
|
+
if (requiredIds.has(parentLayoutId)) {
|
|
63
|
+
required.push(file);
|
|
64
|
+
} else {
|
|
65
|
+
orphaned.push(file);
|
|
66
|
+
}
|
|
59
67
|
} else {
|
|
60
68
|
orphaned.push(file);
|
|
61
69
|
}
|
|
@@ -170,6 +170,20 @@ describe('CacheAnalyzer', () => {
|
|
|
170
170
|
vi.unstubAllGlobals();
|
|
171
171
|
});
|
|
172
172
|
|
|
173
|
+
it('should treat widget HTML as required when parent layout is required', async () => {
|
|
174
|
+
mockCache._addFile({ id: '470', type: 'layout', size: 500, cachedAt: 100 });
|
|
175
|
+
mockCache._addFile({ id: '470/213/182', type: 'widget', size: 0, cachedAt: 0 });
|
|
176
|
+
mockCache._addFile({ id: '470/215/184', type: 'widget', size: 0, cachedAt: 0 });
|
|
177
|
+
mockCache._addFile({ id: '99/10/5', type: 'widget', size: 0, cachedAt: 0 });
|
|
178
|
+
|
|
179
|
+
const report = await analyzer.analyze([{ id: '470', type: 'layout' }]);
|
|
180
|
+
|
|
181
|
+
// Layout 470 + its 2 widgets = 3 required; widget for layout 99 = orphaned
|
|
182
|
+
expect(report.files.required).toBe(3);
|
|
183
|
+
expect(report.files.orphaned).toBe(1);
|
|
184
|
+
expect(report.orphaned[0].id).toBe('99/10/5');
|
|
185
|
+
});
|
|
186
|
+
|
|
173
187
|
it('should handle files with missing size or cachedAt', async () => {
|
|
174
188
|
mockCache._addFile({ id: '1', type: 'media' }); // no size, no cachedAt
|
|
175
189
|
|
package/src/download-manager.js
CHANGED
|
@@ -97,6 +97,20 @@ export function isUrlExpired(url, graceSeconds = 30) {
|
|
|
97
97
|
return (Date.now() / 1000) >= (expiry - graceSeconds);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Rewrite an absolute CMS URL through the local proxy when running behind
|
|
102
|
+
* the proxy server (Chromium kiosk or Electron).
|
|
103
|
+
* Detection: SW/window on localhost:8765 = proxy mode.
|
|
104
|
+
*/
|
|
105
|
+
export function rewriteUrlForProxy(url) {
|
|
106
|
+
if (!url.startsWith('http')) return url;
|
|
107
|
+
const loc = typeof self !== 'undefined' ? self.location : undefined;
|
|
108
|
+
if (!loc || loc.hostname !== 'localhost' || loc.port !== '8765') return url;
|
|
109
|
+
const parsed = new URL(url);
|
|
110
|
+
const cmsOrigin = parsed.origin;
|
|
111
|
+
return `/file-proxy?cms=${encodeURIComponent(cmsOrigin)}&url=${encodeURIComponent(parsed.pathname + parsed.search)}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
/**
|
|
101
115
|
* DownloadTask - Single HTTP fetch unit
|
|
102
116
|
*
|
|
@@ -120,7 +134,7 @@ export class DownloadTask {
|
|
|
120
134
|
if (isUrlExpired(url)) {
|
|
121
135
|
throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
|
|
122
136
|
}
|
|
123
|
-
return url;
|
|
137
|
+
return rewriteUrlForProxy(url);
|
|
124
138
|
}
|
|
125
139
|
|
|
126
140
|
async start() {
|
|
@@ -207,7 +221,7 @@ export class FileDownload {
|
|
|
207
221
|
if (isUrlExpired(url)) {
|
|
208
222
|
throw new Error(`URL expired for ${this.fileInfo.type}/${this.fileInfo.id} — waiting for fresh URL from next collection cycle`);
|
|
209
223
|
}
|
|
210
|
-
return url;
|
|
224
|
+
return rewriteUrlForProxy(url);
|
|
211
225
|
}
|
|
212
226
|
|
|
213
227
|
wait() {
|
package/src/widget-html.js
CHANGED
|
@@ -35,18 +35,20 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
35
35
|
const cache = await caches.open(CACHE_NAME);
|
|
36
36
|
|
|
37
37
|
// Inject <base> tag to fix relative paths for widget dependencies
|
|
38
|
-
// Widget HTML has relative paths like "bundle.min.js" that should resolve to
|
|
39
|
-
const baseTag =
|
|
38
|
+
// Widget HTML has relative paths like "bundle.min.js" that should resolve to cache/media/
|
|
39
|
+
const baseTag = `<base href="${BASE}/cache/media/">`;
|
|
40
40
|
let modifiedHtml = html;
|
|
41
41
|
|
|
42
|
-
// Insert base tag after <head> opening tag
|
|
43
|
-
if (html.includes('<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
// Insert base tag after <head> opening tag (skip if already present)
|
|
43
|
+
if (!html.includes('<base ')) {
|
|
44
|
+
if (html.includes('<head>')) {
|
|
45
|
+
modifiedHtml = html.replace('<head>', '<head>' + baseTag);
|
|
46
|
+
} else if (html.includes('<HEAD>')) {
|
|
47
|
+
modifiedHtml = html.replace('<HEAD>', '<HEAD>' + baseTag);
|
|
48
|
+
} else {
|
|
49
|
+
// No head tag, prepend base tag
|
|
50
|
+
modifiedHtml = baseTag + html;
|
|
51
|
+
}
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
// Rewrite absolute CMS signed URLs to local cache paths
|
|
@@ -65,10 +67,12 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
65
67
|
// CMS global-elements.xml uses {{alignId}} {{valignId}} which produces
|
|
66
68
|
// invalid CSS (empty value) when alignment is not configured
|
|
67
69
|
const cssFixTag = '<style>img,video{object-position:center center}</style>';
|
|
68
|
-
if (modifiedHtml.includes('
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
if (!modifiedHtml.includes('object-position:center center')) {
|
|
71
|
+
if (modifiedHtml.includes('</head>')) {
|
|
72
|
+
modifiedHtml = modifiedHtml.replace('</head>', cssFixTag + '</head>');
|
|
73
|
+
} else if (modifiedHtml.includes('</HEAD>')) {
|
|
74
|
+
modifiedHtml = modifiedHtml.replace('</HEAD>', cssFixTag + '</HEAD>');
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
// Rewrite Interactive Control hostAddress to SW-interceptable path
|
|
@@ -80,7 +84,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
80
84
|
`hostAddress: "${BASE}/ic"`
|
|
81
85
|
);
|
|
82
86
|
|
|
83
|
-
log.info('Injected base tag and rewrote CMS URLs in widget HTML');
|
|
87
|
+
log.info('Injected base tag and rewrote CMS/data URLs in widget HTML');
|
|
84
88
|
|
|
85
89
|
// Construct full URL for cache storage
|
|
86
90
|
const cacheUrl = new URL(cacheKey, window.location.origin);
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widget HTML caching tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the URL rewriting and base tag injection that cacheWidgetHtml performs.
|
|
5
|
+
* These are critical for widget rendering — CMS provides HTML with absolute
|
|
6
|
+
* signed URLs that must be rewritten to local cache paths.
|
|
7
|
+
*
|
|
8
|
+
* Real-world scenario (RSS ticker in layout 472, region 223, widget 193):
|
|
9
|
+
* 1. CMS getResource returns HTML with signed URLs for bundle.min.js, fonts.css
|
|
10
|
+
* 2. SW download manager may cache this raw HTML before the main thread processes it
|
|
11
|
+
* 3. cacheWidgetHtml must rewrite CMS URLs → /cache/static/ and fetch the resources
|
|
12
|
+
* 4. Widget iframe loads, SW serves bundle.min.js and fonts.css from static cache
|
|
13
|
+
* 5. bundle.min.js runs getWidgetData → $.ajax("193.json") → SW serves from media cache
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
17
|
+
import { cacheWidgetHtml } from './widget-html.js';
|
|
18
|
+
|
|
19
|
+
// --- Realistic CMS HTML templates (based on actual CMS output) ---
|
|
20
|
+
|
|
21
|
+
const CMS_BASE = 'https://displays.superpantalles.com';
|
|
22
|
+
const SIGNED_PARAMS = 'displayId=152&type=P&itemId=1&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20260222T000000Z&X-Amz-Expires=1771803983&X-Amz-SignedHeaders=host&X-Amz-Signature=abc123';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Simulates actual CMS RSS ticker widget HTML (layout 472, region 223, widget 193).
|
|
26
|
+
* The CMS generates this via getResource — it includes:
|
|
27
|
+
* - Signed URLs for bundle.min.js and fonts.css (must be rewritten)
|
|
28
|
+
* - Relative data URL "193.json" resolved via <base> tag
|
|
29
|
+
* - Interactive Control (xiboIC) with hostAddress pointing at CMS
|
|
30
|
+
*/
|
|
31
|
+
function makeRssTickerHtml() {
|
|
32
|
+
return `<!DOCTYPE html>
|
|
33
|
+
<html>
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<title>RSS Ticker</title>
|
|
37
|
+
<script src="${CMS_BASE}/pwa/file?file=bundle.min.js&fileType=bundle&${SIGNED_PARAMS}"></script>
|
|
38
|
+
<link rel="stylesheet" href="${CMS_BASE}/pwa/file?file=fonts.css&fileType=fontCss&${SIGNED_PARAMS}">
|
|
39
|
+
<style>.rss-item { padding: 10px; }</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<div id="content">
|
|
43
|
+
<script>
|
|
44
|
+
var currentWidget = { url: "193.json", duration: 5 };
|
|
45
|
+
var options = {hostAddress: "${CMS_BASE}"};
|
|
46
|
+
xiboIC.init(options);
|
|
47
|
+
function getWidgetData() {
|
|
48
|
+
$.ajax({ url: currentWidget.url, dataType: "json" });
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
</div>
|
|
52
|
+
</body>
|
|
53
|
+
</html>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** PDF widget (layout 472, region 221, widget 190) — simpler, no data URL */
|
|
57
|
+
function makePdfWidgetHtml() {
|
|
58
|
+
return `<!DOCTYPE html>
|
|
59
|
+
<html>
|
|
60
|
+
<head>
|
|
61
|
+
<meta charset="utf-8">
|
|
62
|
+
<script src="${CMS_BASE}/pwa/file?file=bundle.min.js&fileType=bundle&${SIGNED_PARAMS}"></script>
|
|
63
|
+
<link rel="stylesheet" href="${CMS_BASE}/pwa/file?file=fonts.css&fileType=fontCss&${SIGNED_PARAMS}"></link>
|
|
64
|
+
</head>
|
|
65
|
+
<body>
|
|
66
|
+
<object data="11.pdf" type="application/pdf" width="100%" height="100%"></object>
|
|
67
|
+
</body>
|
|
68
|
+
</html>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Clock widget — uses xmds.php endpoint (older CMS versions) */
|
|
72
|
+
function makeClockWidgetHtml() {
|
|
73
|
+
return `<html>
|
|
74
|
+
<head>
|
|
75
|
+
<script src="${CMS_BASE}/xmds.php?file=bundle.min.js&${SIGNED_PARAMS}"></script>
|
|
76
|
+
<link rel="stylesheet" href="${CMS_BASE}/xmds.php?file=fonts.css&${SIGNED_PARAMS}">
|
|
77
|
+
</head>
|
|
78
|
+
<body><div class="clock"></div></body>
|
|
79
|
+
</html>`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// --- Mock Cache API ---
|
|
83
|
+
|
|
84
|
+
const cacheStore = new Map();
|
|
85
|
+
const mockCache = {
|
|
86
|
+
put: vi.fn(async (url, response) => {
|
|
87
|
+
const key = typeof url === 'string' ? url : url.toString();
|
|
88
|
+
const text = await response.clone().text();
|
|
89
|
+
cacheStore.set(key, text);
|
|
90
|
+
}),
|
|
91
|
+
match: vi.fn(async (key) => {
|
|
92
|
+
const url = typeof key === 'string' ? key : (key.url || key.toString());
|
|
93
|
+
const text = cacheStore.get(url);
|
|
94
|
+
return text ? new Response(text) : undefined;
|
|
95
|
+
}),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const fetchedUrls = [];
|
|
99
|
+
global.fetch = vi.fn(async (url) => {
|
|
100
|
+
fetchedUrls.push(url);
|
|
101
|
+
if (url.includes('bundle.min.js')) {
|
|
102
|
+
return new Response('var xiboIC = { init: function(){} };', { status: 200 });
|
|
103
|
+
}
|
|
104
|
+
if (url.includes('fonts.css')) {
|
|
105
|
+
return new Response(`@font-face { font-family: "Poppins"; src: url("${CMS_BASE}/pwa/file?file=Poppins-Regular.ttf&${SIGNED_PARAMS}"); }`, { status: 200 });
|
|
106
|
+
}
|
|
107
|
+
if (url.includes('.ttf') || url.includes('.woff')) {
|
|
108
|
+
return new Response(new Blob([new Uint8Array(100)]), { status: 200 });
|
|
109
|
+
}
|
|
110
|
+
return new Response('', { status: 404 });
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
global.caches = {
|
|
114
|
+
open: vi.fn(async () => mockCache),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// --- Tests ---
|
|
118
|
+
|
|
119
|
+
describe('cacheWidgetHtml', () => {
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
cacheStore.clear();
|
|
122
|
+
vi.clearAllMocks();
|
|
123
|
+
fetchedUrls.length = 0;
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- Base tag injection ---
|
|
127
|
+
|
|
128
|
+
describe('base tag injection', () => {
|
|
129
|
+
it('injects <base> tag after <head>', async () => {
|
|
130
|
+
const html = '<html><head><title>Widget</title></head><body>content</body></html>';
|
|
131
|
+
await cacheWidgetHtml('472', '223', '193', html);
|
|
132
|
+
|
|
133
|
+
const stored = cacheStore.values().next().value;
|
|
134
|
+
expect(stored).toContain('<base href=');
|
|
135
|
+
expect(stored).toContain('/cache/media/">');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('injects <base> tag when no <head> tag exists', async () => {
|
|
139
|
+
const html = '<div>no head tag</div>';
|
|
140
|
+
await cacheWidgetHtml('472', '223', '193', html);
|
|
141
|
+
|
|
142
|
+
const stored = cacheStore.values().next().value;
|
|
143
|
+
expect(stored).toContain('<base href=');
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// --- RSS ticker: layout 472, region 223, widget 193 ---
|
|
148
|
+
|
|
149
|
+
describe('RSS ticker (layout 472 / region 223 / widget 193)', () => {
|
|
150
|
+
it('rewrites bundle.min.js and fonts.css signed URLs', async () => {
|
|
151
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
152
|
+
|
|
153
|
+
const stored = cacheStore.values().next().value;
|
|
154
|
+
expect(stored).not.toContain(CMS_BASE);
|
|
155
|
+
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
156
|
+
expect(stored).toContain('/cache/static/fonts.css');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('preserves the data URL (193.json) for SW interception', async () => {
|
|
160
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
161
|
+
|
|
162
|
+
const stored = cacheStore.values().next().value;
|
|
163
|
+
// 193.json is relative — resolved by <base> tag, not rewritten
|
|
164
|
+
expect(stored).toContain('"193.json"');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('rewrites xiboIC hostAddress from CMS to local path', async () => {
|
|
168
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
169
|
+
|
|
170
|
+
const stored = cacheStore.values().next().value;
|
|
171
|
+
expect(stored).not.toContain(`hostAddress: "${CMS_BASE}"`);
|
|
172
|
+
expect(stored).toContain('/ic"');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('fetches bundle.min.js and fonts.css from CMS for local caching', async () => {
|
|
176
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
177
|
+
|
|
178
|
+
const bundleFetched = fetchedUrls.some(u => u.includes('bundle.min.js'));
|
|
179
|
+
const fontsFetched = fetchedUrls.some(u => u.includes('fonts.css'));
|
|
180
|
+
expect(bundleFetched).toBe(true);
|
|
181
|
+
expect(fontsFetched).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('stores processed HTML at correct cache key', async () => {
|
|
185
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
186
|
+
|
|
187
|
+
const keys = [...cacheStore.keys()];
|
|
188
|
+
const widgetKey = keys.find(k => k.includes('/cache/widget/472/223/193'));
|
|
189
|
+
expect(widgetKey).toBeTruthy();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('caches font files referenced in fonts.css', async () => {
|
|
193
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
194
|
+
|
|
195
|
+
// fonts.css contains url("...Poppins-Regular.ttf") which should be fetched
|
|
196
|
+
const fontFetched = fetchedUrls.some(u => u.includes('Poppins-Regular.ttf'));
|
|
197
|
+
expect(fontFetched).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// --- PDF widget: layout 472, region 221, widget 190 ---
|
|
202
|
+
|
|
203
|
+
describe('PDF widget (layout 472 / region 221 / widget 190)', () => {
|
|
204
|
+
it('rewrites CMS URLs and preserves relative PDF path', async () => {
|
|
205
|
+
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
206
|
+
|
|
207
|
+
const stored = cacheStore.values().next().value;
|
|
208
|
+
expect(stored).not.toContain(CMS_BASE);
|
|
209
|
+
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
210
|
+
// PDF data attribute "11.pdf" is relative — resolved by <base> tag
|
|
211
|
+
expect(stored).toContain('"11.pdf"');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('stores at correct widget cache key with layout/region/media IDs', async () => {
|
|
215
|
+
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
216
|
+
|
|
217
|
+
const keys = [...cacheStore.keys()];
|
|
218
|
+
expect(keys.some(k => k.includes('/cache/widget/472/221/190'))).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// --- Clock widget: xmds.php endpoint (legacy) ---
|
|
223
|
+
|
|
224
|
+
describe('clock widget with xmds.php URLs', () => {
|
|
225
|
+
it('rewrites xmds.php signed URLs to local cache paths', async () => {
|
|
226
|
+
await cacheWidgetHtml('1', '1', '1', makeClockWidgetHtml());
|
|
227
|
+
|
|
228
|
+
const stored = cacheStore.values().next().value;
|
|
229
|
+
expect(stored).not.toContain('xmds.php');
|
|
230
|
+
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
231
|
+
expect(stored).toContain('/cache/static/fonts.css');
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// --- Idempotency (regression: duplicate base/style tags) ---
|
|
236
|
+
|
|
237
|
+
describe('idempotency', () => {
|
|
238
|
+
it('does not add duplicate <base> tags on re-processing', async () => {
|
|
239
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
240
|
+
const firstPass = cacheStore.values().next().value;
|
|
241
|
+
|
|
242
|
+
await cacheWidgetHtml('472', '223', '193', firstPass);
|
|
243
|
+
const secondPass = cacheStore.values().next().value;
|
|
244
|
+
|
|
245
|
+
expect(secondPass).toBe(firstPass);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not add duplicate CSS fix tags on re-processing', async () => {
|
|
249
|
+
const html = '<html><head></head><body></body></html>';
|
|
250
|
+
await cacheWidgetHtml('472', '223', '193', html);
|
|
251
|
+
const firstPass = cacheStore.values().next().value;
|
|
252
|
+
|
|
253
|
+
await cacheWidgetHtml('472', '223', '193', firstPass);
|
|
254
|
+
const secondPass = cacheStore.values().next().value;
|
|
255
|
+
|
|
256
|
+
const baseCount = (secondPass.match(/<base /g) || []).length;
|
|
257
|
+
const styleCount = (secondPass.match(/object-position:center center/g) || []).length;
|
|
258
|
+
expect(baseCount).toBe(1);
|
|
259
|
+
expect(styleCount).toBe(1);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// --- SW pre-cache scenario (the bug we fixed) ---
|
|
264
|
+
|
|
265
|
+
describe('SW pre-cached raw HTML (regression)', () => {
|
|
266
|
+
it('processes raw HTML that SW cached without rewriting', async () => {
|
|
267
|
+
// Simulate: SW downloads getResource HTML and caches it raw
|
|
268
|
+
const rawCmsHtml = makeRssTickerHtml();
|
|
269
|
+
|
|
270
|
+
// Main thread finds it in cache and re-processes
|
|
271
|
+
await cacheWidgetHtml('472', '223', '193', rawCmsHtml);
|
|
272
|
+
|
|
273
|
+
const stored = cacheStore.values().next().value;
|
|
274
|
+
// CMS URLs must be rewritten even though HTML came from cache
|
|
275
|
+
expect(stored).not.toContain(CMS_BASE);
|
|
276
|
+
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
277
|
+
expect(stored).toContain('/cache/static/fonts.css');
|
|
278
|
+
expect(stored).toContain('<base href=');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('handles different widgets in different regions of same layout', async () => {
|
|
282
|
+
// Layout 472 has region 221 (PDF) and region 223 (RSS ticker)
|
|
283
|
+
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
284
|
+
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
285
|
+
|
|
286
|
+
const keys = [...cacheStore.keys()];
|
|
287
|
+
const pdfKey = keys.find(k => k.includes('/cache/widget/472/221/190'));
|
|
288
|
+
const rssKey = keys.find(k => k.includes('/cache/widget/472/223/193'));
|
|
289
|
+
expect(pdfKey).toBeTruthy();
|
|
290
|
+
expect(rssKey).toBeTruthy();
|
|
291
|
+
|
|
292
|
+
// Both should have CMS URLs rewritten
|
|
293
|
+
expect(cacheStore.get(pdfKey)).not.toContain(CMS_BASE);
|
|
294
|
+
expect(cacheStore.get(rssKey)).not.toContain(CMS_BASE);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|