2025-08-04 21:23:22 -07:00
#![ cfg(feature = " vt100-tests " ) ]
#![ expect(clippy::expect_used) ]
2025-09-26 16:35:56 -07:00
use crate ::test_backend ::VT100Backend ;
2025-08-04 21:23:22 -07:00
use ratatui ::layout ::Rect ;
2025-09-02 16:19:54 -07:00
use ratatui ::style ::Stylize ;
2025-08-04 21:23:22 -07:00
use ratatui ::text ::Line ;
// Small helper macro to assert a collection contains an item with a clearer
// failure message.
macro_rules ! assert_contains {
( $collection :expr , $item :expr $(, ) ? ) = > {
assert! (
$collection . contains ( & $item ) ,
" Expected {:?} to contain {:?} " ,
$collection ,
$item
) ;
} ;
( $collection :expr , $item :expr , $( $arg :tt ) + ) = > {
assert! ( $collection . contains ( & $item ) , $( $arg ) + ) ;
} ;
}
struct TestScenario {
2025-09-26 16:35:56 -07:00
term : codex_tui ::custom_terminal ::Terminal < VT100Backend > ,
2025-08-04 21:23:22 -07:00
}
impl TestScenario {
fn new ( width : u16 , height : u16 , viewport : Rect ) -> Self {
2025-09-26 16:35:56 -07:00
let backend = VT100Backend ::new ( width , height ) ;
2025-08-04 21:23:22 -07:00
let mut term = codex_tui ::custom_terminal ::Terminal ::with_options ( backend )
. expect ( " failed to construct terminal " ) ;
term . set_viewport_area ( viewport ) ;
2025-09-26 16:35:56 -07:00
Self { term }
2025-08-04 21:23:22 -07:00
}
2025-09-26 16:35:56 -07:00
fn run_insert ( & mut self , lines : Vec < Line < 'static > > ) {
codex_tui ::insert_history ::insert_history_lines ( & mut self . term , lines ) ;
2025-08-04 21:23:22 -07:00
}
}
#[ test ]
2025-08-12 17:37:28 -07:00
fn basic_insertion_no_wrap ( ) {
2025-08-04 21:23:22 -07:00
// Screen of 20x6; viewport is the last row (height=1 at y=5)
let area = Rect ::new ( 0 , 5 , 20 , 1 ) ;
let mut scenario = TestScenario ::new ( 20 , 6 , area ) ;
2025-09-02 16:19:54 -07:00
let lines = vec! [ " first " . into ( ) , " second " . into ( ) ] ;
2025-09-26 16:35:56 -07:00
scenario . run_insert ( lines ) ;
let rows = scenario . term . backend ( ) . vt100 ( ) . screen ( ) . contents ( ) ;
2025-08-04 21:23:22 -07:00
assert_contains! ( rows , String ::from ( " first " ) ) ;
assert_contains! ( rows , String ::from ( " second " ) ) ;
}
#[ test ]
2025-08-12 17:37:28 -07:00
fn long_token_wraps ( ) {
2025-08-04 21:23:22 -07:00
let area = Rect ::new ( 0 , 5 , 20 , 1 ) ;
let mut scenario = TestScenario ::new ( 20 , 6 , area ) ;
let long = " A " . repeat ( 45 ) ; // > 2 lines at width 20
2025-09-02 16:19:54 -07:00
let lines = vec! [ long . clone ( ) . into ( ) ] ;
2025-09-26 16:35:56 -07:00
scenario . run_insert ( lines ) ;
let screen = scenario . term . backend ( ) . vt100 ( ) . screen ( ) ;
2025-08-04 21:23:22 -07:00
// Count total A's on the screen
let mut count_a = 0 usize ;
for row in 0 .. 6 {
for col in 0 .. 20 {
2025-08-19 13:22:02 -07:00
if let Some ( cell ) = screen . cell ( row , col )
& & let Some ( ch ) = cell . contents ( ) . chars ( ) . next ( )
& & ch = = 'A'
{
count_a + = 1 ;
2025-08-04 21:23:22 -07:00
}
}
}
assert_eq! (
count_a ,
long . len ( ) ,
" wrapped content did not preserve all characters "
) ;
}
#[ test ]
2025-08-12 17:37:28 -07:00
fn emoji_and_cjk ( ) {
2025-08-04 21:23:22 -07:00
let area = Rect ::new ( 0 , 5 , 20 , 1 ) ;
let mut scenario = TestScenario ::new ( 20 , 6 , area ) ;
let text = String ::from ( " 😀😀😀😀😀 你好世界 " ) ;
2025-09-02 16:19:54 -07:00
let lines = vec! [ text . clone ( ) . into ( ) ] ;
2025-09-26 16:35:56 -07:00
scenario . run_insert ( lines ) ;
let rows = scenario . term . backend ( ) . vt100 ( ) . screen ( ) . contents ( ) ;
2025-08-04 21:23:22 -07:00
for ch in text . chars ( ) . filter ( | c | ! c . is_whitespace ( ) ) {
assert! (
2025-09-26 16:35:56 -07:00
rows . contains ( ch ) ,
2025-08-04 21:23:22 -07:00
" missing character {ch:?} in reconstructed screen "
) ;
}
}
#[ test ]
2025-08-12 17:37:28 -07:00
fn mixed_ansi_spans ( ) {
2025-08-04 21:23:22 -07:00
let area = Rect ::new ( 0 , 5 , 20 , 1 ) ;
let mut scenario = TestScenario ::new ( 20 , 6 , area ) ;
2025-09-02 16:19:54 -07:00
let line = vec! [ " red " . red ( ) , " +plain " . into ( ) ] . into ( ) ;
2025-09-26 16:35:56 -07:00
scenario . run_insert ( vec! [ line ] ) ;
let rows = scenario . term . backend ( ) . vt100 ( ) . screen ( ) . contents ( ) ;
2025-08-04 21:23:22 -07:00
assert_contains! ( rows , String ::from ( " red+plain " ) ) ;
}
#[ test ]
2025-08-12 17:37:28 -07:00
fn cursor_restoration ( ) {
2025-08-04 21:23:22 -07:00
let area = Rect ::new ( 0 , 5 , 20 , 1 ) ;
let mut scenario = TestScenario ::new ( 20 , 6 , area ) ;
2025-09-02 16:19:54 -07:00
let lines = vec! [ " x " . into ( ) ] ;
2025-09-26 16:35:56 -07:00
scenario . run_insert ( lines ) ;
assert_eq! ( scenario . term . last_known_cursor_pos , ( 0 , 0 ) . into ( ) ) ;
2025-08-04 21:23:22 -07:00
}
#[ test ]
2025-08-12 17:37:28 -07:00
fn word_wrap_no_mid_word_split ( ) {
// Screen of 40x10; viewport is the last row
let area = Rect ::new ( 0 , 9 , 40 , 1 ) ;
let mut scenario = TestScenario ::new ( 40 , 10 , area ) ;
let sample = " Years passed, and Willowmere thrived in peace and friendship. Mira’ s herb garden flourished with both ordinary and enchanted plants, and travelers spoke of the kindness of the woman who tended them. " ;
2025-09-26 16:35:56 -07:00
scenario . run_insert ( vec! [ sample . into ( ) ] ) ;
let joined = scenario . term . backend ( ) . vt100 ( ) . screen ( ) . contents ( ) ;
2025-08-12 17:37:28 -07:00
assert! (
! joined . contains ( " bo \n th " ) ,
" word 'both' should not be split across lines: \n {joined} "
) ;
}
#[ test ]
fn em_dash_and_space_word_wrap ( ) {
// Repro from report: ensure we break before "inside", not mid-word.
let area = Rect ::new ( 0 , 9 , 40 , 1 ) ;
let mut scenario = TestScenario ::new ( 40 , 10 , area ) ;
let sample = " Mara found an old key on the shore. Curious, she opened a tarnished box half-buried in sand—and inside lay a single, glowing seed. " ;
2025-09-26 16:35:56 -07:00
scenario . run_insert ( vec! [ sample . into ( ) ] ) ;
let joined = scenario . term . backend ( ) . vt100 ( ) . screen ( ) . contents ( ) ;
2025-08-12 17:37:28 -07:00
assert! (
! joined . contains ( " insi \n de " ) ,
" word 'inside' should not be split across lines: \n {joined} "
) ;
}