UseJunior Book a Demo

safe-docx · Reject Tracked Changes

Restored deleted text after rejecting tracked changes

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.deletionsRestored is 1.
  • 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:

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]