From 387a3365203fdc3cf57553cbd18abd81ab502529 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 11 May 2026 18:12:31 +0200 Subject: [PATCH] test sweep: cover escape parser + render _onScroll listener (T-91) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six tests for the last reachable coverage gaps in the terminal tree: - EscapeParser CSI handlers that read params[0] and normalise 0 → 1: CUF (ESC [ Ps C), CUB (ESC [ Ps D), CPL (ESC [ Ps F). - insertLines with an explicit param (ESC [ Ps L). - DEC private mode 1001 (HighlightMouseTracking) toggles MouseMode .upDownScroll on, none off. - RenderTerminal._onScroll driven via a test ViewportOffset that exposes notifyListeners — verifies the listener body runs without throwing when an offset is swapped in via the setter. Coverage: escape/parser.dart 503/513 -> 510/513 (the 3 remaining lines are _CSI.toString — only reachable from a debug log call that no production path makes); render.dart 245/249 -> 249/249. Co-Authored-By: Claude Opus 4.7 (1M context) --- test/terminal/parser_and_scroll_test.dart | 124 ++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 test/terminal/parser_and_scroll_test.dart diff --git a/test/terminal/parser_and_scroll_test.dart b/test/terminal/parser_and_scroll_test.dart new file mode 100644 index 00000000..9d4cadf9 --- /dev/null +++ b/test/terminal/parser_and_scroll_test.dart @@ -0,0 +1,124 @@ +/// Coverage closeouts for `lib/src/terminal/src/core/escape/parser.dart` +/// (CUF/CUB/CPL with explicit param, insertLines with param, DEC mouse +/// mode 1001) and `lib/src/terminal/src/ui/render.dart` (the _onScroll +/// listener body, driven via a test ViewportOffset that exposes +/// notifyListeners). +library; + +import 'package:clide/src/terminal/src/core/mouse/mode.dart'; +import 'package:clide/src/terminal/src/terminal.dart'; +import 'package:clide/src/terminal/src/terminal_view.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// Minimal ViewportOffset that exposes `notifyListeners` so we can fire +/// it from a test. The real ScrollPositionWithSingleContext is hidden +/// inside Scrollable; this stand-in lets us drive the offset setter +/// path on RenderTerminal directly. +class _TestViewportOffset extends ViewportOffset { + @override + double get pixels => 0; + @override + bool get hasPixels => true; + @override + bool applyViewportDimension(double viewportDimension) => true; + @override + bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) => true; + @override + void correctBy(double correction) {} + @override + void jumpTo(double pixels) {} + @override + Future animateTo(double to, {required Duration duration, required Curve curve}) async {} + @override + ScrollDirection get userScrollDirection => ScrollDirection.idle; + @override + bool get allowImplicitScrolling => false; + + void fire() => notifyListeners(); +} + +Widget _host(Widget child) => Directionality( + textDirection: TextDirection.ltr, + child: MediaQuery( + data: const MediaQueryData(), + child: Center(child: SizedBox(width: 400, height: 200, child: child)), + ), + ); + +void main() { + group('EscapeParser — explicit-param CSI cursor moves', () { + test('CUF (ESC [ Ps C) reads params[0] and normalises 0 → 1', () { + final t = Terminal(maxLines: 100, onOutput: (_) {}); + t.write('A'); + // Cursor at (1,0). Move forward 3 cells, write 'B'. + t.write('\x1b[3C'); + t.write('B'); + expect(t.buffer.cursorX, greaterThanOrEqualTo(4)); + // Param of 0 normalises to 1. + t.write('\x1b[0C'); + t.write('C'); + expect(t.buffer.cursorX, greaterThanOrEqualTo(5)); + }); + + test('CUB (ESC [ Ps D) reads params[0] and normalises 0 → 1', () { + final t = Terminal(maxLines: 100, onOutput: (_) {}); + t.write('ABCDEFG'); + // Now at column 7. Step back 3. + t.write('\x1b[3D'); + // Then 0-normalised step back 1. + t.write('\x1b[0D'); + expect(t.buffer.cursorX, lessThan(7)); + }); + + test('CPL (ESC [ Ps F) reads params[0] and normalises 0 → 1', () { + final t = Terminal(maxLines: 100, onOutput: (_) {}); + t.write('line1\r\nline2\r\nline3'); + // Cursor on row 2. Go up 2 lines. + t.write('\x1b[2F'); + // Then 0-normalised up 1. + t.write('\x1b[0F'); + // Just verify the cursor moved upward — exact row is implementation + // detail, but it shouldn't still be at row 2. + expect(t.buffer.cursorY, lessThan(2)); + }); + + test('insertLines (ESC [ Ps L) reads params[0]', () { + final t = Terminal(maxLines: 100, onOutput: (_) {}); + t.write('row0\r\nrow1\r\nrow2'); + // Move cursor to home and insert 2 blank lines — just verifies the + // path runs without throwing. + t.write('\x1b[H\x1b[2L'); + }); + + test('DEC private mode 1001 routes to MouseMode.upDownScroll', () { + final t = Terminal(maxLines: 100, onOutput: (_) {}); + // Enable mode 1001. + t.write('\x1b[?1001h'); + expect(t.mouseMode, MouseMode.upDownScroll); + // Disable returns to none. + t.write('\x1b[?1001l'); + expect(t.mouseMode, MouseMode.none); + }); + }); + + group('RenderTerminal — _onScroll listener', () { + testWidgets('viewport-offset notifyListeners fires _onScroll without throwing', (tester) async { + final t = Terminal(maxLines: 100, onOutput: (_) {}); + await tester.pumpWidget(_host(TerminalView(t))); + await tester.pump(); + final r = tester.state(find.byType(TerminalView)).renderTerminal; + // Swap in a viewport offset we can fire on. The offset setter + // attaches _onScroll as a listener. + final off = _TestViewportOffset(); + r.offset = off; + // Force a layout so attach() has wired up listeners. + await tester.pump(); + // Drive the listener — runs the body of _onScroll (markNeedsLayout + // + _notifyEditableRect). + off.fire(); + await tester.pump(); + }); + }); +}