Skip to content

Commit 997714a

Browse files
authored
chore: add test (#2459)
1 parent 8802f90 commit 997714a

1 file changed

Lines changed: 197 additions & 0 deletions

File tree

packages/super-editor/src/document-api-adapters/sections-adapter.integration.test.ts

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,200 @@ describe('sections adapter DOCX integration', () => {
354354
}
355355
});
356356
});
357+
358+
/**
359+
* SD-2137 regression suite: clearing header/footer refs must survive export.
360+
*
361+
* The h_f-normal.docx fixture has section-0 with:
362+
* headerRefs: { even: rId7, default: rId8 }
363+
* footerRefs: { even: rId9, default: rId10 }
364+
*
365+
* The exporter has a fallback (exporter.js ~L267) that re-injects a default
366+
* header/footer reference when the sectPr has *no* headerReference elements
367+
* and converter.headerIds.default is still truthy. That fallback must NOT
368+
* fire after an explicit clearHeaderFooterRef mutation.
369+
*/
370+
describe('SD-2137: clearHeaderFooterRef must remove refs from exported DOCX', () => {
371+
let hfDocData: LoadedDocData;
372+
let editor: Editor | undefined;
373+
374+
beforeAll(async () => {
375+
hfDocData = await loadTestDataForEditorTests('h_f-normal.docx');
376+
});
377+
378+
beforeEach(() => {
379+
registerPartDescriptor(settingsPartDescriptor);
380+
});
381+
382+
afterEach(() => {
383+
editor?.destroy();
384+
editor = undefined;
385+
clearPartDescriptors();
386+
clearInvalidationHandlers();
387+
});
388+
389+
function createEditor(): Editor {
390+
const result = initTestEditor({
391+
content: hfDocData.docx,
392+
media: hfDocData.media,
393+
mediaFiles: hfDocData.mediaFiles,
394+
fonts: hfDocData.fonts,
395+
useImmediateSetTimeout: false,
396+
});
397+
editor = result.editor;
398+
return editor;
399+
}
400+
401+
it('clearing the only remaining header/default ref removes it from exported document.xml', async () => {
402+
const ed = createEditor();
403+
const section0 = getSectionAddressByIndex(ed, 0);
404+
405+
// Verify initial state: section-0 has both even + default header refs.
406+
const beforeDomain = resolveSectionProjections(ed).find((s) => s.range.sectionIndex === 0)!.domain;
407+
expect(beforeDomain.headerRefs?.default).toBeTruthy();
408+
expect(beforeDomain.headerRefs?.even).toBeTruthy();
409+
410+
// Remove the even header first, leaving header/default as the sole ref.
411+
const clearEven = sectionsClearHeaderFooterRefAdapter(
412+
ed,
413+
{ target: section0, kind: 'header', variant: 'even' },
414+
DIRECT_MUTATION_OPTIONS,
415+
);
416+
expect(clearEven.success).toBe(true);
417+
418+
// Now clear header/default — this is the SD-2137 operation.
419+
const clearDefault = sectionsClearHeaderFooterRefAdapter(
420+
ed,
421+
{ target: section0, kind: 'header', variant: 'default' },
422+
DIRECT_MUTATION_OPTIONS,
423+
);
424+
expect(clearDefault.success).toBe(true);
425+
426+
// Verify the domain reports no header refs.
427+
const afterDomain = resolveSectionProjections(ed).find((s) => s.range.sectionIndex === 0)!.domain;
428+
expect(afterDomain.headerRefs?.default).toBeUndefined();
429+
430+
// Export and verify no w:headerReference of any type in the body sectPr.
431+
const exported = await exportDocxFiles(ed);
432+
const documentXml = exported['word/document.xml'];
433+
expect(documentXml).not.toContain('w:headerReference');
434+
});
435+
436+
it('clearing footer/default preserves footer/even in exported document.xml', async () => {
437+
const ed = createEditor();
438+
const section0 = getSectionAddressByIndex(ed, 0);
439+
440+
// Verify initial state: section-0 has both even + default footer refs.
441+
const beforeDomain = resolveSectionProjections(ed).find((s) => s.range.sectionIndex === 0)!.domain;
442+
expect(beforeDomain.footerRefs?.default).toBeTruthy();
443+
expect(beforeDomain.footerRefs?.even).toBeTruthy();
444+
const evenFooterRefId = beforeDomain.footerRefs!.even!;
445+
446+
// Clear footer/default — even should survive.
447+
const clearResult = sectionsClearHeaderFooterRefAdapter(
448+
ed,
449+
{ target: section0, kind: 'footer', variant: 'default' },
450+
DIRECT_MUTATION_OPTIONS,
451+
);
452+
expect(clearResult.success).toBe(true);
453+
454+
// Export and verify footer/default is gone but footer/even remains.
455+
const exported = await exportDocxFiles(ed);
456+
const documentXml = exported['word/document.xml'];
457+
expect(documentXml).not.toMatch(/w:footerReference[^>]*w:type="default"/);
458+
expect(documentXml).toContain(`r:id="${evenFooterRefId}"`);
459+
});
460+
461+
it('exact SD-2137 repro: clear header/default + footer/default with only footer/even surviving', async () => {
462+
const ed = createEditor();
463+
const section0 = getSectionAddressByIndex(ed, 0);
464+
465+
// Shape the fixture to match the bug report: headerRefs={default}, footerRefs={default, even}.
466+
// h_f-normal.docx starts with headerRefs={even, default} — remove even header first.
467+
const clearEvenHeader = sectionsClearHeaderFooterRefAdapter(
468+
ed,
469+
{ target: section0, kind: 'header', variant: 'even' },
470+
DIRECT_MUTATION_OPTIONS,
471+
);
472+
expect(clearEvenHeader.success).toBe(true);
473+
474+
const beforeDomain = resolveSectionProjections(ed).find((s) => s.range.sectionIndex === 0)!.domain;
475+
expect(beforeDomain.headerRefs?.default).toBeTruthy();
476+
expect(beforeDomain.headerRefs?.even).toBeUndefined();
477+
expect(beforeDomain.footerRefs?.default).toBeTruthy();
478+
expect(beforeDomain.footerRefs?.even).toBeTruthy();
479+
const evenFooterRefId = beforeDomain.footerRefs!.even!;
480+
481+
// Run the exact SD-2137 operations: clear header/default, then footer/default.
482+
const clearHeaderDefault = sectionsClearHeaderFooterRefAdapter(
483+
ed,
484+
{ target: section0, kind: 'header', variant: 'default' },
485+
DIRECT_MUTATION_OPTIONS,
486+
);
487+
expect(clearHeaderDefault.success).toBe(true);
488+
489+
const clearFooterDefault = sectionsClearHeaderFooterRefAdapter(
490+
ed,
491+
{ target: section0, kind: 'footer', variant: 'default' },
492+
DIRECT_MUTATION_OPTIONS,
493+
);
494+
expect(clearFooterDefault.success).toBe(true);
495+
496+
// Export and verify:
497+
// - No header references of any type.
498+
// - No footer/default, but footer/even survives.
499+
const exported = await exportDocxFiles(ed);
500+
const documentXml = exported['word/document.xml'];
501+
expect(documentXml).not.toContain('w:headerReference');
502+
expect(documentXml).not.toMatch(/w:footerReference[^>]*w:type="default"/);
503+
expect(documentXml).toMatch(/w:footerReference[^>]*w:type="even"/);
504+
expect(documentXml).toContain(`r:id="${evenFooterRefId}"`);
505+
});
506+
507+
it('clearing header/default on a paragraph-owned sectPr (non-final section) removes it from export', async () => {
508+
const ed = createEditor();
509+
510+
// Create a section break so section-0 gets a paragraph-owned sectPr.
511+
const breakResult = createSectionBreakAdapter(
512+
ed,
513+
{ at: { kind: 'documentEnd' }, breakType: 'nextPage' },
514+
DIRECT_MUTATION_OPTIONS,
515+
);
516+
expect(breakResult.success).toBe(true);
517+
518+
// Section breaks don't propagate header/footer refs. The body section
519+
// (now section-1) retains the original refs — borrow one for section-0.
520+
const headerRefId = resolveSectionProjections(ed).find((s) => s.range.sectionIndex === 1)?.domain.headerRefs
521+
?.default;
522+
expect(headerRefId).toBeTruthy();
523+
524+
const section0 = getSectionAddressByIndex(ed, 0);
525+
const setResult = sectionsSetHeaderFooterRefAdapter(
526+
ed,
527+
{ target: section0, kind: 'header', variant: 'default', refId: headerRefId! },
528+
DIRECT_MUTATION_OPTIONS,
529+
);
530+
expect(setResult.success).toBe(true);
531+
532+
// Now clear header/default on the paragraph-owned section.
533+
const clearResult = sectionsClearHeaderFooterRefAdapter(
534+
ed,
535+
{ target: section0, kind: 'header', variant: 'default' },
536+
DIRECT_MUTATION_OPTIONS,
537+
);
538+
expect(clearResult.success).toBe(true);
539+
540+
// The paragraph-owned sectPr export path has no fallback injection,
541+
// so clearing should always work. Verify via export.
542+
const exported = await exportDocxFiles(ed);
543+
const documentXml = exported['word/document.xml'];
544+
545+
// Extract all sectPr blocks from the XML.
546+
const sectPrBlocks = documentXml.match(/<w:sectPr[^>]*>[\s\S]*?<\/w:sectPr>/g) ?? [];
547+
expect(sectPrBlocks.length).toBeGreaterThanOrEqual(2);
548+
549+
// The first sectPr (paragraph-owned, section-0) should have no header/default.
550+
const paragraphSectPr = sectPrBlocks[0]!;
551+
expect(paragraphSectPr).not.toMatch(/w:headerReference[^>]*w:type="default"/);
552+
});
553+
});

0 commit comments

Comments
 (0)