okami.mom/ultraviolet
git clone https://git.nkp.pet/okami.mom/ultraviolet
d9e819d
upstream
1package uv 2 3import ( 4"strings" 5"testing" 6 7"github.com/charmbracelet/x/ansi" 8"github.com/rivo/uniseg" 9) 10 11func TestBufferUniseg (t * testing.T ) { 12cases := []struct { 13name string 14input string 15expected string 16 }{ 17 { 18name :"empty buffer" , 19input :"" , 20expected :"" , 21 }, 22 { 23name :"single line" , 24input :"Hello, World!" , 25expected :"Hello, World!" , 26 }, 27 { 28name :"multiple lines" , 29input :"Hello, World!\nThis is a test.\nGoodbye!" , 30expected :"Hello, World!\nThis is a test.\nGoodbye!" , 31 }, 32 } 33 34for _ ,c := range cases { 35t .Run (c .name ,func (t * testing.T ) { 36buf := NewBuffer (width (c .input ),height (c .input )) 37for y ,line := range strings .Split (c .input ,"\n" ) { 38var x int 39seg := uniseg .NewGraphemes (line ) 40for seg .Next () { 41cell := & Cell { 42Content :seg .Str (), 43Width :seg .Width (), 44 } 45buf .SetCell (x ,y ,cell ) 46x += cell .Width 47 } 48 } 49 50if got := TrimSpace (buf .String ());got != c .expected { 51t .Errorf ("expected %q, got %q" ,c .expected ,got ) 52 } 53 }) 54 } 55} 56 57// TestLineSet tests the Set method of Line 58func TestLineSet (t * testing.T ) { 59tests := []struct { 60name string 61lineLen int 62x int 63cell * Cell 64validate func (t * testing.T ,l Line ) 65 }{ 66 { 67name :"set simple cell" , 68lineLen :10 , 69x :5 , 70cell :& Cell {Content :"a" ,Width :1 }, 71validate :func (t * testing.T ,l Line ) { 72if l [5 ].Content != "a" { 73t .Errorf ("expected 'a' at position 5, got %q" ,l [5 ].Content ) 74 } 75 }, 76 }, 77 { 78name :"set out of bounds negative" , 79lineLen :10 , 80x :- 1 , 81cell :& Cell {Content :"a" ,Width :1 }, 82validate :func (t * testing.T ,l Line ) { 83// Should not panic, just return 84 }, 85 }, 86 { 87name :"set out of bounds positive" , 88lineLen :10 , 89x :10 , 90cell :& Cell {Content :"a" ,Width :1 }, 91validate :func (t * testing.T ,l Line ) { 92// Should not panic, just return 93 }, 94 }, 95 { 96name :"overwrite wide cell" , 97lineLen :10 , 98x :2 , 99cell :& Cell {Content :"你" ,Width :2 }, 100validate :func (t * testing.T ,l Line ) { 101// First set a wide cell 102l .Set (2 ,& Cell {Content :"你" ,Width :2 }) 103// Then overwrite it 104l .Set (2 ,& Cell {Content :"a" ,Width :1 }) 105if l [2 ].Content != "a" { 106t .Errorf ("expected 'a' at position 2, got %q" ,l [2 ].Content ) 107 } 108 }, 109 }, 110 { 111name :"overwrite middle of wide cell" , 112lineLen :10 , 113x :3 , 114cell :& Cell {Content :"a" ,Width :1 }, 115validate :func (t * testing.T ,l Line ) { 116// First set a wide cell at position 2 117l .Set (2 ,& Cell {Content :"你" ,Width :2 }) 118// Then overwrite the middle 119l .Set (3 ,& Cell {Content :"a" ,Width :1 }) 120if l [3 ].Content != "a" { 121t .Errorf ("expected 'a' at position 3, got %q" ,l [3 ].Content ) 122 } 123// The wide cell should be cleared 124if l [2 ].Content != " " { 125t .Errorf ("expected empty at position 2, got %q" ,l [2 ].Content ) 126 } 127 }, 128 }, 129 { 130name :"set wide cell at end" , 131lineLen :10 , 132x :9 , 133cell :& Cell {Content :"你" ,Width :2 }, 134validate :func (t * testing.T ,l Line ) { 135l .Set (9 ,& Cell {Content :"你" ,Width :2 }) 136if l [9 ].Content != " " { 137// We replace the wide cell with empty spaces because it 138// doesn't fit in the line. 139t .Errorf ("expected ' ' at position 9, got %q" ,l [9 ].Content ) 140 } 141 }, 142 }, 143 } 144 145for _ ,tt := range tests { 146t .Run (tt .name ,func (t * testing.T ) { 147l := make (Line ,tt .lineLen ) 148l .Set (tt .x ,tt .cell ) 149if tt .validate != nil { 150tt .validate (t ,l ) 151 } 152 }) 153 } 154} 155 156// TestLineString tests the String method of Line 157func TestLineString (t * testing.T ) { 158tests := []struct { 159name string 160setup func ()Line 161expected string 162 }{ 163 { 164name :"empty line" , 165setup :func ()Line { 166return NewLine (5 ) 167 }, 168expected :"" , 169 }, 170 { 171name :"simple text" , 172setup :func ()Line { 173l := make (Line ,5 ) 174l [0 ]= Cell {Content :"H" ,Width :1 } 175l [1 ]= Cell {Content :"e" ,Width :1 } 176l [2 ]= Cell {Content :"l" ,Width :1 } 177l [3 ]= Cell {Content :"l" ,Width :1 } 178l [4 ]= Cell {Content :"o" ,Width :1 } 179return l 180 }, 181expected :"Hello" , 182 }, 183 { 184name :"with wide characters" , 185setup :func ()Line { 186l := make (Line ,6 ) 187l [0 ]= Cell {Content :"你" ,Width :2 } 188l [1 ]= Cell {}// continuation 189l [2 ]= Cell {Content :"好" ,Width :2 } 190l [3 ]= Cell {}// continuation 191l [4 ]= Cell {Content :"!" ,Width :1 } 192l [5 ]= Cell {Content :" " ,Width :1 } 193return l 194 }, 195expected :"你好!" , 196 }, 197 { 198name :"trailing spaces trimmed" , 199setup :func ()Line { 200l := make (Line ,10 ) 201l [0 ]= Cell {Content :"H" ,Width :1 } 202l [1 ]= Cell {Content :"i" ,Width :1 } 203// Rest are empty 204return l 205 }, 206expected :"Hi" , 207 }, 208 } 209 210for _ ,tt := range tests { 211t .Run (tt .name ,func (t * testing.T ) { 212l := tt .setup () 213got := l .String () 214if got != tt .expected { 215t .Errorf ("String() = %q, want %q" ,got ,tt .expected ) 216 } 217 }) 218 } 219} 220 221// TestBufferMethods tests various Buffer methods 222func TestBufferMethods (t * testing.T ) { 223t .Run ("Width" ,func (t * testing.T ) { 224// Test empty buffer 225b := & Buffer {} 226if w := b .Width ();w != 0 { 227t .Errorf ("Width() = %d, want 0" ,w ) 228 } 229 230// Test buffer with lines 231b = NewBuffer (10 ,5 ) 232if w := b .Width ();w != 10 { 233t .Errorf ("Width() = %d, want 10" ,w ) 234 } 235 }) 236 237t .Run ("CellAt" ,func (t * testing.T ) { 238b := NewBuffer (10 ,5 ) 239b .SetCell (2 ,1 ,& Cell {Content :"X" ,Width :1 }) 240 241// Test valid position 242if c := b .CellAt (2 ,1 );c == nil || c .Content != "X" { 243t .Errorf ("CellAt(2, 1) = %v, want X" ,c ) 244 } 245 246// Test out of bounds 247if c := b .CellAt (- 1 ,0 );c != nil { 248t .Errorf ("CellAt(-1, 0) = %v, want nil" ,c ) 249 } 250if c := b .CellAt (0 ,- 1 );c != nil { 251t .Errorf ("CellAt(0, -1) = %v, want nil" ,c ) 252 } 253if c := b .CellAt (10 ,0 );c != nil { 254t .Errorf ("CellAt(10, 0) = %v, want nil" ,c ) 255 } 256if c := b .CellAt (0 ,5 );c != nil { 257t .Errorf ("CellAt(0, 5) = %v, want nil" ,c ) 258 } 259 }) 260 261t .Run ("SetCell" ,func (t * testing.T ) { 262b := NewBuffer (10 ,5 ) 263 264// Test setting cell 265b .SetCell (2 ,1 ,& Cell {Content :"A" ,Width :1 }) 266if c := b .CellAt (2 ,1 );c == nil || c .Content != "A" { 267t .Errorf ("SetCell failed, got %v" ,c ) 268 } 269 270// Test out of bounds 271b .SetCell (- 1 ,0 ,& Cell {Content :"B" ,Width :1 }) 272b .SetCell (0 ,- 1 ,& Cell {Content :"C" ,Width :1 }) 273b .SetCell (10 ,0 ,& Cell {Content :"D" ,Width :1 }) 274b .SetCell (0 ,5 ,& Cell {Content :"E" ,Width :1 }) 275// Should not panic 276 277// Test nil cell 278b .SetCell (3 ,1 ,nil ) 279// Should not panic 280 }) 281 282t .Run ("Resize" ,func (t * testing.T ) { 283b := NewBuffer (10 ,5 ) 284b .SetCell (2 ,1 ,& Cell {Content :"X" ,Width :1 }) 285 286// Resize smaller 287b .Resize (5 ,3 ) 288if b .Width ()!= 5 || b .Height ()!= 3 { 289t .Errorf ("Resize(5, 3) failed, got %dx%d" ,b .Width (),b .Height ()) 290 } 291 292// Resize larger 293b .Resize (15 ,10 ) 294if b .Width ()!= 15 || b .Height ()!= 10 { 295t .Errorf ("Resize(15, 10) failed, got %dx%d" ,b .Width (),b .Height ()) 296 } 297 298// Resize to same size 299b .Resize (15 ,10 ) 300if b .Width ()!= 15 || b .Height ()!= 10 { 301t .Errorf ("Resize(15, 10) failed, got %dx%d" ,b .Width (),b .Height ()) 302 } 303 }) 304 305t .Run ("FillArea" ,func (t * testing.T ) { 306b := NewBuffer (10 ,5 ) 307cell := & Cell {Content :"X" ,Width :1 } 308 309// Fill a rectangle 310b .FillArea (cell ,Rect (2 ,1 ,3 ,2 )) 311 312// Check filled area 313for y := 1 ;y < 3 ;y ++ { 314for x := 2 ;x < 5 ;x ++ { 315if c := b .CellAt (x ,y );c == nil || c .Content != "X" { 316t .Errorf ("FillArea failed at (%d, %d)" ,x ,y ) 317 } 318 } 319 } 320 321// Check unfilled area 322if c := b .CellAt (1 ,1 );c != nil && c .Content == "X" { 323t .Error ("FillArea filled outside rectangle" ) 324 } 325 326// Test with nil cell 327b .FillArea (nil ,Rect (0 ,0 ,2 ,2 )) 328// Should not panic 329 }) 330 331t .Run ("TouchLine" ,func (t * testing.T ) { 332b := NewRenderBuffer (10 ,5 ) 333 334// Touch valid line 335b .TouchLine (2 ,1 ,3 ) 336// Should mark line as dirty 337 338// Touch out of bounds 339b .TouchLine (0 ,- 1 ,1 ) 340b .TouchLine (0 ,5 ,1 ) 341// Should not panic 342 }) 343 344t .Run ("Touch" ,func (t * testing.T ) { 345b := NewRenderBuffer (10 ,5 ) 346b .Touch (2 ,1 ) 347// Should mark cell as dirty 348 }) 349 350t .Run ("Clear" ,func (t * testing.T ) { 351b := NewBuffer (10 ,5 ) 352b .SetCell (2 ,1 ,& Cell {Content :"X" ,Width :1 }) 353b .Clear () 354 355// Check all cells are cleared 356for y := 0 ;y < b .Height ();y ++ { 357for x := 0 ;x < b .Width ();x ++ { 358if c := b .CellAt (x ,y );c != nil && c .Content != " " { 359t .Errorf ("Clear failed at (%d, %d)" ,x ,y ) 360 } 361 } 362 } 363 }) 364 365t .Run ("Clone" ,func (t * testing.T ) { 366b := NewBuffer (10 ,5 ) 367b .SetCell (2 ,1 ,& Cell {Content :"X" ,Width :1 }) 368 369clone := b .Clone () 370 371// Check clone has same content 372if c := clone .CellAt (2 ,1 );c == nil || c .Content != "X" { 373t .Error ("Clone failed to copy content" ) 374 } 375 376// Modify clone shouldn't affect original 377clone .SetCell (2 ,1 ,& Cell {Content :"Y" ,Width :1 }) 378if c := b .CellAt (2 ,1 );c == nil || c .Content != "X" { 379t .Error ("Clone is not independent" ) 380 } 381 }) 382 383t .Run ("CloneArea" ,func (t * testing.T ) { 384b := NewBuffer (10 ,5 ) 385b .SetCell (2 ,1 ,& Cell {Content :"X" ,Width :1 }) 386b .SetCell (3 ,2 ,& Cell {Content :"Y" ,Width :1 }) 387 388clone := b .CloneArea (Rect (2 ,1 ,2 ,2 )) 389 390// Check clone has correct size 391if clone .Width ()!= 2 || clone .Height ()!= 2 { 392t .Errorf ("CloneArea size = %dx%d, want 2x2" ,clone .Width (),clone .Height ()) 393 } 394 395// Check content 396if c := clone .CellAt (0 ,0 );c == nil || c .Content != "X" { 397t .Error ("CloneArea failed to copy X" ) 398 } 399if c := clone .CellAt (1 ,1 );c == nil || c .Content != "Y" { 400t .Error ("CloneArea failed to copy Y" ) 401 } 402 }) 403 404t .Run ("Draw" ,func (t * testing.T ) { 405// Create source buffer 406src := NewBuffer (3 ,3 ) 407src .SetCell (1 ,1 ,& Cell {Content :"S" ,Width :1 }) 408 409// Create destination screen buffer (which implements Screen interface) 410dst := NewScreenBuffer (10 ,5 ) 411dst .SetCell (2 ,2 ,& Cell {Content :"D" ,Width :1 }) 412 413// Draw source onto destination at area (1,1) to (4,4) 414src .Draw (& dst ,Rect (1 ,1 ,4 ,4 )) 415 416// Check drawn content 417if c := dst .CellAt (2 ,2 );c == nil || c .Content != "S" { 418t .Error ("Draw failed to copy content" ) 419 } 420 421// Check original destination content is preserved outside draw area 422if c := dst .CellAt (0 ,0 );c != nil && c .Content != " " { 423t .Error ("Draw modified content outside draw area" ) 424 } 425 }) 426 427t .Run ("Render" ,func (t * testing.T ) { 428b := NewBuffer (5 ,2 ) 429b .SetCell (0 ,0 ,& Cell {Content :"H" ,Width :1 }) 430b .SetCell (1 ,0 ,& Cell {Content :"i" ,Width :1 }) 431b .SetCell (0 ,1 ,& Cell {Content :"!" ,Width :1 }) 432 433got := b .Render () 434 435// The exact output depends on the rendering logic 436if ! strings .Contains (got ,"Hi" ) { 437t .Errorf ("Render output doesn't contain expected text" ) 438 } 439 }) 440} 441 442// TestBufferLineOperations tests line insertion and deletion 443func TestBufferLineOperations (t * testing.T ) { 444t .Run ("InsertLine" ,func (t * testing.T ) { 445b := NewBuffer (5 ,3 ) 446b .SetCell (0 ,0 ,& Cell {Content :"A" ,Width :1 }) 447b .SetCell (0 ,1 ,& Cell {Content :"B" ,Width :1 }) 448b .SetCell (0 ,2 ,& Cell {Content :"C" ,Width :1 }) 449 450// Insert line at position 1 451b .InsertLine (1 ,1 ,nil ) 452 453// Check that B moved to line 2 454if c := b .CellAt (0 ,2 );c == nil || c .Content != "B" { 455t .Error ("InsertLine failed to move line down" ) 456 } 457 458// Check that new line is empty 459if c := b .CellAt (0 ,1 );c != nil && c .Content != " " { 460t .Error ("InsertLine didn't create empty line" ) 461 } 462 }) 463 464t .Run ("InsertLineArea" ,func (t * testing.T ) { 465b := NewBuffer (5 ,5 ) 466b .SetCell (0 ,1 ,& Cell {Content :"A" ,Width :1 }) 467b .SetCell (0 ,2 ,& Cell {Content :"B" ,Width :1 }) 468 469// Insert line in area 470b .InsertLineArea (2 ,1 ,nil ,Rect (0 ,1 ,5 ,4 )) 471 472// Check that B moved down 473if c := b .CellAt (0 ,3 );c == nil || c .Content != "B" { 474t .Error ("InsertLineArea failed to move line down" ) 475 } 476 }) 477 478t .Run ("DeleteLine" ,func (t * testing.T ) { 479b := NewBuffer (5 ,3 ) 480b .SetCell (0 ,0 ,& Cell {Content :"A" ,Width :1 }) 481b .SetCell (0 ,1 ,& Cell {Content :"B" ,Width :1 }) 482b .SetCell (0 ,2 ,& Cell {Content :"C" ,Width :1 }) 483 484// Delete line at position 1 485b .DeleteLine (1 ,1 ,nil ) 486 487// Check that C moved to line 1 488if c := b .CellAt (0 ,1 );c == nil || c .Content != "C" { 489t .Error ("DeleteLine failed to move line up" ) 490 } 491 492// Check that last line is empty 493if c := b .CellAt (0 ,2 );c != nil && c .Content != " " { 494t .Error ("DeleteLine didn't clear last line" ) 495 } 496 }) 497 498t .Run ("DeleteLineArea" ,func (t * testing.T ) { 499b := NewBuffer (5 ,5 ) 500b .SetCell (0 ,1 ,& Cell {Content :"A" ,Width :1 }) 501b .SetCell (0 ,2 ,& Cell {Content :"B" ,Width :1 }) 502b .SetCell (0 ,3 ,& Cell {Content :"C" ,Width :1 }) 503 504// Delete line in area 505b .DeleteLineArea (2 ,1 ,nil ,Rect (0 ,1 ,5 ,4 )) 506 507// Check that C moved up 508if c := b .CellAt (0 ,2 );c == nil || c .Content != "C" { 509t .Error ("DeleteLineArea failed to move line up" ) 510 } 511 }) 512} 513 514// TestBufferCellOperations tests cell insertion and deletion 515func TestBufferCellOperations (t * testing.T ) { 516t .Run ("InsertCell" ,func (t * testing.T ) { 517b := NewBuffer (5 ,2 ) 518l := b .Line (0 ) 519l [0 ]= Cell {Content :"A" ,Width :1 } 520l [1 ]= Cell {Content :"B" ,Width :1 } 521l [2 ]= Cell {Content :"C" ,Width :1 } 522 523// Insert cell at position 1 524b .InsertCell (1 ,0 ,1 ,nil ) 525 526// Check that B moved right 527if c := b .CellAt (2 ,0 );c == nil || c .Content != "B" { 528t .Error ("InsertCell failed to move cell right" ) 529 } 530 }) 531 532t .Run ("InsertCellArea" ,func (t * testing.T ) { 533b := NewBuffer (5 ,3 ) 534l := b .Line (1 ) 535l [1 ]= Cell {Content :"A" ,Width :1 } 536l [2 ]= Cell {Content :"B" ,Width :1 } 537 538// Insert cell in area 539b .InsertCellArea (1 ,1 ,1 ,nil ,Rect (1 ,1 ,4 ,2 )) 540 541// Check that A moved right 542if c := b .CellAt (2 ,1 );c == nil || c .Content != "A" { 543t .Error ("InsertCellArea failed to move cell right" ) 544 } 545 }) 546 547t .Run ("DeleteCell" ,func (t * testing.T ) { 548b := NewBuffer (5 ,2 ) 549l := b .Line (0 ) 550l [0 ]= Cell {Content :"A" ,Width :1 } 551l [1 ]= Cell {Content :"B" ,Width :1 } 552l [2 ]= Cell {Content :"C" ,Width :1 } 553 554// Delete cell at position 1 555b .DeleteCell (1 ,0 ,1 ,nil ) 556 557// Check that C moved left 558if c := b .CellAt (1 ,0 );c == nil || c .Content != "C" { 559t .Error ("DeleteCell failed to move cell left" ) 560 } 561 }) 562 563t .Run ("DeleteCellArea" ,func (t * testing.T ) { 564b := NewBuffer (5 ,3 ) 565l := b .Line (1 ) 566l [1 ]= Cell {Content :"A" ,Width :1 } 567l [2 ]= Cell {Content :"B" ,Width :1 } 568l [3 ]= Cell {Content :"C" ,Width :1 } 569 570// Delete cell in area 571b .DeleteCellArea (2 ,1 ,1 ,nil ,Rect (1 ,1 ,4 ,2 )) 572 573// Check that C moved left 574if c := b .CellAt (2 ,1 );c == nil || c .Content != "C" { 575t .Error ("DeleteCellArea failed to move cell left" ) 576 } 577 }) 578} 579 580// TestScreenBuffer tests ScreenBuffer specific functionality 581func TestScreenBuffer (t * testing.T ) { 582t .Run ("NewScreenBuffer" ,func (t * testing.T ) { 583sb := NewScreenBuffer (10 ,5 ) 584if sb .Width ()!= 10 || sb .Height ()!= 5 { 585t .Errorf ("NewScreenBuffer size = %dx%d, want 10x5" ,sb .Width (),sb .Height ()) 586 } 587 }) 588 589t .Run ("WidthMethod" ,func (t * testing.T ) { 590sb := NewScreenBuffer (10 ,5 ) 591 592// Get the default width method 593method := sb .WidthMethod () 594if method == nil { 595t .Error ("WidthMethod returned nil" ) 596 } 597 598// The method should be set to ansi.WcWidth by default 599// We can test it works by checking a simple character 600if width := method .StringWidth ("a" );width != 1 { 601t .Errorf ("Default width method returned %d for 'a', want 1" ,width ) 602 } 603 }) 604} 605 606// TestLineRenderLine tests the renderLine function 607func TestLineRenderLine (t * testing.T ) { 608tests := []struct { 609name string 610setup func ()Line 611validate func (t * testing.T ,output string ) 612 }{ 613 { 614name :"render with styles" , 615setup :func ()Line { 616l := make (Line ,5 ) 617l [0 ]= Cell {Content :"H" ,Width :1 ,Style :Style {Fg :ansi .Red }} 618l [1 ]= Cell {Content :"i" ,Width :1 } 619return l 620 }, 621validate :func (t * testing.T ,output string ) { 622// Should contain style sequences 623if ! strings .Contains (output ,"H" )|| ! strings .Contains (output ,"i" ) { 624t .Error ("renderLine failed to include content" ) 625 } 626 }, 627 }, 628 { 629name :"render with hyperlink" , 630setup :func ()Line { 631l := make (Line ,5 ) 632l [0 ]= Cell {Content :"L" ,Width :1 ,Link :Link {URL :"http://example.com" }} 633l [1 ]= Cell {Content :"i" ,Width :1 ,Link :Link {URL :"http://example.com" }} 634l [2 ]= Cell {Content :"n" ,Width :1 ,Link :Link {URL :"http://example.com" }} 635l [3 ]= Cell {Content :"k" ,Width :1 ,Link :Link {URL :"http://example.com" }} 636return l 637 }, 638validate :func (t * testing.T ,output string ) { 639if ! strings .Contains (output ,"Link" ) { 640t .Error ("renderLine failed to include link content" ) 641 } 642 }, 643 }, 644 } 645 646for _ ,tt := range tests { 647t .Run (tt .name ,func (t * testing.T ) { 648l := tt .setup () 649var sb strings.Builder 650renderLine (& sb ,l ) 651output := sb .String () 652if tt .validate != nil { 653tt .validate (t ,output ) 654 } 655 }) 656 } 657} 658 659func TestRenderLinePreservesTrailingSpaces (t * testing.T ) { 660// renderLine should preserve trailing empty cells as spaces. 661// A line of width 10 with "Hi" at positions 0-1 and empty cells at 2-9 662// should render all 10 visible characters: "Hi" followed by 8 spaces. 663l := NewLine (10 ) 664l [0 ]= Cell {Content :"H" ,Width :1 } 665l [1 ]= Cell {Content :"i" ,Width :1 } 666// Positions 2-9 remain EmptyCell (space, width 1, no style) 667 668output := l .Render () 669 670// Strip any ANSI sequences to count visible characters. 671visible := ansi .Strip (output ) 672if len (visible )!= 10 { 673t .Errorf ("Render() produced %d visible chars, want 10; output=%q visible=%q" , 674len (visible ),output ,visible ) 675 } 676} 677 678func TestRenderLinePreservesStyledTrailingSpaces (t * testing.T ) { 679// When trailing cells have explicit styling (e.g. background color), 680// renderLine must emit them with their ANSI attributes, not drop them. 681l := NewLine (5 ) 682l [0 ]= Cell {Content :"A" ,Width :1 } 683// Positions 1-4: spaces with a red background. 684for i := 1 ;i < 5 ;i ++ { 685l [i ]= Cell {Content :" " ,Width :1 ,Style :Style {Bg :ansi .Red }} 686 } 687 688output := l .Render () 689 690visible := ansi .Strip (output ) 691if len (visible )!= 5 { 692t .Errorf ("Render() produced %d visible chars, want 5; output=%q visible=%q" , 693len (visible ),output ,visible ) 694 } 695// The output must contain the background color sequence for the trailing 696// spaces. Derive the expected sequence from the style itself rather than 697// hardcoding an SGR number. 698red := Style {Bg :ansi .Red } 699wantSeq := red .Diff (& Style {}) 700if ! strings .Contains (output ,wantSeq ) { 701t .Errorf ("Render() missing background SGR %q for trailing styled spaces; output=%q" ,wantSeq ,output ) 702 } 703} 704 705func TestRenderLineWideCellTail (t * testing.T ) { 706// A line ending in a wide cell's continuation column (a zero cell) must 707// not disturb pending empty cells: the zero cell is skipped, and the 708// buffered spaces are still flushed at end of line. 709l := NewLine (6 ) 710l [0 ]= Cell {Content :"A" ,Width :1 } 711// Positions 1-3 remain EmptyCell. 712l [4 ]= Cell {Content :"你" ,Width :2 } 713l [5 ]= Cell {}// continuation column of the wide cell 714 715output := l .Render () 716 717visible := ansi .Strip (output ) 718// "A", 3 buffered spaces, then the wide grapheme. The continuation 719// column contributes nothing. 720if want := "A 你" ;visible != want { 721t .Errorf ("Render() = %q, want %q; output=%q" ,visible ,want ,output ) 722 } 723if w := uniseg .StringWidth (visible );w != 6 { 724t .Errorf ("Render() visible width = %d, want 6; output=%q" ,w ,output ) 725 } 726} 727 728func BenchmarkBufferSetCell (b * testing.B ) { 729buf := NewBuffer (80 ,24 ) 730cell := & Cell {Content :"A" ,Width :1 } 731b .ResetTimer () 732for i := 0 ;i < b .N ;i ++ { 733x := i % 80 734y := (i / 80 )% 24 735buf .SetCell (x ,y ,cell ) 736 } 737} 738 739func BenchmarkBufferResize (b * testing.B ) { 740buf := NewBuffer (80 ,24 ) 741b .ResetTimer () 742for i := 0 ;i < b .N ;i ++ { 743width := 80 + (i % 20 ) 744height := 24 + (i % 10 ) 745buf .Resize (width ,height ) 746bufWidth ,bufHeight := buf .Width (),buf .Height () 747if bufWidth != width || bufHeight != height { 748b .Errorf ("Resize failed: got %dx%d, want %dx%d" ,bufWidth ,bufHeight ,width ,height ) 749 } 750 } 751} 752 753func width (s string )int { 754width := 0 755for _ ,line := range strings .Split (s ,"\n" ) { 756width = max (width ,ansi .StringWidth (line )) 757 } 758return width 759} 760 761func height (s string )int { 762return strings .Count (s ,"\n" )+ 1 763}