char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
bdffafa5d
char/aliceai-t5-moe
1// Guards the newline contract of the ChatFormInputRich: browsers 2// restructure the flat DOM on Enter (`<div>` wrappers, `<br>` shapes) and 3// serialization must fold those back into `\n` so the emitted value never 4// diverges from what is on screen. 5 6import ChatFormInputRichHarness from './components/ChatFormInputRichHarness.svelte' ; 7import { tick } from 'svelte' ; 8import { describe , expect , it } from 'vitest' ; 9import { render } from 'vitest-browser-svelte' ; 10 11const SOURCE = 'see [docs](file:///a/b) here' ; 12 13function editableIn ( container :HTMLElement ) :HTMLElement { 14const el = container . querySelector ( '[role="textbox"]' ); 15 16if ( ! ( el instanceof HTMLElement )) throw new Error ( 'ChatFormInputRich not rendered' ); 17 18return el ; 19} 20 21function fireInput ( root :HTMLElement ) { 22root . dispatchEvent ( new InputEvent ( 'input' , { bubbles :true })); 23} 24 25function setCaret ( node :Node , offset :number ) { 26const range = document . createRange (); 27 28range . setStart ( node , offset ); 29range . setEnd ( node , offset ); 30const selection = window . getSelection (); 31 32if ( ! selection ) throw new Error ( 'no selection' ); 33 34selection . removeAllRanges (); 35selection . addRange ( range ); 36} 37 38describe ( 'ChatFormInputRich browser newline shapes' , () => { 39it ( 'serializes a Chromium Enter <div> wrapper as a newline' , async () => { 40const screen = render ( ChatFormInputRichHarness , { value :SOURCE }); 41 42await tick (); 43 44const root = editableIn ( screen . container ); 45const div = document . createElement ( 'div' ); 46 47div . textContent = 'second line' ; 48root . appendChild ( div ); 49fireInput ( root ); 50await tick (); 51 52expect ( screen . component . getValue ()). toBe ( ` ${ SOURCE } \nsecond line` ); 53}); 54 55it ( 'serializes a Firefox full <div> wrap as lines, badge included' , async () => { 56const screen = render ( ChatFormInputRichHarness , { value :SOURCE }); 57 58await tick (); 59 60const root = editableIn ( screen . container ); 61const first = document . createElement ( 'div' ); 62 63while ( root . firstChild ) first . appendChild ( root . firstChild ); 64const second = document . createElement ( 'div' ); 65 66second . textContent = 'second line' ; 67root . appendChild ( first ); 68root . appendChild ( second ); 69fireInput ( root ); 70await tick (); 71 72expect ( screen . component . getValue ()). toBe ( ` ${ SOURCE } \nsecond line` ); 73}); 74 75it ( 'serializes a <br> as a newline' , async () => { 76const screen = render ( ChatFormInputRichHarness , { value :'here' }); 77 78await tick (); 79 80const root = editableIn ( screen . container ); 81 82root . appendChild ( document . createElement ( 'br' )); 83root . appendChild ( document . createTextNode ( 'second line' )); 84fireInput ( root ); 85await tick (); 86 87expect ( screen . component . getValue ()). toBe ( 'here\nsecond line' ); 88}); 89 90it ( 'ignores a trailing <br> (browser caret placeholder)' , async () => { 91const screen = render ( ChatFormInputRichHarness , { value :'abc' }); 92 93await tick (); 94 95const root = editableIn ( screen . container ); 96 97root . appendChild ( document . createElement ( 'br' )); 98fireInput ( root ); 99await tick (); 100 101expect ( screen . component . getValue ()). toBe ( 'abc' ); 102}); 103 104it ( 'serializes one newline per empty-line <div><br></div>' , async () => { 105const screen = render ( ChatFormInputRichHarness , { value :'abc' }); 106 107await tick (); 108 109const root = editableIn ( screen . container ); 110 111for ( let i = 0 ; i < 2 ; i ++ ) { 112const div = document . createElement ( 'div' ); 113 114div . appendChild ( document . createElement ( 'br' )); 115root . appendChild ( div ); 116} 117fireInput ( root ); 118await tick (); 119 120expect ( screen . component . getValue ()). toBe ( 'abc\n\n' ); 121}); 122 123it ( 'treats a <div><br></div>-only buffer as empty for the placeholder' , async () => { 124const screen = render ( ChatFormInputRichHarness , { value :'abc' }); 125 126await tick (); 127 128const root = editableIn ( screen . container ); 129const div = document . createElement ( 'div' ); 130 131div . appendChild ( document . createElement ( 'br' )); 132root . replaceChildren ( div ); 133fireInput ( root ); 134await tick (); 135 136expect ( screen . component . getValue ()). toBe ( '' ); 137expect ( root . dataset . empty ). toBe ( 'true' ); 138}); 139 140it ( 'maps the caret across block boundaries in both directions' , async () => { 141const screen = render ( ChatFormInputRichHarness , { value :'abc\ndef' }); 142 143await tick (); 144 145// Rebuild into the Chromium block shape; the source is unchanged, 146// so no re-render fires. 147const root = editableIn ( screen . container ); 148const div = document . createElement ( 'div' ); 149 150div . textContent = 'def' ; 151root . replaceChildren ( document . createTextNode ( 'abc' ), div ); 152fireInput ( root ); 153await tick (); 154expect ( screen . component . getValue ()). toBe ( 'abc\ndef' ); 155 156const divText = div . firstChild ; 157 158if ( ! divText ) throw new Error ( 'div text missing' ); 159 160setCaret ( divText , 2 ); 161expect ( screen . component . getCaretOffset ()). toBe ( 6 ); 162 163screen . component . setCaretOffset ( 6 ); 164const selection = window . getSelection (); 165 166expect ( selection ?. anchorNode ). toBe ( divText ); 167expect ( selection ?. anchorOffset ). toBe ( 2 ); 168 169// The boundary newline itself: offset 3 is the end of "abc", offset 170// 4 the start of the "def" line. 171screen . component . setCaretOffset ( 4 ); 172expect ( window . getSelection ()?. anchorNode ). toBe ( divText ); 173expect ( window . getSelection ()?. anchorOffset ). toBe ( 0 ); 174 175screen . component . setCaretOffset ( 3 ); 176expect ( window . getSelection ()?. anchorNode ). toBe ( root . firstChild ); 177expect ( window . getSelection ()?. anchorOffset ). toBe ( 3 ); 178}); 179});