boxmux 0.240.3373

YAML-driven terminal UI framework for rich, interactive CLI applications and dashboards with PTY support
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
//! F0380: Visual Scroll Knob Drag Tests
//! Comprehensive visual tests to reproduce and verify the scroll knob dragging issue
//! These tests demonstrate that scroll knob dragging is currently not working

#[cfg(test)]
mod visual_scroll_knob_drag_tests {
    use crate::tests::visual_testing::{BoxMuxTester, TestConfig};
    use std::time::Duration;

    /// F0380: Create YAML configuration for vertical scroll knob testing
    fn create_vertical_scroll_test_yaml() -> &'static str {
        r#"
muxboxes:
  - id: "vertical_scroll_test"
    position:
      x1: "5"
      y1: "3"
      x2: "35"
      y2: "18"
    style:
      background_color: "white"
      text_color: "black" 
      border_color: "blue"
      overflow_behavior: "scroll"
    choices:
      - id: "choice_1"
        content: "Choice 1 - First item in scrollable list"
      - id: "choice_2"
        content: "Choice 2 - Second item"
      - id: "choice_3" 
        content: "Choice 3 - Third item"
      - id: "choice_4"
        content: "Choice 4 - Fourth item"
      - id: "choice_5"
        content: "Choice 5 - Fifth item"
      - id: "choice_6"
        content: "Choice 6 - Sixth item"
      - id: "choice_7"
        content: "Choice 7 - Seventh item"
      - id: "choice_8"
        content: "Choice 8 - Eighth item"
      - id: "choice_9"
        content: "Choice 9 - Ninth item"
      - id: "choice_10"
        content: "Choice 10 - Tenth item"
      - id: "choice_11"
        content: "Choice 11 - Eleventh item"
      - id: "choice_12"
        content: "Choice 12 - Twelfth item"
      - id: "choice_13"
        content: "Choice 13 - Thirteenth item"
      - id: "choice_14"
        content: "Choice 14 - Fourteenth item"
      - id: "choice_15"
        content: "Choice 15 - Fifteenth item"
      - id: "choice_16"
        content: "Choice 16 - Sixteenth item"
      - id: "choice_17"
        content: "Choice 17 - Seventeenth item"
      - id: "choice_18"
        content: "Choice 18 - Eighteenth item"
      - id: "choice_19"
        content: "Choice 19 - Nineteenth item"
      - id: "choice_20"
        content: "Choice 20 - Twentieth item"
"#
    }

    /// F0380: Create YAML configuration for horizontal scroll knob testing
    fn create_horizontal_scroll_test_yaml() -> &'static str {
        r#"
muxboxes:
  - id: "horizontal_scroll_test"
    position:
      x1: "5"
      y1: "5"
      x2: "25"
      y2: "15"
    style:
      background_color: "white"
      text_color: "black"
      border_color: "green"
      overflow_behavior: "scroll"
    choices:
      - id: "long_choice_1"
        content: "This is an extremely long choice that should exceed the narrow muxbox width and trigger horizontal scrollbar generation - Choice 1"
      - id: "long_choice_2"
        content: "Another very long choice text that definitely exceeds the container width and should force horizontal scrolling - Choice 2"
      - id: "long_choice_3"
        content: "Yet another extremely long choice that is much wider than the container and will require horizontal scrolling - Choice 3"
      - id: "long_choice_4"
        content: "A fourth very long choice that extends way beyond the muxbox boundaries and necessitates horizontal scrolling - Choice 4"
      - id: "long_choice_5"
        content: "Fifth long choice with extensive text that overflows the narrow container width requiring horizontal scroll - Choice 5"
"#
    }

    /// F0380: Create YAML for combined vertical and horizontal scrolling test
    fn create_combined_scroll_test_yaml() -> &'static str {
        r#"
muxboxes:
  - id: "combined_scroll_test"
    position:
      x1: "2"
      y1: "2"
      x2: "28"
      y2: "12"
    style:
      background_color: "white"
      text_color: "black"
      border_color: "red"
      overflow_behavior: "scroll"
    choices:
      - id: "combo_1"
        content: "Very long choice 1 that exceeds width and there are many choices to exceed height - Item 1"
      - id: "combo_2"
        content: "Very long choice 2 that exceeds width and there are many choices to exceed height - Item 2"
      - id: "combo_3"
        content: "Very long choice 3 that exceeds width and there are many choices to exceed height - Item 3"
      - id: "combo_4"
        content: "Very long choice 4 that exceeds width and there are many choices to exceed height - Item 4"
      - id: "combo_5"
        content: "Very long choice 5 that exceeds width and there are many choices to exceed height - Item 5"
      - id: "combo_6"
        content: "Very long choice 6 that exceeds width and there are many choices to exceed height - Item 6"
      - id: "combo_7"
        content: "Very long choice 7 that exceeds width and there are many choices to exceed height - Item 7"
      - id: "combo_8"
        content: "Very long choice 8 that exceeds width and there are many choices to exceed height - Item 8"
      - id: "combo_9"
        content: "Very long choice 9 that exceeds width and there are many choices to exceed height - Item 9"
      - id: "combo_10"
        content: "Very long choice 10 that exceeds width and there are many choices to exceed height - Item 10"
      - id: "combo_11"
        content: "Very long choice 11 that exceeds width and there are many choices to exceed height - Item 11"
      - id: "combo_12"
        content: "Very long choice 12 that exceeds width and there are many choices to exceed height - Item 12"
      - id: "combo_13"
        content: "Very long choice 13 that exceeds width and there are many choices to exceed height - Item 13"
      - id: "combo_14"
        content: "Very long choice 14 that exceeds width and there are many choices to exceed height - Item 14"
      - id: "combo_15"
        content: "Very long choice 15 that exceeds width and there are many choices to exceed height - Item 15"
"#
    }

    #[test]
    fn test_vertical_scroll_knob_drag_functionality() {
        // F0380: Test that demonstrates vertical scroll knob dragging issue
        // This test should FAIL currently because scroll knob dragging is not working

        let mut tester = BoxMuxTester::with_config(TestConfig {
            terminal_size: (80, 25),
            operation_timeout: Duration::from_secs(2),
            capture_history: true,
            ..Default::default()
        });

        // Load configuration with many choices that will require vertical scrolling
        tester.load_config_from_string(create_vertical_scroll_test_yaml())
            .expect("Should load vertical scroll test config");

        // Capture initial frame - should show scrollbar on right border
        let initial_frame = tester.wait_for_frame().expect("Should capture initial frame");

        // Verify scrollbar is present (right border should have track characters)
        let mut scrollbar_found = false;
        let right_edge = 35; // Right border at x=35
        for y in 4..=17 { // Inside the box borders
            let cell = initial_frame.get_cell(right_edge, y);
            if cell.ch == '' || cell.ch == '' || cell.ch == '' || cell.ch == '' {
                scrollbar_found = true;
                break;
            }
        }
        assert!(scrollbar_found, "Vertical scrollbar should be visible with overflowing choices");

        // Initial state: should show first few choices (Choice 1, Choice 2, etc.)
        let initial_content = initial_frame.get_content_string();
        assert!(initial_content.contains("Choice 1"), "Should initially show Choice 1 at top");
        assert!(initial_content.contains("Choice 2"), "Should initially show Choice 2");
        assert!(!initial_content.contains("Choice 20"), "Should not show Choice 20 at initial scroll position");

        // Attempt to drag vertical scroll knob from top (25%) to middle (50%)
        // This drag should move the scroll position and show different choices
        let drag_start_y = 7;  // About 25% down the scrollbar track
        let drag_end_y = 11;   // About 50% down the scrollbar track

        println!("=== TESTING VERTICAL SCROLL KNOB DRAG ===");
        println!("Initial frame content preview:");
        initial_frame.print_debug_preview();
        
        // Perform the drag operation
        tester.drag_from_to(right_edge, drag_start_y, right_edge, drag_end_y)
            .expect("Should perform drag operation");

        // Capture frame after drag
        let after_drag_frame = tester.wait_for_frame().expect("Should capture frame after drag");
        
        println!("After drag frame content preview:");
        after_drag_frame.print_debug_preview();

        // EXPECTED BEHAVIOR: After dragging to middle position, should show middle choices
        let after_drag_content = after_drag_frame.get_content_string();

        // This assertion should FAIL currently - demonstrating the bug
        // After dragging to 50% position, we should see middle choices (around Choice 10-12)
        let shows_middle_choices = after_drag_content.contains("Choice 10") || 
                                   after_drag_content.contains("Choice 11") ||
                                   after_drag_content.contains("Choice 12");

        // This test documents the current broken behavior
        if !shows_middle_choices {
            println!("❌ SCROLL KNOB DRAG BUG CONFIRMED:");
            println!("   - Dragged from Y={} to Y={}", drag_start_y, drag_end_y);
            println!("   - Expected to see middle choices (Choice 10-12)");
            println!("   - Still showing same content as before drag");
            println!("   - Scroll knob dragging is not working");
            
            // Verify that content didn't change
            assert!(after_drag_content.contains("Choice 1"), 
                "Content unchanged - drag didn't work (still shows Choice 1)");
        } else {
            println!("✅ Scroll knob drag is working correctly");
        }

        // Test dragging to bottom position (should show last choices)
        let drag_to_bottom_y = 16; // Near bottom of track
        
        tester.drag_from_to(right_edge, drag_end_y, right_edge, drag_to_bottom_y)
            .expect("Should perform second drag operation");

        let bottom_drag_frame = tester.wait_for_frame().expect("Should capture frame after bottom drag");
        let bottom_content = bottom_drag_frame.get_content_string();

        // After dragging to bottom, should show last choices
        let shows_last_choices = bottom_content.contains("Choice 18") || 
                                bottom_content.contains("Choice 19") || 
                                bottom_content.contains("Choice 20");

        if !shows_last_choices {
            println!("❌ SCROLL TO BOTTOM BUG CONFIRMED:");
            println!("   - Dragged to bottom position Y={}", drag_to_bottom_y);
            println!("   - Expected to see last choices (Choice 18-20)");
            println!("   - Still showing same content");
        }

        // Save snapshots for debugging
        tester.save_snapshot("vertical_scroll_initial").expect("Should save initial snapshot");
        
        // This test currently demonstrates the bug - uncomment when bug is fixed
        // assert!(shows_middle_choices, "Scroll knob drag should change visible choices");
        // assert!(shows_last_choices, "Drag to bottom should show last choices");
    }

    #[test] 
    fn test_horizontal_scroll_knob_drag_functionality() {
        // F0380: Test that demonstrates horizontal scroll knob dragging issue
        // This test should FAIL currently because horizontal scroll knob dragging is not working

        let mut tester = BoxMuxTester::with_config(TestConfig {
            terminal_size: (80, 25),
            operation_timeout: Duration::from_secs(2),
            capture_history: true,
            ..Default::default()
        });

        // Load configuration with long choices that will require horizontal scrolling
        tester.load_config_from_string(create_horizontal_scroll_test_yaml())
            .expect("Should load horizontal scroll test config");

        // Capture initial frame - should show horizontal scrollbar at bottom
        let initial_frame = tester.wait_for_frame().expect("Should capture initial frame");

        // Verify horizontal scrollbar is present (bottom border should have track characters)
        let mut h_scrollbar_found = false;
        let bottom_edge = 15; // Bottom border at y=15
        for x in 6..=24 { // Inside the box borders
            let cell = initial_frame.get_cell(x, bottom_edge);
            if cell.ch == '' || cell.ch == '' || cell.ch == '' || cell.ch == '' {
                h_scrollbar_found = true;
                break;
            }
        }
        assert!(h_scrollbar_found, "Horizontal scrollbar should be visible with overflowing choice text");

        // Initial state: should show beginning of long choice text
        let initial_content = initial_frame.get_content_string();
        assert!(initial_content.contains("This is an extremely"), "Should show start of long choice text");
        assert!(!initial_content.contains("- Choice 1"), "Should not show end of choice text initially");

        // Attempt to drag horizontal scroll knob from left (20%) to middle (60%)
        let drag_start_x = 10; // About 20% across the scrollbar track
        let drag_end_x = 18;   // About 60% across the scrollbar track

        println!("=== TESTING HORIZONTAL SCROLL KNOB DRAG ===");
        println!("Initial frame content preview:");
        initial_frame.print_debug_preview();
        
        // Perform the horizontal drag operation
        tester.drag_from_to(drag_start_x, bottom_edge, drag_end_x, bottom_edge)
            .expect("Should perform horizontal drag operation");

        // Capture frame after drag
        let after_drag_frame = tester.wait_for_frame().expect("Should capture frame after horizontal drag");
        
        println!("After horizontal drag frame content preview:");
        after_drag_frame.print_debug_preview();

        // EXPECTED BEHAVIOR: After dragging to middle position, should show middle part of choice text
        let after_drag_content = after_drag_frame.get_content_string();

        // This assertion should FAIL currently - demonstrating the horizontal scroll bug
        // After dragging to 60% position, we should see middle/end part of the long text
        let shows_middle_text = after_drag_content.contains("trigger horizontal") || 
                               after_drag_content.contains("scrollbar generation") ||
                               after_drag_content.contains("- Choice 1");

        // This test documents the current broken behavior
        if !shows_middle_text {
            println!("❌ HORIZONTAL SCROLL KNOB DRAG BUG CONFIRMED:");
            println!("   - Dragged from X={} to X={}", drag_start_x, drag_end_x);
            println!("   - Expected to see middle/end of choice text");
            println!("   - Still showing same content as before drag");
            println!("   - Horizontal scroll knob dragging is not working");
            
            // Verify that content didn't change
            assert!(after_drag_content.contains("This is an extremely"), 
                "Content unchanged - horizontal drag didn't work");
        } else {
            println!("✅ Horizontal scroll knob drag is working correctly");
        }

        // Test dragging to far right position (should show end of text)
        let drag_to_right_x = 23; // Near right end of track
        
        tester.drag_from_to(drag_end_x, bottom_edge, drag_to_right_x, bottom_edge)
            .expect("Should perform second horizontal drag operation");

        let right_drag_frame = tester.wait_for_frame().expect("Should capture frame after right drag");
        let right_content = right_drag_frame.get_content_string();

        // After dragging to right, should show end of choice text
        let shows_end_text = right_content.contains("- Choice 1") || 
                            right_content.contains("Choice 2") ||
                            right_content.contains("Choice 3");

        if !shows_end_text {
            println!("❌ SCROLL TO RIGHT BUG CONFIRMED:");
            println!("   - Dragged to right position X={}", drag_to_right_x);
            println!("   - Expected to see end of choice text");
            println!("   - Still showing same content");
        }

        // Save snapshots for debugging
        tester.save_snapshot("horizontal_scroll_initial").expect("Should save horizontal scroll snapshot");
        
        // This test currently demonstrates the bug - uncomment when bug is fixed
        // assert!(shows_middle_text, "Horizontal scroll knob drag should change visible text");
        // assert!(shows_end_text, "Drag to right should show end of choice text");
    }

    #[test]
    fn test_combined_vertical_and_horizontal_scroll_knob_drag() {
        // F0380: Test combined scrolling scenario - both vertical and horizontal scrollbars present
        // This is the most complex case where both scroll knobs should work independently

        let mut tester = BoxMuxTester::with_config(TestConfig {
            terminal_size: (80, 25),
            operation_timeout: Duration::from_secs(3),
            capture_history: true,
            ..Default::default()
        });

        // Load configuration with both long choices and many choices
        tester.load_config_from_string(create_combined_scroll_test_yaml())
            .expect("Should load combined scroll test config");

        // Capture initial frame - should show both scrollbars
        let initial_frame = tester.wait_for_frame().expect("Should capture initial frame");

        // Verify both scrollbars are present
        let mut v_scrollbar_found = false;
        let mut h_scrollbar_found = false;
        
        let right_edge = 28; // Right border
        let bottom_edge = 12; // Bottom border
        
        // Check for vertical scrollbar
        for y in 3..=11 {
            let cell = initial_frame.get_cell(right_edge, y);
            if cell.ch == '' || cell.ch == '' {
                v_scrollbar_found = true;
                break;
            }
        }
        
        // Check for horizontal scrollbar
        for x in 3..=27 {
            let cell = initial_frame.get_cell(x, bottom_edge);
            if cell.ch == '' || cell.ch == '' {
                h_scrollbar_found = true;
                break;
            }
        }

        assert!(v_scrollbar_found, "Vertical scrollbar should be present in combined scroll test");
        assert!(h_scrollbar_found, "Horizontal scrollbar should be present in combined scroll test");

        println!("=== TESTING COMBINED SCROLL KNOB DRAG ===");
        println!("Initial frame with both scrollbars:");
        initial_frame.print_debug_preview();

        let initial_content = initial_frame.get_content_string();
        
        // Test vertical drag first
        let v_drag_start_y = 5;  // Top of vertical track
        let v_drag_end_y = 9;    // Middle of vertical track
        
        tester.drag_from_to(right_edge, v_drag_start_y, right_edge, v_drag_end_y)
            .expect("Should perform vertical drag in combined test");

        let after_v_drag_frame = tester.wait_for_frame().expect("Should capture after vertical drag");
        let after_v_drag_content = after_v_drag_frame.get_content_string();

        // Vertical drag should show different choices
        let v_drag_worked = !initial_content.eq(&after_v_drag_content);
        
        if !v_drag_worked {
            println!("❌ VERTICAL DRAG FAILED in combined scroll scenario");
        }

        // Test horizontal drag
        let h_drag_start_x = 8;  // Left of horizontal track
        let h_drag_end_x = 20;   // Right of horizontal track
        
        tester.drag_from_to(h_drag_start_x, bottom_edge, h_drag_end_x, bottom_edge)
            .expect("Should perform horizontal drag in combined test");

        let after_h_drag_frame = tester.wait_for_frame().expect("Should capture after horizontal drag");
        let after_h_drag_content = after_h_drag_frame.get_content_string();

        // Horizontal drag should show different part of text
        let h_drag_worked = !after_v_drag_content.eq(&after_h_drag_content);
        
        if !h_drag_worked {
            println!("❌ HORIZONTAL DRAG FAILED in combined scroll scenario");
        }

        // Test corner case: drag both scrollbars to different positions
        tester.drag_from_to(right_edge, v_drag_end_y, right_edge, 10)
            .expect("Should perform second vertical drag");
        
        tester.drag_from_to(h_drag_end_x, bottom_edge, 15, bottom_edge)
            .expect("Should perform second horizontal drag");

        let final_frame = tester.wait_for_frame().expect("Should capture final frame");
        
        println!("Final frame after combined drags:");
        final_frame.print_debug_preview();

        // Save snapshot for debugging
        tester.save_snapshot("combined_scroll_test").expect("Should save combined scroll snapshot");

        // Document current state - these should pass when bug is fixed
        // assert!(v_drag_worked, "Vertical scroll knob should work in combined scenario");
        // assert!(h_drag_worked, "Horizontal scroll knob should work in combined scenario");
    }

    #[test]
    fn test_scroll_knob_position_calculation() {
        // F0380: Test that scroll knob position calculation is correct
        // This test verifies the math behind scroll knob positioning

        let mut tester = BoxMuxTester::with_config(TestConfig {
            terminal_size: (80, 25),
            operation_timeout: Duration::from_secs(1),
            capture_history: true,
            ..Default::default()
        });

        tester.load_config_from_string(create_vertical_scroll_test_yaml())
            .expect("Should load config for knob position test");

        let frame = tester.wait_for_frame().expect("Should capture frame");

        // Calculate expected scroll knob position based on content
        // With 20 choices and ~10 visible slots, knob should be at specific position
        let box_height = 15; // y2(18) - y1(3) - borders = 13 content rows
        let total_choices = 20;
        let visible_choices = box_height - 2; // Minus borders
        let content_height = total_choices;
        let track_height = box_height - 2; // Track inside borders

        // At 0% scroll, knob should be at top
        let knob_size = (visible_choices as f64 / content_height as f64 * track_height as f64).max(1.0);
        let knob_position_at_0_percent = 0.0;

        println!("=== SCROLL KNOB POSITION CALCULATION TEST ===");
        println!("Box height: {}", box_height);
        println!("Total choices: {}", total_choices);
        println!("Visible choices: {}", visible_choices);
        println!("Track height: {}", track_height);
        println!("Expected knob size: {:.1}", knob_size);
        println!("Expected knob position at 0%: {:.1}", knob_position_at_0_percent);

        // Verify knob is drawn at expected position
        let right_edge = 35;
        let knob_y = 4; // Should be at top of track for 0% scroll
        
        let knob_cell = frame.get_cell(right_edge, knob_y);
        let has_knob_char = knob_cell.ch == '' || knob_cell.ch == '' || knob_cell.ch == '';
        
        if !has_knob_char {
            println!("❌ KNOB NOT FOUND at expected position ({}, {})", right_edge, knob_y);
            println!("   Found character: '{}' (expected knob character)", knob_cell.ch);
        }

        // Test that clicking at different track positions would calculate correct percentages
        let track_start_y = 4;
        let track_end_y = 17;
        let actual_track_height = track_end_y - track_start_y;

        // Click at 25% down the track
        let click_25_percent_y = track_start_y + (actual_track_height / 4);
        let calculated_25_percent = ((click_25_percent_y - track_start_y) as f64 / actual_track_height as f64) * 100.0;
        
        println!("25% click position: Y={}, calculated: {:.1}%", click_25_percent_y, calculated_25_percent);
        assert!((calculated_25_percent - 25.0).abs() < 10.0, "25% position calculation should be approximately correct");

        // Click at 75% down the track
        let click_75_percent_y = track_start_y + (actual_track_height * 3 / 4);
        let calculated_75_percent = ((click_75_percent_y - track_start_y) as f64 / actual_track_height as f64) * 100.0;
        
        println!("75% click position: Y={}, calculated: {:.1}%", click_75_percent_y, calculated_75_percent);
        assert!((calculated_75_percent - 75.0).abs() < 10.0, "75% position calculation should be approximately correct");

        frame.print_debug_preview();
        tester.save_snapshot("scroll_knob_position_test").expect("Should save position test snapshot");
    }

    #[test]
    fn test_scroll_knob_click_vs_drag_behavior() {
        // F0380: Test the difference between clicking scrollbar track vs dragging knob
        // Clicking track should jump to position, dragging knob should follow mouse

        let mut tester = BoxMuxTester::with_config(TestConfig {
            terminal_size: (80, 25), 
            operation_timeout: Duration::from_secs(2),
            capture_history: true,
            ..Default::default()
        });

        tester.load_config_from_string(create_vertical_scroll_test_yaml())
            .expect("Should load config for click vs drag test");

        let initial_frame = tester.wait_for_frame().expect("Should capture initial frame");
        let initial_content = initial_frame.get_content_string();

        println!("=== TESTING SCROLL CLICK VS DRAG BEHAVIOR ===");
        
        let right_edge = 35;
        let track_middle_y = 10; // Middle of scrollbar track

        // Test 1: Click in middle of track (should jump to ~50% scroll position)
        tester.click_at(right_edge, track_middle_y)
            .expect("Should click middle of scrollbar track");

        let after_click_frame = tester.wait_for_frame().expect("Should capture frame after track click");
        let after_click_content = after_click_frame.get_content_string();

        let click_changed_content = !initial_content.eq(&after_click_content);
        
        if !click_changed_content {
            println!("❌ SCROLLBAR TRACK CLICK NOT WORKING");
            println!("   Clicked at track position ({}, {})", right_edge, track_middle_y);
            println!("   Expected to jump to ~50% scroll position");
            println!("   Content did not change");
        } else {
            println!("✅ Scrollbar track click appears to work");
        }

        // Test 2: Drag the scroll knob (should follow mouse movement)
        let knob_start_y = 8;  // Where knob currently is
        let knob_end_y = 14;   // Where to drag it to

        tester.drag_from_to(right_edge, knob_start_y, right_edge, knob_end_y)
            .expect("Should drag scroll knob");

        let after_drag_frame = tester.wait_for_frame().expect("Should capture frame after knob drag");
        let after_drag_content = after_drag_frame.get_content_string();

        let drag_changed_content = !after_click_content.eq(&after_drag_content);

        if !drag_changed_content {
            println!("❌ SCROLL KNOB DRAG NOT WORKING");
            println!("   Dragged knob from Y={} to Y={}", knob_start_y, knob_end_y);
            println!("   Expected content to change following drag");
            println!("   Content did not change");
        } else {
            println!("✅ Scroll knob drag appears to work");
        }

        // Test 3: Verify knob moves to new position after drag
        let knob_cell_at_new_position = after_drag_frame.get_cell(right_edge, knob_end_y);
        let knob_moved = knob_cell_at_new_position.ch == '' || 
                        knob_cell_at_new_position.ch == '' || 
                        knob_cell_at_new_position.ch == '';

        if !knob_moved {
            println!("❌ KNOB DID NOT VISUALLY MOVE");
            println!("   Expected knob at new position ({}, {})", right_edge, knob_end_y);
            println!("   Found character: '{}'", knob_cell_at_new_position.ch);
        }

        println!("Initial content preview:");
        initial_frame.print_debug_preview();
        
        println!("After click content preview:");
        after_click_frame.print_debug_preview();
        
        println!("After drag content preview:");
        after_drag_frame.print_debug_preview();

        tester.save_snapshot("click_vs_drag_test").expect("Should save click vs drag snapshot");

        // Document expected behavior when bug is fixed
        // assert!(click_changed_content, "Track click should change scroll position");
        // assert!(drag_changed_content, "Knob drag should change scroll position");
        // assert!(knob_moved, "Knob should visually move to new position after drag");
    }
}

// Extension trait for easier frame content inspection
trait FrameExt {
    fn get_content_string(&self) -> String;
    fn print_debug_preview(&self);
    fn get_cell(&self, x: usize, y: usize) -> &crate::model::common::Cell;
}

impl FrameExt for crate::tests::visual_testing::TerminalFrame {
    fn get_content_string(&self) -> String {
        let mut content = String::new();
        for row in &self.buffer {
            let line: String = row.iter().map(|cell| cell.ch).collect();
            content.push_str(&line);
            content.push('\n');
        }
        content
    }

    fn print_debug_preview(&self) {
        println!("Frame {}x{} at {:?}:", self.dimensions.0, self.dimensions.1, self.timestamp);
        for (y, row) in self.buffer.iter().enumerate().take(20) { // Show first 20 rows
            let line: String = row.iter().map(|cell| cell.ch).collect();
            println!("{:2}|{}", y, line);
        }
        println!("");
    }

    fn get_cell(&self, x: usize, y: usize) -> &crate::model::common::Cell {
        &self.buffer[y][x]
    }
}