@@ -11,7 +11,7 @@ import { TextRenderable } from "@opentui/core";
1111
1212import { defined } from "../../tests/helpers/defined.js" ;
1313import { withTestRenderer } from "./harness" ;
14- import { appendStreamRow } from "./shell/chrome" ;
14+ import { appendStreamRow , replaceStreamRowAt } from "./shell/chrome" ;
1515import { createAppShell } from "./shell/index" ;
1616import {
1717 isUnderlined ,
@@ -188,4 +188,69 @@ describe("Ctrl+clicking a transcript URL", () => {
188188 { width : 80 , height : 24 } ,
189189 ) ;
190190 } ) ;
191+
192+ test ( "retexting a plain row's URL away through the row path disarms it" , async ( ) => {
193+ await withTestRenderer (
194+ async ( h ) => {
195+ const shell = createAppShell ( h . renderer , {
196+ terminal : { columns : 80 , rows : 24 } ,
197+ wireKeys : false ,
198+ run : "idle" ,
199+ } ) ;
200+ const opened : string [ ] = [ ] ;
201+ setUrlOpener ( ( url ) => {
202+ opened . push ( url ) ;
203+ } ) ;
204+ try {
205+ // A user row paints literal text through paintPlainRowNode, so the
206+ // arm and the later disarm both run on the production row path.
207+ appendStreamRow ( shell , {
208+ role : "user" ,
209+ text : "see https://example.com/x ok" ,
210+ } ) ;
211+ await h . renderOnce ( ) ;
212+
213+ const link = findCell ( h . captureCharFrame ( ) , "example.com" ) ;
214+ expect ( link ) . not . toBeNull ( ) ;
215+ const at = defined ( link ) ;
216+
217+ await h . mockMouse . click ( at . x , at . y , 0 , {
218+ modifiers : { ctrl : true } ,
219+ } ) ;
220+ await h . renderOnce ( ) ;
221+ expect ( opened ) . toEqual ( [ "https://example.com/x" ] ) ;
222+
223+ // Retext in place through replaceStreamRowAt -> retextStreamRow ->
224+ // paintPlainRowNode's URL-free branch. Reverting that branch's
225+ // disarm must fail this test (stale handlers survive on the node).
226+ replaceStreamRowAt ( shell , 0 , {
227+ role : "user" ,
228+ text : "see nothing here" ,
229+ } ) ;
230+ await h . renderOnce ( ) ;
231+ const frame = h . captureCharFrame ( ) ;
232+ expect ( frame ) . toContain ( "nothing here" ) ;
233+ expect ( frame ) . not . toContain ( "example.com" ) ;
234+
235+ opened . length = 0 ;
236+ await h . mockMouse . click ( at . x , at . y , 0 , {
237+ modifiers : { ctrl : true } ,
238+ } ) ;
239+ await h . renderOnce ( ) ;
240+ expect ( opened ) . toEqual ( [ ] ) ;
241+
242+ const before = h . captureCharFrame ( ) ;
243+ await h . mockMouse . moveTo ( at . x , at . y , {
244+ modifiers : { ctrl : true } ,
245+ } ) ;
246+ await h . renderOnce ( ) ;
247+ expect ( h . captureCharFrame ( ) ) . toBe ( before ) ;
248+ } finally {
249+ resetUrlOpener ( ) ;
250+ shell . dispose ( ) ;
251+ }
252+ } ,
253+ { width : 80 , height : 24 } ,
254+ ) ;
255+ } ) ;
191256} ) ;
0 commit comments