From 06c3bfa3c079cd0900470648b9f6d4ba9f4476bc Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 4 May 2026 21:04:33 -0400 Subject: [PATCH] fix: Detect Empty PPTX Preview Slides --- packages/api/src/files/documents/html.spec.ts | 5 ++- packages/api/src/files/documents/html.ts | 39 +++++++++++-------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/packages/api/src/files/documents/html.spec.ts b/packages/api/src/files/documents/html.spec.ts index a5d1d54f77..eb175e8262 100644 --- a/packages/api/src/files/documents/html.spec.ts +++ b/packages/api/src/files/documents/html.spec.ts @@ -510,6 +510,8 @@ describe('Office HTML producers', () => { expect(html).toContain("addEventListener('error'"); // The 8-second timeout safety net for silent renderer failures. expect(html).toContain('renderer-timeout'); + expect(html).toContain('renderer-empty-slide-list'); + expect(html).toContain("querySelectorAll('.pptx-preview-slide-wrapper')"); /* Diagnostic surfacing: the failure reason must be exposed * three ways so a debugging user can find it without reading * source — manual e2e on PR #12934 ("'Preview unavailable' @@ -546,7 +548,8 @@ describe('Office HTML producers', () => { expect(html).toContain('SENTINEL_FALLBACK'); /* Empty-render detection helper exists in the bootstrap. */ expect(html).toContain('hasRenderedContent'); - expect(html).toContain('renderer-produced-empty-wrappers'); + expect(html).toContain('renderer-empty-slide-list'); + expect(html).toContain("querySelectorAll('.pptx-preview-slide-wrapper')"); /* The slide-list fallback CSS is inlined so the fallback * renders with the same look as the standalone slide-list * path (CSP locks `style-src` to inline only). */ diff --git a/packages/api/src/files/documents/html.ts b/packages/api/src/files/documents/html.ts index 7c8eb71b25..c9e7ba35bd 100644 --- a/packages/api/src/files/documents/html.ts +++ b/packages/api/src/files/documents/html.ts @@ -1208,6 +1208,9 @@ ${PPTX_SLIDE_LIST_CSS} } } function markSuccess() { settled = true; } + function renderedSlideCount() { + return container ? container.querySelectorAll('.pptx-preview-slide-wrapper').length : 0; + } /* pptx-preview bundled deps (echarts, etc.) raise rejections deep * inside their async pipelines that the outer Promise from @@ -1322,20 +1325,16 @@ ${PPTX_SLIDE_LIST_CSS} } } - /* Detect the silent-failure case: pptx-preview created the slide - * wrappers but didnt populate them with content. Common with - * pptxgenjs-generated PPTXs — pptx-previews parser handles - * Microsoft PowerPoint XML but chokes on subtle convention - * differences and produces empty 960x540 wrappers. Check if every - * wrapped slide has zero children; if so, the render failed and - * the slide-list fallback should take over. Manual e2e on PR - * #12934. */ + /* Detect the silent-failure case: pptx-preview resolves without + * rendering any real slide nodes. Common with pptxgenjs-generated + * PPTXs — the parser creates an empty host wrapper, but never + * populates pptx-preview slide nodes with slide content. + * Manual e2e on PR #12934. */ function hasRenderedContent() { - var wraps = container.querySelectorAll('.lc-slide-wrap'); - if (wraps.length === 0) { return false; } - for (var i = 0; i < wraps.length; i++) { - var inner = wraps[i].firstElementChild; - if (inner && inner.children.length > 0) { + var slides = container.querySelectorAll('.pptx-preview-slide-wrapper'); + if (slides.length === 0) { return false; } + for (var i = 0; i < slides.length; i++) { + if (slides[i].children.length > 0 || (slides[i].textContent || '').trim().length > 0) { return true; } } @@ -1345,7 +1344,7 @@ ${PPTX_SLIDE_LIST_CSS} function finalize() { wrapSlides(); if (!hasRenderedContent()) { - showFallback('renderer-produced-empty-wrappers (pptx-preview compatibility issue with this PPTX format — falling back to slide-list view)'); + showFallback('renderer-empty-slide-list'); return; } container.style.visibility = 'visible'; @@ -1361,14 +1360,22 @@ ${PPTX_SLIDE_LIST_CSS} var result = previewer.preview(bytes.buffer); if (result && typeof result.then === 'function') { - result.then(finalize).catch(function (err) { showFallback(err && err.message); }); + result + .then(function (pptx) { + if (!pptx || !pptx.slides || pptx.slides.length === 0) { + showFallback('renderer-empty-slide-list'); + return; + } + finalize(); + }) + .catch(function (err) { showFallback(err && err.message); }); } /* Safety net: if 8 seconds in the preview promise hasnt resolved, * accept whatever pptx-preview managed to render and apply the * wrap+scale to it. Empty container → fallback. */ setTimeout(function () { if (settled) { return; } - if (container && container.children.length > 0) { + if (renderedSlideCount() > 0) { finalize(); return; }