Files
clide/test/terminal/parser_and_scroll_test.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

125 lines
4.5 KiB
Dart

/// 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<void> 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<TerminalViewState>(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();
});
});
}