diff --git a/packages/docx-compare/src/tagged/terminalMarkCleanupParity.test.ts b/packages/docx-compare/src/tagged/terminalMarkCleanupParity.test.ts new file mode 100644 index 00000000..4c871f73 --- /dev/null +++ b/packages/docx-compare/src/tagged/terminalMarkCleanupParity.test.ts @@ -0,0 +1,78 @@ +import { describe, expect } from 'vitest'; +import { XMLSerializer } from '@xmldom/xmldom'; +import { testAllure } from '../testing/allure-test.js'; +import { parseXml, serializeXml, acceptChanges, rejectChanges } from '@usejunior/docx-core'; +import { acceptAllChanges, rejectAllChanges } from './trackChangesAcceptorAst.js'; +const test = testAllure.epic('Document Comparison').withLabels({ feature: 'Terminal mark cleanup parity' }); +const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'; +const subtree = (element: Element) => new XMLSerializer().serializeToString(element); +const wrap = (body: string) => `${body}`; +const paragraph = (kind: string, color: boolean, text: boolean) => '' + + `${color ? '' : ''}` + + `${text ? 'Surviving text' : ''}`; +const cell = (p: string) => '' + p + ''; + +describe('resolved terminal paragraph-mark properties', () => { + for (const [name, kind, nativeProject, astProject] of [ + ['Accept', 'del', acceptChanges, acceptAllChanges], ['Reject', 'ins', rejectChanges, rejectAllChanges], + ['Accept inserted', 'ins', acceptChanges, acceptAllChanges], ['Reject deleted', 'del', rejectChanges, rejectAllChanges], + ] as const) { + const revisionTest = test.conformance({ spec: 'ECMA-376', edition: 5, part: 1, section: kind === 'del' ? '17.13.5.15' : '17.13.5.20' }); + for (const inCell of [false, true]) { + for (const color of [false, true]) { + revisionTest(`${name} terminal ${inCell ? 'cell' : 'body'} mark ${color ? 'retains color' : 'drops empty rPr'}`, () => { + const p = paragraph(kind, color, true); + const input = wrap(inCell ? cell(p) : p); + const doc = parseXml(input); + nativeProject(doc); + expect(serializeXml(doc)).toBe(serializeXml(parseXml(astProject(input)))); + expect(doc.getElementsByTagNameNS(W, 'p').length).toBe(1); + expect(doc.getElementsByTagNameNS(W, 't')[0]!.textContent).toBe('Surviving text'); + expect(doc.getElementsByTagNameNS(W, 'rPr').length).toBe(color ? 1 : 0); + }); + } + } + + revisionTest(`${name} preserves the required empty last table-cell paragraph`, () => { + const input = wrap(cell(paragraph(kind, false, false))); + const doc = parseXml(input); + nativeProject(doc); + expect(doc.getElementsByTagNameNS(W, 'p').length).toBe(1); + expect(doc.getElementsByTagNameNS(W, 'rPr').length).toBe(0); + expect(serializeXml(doc)).toBe(serializeXml(parseXml(astProject(input)))); + }); + + revisionTest(`${name} resolves one author while preserving a foreign terminal mark and formatting`, () => { + const foreign = paragraph(kind, true, true).replace('w:author="AI"', 'w:author="Human"'); + const input = wrap(cell(paragraph(kind, false, true)) + cell(foreign)); + const doc = parseXml(input); + const foreignBefore = subtree(doc.getElementsByTagNameNS(W, 'p')[1]!); + nativeProject(doc, { filter: e => e.getAttributeNS(W, 'author') === 'AI' }); + expect(subtree(doc.getElementsByTagNameNS(W, 'p')[1]!)).toBe(foreignBefore); + expect(doc.getElementsByTagNameNS(W, 'p')[0]!.getElementsByTagNameNS(W, 'rPr').length).toBe(0); + expect(doc.getElementsByTagNameNS(W, 'p').length).toBe(2); + }); + } + + for (const [name, kind, project, ast] of [['Accept', 'del', acceptChanges, acceptAllChanges], ['Reject', 'ins', rejectChanges, rejectAllChanges]] as const) { + for (const metadata of ['', '']) { + for (const scope of ['pPr', 'rPr']) { + test.conformance({ spec: 'ECMA-376', edition: 5, part: 1, section: kind === 'del' ? '17.13.5.15' : '17.13.5.20' })(`${name} retains ${metadata.startsWith('