diff --git a/packages/api/src/files/documents/html.spec.ts b/packages/api/src/files/documents/html.spec.ts index 5b460b2e27..99f27f5756 100644 --- a/packages/api/src/files/documents/html.spec.ts +++ b/packages/api/src/files/documents/html.spec.ts @@ -373,14 +373,29 @@ describe('Office HTML producers', () => { expect(csp).toMatch(/form-action 'none'/); expect(csp).toMatch(/script-src https:\/\/cdn\.jsdelivr\.net 'unsafe-inline'/); expect(csp).not.toMatch(/unsafe-eval/); + /* PPTX must allow blob:-only Web Workers — pptx-preview's + * bundled echarts dep spins up workers via blob: URLs for + * chart rendering. Without this, the renderer's async + * pipeline throws unhandled rejections and the iframe shows + * a black screen. */ + expect(csp).toMatch(/worker-src blob:/); }); - test('exposes a fallback message that surfaces if the renderer fails to load', async () => { + test('exposes a fallback message that surfaces if the renderer fails to load or times out', async () => { const pptx = await buildPptx([{ title: 'X' }]); const html = await _internal.pptxToHtmlViaCdn(pptx); + // Visible loading state + fallback that swaps in on error. expect(html).toContain('Loading preview…'); expect(html).toContain('Preview unavailable'); + // The renderer-not-loaded check. expect(html).toContain("typeof pptxPreview === 'undefined'"); + // The unhandledrejection + error listeners — pptx-preview's + // bundled deps raise async rejections that don't surface + // through the outer Promise. + expect(html).toContain("addEventListener('unhandledrejection'"); + expect(html).toContain("addEventListener('error'"); + // The 8-second timeout safety net for silent renderer failures. + expect(html).toContain('renderer-timeout'); }); test('size-fallback threshold is the documented 350 KB', () => { diff --git a/packages/api/src/files/documents/html.ts b/packages/api/src/files/documents/html.ts index e073b71ccc..25656d753c 100644 --- a/packages/api/src/files/documents/html.ts +++ b/packages/api/src/files/documents/html.ts @@ -972,10 +972,20 @@ const MAX_PPTX_CDN_BINARY_BYTES = 350 * 1024; * once the renderer paints them. */ function buildPptxCdnDocument(base64: string): string { + /* PPTX-specific CSP relaxations vs DOCX: + * - `worker-src blob:` — pptx-preview's bundled echarts dep spins up + * Web Workers via blob: URLs for chart rendering. Without this, + * workers default to `default-src 'none'`, get blocked, and + * echarts throws an unhandled-rejection deep inside its async + * pipeline that we can't catch from the bootstrap script. The + * visible symptom is a black iframe with the renderer half- + * started. Allowing blob:-only workers is the minimum surface to + * unblock rendering without permitting arbitrary worker URLs. */ const csp = [ "default-src 'none'", "script-src https://cdn.jsdelivr.net 'unsafe-inline'", "style-src 'unsafe-inline'", + 'worker-src blob:', "img-src 'self' data: blob:", 'font-src data:', "connect-src 'none'", @@ -1006,15 +1016,39 @@ html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); fon