From b239a0b21f3926d85b5ea13cec2e3bcc0962139c Mon Sep 17 00:00:00 2001 From: Tadeu Tupinamba Date: Mon, 2 Mar 2026 19:38:31 -0300 Subject: [PATCH] fix(comments): prevent comment mark from extending to adjacent typed text Set `inclusive: false` on CommentsMark so new text typed at the boundary of a comment range does not inherit the comment mark. This aligns the runtime definition with what the test schemas already specify and matches the pattern used by Link, TrackInsert, TrackDelete, and TrackFormat marks. Closes SD-2029 --- .../src/extensions/comment/comments-marks.js | 2 + .../comment-mark-non-inclusive.spec.ts | 86 +++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 tests/behavior/tests/comments/comment-mark-non-inclusive.spec.ts diff --git a/packages/super-editor/src/extensions/comment/comments-marks.js b/packages/super-editor/src/extensions/comment/comments-marks.js index 9da39361c6..a42c387569 100644 --- a/packages/super-editor/src/extensions/comment/comments-marks.js +++ b/packages/super-editor/src/extensions/comment/comments-marks.js @@ -8,6 +8,8 @@ export const CommentsMark = Mark.create({ excludes: '', + inclusive: false, + addOptions() { return { htmlAttributes: { class: 'sd-editor-comment' }, diff --git a/tests/behavior/tests/comments/comment-mark-non-inclusive.spec.ts b/tests/behavior/tests/comments/comment-mark-non-inclusive.spec.ts new file mode 100644 index 0000000000..04c181972f --- /dev/null +++ b/tests/behavior/tests/comments/comment-mark-non-inclusive.spec.ts @@ -0,0 +1,86 @@ +import { test, expect } from '../../fixtures/superdoc.js'; +import { addCommentByText, assertDocumentApiReady } from '../../helpers/document-api.js'; + +test.use({ config: { toolbar: 'full', comments: 'on' } }); + +test.describe('comment mark non-inclusive boundary', () => { + test('typing after commented text does not extend the comment mark', async ({ superdoc }) => { + await assertDocumentApiReady(superdoc.page); + + // 1. Type initial text + await superdoc.type('Commented'); + await superdoc.waitForStable(); + await superdoc.assertTextContains('Commented'); + + // 2. Add a comment on the word "Commented" + const commentId = await addCommentByText(superdoc.page, { + pattern: 'Commented', + text: 'Test comment', + }); + await superdoc.waitForStable(); + await superdoc.assertCommentHighlightExists({ text: 'Commented', commentId, timeoutMs: 20_000 }); + + // 3. Place cursor right after the commented text and type new text + const pos = await superdoc.findTextPos('Commented'); + await superdoc.setTextSelection(pos + 'Commented'.length); + await superdoc.waitForStable(); + + await superdoc.type(' after'); + await superdoc.waitForStable(); + + // 4. Verify the new text exists + await superdoc.assertTextContains('Commented after'); + + // 5. Verify the comment mark stays only on "Commented", not " after" + // Check that "Commented" has the comment mark + const commentedMarks = await superdoc.getMarksAtPos(await superdoc.findTextPos('Commented')); + expect(commentedMarks).toContain('commentMark'); + + // Check that " after" does NOT have the comment mark + const afterPos = await superdoc.findTextPos(' after'); + const afterMarks = await superdoc.getMarksAtPos(afterPos); + expect(afterMarks).not.toContain('commentMark'); + + await superdoc.snapshot('comment-mark-non-inclusive'); + }); + + test('typing before commented text does not extend the comment mark', async ({ superdoc }) => { + await assertDocumentApiReady(superdoc.page); + + // 1. Type initial text + await superdoc.type('Commented'); + await superdoc.waitForStable(); + + // 2. Add a comment on the word "Commented" + const commentId = await addCommentByText(superdoc.page, { + pattern: 'Commented', + text: 'Test comment', + }); + await superdoc.waitForStable(); + await superdoc.assertCommentHighlightExists({ text: 'Commented', commentId, timeoutMs: 20_000 }); + + // 3. Place cursor right before the commented text and type new text + const pos = await superdoc.findTextPos('Commented'); + await superdoc.setTextSelection(pos); + await superdoc.waitForStable(); + + await superdoc.type('before '); + await superdoc.waitForStable(); + + // 4. Verify the new text exists + await superdoc.assertTextContains('before Commented'); + + // 5. Verify the comment mark stays only on "Commented", not "before " + // Check that "before " does NOT have the comment mark + const beforePos = await superdoc.findTextPos('before '); + const beforeMarks = await superdoc.getMarksAtPos(beforePos); + expect(beforeMarks).not.toContain('commentMark'); + + // Check that "Commented" still has the comment mark + const commentedPos = await superdoc.findTextPos('Commented'); + const commentedMarks = await superdoc.getMarksAtPos(commentedPos); + expect(commentedMarks).toContain('commentMark'); + + await superdoc.snapshot('comment-mark-non-inclusive-before'); + }); +});