When reversing tracked deletions in a WordprocessingML document, restored content must return as ordinary paragraph text rather than remain inside deletion markup. Deleted text often lives in a run element (i.e., a <w:r> container for text and its run-level properties), so rejecting the deletion has to preserve the run while changing the deletion-only text node back into a normal text node.
The rejectChanges primitive (i.e., a focused document-mutation operation) unwraps deletion markers and renames <w:delText> elements to <w:t> elements.[1] That rename matters because paragraph text readers collect ordinary text nodes, so a restored deletion must no longer look like revision-only content.
Below is a test scenario of the deletion-restoration case of rejectChanges: tracked deletion text becomes ordinary paragraph text.
The scenario
Given a document with kept text and a deletion,
When rejectChanges is called,
Then
result.deletionsRestoredis1.getAllParagraphTexts(doc)returns['Keep deleted'].- the document contains no
<w:delText>elements.
The Test Fixture
The fixture builds a single paragraph that contains ordinary kept content followed by a tracked deletion.[2] It then rejects tracked changes and checks both the reported deletion count and the mutated document content.
Below is the test fixture code.
test('should restore deleted text (w:delText -> w:t conversion)', async ({ given, when, then, and }: AllureBddContext) => {
let doc: Document;
let result: ReturnType<typeof rejectChanges>;
await given('a document with kept text and a deletion', async () => {
doc = makeDoc(
'<w:p>' +
'<w:r><w:t>Keep</w:t></w:r>' +
'<w:del w:author="Author" w:date="2024-01-01T00:00:00Z">' +
'<w:r><w:delText> deleted</w:delText></w:r>' +
'</w:del>' +
'</w:p>',
);
});
await when('rejectChanges is called', async () => {
result = rejectChanges(doc);
});
await then('deleted text is restored', async () => {
expect(result.deletionsRestored).toBe(1);
expect(getAllParagraphTexts(doc)).toEqual(['Keep deleted']);
});
await and('w:delText was renamed to w:t', async () => {
const delTexts = doc.getElementsByTagNameNS(W_NS, 'delText');
expect(delTexts.length).toBe(0);
});
});
The Expected Outcome
The scenario asserts a side effect on the document and one counter on the result object. The document state is the main outcome because restored deletion content has to become visible through the paragraph-text reader.
Below is the expected outcome for this scenario.
expect(result.deletionsRestored).toBe(1);
expect(getAllParagraphTexts(doc)).toEqual(['Keep deleted']);
expect(delTexts.length).toBe(0);
Below is a description of the expected fields:
result.deletionsRestoredis expected to be1, because the fixture contains one<w:del>wrapper that is unwrapped during rejection.getAllParagraphTexts(doc)is expected to be['Keep deleted'], because the kept text remains in place and the deleted run is restored into the same paragraph.delTexts.lengthis expected to be0, because every<w:delText>node created for deletion tracking is renamed to<w:t>after the deletion wrapper is removed.
A Non-Obvious Detail
Rejecting a deletion is not just a wrapper-removal step. The deletion text element also has to be renamed, because a remaining <w:delText> element would still mark the content as deletion-only text even after the surrounding <w:del> element has been removed.
The scenario is tied to the paragraph-level tracked-change model in ECMA-376, where revision wrappers can contain paragraph content that must be restored without placing paragraph-level markers inside the wrong XML shape.[3]