UseJunior Book a Demo

safe-docx · Comments

Single-paragraph comment range metadata

When reviewing comments anchored to a paragraph range, editors need the comment body and the range metadata to describe the same marked span. Range metadata (the paragraph and run-position details for the marked span) matters because a Word comment can wrap only part of a paragraph instead of the whole paragraph.

getComments reads comment records and connects them to range markers in the main document XML.[1] Locating those markers requires preserving ordinary comment fields while also resolving run elements (the <w:r> containers that hold paragraph text) into start and end positions.

Scenario

The scenario states the document condition, the comment-reading operation, and the fields that must remain correct together.

Below is a test scenario of the baseline successful case of getComments: resolves single-paragraph range metadata without changing existing fields.

The scenario

Given a bookmarked paragraph whose comment markers wrap a middle run,
When getComments reads the document comments and document XML,
Then

  • exactly one root comment is returned.
  • the existing comment fields match the comment record.
  • the range metadata points to the bookmarked paragraph and the wrapped run.

The test fixture

The fixture builds a paragraph with text before and after the marked range, then loads one comment record through the comment fixture helper.[2]

Below is the test fixture code.

test('resolves single-paragraph range metadata without changing existing fields', async ({ given, then }: AllureBddContext) => {
  const commentText = 'Single paragraph note';
  const rangeText = 'Beta';
  let comments: Awaited<ReturnType<typeof getComments>>;

  await given('a bookmarked paragraph whose comment markers wrap a middle run', async () => {
    const bodyXml = withParagraphBookmark({
      bookmarkId: 101,
      name: '_bk_single_range',
      paragraphInnerXml:
        `<w:r><w:t>Alpha </w:t></w:r>` +
        `<w:commentRangeStart w:id="10"/>` +
        `<w:r><w:t>${rangeText}</w:t></w:r>` +
        `<w:commentRangeEnd w:id="10"/>` +
        makeCommentReferenceRun(10) +
        `<w:r><w:t> Gamma</w:t></w:r>`,
    });

    comments = await loadCommentFixture({
      bodyXml,
      comments: [
        { id: 10, author: 'Alice', initials: 'A', text: commentText, paraId: '00000010', date: '2025-02-01T00:00:00Z' },
      ],
    });
  });

  await then('existing fields and range metadata are both correct', async () => {
    expect(comments).toHaveLength(1);
    expect(comments[0]!.id).toBe(10);
    expect(comments[0]!.author).toBe('Alice');
    expect(comments[0]!.date).toBe('2025-02-01T00:00:00Z');
    expect(comments[0]!.initials).toBe('A');
    expect(comments[0]!.text).toBe(commentText);
    expect(comments[0]!.paragraphId).toBe('00000010');
    expect(comments[0]!.replies).toEqual([]);
    expect(comments[0]!.anchoredParagraphId).toBe('_bk_single_range');
    expect(comments[0]!.endParagraphId).toBe('_bk_single_range');
    expect(comments[0]!.startRunIndex).toBe(1);
    expect(comments[0]!.startCharOffset).toBe(0);
    expect(comments[0]!.endRunIndex).toBe(1);
    expect(comments[0]!.endCharOffset).toBe(rangeText.length);
  });
});

The expected result shape

The assertions check the returned comment object, including both the existing comment fields and the resolved range fields.

Below is the result that getComments is expected to return for this scenario.

[
  {
    id: 10,
    author: 'Alice',
    date: '2025-02-01T00:00:00Z',
    initials: 'A',
    text: 'Single paragraph note',
    paragraphId: '00000010',
    replies: [],
    anchoredParagraphId: '_bk_single_range',
    endParagraphId: '_bk_single_range',
    startRunIndex: 1,
    startCharOffset: 0,
    endRunIndex: 1,
    endCharOffset: 4,
  },
]

Below is a description of the expected fields: