@xiboplayer/cache 0.5.8 → 0.5.9
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 +22 -13
- package/docs/CACHE_PROXY_ARCHITECTURE.md +165 -368
- package/package.json +2 -2
- package/src/cache-analyzer.js +9 -5
- package/src/cache-analyzer.test.js +6 -6
- package/src/cache-proxy.test.js +239 -237
- package/src/cache.js +3 -6
- package/src/cache.test.js +2 -30
- package/src/download-client.js +222 -0
- package/src/download-manager.js +48 -5
- package/src/index.js +3 -2
- package/src/store-client.js +114 -0
- package/src/widget-html.js +70 -54
- package/src/widget-html.test.js +71 -62
- package/src/cache-proxy.js +0 -532
package/src/widget-html.js
CHANGED
|
@@ -1,30 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Widget HTML
|
|
2
|
+
* Widget HTML processing — preprocesses widget HTML and stores via REST
|
|
3
3
|
*
|
|
4
4
|
* Handles:
|
|
5
5
|
* - <base> tag injection for relative path resolution
|
|
6
|
-
* - CMS signed URL → local
|
|
6
|
+
* - CMS signed URL → local store path rewriting
|
|
7
7
|
* - CSS font URL rewriting and font file caching
|
|
8
8
|
* - Interactive Control hostAddress rewriting
|
|
9
9
|
* - CSS object-position fix for CMS template alignment
|
|
10
10
|
*
|
|
11
11
|
* Runs on the main thread (needs window.location for URL construction).
|
|
12
|
-
*
|
|
12
|
+
* Stores content via PUT /store/... — no Cache API needed.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { createLogger } from '@xiboplayer/utils';
|
|
16
|
-
import {
|
|
16
|
+
import { toProxyUrl } from './download-manager.js';
|
|
17
17
|
|
|
18
18
|
const log = createLogger('Cache');
|
|
19
|
-
const CACHE_NAME = 'xibo-media-v1';
|
|
20
19
|
|
|
21
20
|
// Dynamic base path for multi-variant deployment (pwa, pwa-xmds, pwa-xlr)
|
|
22
21
|
const BASE = (typeof window !== 'undefined')
|
|
23
22
|
? window.location.pathname.replace(/\/[^/]*$/, '').replace(/\/$/, '') || '/player/pwa'
|
|
24
23
|
: '/player/pwa';
|
|
25
24
|
|
|
25
|
+
// Dedup concurrent static resource fetches (two widgets both need bundle.min.js)
|
|
26
|
+
const _pendingStatic = new Map(); // filename → Promise<void>
|
|
27
|
+
|
|
26
28
|
/**
|
|
27
|
-
* Store widget HTML in
|
|
29
|
+
* Store widget HTML in ContentStore for iframe loading
|
|
28
30
|
* @param {string} layoutId - Layout ID
|
|
29
31
|
* @param {string} regionId - Region ID
|
|
30
32
|
* @param {string} mediaId - Media ID
|
|
@@ -33,7 +35,6 @@ const BASE = (typeof window !== 'undefined')
|
|
|
33
35
|
*/
|
|
34
36
|
export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
35
37
|
const cacheKey = `${BASE}/cache/widget/${layoutId}/${regionId}/${mediaId}`;
|
|
36
|
-
const cache = await caches.open(CACHE_NAME);
|
|
37
38
|
|
|
38
39
|
// Inject <base> tag to fix relative paths for widget dependencies
|
|
39
40
|
// Widget HTML has relative paths like "bundle.min.js" that should resolve to cache/media/
|
|
@@ -52,7 +53,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
// Rewrite absolute CMS signed URLs to local
|
|
56
|
+
// Rewrite absolute CMS signed URLs to local store paths
|
|
56
57
|
// Matches: https://cms/xmds.php?file=... or https://cms/pwa/file?file=...
|
|
57
58
|
// These absolute URLs bypass the <base> tag entirely, causing slow CMS fetches
|
|
58
59
|
const cmsUrlRegex = /https?:\/\/[^"'\s)]+(?:xmds\.php|pwa\/file)\?[^"'\s)]*file=([^&"'\s)]+)[^"'\s)]*/g;
|
|
@@ -65,8 +66,6 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
// Inject CSS default for object-position to suppress CMS template warning
|
|
68
|
-
// CMS global-elements.xml uses {{alignId}} {{valignId}} which produces
|
|
69
|
-
// invalid CSS (empty value) when alignment is not configured
|
|
70
69
|
const cssFixTag = '<style>img,video{object-position:center center}</style>';
|
|
71
70
|
if (!modifiedHtml.includes('object-position:center center')) {
|
|
72
71
|
if (modifiedHtml.includes('</head>')) {
|
|
@@ -77,9 +76,6 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
// Rewrite Interactive Control hostAddress to SW-interceptable path
|
|
80
|
-
// The IC library uses hostAddress + '/info', '/trigger', etc.
|
|
81
|
-
// Original: hostAddress: "https://cms.example.com" → XHR to /info goes to CMS (fails)
|
|
82
|
-
// Rewritten: hostAddress: "/player/pwa/ic" → XHR to /player/pwa/ic/info (intercepted by SW)
|
|
83
79
|
modifiedHtml = modifiedHtml.replace(
|
|
84
80
|
/hostAddress\s*:\s*["']https?:\/\/[^"']+["']/g,
|
|
85
81
|
`hostAddress: "${BASE}/ic"`
|
|
@@ -87,32 +83,26 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
87
83
|
|
|
88
84
|
log.info('Injected base tag and rewrote CMS/data URLs in widget HTML');
|
|
89
85
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const response = new Response(modifiedHtml, {
|
|
94
|
-
headers: {
|
|
95
|
-
'Content-Type': 'text/html; charset=utf-8',
|
|
96
|
-
'Access-Control-Allow-Origin': '*'
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
await cache.put(cacheUrl, response);
|
|
101
|
-
log.info(`Stored widget HTML at ${cacheKey} (${modifiedHtml.length} bytes)`);
|
|
102
|
-
|
|
103
|
-
// Fetch and cache static resources (shared Cache API - accessible from main thread and SW)
|
|
86
|
+
// Store static resources FIRST — widget iframe loads immediately after HTML is stored,
|
|
87
|
+
// and its <script>/<link> tags will 404 if deps aren't ready yet
|
|
104
88
|
if (staticResources.length > 0) {
|
|
105
|
-
|
|
106
|
-
|
|
89
|
+
await Promise.all(staticResources.map(({ filename, originalUrl }) => {
|
|
90
|
+
// Dedup: if another widget is already fetching the same resource, wait for it
|
|
91
|
+
if (_pendingStatic.has(filename)) {
|
|
92
|
+
return _pendingStatic.get(filename);
|
|
93
|
+
}
|
|
107
94
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
const work = (async () => {
|
|
96
|
+
// Check if already stored
|
|
97
|
+
try {
|
|
98
|
+
const headResp = await fetch(`/store/static/${filename}`, { method: 'HEAD' });
|
|
99
|
+
if (headResp.ok) return; // Already stored
|
|
100
|
+
} catch { /* proceed to fetch */ }
|
|
112
101
|
|
|
113
102
|
try {
|
|
114
|
-
const resp = await fetch(
|
|
103
|
+
const resp = await fetch(toProxyUrl(originalUrl));
|
|
115
104
|
if (!resp.ok) {
|
|
105
|
+
resp.body?.cancel();
|
|
116
106
|
log.warn(`Failed to fetch static resource: ${filename} (HTTP ${resp.status})`);
|
|
117
107
|
return;
|
|
118
108
|
}
|
|
@@ -127,7 +117,7 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
127
117
|
'svg': 'image/svg+xml'
|
|
128
118
|
}[ext] || 'application/octet-stream';
|
|
129
119
|
|
|
130
|
-
// For CSS files, rewrite font URLs and
|
|
120
|
+
// For CSS files, rewrite font URLs and store referenced font files
|
|
131
121
|
if (ext === 'css') {
|
|
132
122
|
let cssText = await resp.text();
|
|
133
123
|
const fontResources = [];
|
|
@@ -138,20 +128,26 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
138
128
|
return `url(${quote}${BASE}/cache/static/${encodeURIComponent(fontFilename)}${quote})`;
|
|
139
129
|
});
|
|
140
130
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
131
|
+
const cssResp = await fetch(`/store/static/${filename}`, {
|
|
132
|
+
method: 'PUT',
|
|
133
|
+
headers: { 'Content-Type': 'text/css' },
|
|
134
|
+
body: cssText,
|
|
135
|
+
});
|
|
136
|
+
cssResp.body?.cancel();
|
|
137
|
+
log.info(`Stored CSS with ${fontResources.length} rewritten font URLs: ${filename}`);
|
|
145
138
|
|
|
146
|
-
// Fetch and
|
|
139
|
+
// Fetch and store referenced font files
|
|
147
140
|
await Promise.all(fontResources.map(async ({ filename: fontFile, originalUrl: fontUrl }) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
141
|
+
// Check if already stored
|
|
142
|
+
try {
|
|
143
|
+
const headResp = await fetch(`/store/static/${encodeURIComponent(fontFile)}`, { method: 'HEAD' });
|
|
144
|
+
if (headResp.ok) return;
|
|
145
|
+
} catch { /* proceed */ }
|
|
151
146
|
|
|
152
147
|
try {
|
|
153
|
-
const fontResp = await fetch(
|
|
148
|
+
const fontResp = await fetch(toProxyUrl(fontUrl));
|
|
154
149
|
if (!fontResp.ok) {
|
|
150
|
+
fontResp.body?.cancel();
|
|
155
151
|
log.warn(`Failed to fetch font: ${fontFile} (HTTP ${fontResp.status})`);
|
|
156
152
|
return;
|
|
157
153
|
}
|
|
@@ -164,26 +160,46 @@ export async function cacheWidgetHtml(layoutId, regionId, mediaId, html) {
|
|
|
164
160
|
'svg': 'image/svg+xml'
|
|
165
161
|
}[fontExt] || 'application/octet-stream';
|
|
166
162
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
163
|
+
const fontPutResp = await fetch(`/store/static/${encodeURIComponent(fontFile)}`, {
|
|
164
|
+
method: 'PUT',
|
|
165
|
+
headers: { 'Content-Type': fontContentType },
|
|
166
|
+
body: fontBlob,
|
|
167
|
+
});
|
|
168
|
+
fontPutResp.body?.cancel();
|
|
169
|
+
log.info(`Stored font: ${fontFile} (${fontContentType}, ${fontBlob.size} bytes)`);
|
|
171
170
|
} catch (fontErr) {
|
|
172
|
-
log.warn(`Failed to
|
|
171
|
+
log.warn(`Failed to store font: ${fontFile}`, fontErr);
|
|
173
172
|
}
|
|
174
173
|
}));
|
|
175
174
|
} else {
|
|
176
175
|
const blob = await resp.blob();
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
const staticResp = await fetch(`/store/static/${filename}`, {
|
|
177
|
+
method: 'PUT',
|
|
178
|
+
headers: { 'Content-Type': contentType },
|
|
179
|
+
body: blob,
|
|
180
|
+
});
|
|
181
|
+
staticResp.body?.cancel();
|
|
182
|
+
log.info(`Stored static resource: ${filename} (${contentType}, ${blob.size} bytes)`);
|
|
181
183
|
}
|
|
182
184
|
} catch (error) {
|
|
183
|
-
log.warn(`Failed to
|
|
185
|
+
log.warn(`Failed to store static resource: ${filename}`, error);
|
|
184
186
|
}
|
|
187
|
+
})();
|
|
188
|
+
|
|
189
|
+
_pendingStatic.set(filename, work);
|
|
190
|
+
return work.finally(() => _pendingStatic.delete(filename));
|
|
185
191
|
}));
|
|
186
192
|
}
|
|
187
193
|
|
|
194
|
+
// Store widget HTML AFTER all static deps are ready — iframe loads instantly on store,
|
|
195
|
+
// so bundle.min.js/fonts.css/fonts must already be in the ContentStore
|
|
196
|
+
const putResp = await fetch(`/store/widget/${layoutId}/${regionId}/${mediaId}`, {
|
|
197
|
+
method: 'PUT',
|
|
198
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
199
|
+
body: modifiedHtml,
|
|
200
|
+
});
|
|
201
|
+
putResp.body?.cancel();
|
|
202
|
+
log.info(`Stored widget HTML at ${cacheKey} (${modifiedHtml.length} bytes)`);
|
|
203
|
+
|
|
188
204
|
return cacheKey;
|
|
189
205
|
}
|
package/src/widget-html.test.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* 1. CMS getResource returns HTML with signed URLs for bundle.min.js, fonts.css
|
|
10
10
|
* 2. SW download manager may cache this raw HTML before the main thread processes it
|
|
11
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
|
|
13
|
-
* 5. bundle.min.js runs getWidgetData → $.ajax("193.json") → SW serves from
|
|
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
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
@@ -23,10 +23,6 @@ const SIGNED_PARAMS = 'displayId=152&type=P&itemId=1&X-Amz-Algorithm=AWS4-HMAC-S
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
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
26
|
*/
|
|
31
27
|
function makeRssTickerHtml() {
|
|
32
28
|
return `<!DOCTYPE html>
|
|
@@ -79,50 +75,61 @@ function makeClockWidgetHtml() {
|
|
|
79
75
|
</html>`;
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
// --- Mock
|
|
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
|
-
};
|
|
78
|
+
// --- Mock: track PUT /store/... calls via fetch() ---
|
|
97
79
|
|
|
80
|
+
/** Map of storeKey → body text, populated by the fetch mock */
|
|
81
|
+
const storeContents = new Map();
|
|
98
82
|
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
83
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
84
|
+
function createFetchMock() {
|
|
85
|
+
return vi.fn(async (url, opts) => {
|
|
86
|
+
fetchedUrls.push(url);
|
|
87
|
+
|
|
88
|
+
// PUT /store/... — store content
|
|
89
|
+
if (opts?.method === 'PUT' && url.startsWith('/store/')) {
|
|
90
|
+
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
|
+
storeContents.set(url, body);
|
|
93
|
+
return new Response(JSON.stringify({ ok: true }), { status: 200 });
|
|
94
|
+
}
|
|
95
|
+
|
|
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
|
+
return new Response('', { status: 404 });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
116
114
|
|
|
117
115
|
// --- Tests ---
|
|
118
116
|
|
|
119
117
|
describe('cacheWidgetHtml', () => {
|
|
120
118
|
beforeEach(() => {
|
|
121
|
-
|
|
122
|
-
vi.clearAllMocks();
|
|
119
|
+
storeContents.clear();
|
|
123
120
|
fetchedUrls.length = 0;
|
|
121
|
+
vi.clearAllMocks();
|
|
122
|
+
global.fetch = createFetchMock();
|
|
124
123
|
});
|
|
125
124
|
|
|
125
|
+
// Helper: get stored widget HTML (first widget entry)
|
|
126
|
+
function getStoredWidget() {
|
|
127
|
+
for (const [key, value] of storeContents) {
|
|
128
|
+
if (key.startsWith('/store/widget/')) return value;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
126
133
|
// --- Base tag injection ---
|
|
127
134
|
|
|
128
135
|
describe('base tag injection', () => {
|
|
@@ -130,7 +137,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
130
137
|
const html = '<html><head><title>Widget</title></head><body>content</body></html>';
|
|
131
138
|
await cacheWidgetHtml('472', '223', '193', html);
|
|
132
139
|
|
|
133
|
-
const stored =
|
|
140
|
+
const stored = getStoredWidget();
|
|
134
141
|
expect(stored).toContain('<base href=');
|
|
135
142
|
expect(stored).toContain('/cache/media/">');
|
|
136
143
|
});
|
|
@@ -139,7 +146,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
139
146
|
const html = '<div>no head tag</div>';
|
|
140
147
|
await cacheWidgetHtml('472', '223', '193', html);
|
|
141
148
|
|
|
142
|
-
const stored =
|
|
149
|
+
const stored = getStoredWidget();
|
|
143
150
|
expect(stored).toContain('<base href=');
|
|
144
151
|
});
|
|
145
152
|
});
|
|
@@ -150,7 +157,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
150
157
|
it('rewrites bundle.min.js and fonts.css signed URLs', async () => {
|
|
151
158
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
152
159
|
|
|
153
|
-
const stored =
|
|
160
|
+
const stored = getStoredWidget();
|
|
154
161
|
expect(stored).not.toContain(CMS_BASE);
|
|
155
162
|
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
156
163
|
expect(stored).toContain('/cache/static/fonts.css');
|
|
@@ -159,7 +166,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
159
166
|
it('preserves the data URL (193.json) for SW interception', async () => {
|
|
160
167
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
161
168
|
|
|
162
|
-
const stored =
|
|
169
|
+
const stored = getStoredWidget();
|
|
163
170
|
// 193.json is relative — resolved by <base> tag, not rewritten
|
|
164
171
|
expect(stored).toContain('"193.json"');
|
|
165
172
|
});
|
|
@@ -167,7 +174,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
167
174
|
it('rewrites xiboIC hostAddress from CMS to local path', async () => {
|
|
168
175
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
169
176
|
|
|
170
|
-
const stored =
|
|
177
|
+
const stored = getStoredWidget();
|
|
171
178
|
expect(stored).not.toContain(`hostAddress: "${CMS_BASE}"`);
|
|
172
179
|
expect(stored).toContain('/ic"');
|
|
173
180
|
});
|
|
@@ -181,11 +188,11 @@ describe('cacheWidgetHtml', () => {
|
|
|
181
188
|
expect(fontsFetched).toBe(true);
|
|
182
189
|
});
|
|
183
190
|
|
|
184
|
-
it('stores processed HTML at correct
|
|
191
|
+
it('stores processed HTML at correct store key', async () => {
|
|
185
192
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
186
193
|
|
|
187
|
-
const keys = [...
|
|
188
|
-
const widgetKey = keys.find(k => k.includes('/
|
|
194
|
+
const keys = [...storeContents.keys()];
|
|
195
|
+
const widgetKey = keys.find(k => k.includes('/store/widget/472/223/193'));
|
|
189
196
|
expect(widgetKey).toBeTruthy();
|
|
190
197
|
});
|
|
191
198
|
|
|
@@ -204,18 +211,18 @@ describe('cacheWidgetHtml', () => {
|
|
|
204
211
|
it('rewrites CMS URLs and preserves relative PDF path', async () => {
|
|
205
212
|
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
206
213
|
|
|
207
|
-
const stored =
|
|
214
|
+
const stored = getStoredWidget();
|
|
208
215
|
expect(stored).not.toContain(CMS_BASE);
|
|
209
216
|
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
210
217
|
// PDF data attribute "11.pdf" is relative — resolved by <base> tag
|
|
211
218
|
expect(stored).toContain('"11.pdf"');
|
|
212
219
|
});
|
|
213
220
|
|
|
214
|
-
it('stores at correct widget
|
|
221
|
+
it('stores at correct widget store key with layout/region/media IDs', async () => {
|
|
215
222
|
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
216
223
|
|
|
217
|
-
const keys = [...
|
|
218
|
-
expect(keys.some(k => k.includes('/
|
|
224
|
+
const keys = [...storeContents.keys()];
|
|
225
|
+
expect(keys.some(k => k.includes('/store/widget/472/221/190'))).toBe(true);
|
|
219
226
|
});
|
|
220
227
|
});
|
|
221
228
|
|
|
@@ -225,7 +232,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
225
232
|
it('rewrites xmds.php signed URLs to local cache paths', async () => {
|
|
226
233
|
await cacheWidgetHtml('1', '1', '1', makeClockWidgetHtml());
|
|
227
234
|
|
|
228
|
-
const stored =
|
|
235
|
+
const stored = getStoredWidget();
|
|
229
236
|
expect(stored).not.toContain('xmds.php');
|
|
230
237
|
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
231
238
|
expect(stored).toContain('/cache/static/fonts.css');
|
|
@@ -237,10 +244,11 @@ describe('cacheWidgetHtml', () => {
|
|
|
237
244
|
describe('idempotency', () => {
|
|
238
245
|
it('does not add duplicate <base> tags on re-processing', async () => {
|
|
239
246
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
240
|
-
const firstPass =
|
|
247
|
+
const firstPass = getStoredWidget();
|
|
241
248
|
|
|
249
|
+
storeContents.clear();
|
|
242
250
|
await cacheWidgetHtml('472', '223', '193', firstPass);
|
|
243
|
-
const secondPass =
|
|
251
|
+
const secondPass = getStoredWidget();
|
|
244
252
|
|
|
245
253
|
expect(secondPass).toBe(firstPass);
|
|
246
254
|
});
|
|
@@ -248,10 +256,11 @@ describe('cacheWidgetHtml', () => {
|
|
|
248
256
|
it('does not add duplicate CSS fix tags on re-processing', async () => {
|
|
249
257
|
const html = '<html><head></head><body></body></html>';
|
|
250
258
|
await cacheWidgetHtml('472', '223', '193', html);
|
|
251
|
-
const firstPass =
|
|
259
|
+
const firstPass = getStoredWidget();
|
|
252
260
|
|
|
261
|
+
storeContents.clear();
|
|
253
262
|
await cacheWidgetHtml('472', '223', '193', firstPass);
|
|
254
|
-
const secondPass =
|
|
263
|
+
const secondPass = getStoredWidget();
|
|
255
264
|
|
|
256
265
|
const baseCount = (secondPass.match(/<base /g) || []).length;
|
|
257
266
|
const styleCount = (secondPass.match(/object-position:center center/g) || []).length;
|
|
@@ -270,7 +279,7 @@ describe('cacheWidgetHtml', () => {
|
|
|
270
279
|
// Main thread finds it in cache and re-processes
|
|
271
280
|
await cacheWidgetHtml('472', '223', '193', rawCmsHtml);
|
|
272
281
|
|
|
273
|
-
const stored =
|
|
282
|
+
const stored = getStoredWidget();
|
|
274
283
|
// CMS URLs must be rewritten even though HTML came from cache
|
|
275
284
|
expect(stored).not.toContain(CMS_BASE);
|
|
276
285
|
expect(stored).toContain('/cache/static/bundle.min.js');
|
|
@@ -283,15 +292,15 @@ describe('cacheWidgetHtml', () => {
|
|
|
283
292
|
await cacheWidgetHtml('472', '221', '190', makePdfWidgetHtml());
|
|
284
293
|
await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
|
|
285
294
|
|
|
286
|
-
const keys = [...
|
|
287
|
-
const pdfKey = keys.find(k => k.includes('/
|
|
288
|
-
const rssKey = keys.find(k => k.includes('/
|
|
295
|
+
const keys = [...storeContents.keys()];
|
|
296
|
+
const pdfKey = keys.find(k => k.includes('/store/widget/472/221/190'));
|
|
297
|
+
const rssKey = keys.find(k => k.includes('/store/widget/472/223/193'));
|
|
289
298
|
expect(pdfKey).toBeTruthy();
|
|
290
299
|
expect(rssKey).toBeTruthy();
|
|
291
300
|
|
|
292
301
|
// Both should have CMS URLs rewritten
|
|
293
|
-
expect(
|
|
294
|
-
expect(
|
|
302
|
+
expect(storeContents.get(pdfKey)).not.toContain(CMS_BASE);
|
|
303
|
+
expect(storeContents.get(rssKey)).not.toContain(CMS_BASE);
|
|
295
304
|
});
|
|
296
305
|
});
|
|
297
306
|
});
|