@@ -2,9 +2,9 @@ import { baseTestEnvironment } from "../react-test.utils";
22import { AnnotationPlugin , AnnotationRef } from "./AnnotationPlugin" ;
33import { AnnotationRange } from "./selection.model" ;
44import { act } from "@testing-library/react" ;
5- import { $createTextNode , $getRoot } from "lexical" ;
5+ import { $createTextNode , $getRoot , $isTextNode } from "lexical" ;
66import { createRef } from "react" ;
7- import { $createParaNode , $createTypedMarkNode } from "shared" ;
7+ import { $createParaNode , $createTypedMarkNode , $isParaNode , $isTypedMarkNode } from "shared" ;
88import { vi } from "vitest" ;
99
1010describe ( "AnnotationPlugin" , ( ) => {
@@ -30,6 +30,62 @@ describe("AnnotationPlugin", () => {
3030 // Check: spelling removal callback should not fire during internal rewrap.
3131 expect ( spellingOnRemove ) . not . toHaveBeenCalled ( ) ;
3232 } ) ;
33+
34+ it ( "correctly handles adding a second annotation at the same start position as the first" , async ( ) => {
35+ // Initial state: "the " + "man" (spelling annotation) + " who stands"
36+ const { annotationPlugin, editor } = await testEnvironment ( ( ) => {
37+ $getRoot ( ) . append (
38+ $createParaNode ( ) . append (
39+ $createTextNode ( "the " ) ,
40+ $createTypedMarkNode ( { spelling : [ "spell-1" ] } ) . append ( $createTextNode ( "man" ) ) ,
41+ $createTextNode ( " who stands" ) ,
42+ ) ,
43+ ) ;
44+ } ) ;
45+ const jsonPath = "$.content[0].content[0]" ;
46+ // Second annotation: "man who" (starts at same position as first but extends further)
47+ const secondAnnotationRange : AnnotationRange = {
48+ start : { jsonPath, offset : 4 } ,
49+ end : { jsonPath, offset : 11 } ,
50+ } ;
51+
52+ // SUT: add second annotation that starts at the same position as the first
53+ await act ( async ( ) => {
54+ annotationPlugin . setAnnotation ( secondAnnotationRange , "grammar" , "grammar-1" ) ;
55+ } ) ;
56+
57+ // Verify the structure: both annotations should start at the same position
58+ editor . getEditorState ( ) . read ( ( ) => {
59+ const para = $getRoot ( ) . getFirstChild ( ) ;
60+ if ( ! $isParaNode ( para ) ) throw new Error ( "Expected a ParaNode" ) ;
61+
62+ // First child should be plain text "the "
63+ const firstChild = para . getFirstChild ( ) ;
64+ if ( ! $isTextNode ( firstChild ) ) throw new Error ( "Expected a TextNode" ) ;
65+ expect ( firstChild . getTextContent ( ) ) . toBe ( "the " ) ;
66+
67+ // Second child should be a TypedMarkNode with both spelling AND grammar at "man"
68+ const secondChild = firstChild . getNextSibling ( ) ;
69+ if ( ! $isTypedMarkNode ( secondChild ) ) throw new Error ( "Expected a TypedMarkNode" ) ;
70+ const secondTypedIDs = secondChild . getTypedIDs ( ) ;
71+ expect ( secondTypedIDs . spelling ) . toContain ( "spell-1" ) ;
72+ expect ( secondTypedIDs . grammar ) . toContain ( "grammar-1" ) ;
73+ expect ( secondChild . getTextContent ( ) ) . toBe ( "man" ) ;
74+
75+ // Third child should be a TypedMarkNode with only grammar for " who"
76+ const thirdChild = secondChild . getNextSibling ( ) ;
77+ if ( ! $isTypedMarkNode ( thirdChild ) ) throw new Error ( "Expected a TypedMarkNode" ) ;
78+ const thirdTypedIDs = thirdChild . getTypedIDs ( ) ;
79+ expect ( thirdTypedIDs . spelling ) . toBeUndefined ( ) ;
80+ expect ( thirdTypedIDs . grammar ) . toContain ( "grammar-1" ) ;
81+ expect ( thirdChild . getTextContent ( ) ) . toBe ( " who" ) ;
82+
83+ // Fourth child should be plain text " stands"
84+ const fourthChild = thirdChild . getNextSibling ( ) ;
85+ if ( ! $isTextNode ( fourthChild ) ) throw new Error ( "Expected a TextNode" ) ;
86+ expect ( fourthChild . getTextContent ( ) ) . toBe ( " stands" ) ;
87+ } ) ;
88+ } ) ;
3389} ) ;
3490
3591async function testEnvironment ( $initialEditorState : ( ) => void ) {
0 commit comments