When reviewing tracked deletions in a Word document, the deleted substring (i.e., a contiguous piece of paragraph text) must appear in the before state and disappear from the after state. This distinction matters because a deletion record describes both the paragraph as it existed before accepting the change and the paragraph that remains after accepting it.
The extractRevisions primitive (i.e., a small document operation) compares rejected and accepted document states, then returns changed paragraphs with their before text, after text, and revision entries.[1] For OOXML deletion wrappers, the <w:delText> content belongs in the before text even though it is not ordinary visible text in the accepted paragraph; the CT_TrackChange wrapper carries the tracked-change metadata around that deleted content.[2]
Below is a test scenario of the baseline successful case of extractRevisions: extracting a tracked deletion while restoring <w:delText> into the before text.
The scenario
Given a document with a deletion by Bob,
When extractRevisions is called,
Then
- one change is returned,
before_textincludes the deleted substring,after_textexcludes the deleted substring,- the revision is a
DELETION.
The test fixture
The fixture builds a small WordprocessingML paragraph with a tracked deletion, then checks the returned revision summary.[3]
Below is the test fixture code.
test('should extract deletions with before/after text (w:delText restored)', async ({ given, when, then, and }: AllureBddContext) => {
let doc: Document;
let result: ReturnType<typeof extractRevisions>;
await given('a document with a deletion by Bob', async () => {
doc = makeDoc(
'<w:p>' +
'<w:r><w:t>Keep</w:t></w:r>' +
'<w:del w:author="Bob">' +
'<w:r><w:delText> deleted</w:delText></w:r>' +
'</w:del>' +
'</w:p>',
);
});
await when('extractRevisions is called', async () => {
result = extractRevisions(doc, []);
});
await then('one change is returned', async () => {
expect(result.total_changes).toBe(1);
});
await and('before_text includes the deleted text', async () => {
expect(result.changes[0]!.before_text).toBe('Keep deleted');
});
await and('after_text excludes the deleted text', async () => {
expect(result.changes[0]!.after_text).toBe('Keep');
});
await and('the revision is a DELETION', async () => {
expect(result.changes[0]!.revisions[0]!.type).toBe('DELETION');
expect(result.changes[0]!.revisions[0]!.text).toBe(' deleted');
});
});
The expected result shape
The scenario asserts specific fields on the extractRevisions return value, so the expected result is expressed as the same field checks.
Below is the result that extractRevisions is expected to return for this scenario.
expect(result.total_changes).toBe(1);
expect(result.changes[0]!.before_text).toBe('Keep deleted');
expect(result.changes[0]!.after_text).toBe('Keep');
expect(result.changes[0]!.revisions[0]!.type).toBe('DELETION');
expect(result.changes[0]!.revisions[0]!.text).toBe(' deleted');
Below is a description of the expected fields:
total_changesis expected to be1, because the fixture contains one paragraph with a tracked deletion.changes[0]!.before_textis expected to be'Keep deleted', because rejecting the deletion restores the<w:delText>substring into the paragraph text.changes[0]!.after_textis expected to be'Keep', because accepting the deletion removes the deleted substring from the paragraph text.changes[0]!.revisions[0]!.typeis expected to be'DELETION', because the revision entry comes from a<w:del>wrapper.changes[0]!.revisions[0]!.textis expected to be' deleted', because the deletion entry records the deleted substring exactly as it appears in<w:delText>.