@@ -883,7 +883,9 @@ test("keeps a line added at the end of a scrolling prompt clear of the bottom fa
883883 const viewport = element . closest ( '[data-component="composer-scroll"]' ) !
884884 const range = document . createRange ( )
885885 range . selectNodeContents ( element )
886- const line = Array . from ( range . getClientRects ( ) ) . reduce ( ( last , rect ) => ( rect . bottom > last . bottom ? rect : last ) )
886+ const line = Array . from ( range . getClientRects ( ) ) . reduce ( ( last , rect ) =>
887+ rect . bottom > last . bottom ? rect : last ,
888+ )
887889 const fade = parseFloat ( getComputedStyle ( viewport ) . getPropertyValue ( "--composer-fade-bottom" ) )
888890
889891 return Math . max ( 0 , Math . round ( line . bottom - ( viewport . getBoundingClientRect ( ) . bottom - fade ) ) )
@@ -892,6 +894,65 @@ test("keeps a line added at the end of a scrolling prompt clear of the bottom fa
892894 . toBe ( 0 )
893895} )
894896
897+ test ( "Enter that confirms an IME candidate never sends, even after Safari's early compositionend" , async ( { page } ) => {
898+ const prompts : Parameters < NonNullable < MockServerConfig [ "onPrompt" ] > > [ 0 ] [ "body" ] [ ] = [ ]
899+ const view = await openSession ( page , { name : "ComposerImeEnter" , onPrompt : ( input ) => void prompts . push ( input . body ) } )
900+
901+ const enter = ( keyCode : number ) =>
902+ view . editor . evaluate (
903+ ( element , keyCode ) =>
904+ element . dispatchEvent ( new KeyboardEvent ( "keydown" , { key : "Enter" , keyCode, bubbles : true , cancelable : true } ) ) ,
905+ keyCode ,
906+ )
907+
908+ await view . editor . fill ( "にほん" )
909+ // Safari delivers the confirming Enter after compositionend, so isComposing is false and only keyCode marks it.
910+ await enter ( 229 )
911+ await expect ( view . editor ) . toHaveText ( "にほん" )
912+ await enter ( 13 )
913+ await expect . poll ( ( ) => prompts . map ( ( body ) => body . text ) ) . toEqual ( [ "にほん" ] )
914+ } )
915+
916+ test . describe ( "touch keyboard" , ( ) => {
917+ test . use ( { viewport : { width : 390 , height : 844 } , hasTouch : true , isMobile : true } )
918+
919+ test ( "Return only adds a line, even over suggestions; the touch bar and Cmd+Return send" , async ( { page } ) => {
920+ const prompts : Parameters < NonNullable < MockServerConfig [ "onPrompt" ] > > [ 0 ] [ "body" ] [ ] = [ ]
921+ const view = await openSession ( page , {
922+ name : "ComposerTouchReturn" ,
923+ onPrompt : ( input ) => void prompts . push ( input . body ) ,
924+ } )
925+ const bar = page . locator ( '[data-slot="composer-touch-bar"]' )
926+ await expect ( bar ) . toBeVisible ( )
927+ await expect ( page . locator ( '[data-action="composer-attach"]' ) ) . toHaveCount ( 0 )
928+ await expect ( view . editor ) . toHaveAttribute ( "enterkeyhint" , "enter" )
929+
930+ await view . editor . fill ( "/model" )
931+ await expect ( page . locator ( '[data-suggestion-id="model.choose"]' ) ) . toHaveAttribute ( "data-active" , "" )
932+ await view . editor . press ( "Enter" )
933+ await expect ( page . locator ( '[data-component="composer-suggestions"]' ) ) . toBeHidden ( )
934+ await expect . poll ( ( ) => view . editor . innerText ( ) ) . toMatch ( / ^ \/ m o d e l \n / )
935+ await expect ( page . getByRole ( "dialog" , { name : "Select model" , exact : true } ) ) . toBeHidden ( )
936+
937+ await view . editor . fill ( "first line" )
938+ await view . editor . press ( "Enter" )
939+ await view . editor . pressSequentially ( "second line" )
940+ await expect . poll ( ( ) => view . editor . innerText ( ) ) . toBe ( "first line\nsecond line" )
941+ expect ( prompts ) . toEqual ( [ ] )
942+
943+ await bar . getByRole ( "button" , { name : "Send" , exact : true } ) . tap ( )
944+ await expect . poll ( ( ) => prompts . map ( ( body ) => body . text ) ) . toEqual ( [ "first line\nsecond line" ] )
945+ await expect ( view . editor ) . toBeEmpty ( )
946+
947+ // A hardware keyboard, as on an iPad, still sends with Cmd+Return.
948+ await view . editor . fill ( "from the iPad keyboard" )
949+ await view . editor . press ( "ControlOrMeta+Enter" )
950+ await expect
951+ . poll ( ( ) => prompts . map ( ( body ) => body . text ) )
952+ . toEqual ( [ "first line\nsecond line" , "from the iPad keyboard" ] )
953+ } )
954+ } )
955+
895956test ( "a key-up never moves the caret back after later navigation" , async ( { page } ) => {
896957 const input = await draft ( page )
897958 await input . pressSequentially ( "first line" )
0 commit comments