@@ -1259,7 +1259,7 @@ it.each([
12591259 }
12601260} ) ;
12611261
1262- it . each ( [
1262+ const EXPANDED_WORD_ROLES = [
12631263 "footnotes" ,
12641264 "note roots" ,
12651265 "endnotes" ,
@@ -1272,8 +1272,9 @@ it.each([
12721272 "style visits" ,
12731273 "numbering" ,
12741274 "picture numbering" ,
1275- ] ) ( "keeps the body and warns when omitting expanded %s" , async ( role ) => {
1276- const { default : JSZip } = await import ( "jszip" ) ;
1275+ ] as const ;
1276+
1277+ async function expandedWordFixture ( role : ( typeof EXPANDED_WORD_ROLES ) [ number ] ) {
12771278 const archive = new JSZip ( ) ;
12781279 const ns = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" ;
12791280 const type = role . startsWith ( "style" )
@@ -1289,8 +1290,7 @@ it.each([
12891290 ) ;
12901291 const text = ( value : string ) => `<w:p><w:r><w:t>${ value } </w:t></w:r></w:p>` ;
12911292 // Each copy of the 2,000-paragraph payload costs 8,000 markup units, so 20
1292- // references (or pages) exceed the 100,000-unit expansion budget. More pages
1293- // only add layout time to the preview.
1293+ // references (or pages) exceed the 100,000-unit expansion budget.
12941294 archive . file (
12951295 "word/document.xml" ,
12961296 `<w:document xmlns:w="${ ns } " xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body>${ text ( "body retained" ) } ${ Array . from ( { length : 20 } , ( ) => `<w:p><w:r><w:footnoteReference w:id="1"/><w:endnoteReference w:id="1"/><w:commentReference w:id="1"/><w:br w:type="page"/></w:r></w:p>` ) . join ( "" ) } <w:sectPr><w:headerReference w:type="default" r:id="aux"/><w:footerReference w:type="default" r:id="aux"/></w:sectPr></w:body></w:document>` ,
@@ -1320,47 +1320,73 @@ it.each([
13201320 `<w:footnotes xmlns:w="${ ns } " padding="${ "x" . repeat ( 32_000 ) } ">${ Array . from ( { length : 200 } , ( _ , id ) => `<w:footnote w:id="${ id } ">${ text ( "omitted payload" ) } </w:footnote>` ) . join ( "" ) } </w:footnotes>` ,
13211321 ) ;
13221322 }
1323- const file = await archive . generateAsync ( { type : "blob" , compression : "DEFLATE" } ) ;
1324- if ( role === "note roots" ) {
1323+ return archive . generateAsync ( { type : "blob" , compression : "DEFLATE" } ) ;
1324+ }
1325+
1326+ // Rendering starts a layout engine per case, which dominated this file on loaded CI
1327+ // runners. The omission decision lives in prepareOffice, so every role is checked there.
1328+ it . each ( EXPANDED_WORD_ROLES ) (
1329+ "keeps the body and omits expanded %s from the prepared document" ,
1330+ async ( role ) => {
1331+ const file = await expandedWordFixture ( role ) ;
13251332 const { SaxesParser } = await import ( "saxes" ) ;
13261333 const { prepareOffice } = await import ( "./officeArchive" ) ;
13271334 const parse = vi . spyOn ( SaxesParser . prototype , "write" ) ;
13281335 try {
1329- expect ( ( await prepareOffice ( await file . arrayBuffer ( ) , "docx" ) ) . truncated ) . toBe ( true ) ;
1330- expect (
1331- parse . mock . calls . reduce (
1332- ( sum , [ chunk ] ) => sum + ( typeof chunk === "string" ? chunk . length : 0 ) ,
1333- 0 ,
1334- ) ,
1335- ) . toBeLessThan ( 4 * 1024 * 1024 ) ;
1336+ const prepared = await prepareOffice ( await file . arrayBuffer ( ) , "docx" ) ;
1337+ expect ( prepared . truncated ) . toBe ( true ) ;
1338+ if ( role === "note roots" )
1339+ expect (
1340+ parse . mock . calls . reduce (
1341+ ( sum , [ chunk ] ) => sum + ( typeof chunk === "string" ? chunk . length : 0 ) ,
1342+ 0 ,
1343+ ) ,
1344+ ) . toBeLessThan ( 4 * 1024 * 1024 ) ;
1345+ const bounded = await JSZip . loadAsync ( prepared . bytes ) ;
1346+ const markup = await bounded . file ( "word/document.xml" ) ! . async ( "string" ) ;
1347+ expect ( markup ) . toContain ( "body retained" ) ;
1348+ expect ( bounded . file ( "word/media/aux.xml" ) ) . toBeNull ( ) ;
1349+ // A dangling header or footer reference makes the engine reject the whole body.
1350+ expect ( markup ) . not . toMatch ( / < w : ( h e a d e r | f o o t e r ) R e f e r e n c e / ) ;
1351+ for ( const entry of Object . values ( bounded . files ) )
1352+ if ( ! entry . dir ) expect ( await entry . async ( "string" ) ) . not . toContain ( "omitted payload" ) ;
13361353 } finally {
13371354 parse . mockRestore ( ) ;
13381355 }
1339- }
1340- const src = URL . createObjectURL ( file ) ;
1341- try {
1342- const container = document . createElement ( "div" ) ;
1343- container . style . cssText = "display:flex;flex-direction:column;width:640px;height:700px" ;
1344- await using screen = await render (
1345- < OfficeFilePreview src = { src } name = "expanded.docx" kind = "docx" sizeBytes = { file . size } /> ,
1346- { container } ,
1347- ) ;
1348- await vi . waitFor (
1349- ( ) => {
1350- expect ( screen . container . querySelector ( '[role="alert"]' ) ?. textContent ?? "" ) . toBe ( "" ) ;
1351- expect ( screen . container . textContent ) . toContain ( "body retained" ) ;
1352- } ,
1353- { timeout : 30_000 } ,
1354- ) ;
1355- expect ( screen . container . textContent ) . toContain ( "Preview limited" ) ;
1356- expect ( screen . container . textContent ) . not . toContain ( "omitted payload" ) ;
1357- await vi . waitFor ( ( ) =>
1358- expect ( screen . container . querySelector ( "canvas" ) ! . width ) . toBeGreaterThan ( 0 ) ,
1359- ) ;
1360- } finally {
1361- URL . revokeObjectURL ( src ) ;
1362- }
1363- } ) ;
1356+ } ,
1357+ ) ;
1358+
1359+ // Two full renders prove the engine accepts a prepared body whose parts were removed
1360+ // (body references for notes, section references for headers and footers) and show the warning.
1361+ it . each ( [ "footnotes" , "footer" ] as const ) (
1362+ "renders the body and warns when omitting expanded %s" ,
1363+ async ( role ) => {
1364+ const file = await expandedWordFixture ( role ) ;
1365+ const src = URL . createObjectURL ( file ) ;
1366+ try {
1367+ const container = document . createElement ( "div" ) ;
1368+ container . style . cssText = "display:flex;flex-direction:column;width:640px;height:700px" ;
1369+ await using screen = await render (
1370+ < OfficeFilePreview src = { src } name = "expanded.docx" kind = "docx" sizeBytes = { file . size } /> ,
1371+ { container } ,
1372+ ) ;
1373+ await vi . waitFor (
1374+ ( ) => {
1375+ expect ( screen . container . querySelector ( '[role="alert"]' ) ?. textContent ?? "" ) . toBe ( "" ) ;
1376+ expect ( screen . container . textContent ) . toContain ( "body retained" ) ;
1377+ } ,
1378+ { timeout : 30_000 } ,
1379+ ) ;
1380+ expect ( screen . container . textContent ) . toContain ( "Preview limited" ) ;
1381+ expect ( screen . container . textContent ) . not . toContain ( "omitted payload" ) ;
1382+ await vi . waitFor ( ( ) =>
1383+ expect ( screen . container . querySelector ( "canvas" ) ! . width ) . toBeGreaterThan ( 0 ) ,
1384+ ) ;
1385+ } finally {
1386+ URL . revokeObjectURL ( src ) ;
1387+ }
1388+ } ,
1389+ ) ;
13641390
13651391it . each ( [
13661392 "ordinary parts" ,
0 commit comments