@xiboplayer/cache 0.5.20 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- * Note: CSS font URL rewriting is primarily handled by the proxy layer (proxy.js).
11
- * As defense-in-depth, this module also rewrites font URLs inside CSS files
12
- * before storing them, in case the proxy misses the CSS detection.
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 = `${BASE}/cache/widget/${layoutId}/${regionId}/${mediaId}`;
38
+ const cacheKey = `${PLAYER_API}/widgets/${layoutId}/${regionId}/${mediaId}`;
41
39
 
42
- // Inject <base> tag to fix relative paths for widget dependencies
43
- // Widget HTML has relative paths like "bundle.min.js" that should resolve to cache/media/
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 and rewrote CMS/data URLs in widget HTML');
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 AFTER all static deps are ready iframe loads instantly on store,
203
- // so bundle.min.js/fonts.css/fonts must already be in the ContentStore
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,
@@ -1,28 +1,20 @@
1
1
  /**
2
2
  * Widget HTML caching tests
3
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 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 removedthe 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
- * Simulates actual CMS RSS ticker widget HTML (layout 472, region 223, widget 193).
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="${CMS_BASE}/pwa/file?file=bundle.min.js&fileType=bundle&${SIGNED_PARAMS}"></script>
34
- <link rel="stylesheet" href="${CMS_BASE}/pwa/file?file=fonts.css&fileType=fontCss&${SIGNED_PARAMS}">
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 (layout 472, region 221, widget 190) — simpler, no data URL */
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="${CMS_BASE}/pwa/file?file=bundle.min.js&fileType=bundle&${SIGNED_PARAMS}"></script>
59
- <link rel="stylesheet" href="${CMS_BASE}/pwa/file?file=fonts.css&fileType=fontCss&${SIGNED_PARAMS}"></link>
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/widget/')) return value;
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 after <head>', async () => {
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: layout 472, region 223, widget 193 ---
116
+ // --- RSS ticker ---
155
117
 
156
118
  describe('RSS ticker (layout 472 / region 223 / widget 193)', () => {
157
- it('rewrites bundle.min.js and fonts.css signed URLs', async () => {
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).not.toContain(CMS_BASE);
162
- expect(stored).toContain('/cache/static/bundle.min.js');
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 SW interception', async () => {
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('fetches bundle.min.js and fonts.css from CMS for local caching', async () => {
142
+ it('does NOT fetch any static resources (pipeline handles deps)', async () => {
183
143
  await cacheWidgetHtml('472', '223', '193', makeRssTickerHtml());
184
144
 
185
- const bundleFetched = fetchedUrls.some(u => u.includes('bundle.min.js'));
186
- const fontsFetched = fetchedUrls.some(u => u.includes('fonts.css'));
187
- expect(bundleFetched).toBe(true);
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
- const widgetKey = keys.find(k => k.includes('/store/widget/472/223/193'));
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: layout 472, region 221, widget 190 ---
158
+ // --- PDF widget ---
212
159
 
213
160
  describe('PDF widget (layout 472 / region 221 / widget 190)', () => {
214
- it('rewrites CMS URLs and preserves relative PDF path', async () => {
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 with layout/region/media IDs', async () => {
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/widget/472/221/190'))).toBe(true);
172
+ expect(keys.some(k => k.includes('/store/api/v2/player/widgets/472/221/190'))).toBe(true);
229
173
  });
230
174
  });
231
175
 
232
- // --- Clock widget: xmds.php endpoint (legacy) ---
176
+ // --- CSS object-position fix ---
233
177
 
234
- describe('clock widget with xmds.php URLs', () => {
235
- it('rewrites xmds.php signed URLs to local cache paths', async () => {
236
- await cacheWidgetHtml('1', '1', '1', makeClockWidgetHtml());
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).not.toContain('xmds.php');
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 (regression: duplicate base/style tags) ---
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
- // --- SW pre-cache scenario (the bug we fixed) ---
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
- it('handles different widgets in different regions of same layout', async () => {
294
- // Layout 472 has region 221 (PDF) and region 223 (RSS ticker)
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
- const pdfKey = keys.find(k => k.includes('/store/widget/472/221/190'));
300
- const rssKey = keys.find(k => k.includes('/store/widget/472/223/193'));
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
  });