@@ -52,6 +52,36 @@ class PromptInputTokenModePage extends BasePageObject {
5252 return sel ? sel . toString ( ) . replace ( / \u200B / g, '' ) : '' ;
5353 } ) ;
5454 }
55+
56+ getParagraphStructure ( ) : Promise <
57+ Array < { childCount : number ; hasTrailingBR : boolean ; firstChildType : string ; firstChildText : string } >
58+ > {
59+ return this . browser . execute ( ( selector : string ) => {
60+ const editable = document . querySelector ( selector ) ;
61+ if ( ! editable ) {
62+ return [ ] ;
63+ }
64+ const paragraphs = editable . querySelectorAll ( 'p' ) ;
65+ const result : Array < {
66+ childCount : number ;
67+ hasTrailingBR : boolean ;
68+ firstChildType : string ;
69+ firstChildText : string ;
70+ } > = [ ] ;
71+ for ( let i = 0 ; i < paragraphs . length ; i ++ ) {
72+ const p = paragraphs [ i ] ;
73+ const firstChild = p . firstChild ;
74+ const lastChild = p . lastChild ;
75+ result . push ( {
76+ childCount : p . childNodes . length ,
77+ hasTrailingBR : ! ! ( lastChild && lastChild . nodeName === 'BR' ) ,
78+ firstChildType : firstChild ? firstChild . nodeName : 'none' ,
79+ firstChildText : ( firstChild ?. textContent ?? '' ) . replace ( / \u200B / g, '' ) ,
80+ } ) ;
81+ }
82+ return result ;
83+ } , contentEditableSelector ) ;
84+ }
5585}
5686
5787const setupTest = ( testFn : ( page : PromptInputTokenModePage ) => Promise < void > ) => {
@@ -171,15 +201,13 @@ const setupTest = (testFn: (page: PromptInputTokenModePage) => Promise<void>) =>
171201 'backspace through trigger with filter text removes all of it' ,
172202 setupTest ( async page => {
173203 await page . focusInput ( ) ;
174- await page . keys ( [ 'h' , 'i' , ' ' , '@' , 'a' , 'l' , 'i' ] ) ;
175- await page . pause ( 300 ) ;
204+ await page . keys ( [ 'h' , 'i' , ' ' , '@' , 'a' ] ) ;
176205
177- await page . keys ( [ 'Escape' ] ) ;
178- await page . pause ( 100 ) ;
179- await page . keys ( [ 'Backspace' , 'Backspace' , 'Backspace' , 'Backspace' ] ) ;
180- await page . pause ( 300 ) ;
206+ // Backspace filter char, then backspace trigger char
207+ await page . keys ( [ 'Backspace' ] ) ;
208+ await page . keys ( [ 'Backspace' ] ) ;
181209
182- expect ( ( await page . getEditorText ( ) ) . trim ( ) ) . toBe ( 'hi' ) ;
210+ expect ( await page . getEditorText ( ) ) . toBe ( 'hi ' ) ;
183211 expect ( await page . getCaretOffset ( ) ) . toBe ( 3 ) ;
184212 } )
185213 ) ;
@@ -383,7 +411,7 @@ const setupTest = (testFn: (page: PromptInputTokenModePage) => Promise<void>) =>
383411 await page . pause ( 200 ) ;
384412
385413 const text = await page . getEditorText ( ) ;
386- expect ( text . trim ( ) ) . toBe ( 'hi' ) ;
414+ expect ( text ) . toBe ( ' hi' ) ;
387415 expect ( text ) . not . toContain ( 'Jane Smith' ) ;
388416 } )
389417 ) ;
@@ -403,7 +431,7 @@ const setupTest = (testFn: (page: PromptInputTokenModePage) => Promise<void>) =>
403431 await page . pause ( 200 ) ;
404432
405433 const text = await page . getEditorText ( ) ;
406- expect ( text . trim ( ) ) . toBe ( 'hi' ) ;
434+ expect ( text ) . toBe ( 'hi ' ) ;
407435 expect ( text ) . not . toContain ( 'Jane Smith' ) ;
408436 expect ( await page . getCaretOffset ( ) ) . toBe ( 3 ) ;
409437 } )
@@ -455,3 +483,106 @@ const setupTest = (testFn: (page: PromptInputTokenModePage) => Promise<void>) =>
455483 } )
456484 ) ;
457485} ) ;
486+
487+ ( isReact18 ? describe : describe . skip ) (
488+ 'PromptInput token mode - typing into empty line (isTypingIntoEmptyLine)' ,
489+ ( ) => {
490+ test (
491+ 'typing on a new line after shift+enter replaces trailing BR with text node' ,
492+ setupTest ( async page => {
493+ await page . focusInput ( ) ;
494+ await page . keys ( [ 'h' , 'e' , 'l' , 'l' , 'o' ] ) ;
495+ await page . pause ( 100 ) ;
496+
497+ await page . keys ( [ 'Shift' , 'Enter' , 'Shift' ] ) ;
498+ await page . pause ( 100 ) ;
499+
500+ // Before typing: second paragraph should have a trailing BR (empty line)
501+ const beforeStructure = await page . getParagraphStructure ( ) ;
502+ expect ( beforeStructure . length ) . toBe ( 2 ) ;
503+ expect ( beforeStructure [ 1 ] . hasTrailingBR ) . toBe ( true ) ;
504+
505+ // Type on the empty second line
506+ await page . keys ( [ 'w' ] ) ;
507+ await page . pause ( 200 ) ;
508+
509+ // After typing: second paragraph should have a text node, no trailing BR
510+ const afterStructure = await page . getParagraphStructure ( ) ;
511+ expect ( afterStructure . length ) . toBe ( 2 ) ;
512+ expect ( afterStructure [ 1 ] . hasTrailingBR ) . toBe ( false ) ;
513+ expect ( afterStructure [ 1 ] . firstChildType ) . toBe ( '#text' ) ;
514+ expect ( afterStructure [ 1 ] . firstChildText ) . toContain ( 'w' ) ;
515+ expect ( await page . getCaretOffset ( ) ) . toBe ( 6 ) ;
516+ } )
517+ ) ;
518+
519+ test (
520+ 'typing trigger on empty line after shift+enter opens menu and replaces BR' ,
521+ setupTest ( async page => {
522+ await page . focusInput ( ) ;
523+ await page . keys ( [ 'h' , 'e' , 'l' , 'l' , 'o' ] ) ;
524+ await page . pause ( 100 ) ;
525+
526+ await page . keys ( [ 'Shift' , 'Enter' , 'Shift' ] ) ;
527+ await page . pause ( 100 ) ;
528+
529+ await page . keys ( [ '@' ] ) ;
530+ await page . pause ( 300 ) ;
531+
532+ await expect ( page . isMenuOpen ( ) ) . resolves . toBe ( true ) ;
533+
534+ // The second paragraph should now contain a trigger element, not a trailing BR
535+ const structure = await page . getParagraphStructure ( ) ;
536+ expect ( structure . length ) . toBe ( 2 ) ;
537+ expect ( structure [ 1 ] . hasTrailingBR ) . toBe ( false ) ;
538+ } )
539+ ) ;
540+
541+ test (
542+ 'typing into completely empty input replaces trailing BR with text node' ,
543+ setupTest ( async page => {
544+ await page . focusInput ( ) ;
545+
546+ // Before typing: single paragraph with trailing BR
547+ const beforeStructure = await page . getParagraphStructure ( ) ;
548+ expect ( beforeStructure . length ) . toBe ( 1 ) ;
549+ expect ( beforeStructure [ 0 ] . hasTrailingBR ) . toBe ( true ) ;
550+
551+ await page . keys ( [ 'a' ] ) ;
552+ await page . pause ( 200 ) ;
553+
554+ // After typing: paragraph has text node, no trailing BR
555+ const afterStructure = await page . getParagraphStructure ( ) ;
556+ expect ( afterStructure . length ) . toBe ( 1 ) ;
557+ expect ( afterStructure [ 0 ] . hasTrailingBR ) . toBe ( false ) ;
558+ expect ( afterStructure [ 0 ] . firstChildType ) . toBe ( '#text' ) ;
559+ expect ( afterStructure [ 0 ] . firstChildText ) . toBe ( 'a' ) ;
560+ expect ( await page . getCaretOffset ( ) ) . toBe ( 1 ) ;
561+ } )
562+ ) ;
563+ }
564+ ) ;
565+
566+ ( isReact18 ? describe : describe . skip ) ( 'PromptInput token mode - mouseup selection normalization' , ( ) => {
567+ test (
568+ 'clicking on a reference token and dragging produces a valid selection' ,
569+ setupTest ( async page => {
570+ await page . focusInput ( ) ;
571+ await page . keys ( [ 'h' , 'i' , ' ' ] ) ;
572+ await page . keys ( [ '@' ] ) ;
573+ await page . pause ( 200 ) ;
574+ await page . keys ( [ 'ArrowDown' , 'Enter' ] ) ;
575+ await page . pause ( 200 ) ;
576+ await page . keys ( [ ' ' , 'b' , 'y' , 'e' ] ) ;
577+ await page . pause ( 200 ) ;
578+
579+ // Click at the start of the input to position caret
580+ await page . click ( contentEditableSelector ) ;
581+ await page . pause ( 100 ) ;
582+
583+ // The caret should be at a valid position (not inside a reference's internal structure)
584+ const offset = await page . getCaretOffset ( ) ;
585+ expect ( offset ) . toBeGreaterThanOrEqual ( 0 ) ;
586+ } )
587+ ) ;
588+ } ) ;
0 commit comments