@xiboplayer/cache 0.5.20 → 0.6.1
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/docs/CACHE_PROXY_ARCHITECTURE.md +42 -3
- package/package.json +2 -2
- package/src/cache-proxy.test.js +1 -206
- package/src/download-manager.js +37 -78
- package/src/file-types.js +26 -0
- package/src/index.d.ts +15 -16
- package/src/index.js +2 -2
- package/src/widget-html.js +24 -146
- package/src/widget-html.test.js +40 -119
- package/src/download-client.js +0 -222
package/src/widget-html.js
CHANGED
|
@@ -2,21 +2,19 @@
|
|
|
2
2
|
* Widget HTML processing — preprocesses widget HTML and stores via REST
|
|
3
3
|
*
|
|
4
4
|
* Handles:
|
|
5
|
-
* - <base> tag injection for relative path resolution
|
|
6
|
-
* - CMS signed URL → local store path rewriting
|
|
5
|
+
* - <base> tag injection for relative path resolution (CMS mirror paths)
|
|
7
6
|
* - Interactive Control hostAddress rewriting
|
|
8
7
|
* - CSS object-position fix for CMS template alignment
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* URL rewriting is no longer needed — the CMS serves CSS with relative paths
|
|
10
|
+
* (/api/v2/player/dependencies/font.otf), and the <base> tag resolves widget
|
|
11
|
+
* media references via mirror routes. Zero translation, zero regex.
|
|
13
12
|
*
|
|
14
13
|
* Runs on the main thread (needs window.location for URL construction).
|
|
15
14
|
* Stores content via PUT /store/... — no Cache API needed.
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
|
-
import { createLogger } from '@xiboplayer/utils';
|
|
19
|
-
import { toProxyUrl } from './download-manager.js';
|
|
17
|
+
import { createLogger, PLAYER_API } from '@xiboplayer/utils';
|
|
20
18
|
|
|
21
19
|
const log = createLogger('Cache');
|
|
22
20
|
|
|
@@ -25,23 +23,22 @@ const BASE = (typeof window !== 'undefined')
|
|
|
25
23
|
? window.location.pathname.replace(/\/[^/]*$/, '').replace(/\/$/, '') || '/player/pwa'
|
|
26
24
|
: '/player/pwa';
|
|
27
25
|
|
|
28
|
-
// Dedup concurrent static resource fetches (two widgets both need bundle.min.js)
|
|
29
|
-
const _pendingStatic = new Map(); // filename → Promise<void>
|
|
30
|
-
|
|
31
26
|
/**
|
|
32
|
-
* Store widget HTML in ContentStore for iframe loading
|
|
27
|
+
* Store widget HTML in ContentStore for iframe loading.
|
|
28
|
+
* Stored at mirror path /api/v2/player/widgets/{L}/{R}/{M} — same URL the
|
|
29
|
+
* CMS serves from, so iframes load directly from Express mirror routes.
|
|
30
|
+
*
|
|
33
31
|
* @param {string} layoutId - Layout ID
|
|
34
32
|
* @param {string} regionId - Region ID
|
|
35
33
|
* @param {string} mediaId - Media ID
|
|
36
34
|
* @param {string} html - Widget HTML content
|
|
37
|
-
* @returns {Promise<string>} Cache key URL
|
|
35
|
+
* @returns {Promise<string>} Cache key URL (absolute path for iframe src)
|
|
38
36
|
*/
|
|
39
37
|
export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
40
|
-
const cacheKey = `${
|
|
38
|
+
const cacheKey = `${PLAYER_API}/widgets/${layoutId}/${regionId}/${mediaId}`;
|
|
41
39
|
|
|
42
|
-
// Inject <base> tag
|
|
43
|
-
|
|
44
|
-
const baseTag = `<base href="${BASE}/cache/media/">`;
|
|
40
|
+
// Inject <base> tag — resolves relative media refs (e.g. "42") to mirror route
|
|
41
|
+
const baseTag = `<base href="${PLAYER_API}/media/">`;
|
|
45
42
|
let modifiedHtml = html;
|
|
46
43
|
|
|
47
44
|
// Insert base tag after <head> opening tag (skip if already present)
|
|
@@ -51,23 +48,10 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
51
48
|
} else if (html.includes('<HEAD>')) {
|
|
52
49
|
modifiedHtml = html.replace('<HEAD>', '<HEAD>' + baseTag);
|
|
53
50
|
} else {
|
|
54
|
-
// No head tag, prepend base tag
|
|
55
51
|
modifiedHtml = baseTag + html;
|
|
56
52
|
}
|
|
57
53
|
}
|
|
58
54
|
|
|
59
|
-
// Rewrite absolute CMS signed URLs to local store paths
|
|
60
|
-
// Matches: https://cms/xmds.php?file=... or https://cms/pwa/file?file=...
|
|
61
|
-
// These absolute URLs bypass the <base> tag entirely, causing slow CMS fetches
|
|
62
|
-
const cmsUrlRegex = /https?:\/\/[^"'\s)]+(?:xmds\.php|pwa\/file)\?[^"'\s)]*file=([^&"'\s)]+)[^"'\s)]*/g;
|
|
63
|
-
const staticResources = [];
|
|
64
|
-
modifiedHtml = modifiedHtml.replace(cmsUrlRegex, (match, filename) => {
|
|
65
|
-
const localPath = `${BASE}/cache/static/${filename}`;
|
|
66
|
-
staticResources.push({ filename, originalUrl: match });
|
|
67
|
-
log.info(`Rewrote widget URL: ${filename} → ${localPath}`);
|
|
68
|
-
return localPath;
|
|
69
|
-
});
|
|
70
|
-
|
|
71
55
|
// Inject CSS default for object-position to suppress CMS template warning
|
|
72
56
|
const cssFixTag = '<style>img,video{object-position:center center}</style>';
|
|
73
57
|
if (!modifiedHtml.includes('object-position:center center')) {
|
|
@@ -78,130 +62,24 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
78
62
|
}
|
|
79
63
|
}
|
|
80
64
|
|
|
65
|
+
// Rewrite dependency URLs to local mirror paths. CMS sends absolute URLs
|
|
66
|
+
// like https://cms.example.com/api/v2/player/dependencies/bundle.min.js
|
|
67
|
+
// which fail due to CORS/auth. Replace with local /api/v2/player/dependencies/...
|
|
68
|
+
modifiedHtml = modifiedHtml.replace(
|
|
69
|
+
/https?:\/\/[^"'\s)]+?(\/api\/v2\/player\/dependencies\/[^"'\s?)]+)(\?[^"'\s)]*)?/g,
|
|
70
|
+
(_, path) => path
|
|
71
|
+
);
|
|
72
|
+
|
|
81
73
|
// Rewrite Interactive Control hostAddress to SW-interceptable path
|
|
82
74
|
modifiedHtml = modifiedHtml.replace(
|
|
83
75
|
/hostAddress\s*:\s*["']https?:\/\/[^"']+["']/g,
|
|
84
76
|
`hostAddress: "${BASE}/ic"`
|
|
85
77
|
);
|
|
86
78
|
|
|
87
|
-
log.info('Injected base tag
|
|
88
|
-
|
|
89
|
-
// Store static resources FIRST — widget iframe loads immediately after HTML is stored,
|
|
90
|
-
// and its <script>/<link> tags will 404 if deps aren't ready yet
|
|
91
|
-
if (staticResources.length > 0) {
|
|
92
|
-
await Promise.all(staticResources.map(({ filename, originalUrl }) => {
|
|
93
|
-
// Dedup: if another widget is already fetching the same resource, wait for it
|
|
94
|
-
if (_pendingStatic.has(filename)) {
|
|
95
|
-
return _pendingStatic.get(filename);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const work = (async () => {
|
|
99
|
-
// Check if already stored
|
|
100
|
-
try {
|
|
101
|
-
const headResp = await fetch(`/store/static/${filename}`, { method: 'HEAD' });
|
|
102
|
-
if (headResp.ok) return; // Already stored
|
|
103
|
-
} catch { /* proceed to fetch */ }
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
const resp = await fetch(toProxyUrl(originalUrl));
|
|
107
|
-
if (!resp.ok) {
|
|
108
|
-
resp.body?.cancel();
|
|
109
|
-
log.warn(`Failed to fetch static resource: ${filename} (HTTP ${resp.status})`);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const ext = filename.split('.').pop().toLowerCase();
|
|
114
|
-
const contentType = {
|
|
115
|
-
'js': 'application/javascript',
|
|
116
|
-
'css': 'text/css',
|
|
117
|
-
'otf': 'font/otf', 'ttf': 'font/ttf',
|
|
118
|
-
'woff': 'font/woff', 'woff2': 'font/woff2',
|
|
119
|
-
'eot': 'application/vnd.ms-fontobject',
|
|
120
|
-
'svg': 'image/svg+xml'
|
|
121
|
-
}[ext] || 'application/octet-stream';
|
|
122
|
-
|
|
123
|
-
// Defense-in-depth: rewrite font URLs inside CSS files before storing.
|
|
124
|
-
// The proxy normally handles this, but if it misses CSS detection
|
|
125
|
-
// (e.g. CMS returns unexpected Content-Type), we catch it here.
|
|
126
|
-
// Also handles truncated CSS from CMS (missing closing ');}) due to
|
|
127
|
-
// Content-Length mismatch after URL expansion).
|
|
128
|
-
if (ext === 'css') {
|
|
129
|
-
let cssText = await resp.text();
|
|
130
|
-
// Match any CMS signed URL with a file= query param in the CSS text.
|
|
131
|
-
// Uses a simple approach: find all https:// URLs with file=<name>, then
|
|
132
|
-
// filter to fonts by extension or fileType=font. This handles both normal
|
|
133
|
-
// url('...') and truncated CSS (where the closing '); is missing).
|
|
134
|
-
const CMS_SIGNED_URL_RE = /https?:\/\/[^\s'")\]]+\?[^\s'")\]]*file=([^&\s'")\]]+)[^\s'")\]]*/g;
|
|
135
|
-
const FONT_EXTS = /\.(?:woff2?|ttf|otf|eot|svg)$/i;
|
|
136
|
-
const fontJobs = [];
|
|
137
|
-
|
|
138
|
-
cssText = cssText.replace(CMS_SIGNED_URL_RE, (fullUrl, fontFilename) => {
|
|
139
|
-
if (!FONT_EXTS.test(fontFilename) && !fullUrl.includes('fileType=font')) return fullUrl;
|
|
140
|
-
fontJobs.push({ filename: fontFilename, originalUrl: fullUrl });
|
|
141
|
-
log.info(`Rewrote CSS font URL: ${fontFilename}`);
|
|
142
|
-
return `${BASE}/cache/static/${encodeURIComponent(fontFilename)}`;
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Fetch and store each referenced font file
|
|
146
|
-
await Promise.all(fontJobs.map(async (font) => {
|
|
147
|
-
try {
|
|
148
|
-
const headResp = await fetch(`/store/static/${font.filename}`, { method: 'HEAD' });
|
|
149
|
-
if (headResp.ok) return; // Already stored
|
|
150
|
-
} catch { /* proceed */ }
|
|
151
|
-
try {
|
|
152
|
-
const fontResp = await fetch(toProxyUrl(font.originalUrl));
|
|
153
|
-
if (!fontResp.ok) { fontResp.body?.cancel(); return; }
|
|
154
|
-
const fontBlob = await fontResp.blob();
|
|
155
|
-
const fontExt = font.filename.split('.').pop().toLowerCase();
|
|
156
|
-
const fontContentType = {
|
|
157
|
-
'otf': 'font/otf', 'ttf': 'font/ttf',
|
|
158
|
-
'woff': 'font/woff', 'woff2': 'font/woff2',
|
|
159
|
-
'eot': 'application/vnd.ms-fontobject', 'svg': 'image/svg+xml',
|
|
160
|
-
}[fontExt] || 'application/octet-stream';
|
|
161
|
-
const putFont = await fetch(`/store/static/${font.filename}`, {
|
|
162
|
-
method: 'PUT',
|
|
163
|
-
headers: { 'Content-Type': fontContentType },
|
|
164
|
-
body: fontBlob,
|
|
165
|
-
});
|
|
166
|
-
putFont.body?.cancel();
|
|
167
|
-
log.info(`Stored font: ${font.filename} (${fontBlob.size} bytes)`);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
log.warn(`Failed to store font: ${font.filename}`, e);
|
|
170
|
-
}
|
|
171
|
-
}));
|
|
172
|
-
|
|
173
|
-
// Store the rewritten CSS
|
|
174
|
-
const cssBlob = new Blob([cssText], { type: 'text/css' });
|
|
175
|
-
const staticResp = await fetch(`/store/static/${filename}`, {
|
|
176
|
-
method: 'PUT',
|
|
177
|
-
headers: { 'Content-Type': 'text/css' },
|
|
178
|
-
body: cssBlob,
|
|
179
|
-
});
|
|
180
|
-
staticResp.body?.cancel();
|
|
181
|
-
log.info(`Stored CSS with ${fontJobs.length} rewritten font URLs: ${filename} (${cssText.length} bytes)`);
|
|
182
|
-
} else {
|
|
183
|
-
const blob = await resp.blob();
|
|
184
|
-
const staticResp = await fetch(`/store/static/${filename}`, {
|
|
185
|
-
method: 'PUT',
|
|
186
|
-
headers: { 'Content-Type': contentType },
|
|
187
|
-
body: blob,
|
|
188
|
-
});
|
|
189
|
-
staticResp.body?.cancel();
|
|
190
|
-
log.info(`Stored static resource: ${filename} (${contentType}, ${blob.size} bytes)`);
|
|
191
|
-
}
|
|
192
|
-
} catch (error) {
|
|
193
|
-
log.warn(`Failed to store static resource: ${filename}`, error);
|
|
194
|
-
}
|
|
195
|
-
})();
|
|
196
|
-
|
|
197
|
-
_pendingStatic.set(filename, work);
|
|
198
|
-
return work.finally(() => _pendingStatic.delete(filename));
|
|
199
|
-
}));
|
|
200
|
-
}
|
|
79
|
+
log.info('Injected base tag in widget HTML');
|
|
201
80
|
|
|
202
|
-
// Store widget HTML
|
|
203
|
-
|
|
204
|
-
const putResp = await fetch(`/store/widget/${layoutId}/${regionId}/${mediaId}`, {
|
|
81
|
+
// Store widget HTML — deps are already downloaded by the pipeline
|
|
82
|
+
const putResp = await fetch(`/store${PLAYER_API}/widgets/${layoutId}/${regionId}/${mediaId}`, {
|
|
205
83
|
method: 'PUT',
|
|
206
84
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
207
85
|
body: modifiedHtml,
|
package/src/widget-html.test.js
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Widget HTML caching tests
|
|
3
3
|
*
|
|
4
|
-
* Tests the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 ContentStore
|
|
13
|
-
* 5. bundle.min.js runs getWidgetData → $.ajax("193.json") → SW serves from store
|
|
4
|
+
* Tests the base tag injection and IC hostAddress rewriting.
|
|
5
|
+
* URL rewriting has been removed — the CMS now serves relative paths,
|
|
6
|
+
* and the proxy mirror routes serve content at CMS URL paths directly.
|
|
14
7
|
*/
|
|
15
8
|
|
|
16
9
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
17
10
|
import { cacheWidgetHtml } from './widget-html.js';
|
|
18
11
|
|
|
19
|
-
// --- Realistic CMS HTML templates (based on actual CMS output) ---
|
|
20
|
-
|
|
21
12
|
const CMS_BASE = 'https://displays.superpantalles.com';
|
|
22
13
|
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
14
|
|
|
24
15
|
/**
|
|
25
|
-
*
|
|
16
|
+
* RSS ticker widget HTML (layout 472, region 223, widget 193).
|
|
17
|
+
* In v2, CMS serves relative dependency URLs — but legacy tests use absolute.
|
|
26
18
|
*/
|
|
27
19
|
function makeRssTickerHtml() {
|
|
28
20
|
return `<!DOCTYPE html>
|
|
@@ -30,8 +22,8 @@ function makeRssTickerHtml() {
|
|
|
30
22
|
<head>
|
|
31
23
|
<meta charset="utf-8">
|
|
32
24
|
<title>RSS Ticker</title>
|
|
33
|
-
<script src="
|
|
34
|
-
<link rel="stylesheet" href="
|
|
25
|
+
<script src="/api/v2/player/dependencies/bundle.min.js"></script>
|
|
26
|
+
<link rel="stylesheet" href="/api/v2/player/dependencies/fonts.css">
|
|
35
27
|
<style>.rss-item { padding: 10px; }</style>
|
|
36
28
|
</head>
|
|
37
29
|
<body>
|
|
@@ -49,14 +41,14 @@ function getWidgetData() {
|
|
|
49
41
|
</html>`;
|
|
50
42
|
}
|
|
51
43
|
|
|
52
|
-
/** PDF widget
|
|
44
|
+
/** PDF widget — relative paths, no data URL rewriting needed */
|
|
53
45
|
function makePdfWidgetHtml() {
|
|
54
46
|
return `<!DOCTYPE html>
|
|
55
47
|
<html>
|
|
56
48
|
<head>
|
|
57
49
|
<meta charset="utf-8">
|
|
58
|
-
<script src="
|
|
59
|
-
<link rel="stylesheet" href="
|
|
50
|
+
<script src="/api/v2/player/dependencies/bundle.min.js"></script>
|
|
51
|
+
<link rel="stylesheet" href="/api/v2/player/dependencies/fonts.css"></link>
|
|
60
52
|
</head>
|
|
61
53
|
<body>
|
|
62
54
|
<object data="11.pdf" type="application/pdf" width="100%" height="100%"></object>
|
|
@@ -64,20 +56,8 @@ function makePdfWidgetHtml() {
|
|
|
64
56
|
</html>`;
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
/** Clock widget — uses xmds.php endpoint (older CMS versions) */
|
|
68
|
-
function makeClockWidgetHtml() {
|
|
69
|
-
return `<html>
|
|
70
|
-
<head>
|
|
71
|
-
<script src="${CMS_BASE}/xmds.php?file=bundle.min.js&${SIGNED_PARAMS}"></script>
|
|
72
|
-
<link rel="stylesheet" href="${CMS_BASE}/xmds.php?file=fonts.css&${SIGNED_PARAMS}">
|
|
73
|
-
</head>
|
|
74
|
-
<body><div class="clock"></div></body>
|
|
75
|
-
</html>`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
59
|
// --- Mock: track PUT /store/... calls via fetch() ---
|
|
79
60
|
|
|
80
|
-
/** Map of storeKey → body text, populated by the fetch mock */
|
|
81
61
|
const storeContents = new Map();
|
|
82
62
|
const fetchedUrls = [];
|
|
83
63
|
|
|
@@ -88,26 +68,10 @@ function createFetchMock() {
|
|
|
88
68
|
// PUT /store/... — store content
|
|
89
69
|
if (opts?.method === 'PUT' && url.startsWith('/store/')) {
|
|
90
70
|
const body = typeof opts.body === 'string' ? opts.body : await opts.body?.text?.() || '';
|
|
91
|
-
// Use full URL path as key (e.g. /store/widget/472/223/193)
|
|
92
71
|
storeContents.set(url, body);
|
|
93
72
|
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
94
73
|
}
|
|
95
74
|
|
|
96
|
-
// HEAD /store/... — check existence
|
|
97
|
-
if (opts?.method === 'HEAD' && url.startsWith('/store/')) {
|
|
98
|
-
return new Response(null, { status: 404 }); // nothing pre-stored
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Proxy fetch for CMS resources
|
|
102
|
-
if (url.includes('bundle.min.js')) {
|
|
103
|
-
return new Response('var xiboIC = { init: function(){} };', { status: 200 });
|
|
104
|
-
}
|
|
105
|
-
if (url.includes('fonts.css')) {
|
|
106
|
-
return new Response(`@font-face { font-family: "Poppins"; src: url("${CMS_BASE}/pwa/file?file=Poppins-Regular.ttf&${SIGNED_PARAMS}"); }`, { status: 200 });
|
|
107
|
-
}
|
|
108
|
-
if (url.includes('.ttf') || url.includes('.woff')) {
|
|
109
|
-
return new Response(new Blob([new Uint8Array(100)]), { status: 200 });
|
|
110
|
-
}
|
|
111
75
|
return new Response('', { status: 404 });
|
|
112
76
|
});
|
|
113
77
|
}
|
|
@@ -122,10 +86,9 @@ describe('cacheWidgetHtml', () => {
|
|
|
122
86
|
global.fetch = createFetchMock();
|
|
123
87
|
});
|
|
124
88
|
|
|
125
|
-
// Helper: get stored widget HTML (first widget entry)
|
|
126
89
|
function getStoredWidget() {
|
|
127
90
|
for (const [key, value] of storeContents) {
|
|
128
|
-
if (key.startsWith('/store/
|
|
91
|
+
if (key.startsWith('/store/api/v2/player/widgets/')) return value;
|
|
129
92
|
}
|
|
130
93
|
return undefined;
|
|
131
94
|
}
|
|
@@ -133,13 +96,12 @@ describe('cacheWidgetHtml', () => {
|
|
|
133
96
|
// --- Base tag injection ---
|
|
134
97
|
|
|
135
98
|
describe('base tag injection', () => {
|
|
136
|
-
it('injects <base> tag
|
|
99
|
+
it('injects <base> tag pointing to CMS media mirror path', async () => {
|
|
137
100
|
const html = '<html><head><title>Widget</title></head><body>content</body></html>';
|
|
138
101
|
await cacheWidgetHtml('472', '223', '193', html);
|
|
139
102
|
|
|
140
103
|
const stored = getStoredWidget();
|
|
141
|
-
expect(stored).toContain('<base href=');
|
|
142
|
-
expect(stored).toContain('/cache/media/">');
|
|
104
|
+
expect(stored).toContain('<base href="/api/v2/player/media/">');
|
|
143
105
|
});
|
|
144
106
|
|
|
145
107
|
it('injects <base> tag when no <head> tag exists', async () => {
|
|
@@ -147,27 +109,25 @@ describe('cacheWidgetHtml', () => {
|
|
|
147
109
|
await cacheWidgetHtml('472', '223', '193', html);
|
|
148
110
|
|
|
149
111
|
const stored = getStoredWidget();
|
|
150
|
-
expect(stored).toContain('<base href=');
|
|
112
|
+
expect(stored).toContain('<base href="/api/v2/player/media/">');
|
|
151
113
|
});
|
|
152
114
|
});
|
|
153
115
|
|
|
154
|
-
// --- RSS ticker
|
|
116
|
+
// --- RSS ticker ---
|
|
155
117
|
|
|
156
118
|
describe('RSS ticker (layout 472 / region 223 / widget 193)', () => {
|
|
157
|
-
it('
|
|
119
|
+
it('preserves dependency URLs as-is (no rewriting needed)', async () => {
|
|
158
120
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
159
121
|
|
|
160
122
|
const stored = getStoredWidget();
|
|
161
|
-
expect(stored).
|
|
162
|
-
expect(stored).toContain('/
|
|
163
|
-
expect(stored).toContain('/cache/static/fonts.css');
|
|
123
|
+
expect(stored).toContain('/api/v2/player/dependencies/bundle.min.js');
|
|
124
|
+
expect(stored).toContain('/api/v2/player/dependencies/fonts.css');
|
|
164
125
|
});
|
|
165
126
|
|
|
166
|
-
it('preserves the data URL (193.json) for
|
|
127
|
+
it('preserves the data URL (193.json) for resolution via base tag', async () => {
|
|
167
128
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
168
129
|
|
|
169
130
|
const stored = getStoredWidget();
|
|
170
|
-
// 193.json is relative — resolved by <base> tag, not rewritten
|
|
171
131
|
expect(stored).toContain('"193.json"');
|
|
172
132
|
});
|
|
173
133
|
|
|
@@ -179,70 +139,53 @@ describe('cacheWidgetHtml', () => {
|
|
|
179
139
|
expect(stored).toContain('/ic"');
|
|
180
140
|
});
|
|
181
141
|
|
|
182
|
-
it('
|
|
142
|
+
it('does NOT fetch any static resources (pipeline handles deps)', async () => {
|
|
183
143
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
184
144
|
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
expect(
|
|
188
|
-
expect(fontsFetched).toBe(true);
|
|
145
|
+
// Only the widget HTML PUT should be fetched — no proactive resource fetches
|
|
146
|
+
const putCalls = fetchedUrls.filter(u => u.startsWith('/store/api/v2/player/widgets/'));
|
|
147
|
+
expect(putCalls.length).toBe(1);
|
|
189
148
|
});
|
|
190
149
|
|
|
191
150
|
it('stores processed HTML at correct store key', async () => {
|
|
192
151
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
193
152
|
|
|
194
153
|
const keys = [...storeContents.keys()];
|
|
195
|
-
|
|
196
|
-
expect(widgetKey).toBeTruthy();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('stores fonts.css as a blob (font rewriting handled by proxy)', async () => {
|
|
200
|
-
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
201
|
-
|
|
202
|
-
// fonts.css should be fetched and stored, but font files inside it
|
|
203
|
-
// are NOT parsed/fetched here — the proxy rewrites CSS font URLs
|
|
204
|
-
const cssFetched = fetchedUrls.some(u => u.includes('fonts.css'));
|
|
205
|
-
expect(cssFetched).toBe(true);
|
|
206
|
-
const cssStored = [...storeContents.keys()].some(k => k.includes('fonts.css'));
|
|
207
|
-
expect(cssStored).toBe(true);
|
|
154
|
+
expect(keys.some(k => k.includes('/store/api/v2/player/widgets/472/223/193'))).toBe(true);
|
|
208
155
|
});
|
|
209
156
|
});
|
|
210
157
|
|
|
211
|
-
// --- PDF widget
|
|
158
|
+
// --- PDF widget ---
|
|
212
159
|
|
|
213
160
|
describe('PDF widget (layout 472 / region 221 / widget 190)', () => {
|
|
214
|
-
it('
|
|
161
|
+
it('preserves relative PDF path for resolution via base tag', async () => {
|
|
215
162
|
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
216
163
|
|
|
217
164
|
const stored = getStoredWidget();
|
|
218
|
-
expect(stored).not.toContain(CMS_BASE);
|
|
219
|
-
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
220
|
-
// PDF data attribute "11.pdf" is relative — resolved by <base> tag
|
|
221
165
|
expect(stored).toContain('"11.pdf"');
|
|
222
166
|
});
|
|
223
167
|
|
|
224
|
-
it('stores at correct widget store key
|
|
168
|
+
it('stores at correct widget store key', async () => {
|
|
225
169
|
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
226
170
|
|
|
227
171
|
const keys = [...storeContents.keys()];
|
|
228
|
-
expect(keys.some(k => k.includes('/store/
|
|
172
|
+
expect(keys.some(k => k.includes('/store/api/v2/player/widgets/472/221/190'))).toBe(true);
|
|
229
173
|
});
|
|
230
174
|
});
|
|
231
175
|
|
|
232
|
-
// ---
|
|
176
|
+
// --- CSS object-position fix ---
|
|
233
177
|
|
|
234
|
-
describe('
|
|
235
|
-
it('
|
|
236
|
-
|
|
178
|
+
describe('CSS object-position fix', () => {
|
|
179
|
+
it('injects object-position CSS fix', async () => {
|
|
180
|
+
const html = '<html><head></head><body></body></html>';
|
|
181
|
+
await cacheWidgetHtml('472', '223', '193', html);
|
|
237
182
|
|
|
238
183
|
const stored = getStoredWidget();
|
|
239
|
-
expect(stored).
|
|
240
|
-
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
241
|
-
expect(stored).toContain('/cache/static/fonts.css');
|
|
184
|
+
expect(stored).toContain('object-position:center center');
|
|
242
185
|
});
|
|
243
186
|
});
|
|
244
187
|
|
|
245
|
-
// --- Idempotency
|
|
188
|
+
// --- Idempotency ---
|
|
246
189
|
|
|
247
190
|
describe('idempotency', () => {
|
|
248
191
|
it('does not add duplicate <base> tags on re-processing', async () => {
|
|
@@ -272,38 +215,16 @@ describe('cacheWidgetHtml', () => {
|
|
|
272
215
|
});
|
|
273
216
|
});
|
|
274
217
|
|
|
275
|
-
// ---
|
|
276
|
-
|
|
277
|
-
describe('SW pre-cached raw HTML (regression)', () => {
|
|
278
|
-
it('processes raw HTML that SW cached without rewriting', async () => {
|
|
279
|
-
// Simulate: SW downloads getResource HTML and caches it raw
|
|
280
|
-
const rawCmsHtml = makeRssTickerHtml();
|
|
281
|
-
|
|
282
|
-
// Main thread finds it in cache and re-processes
|
|
283
|
-
await cacheWidgetHtml('472', '223', '193', rawCmsHtml);
|
|
284
|
-
|
|
285
|
-
const stored = getStoredWidget();
|
|
286
|
-
// CMS URLs must be rewritten even though HTML came from cache
|
|
287
|
-
expect(stored).not.toContain(CMS_BASE);
|
|
288
|
-
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
289
|
-
expect(stored).toContain('/cache/static/fonts.css');
|
|
290
|
-
expect(stored).toContain('<base href=');
|
|
291
|
-
});
|
|
218
|
+
// --- Multi-region ---
|
|
292
219
|
|
|
293
|
-
|
|
294
|
-
|
|
220
|
+
describe('multi-region layout', () => {
|
|
221
|
+
it('handles different widgets in different regions', async () => {
|
|
295
222
|
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
296
223
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
297
224
|
|
|
298
225
|
const keys = [...storeContents.keys()];
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
expect(pdfKey).toBeTruthy();
|
|
302
|
-
expect(rssKey).toBeTruthy();
|
|
303
|
-
|
|
304
|
-
// Both should have CMS URLs rewritten
|
|
305
|
-
expect(storeContents.get(pdfKey)).not.toContain(CMS_BASE);
|
|
306
|
-
expect(storeContents.get(rssKey)).not.toContain(CMS_BASE);
|
|
226
|
+
expect(keys.some(k => k.includes('/store/api/v2/player/widgets/472/221/190'))).toBe(true);
|
|
227
|
+
expect(keys.some(k => k.includes('/store/api/v2/player/widgets/472/223/193'))).toBe(true);
|
|
307
228
|
});
|
|
308
229
|
});
|
|
309
230
|
});
|