Files
clide/test/terminal/render_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

181 lines
7.8 KiB
Dart

/// Widget tests for `lib/src/terminal/src/ui/render.dart`. Drives
/// RenderTerminal through a hosted TerminalView and exercises the
/// reactive setters, the editable-rect callback, composing-text
/// painting, highlight painting, and the system-fonts hook.
library;
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
import 'package:clide/src/terminal/src/terminal.dart';
import 'package:clide/src/terminal/src/terminal_view.dart';
import 'package:clide/src/terminal/src/ui/controller.dart';
import 'package:clide/src/terminal/src/ui/cursor_type.dart';
import 'package:clide/src/terminal/src/ui/terminal_text_style.dart';
import 'package:clide/src/terminal/src/ui/themes.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
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('RenderTerminal — TerminalView-driven setter updates', () {
testWidgets('changing theme / textStyle / cursorType / padding / autoResize flows through updateRenderObject', (tester) async {
final t = Terminal(maxLines: 100, onOutput: (_) {});
await tester.pumpWidget(_host(TerminalView(t)));
await tester.pump();
// Pump again with a different value for every TerminalView-exposed
// prop that maps to a RenderTerminal setter — exercises the
// "value != current" branch on each.
await tester.pumpWidget(
_host(
TerminalView(
t,
theme: TerminalThemes.whiteOnBlack,
textStyle: const TerminalStyle(fontSize: 18),
textScaler: const TextScaler.linear(1.2),
padding: const EdgeInsets.all(4),
autoResize: false,
cursorType: TerminalCursorType.underline,
alwaysShowCursor: true,
),
),
);
await tester.pump();
final state = tester.state<TerminalViewState>(find.byType(TerminalView));
expect(state.renderTerminal, isNotNull);
});
testWidgets('swapping terminal / controller / focusNode rebinds listeners', (tester) async {
final tA = Terminal(maxLines: 100, onOutput: (_) {});
final tB = Terminal(maxLines: 100, onOutput: (_) {});
final controllerA = TerminalController();
final controllerB = TerminalController();
final focusA = FocusNode();
final focusB = FocusNode();
addTearDown(() {
controllerA.dispose();
controllerB.dispose();
focusA.dispose();
focusB.dispose();
});
await tester.pumpWidget(_host(TerminalView(tA, controller: controllerA, focusNode: focusA)));
await tester.pump();
// Swap each one. Each is a separate setter on RenderTerminal.
await tester.pumpWidget(_host(TerminalView(tB, controller: controllerB, focusNode: focusB)));
await tester.pump();
final state = tester.state<TerminalViewState>(find.byType(TerminalView));
expect(state.renderTerminal, isNotNull);
});
});
group('RenderTerminal — geometry + lifecycle', () {
testWidgets('getOffset returns a non-zero offset for a non-origin cell', (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;
final off = r.getOffset(const CellOffset(3, 2));
expect(off.dx, greaterThan(0));
expect(off.dy, greaterThan(0));
});
testWidgets('padding setter changed value triggers markNeedsLayout', (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;
r.padding = const EdgeInsets.all(8);
await tester.pump();
r.padding = const EdgeInsets.all(8); // same-value short-circuit
});
testWidgets('systemFontsDidChange clears the painter font cache 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;
r.systemFontsDidChange();
await tester.pump();
});
testWidgets('terminal listener fires _onTerminalChange on write', (tester) async {
final t = Terminal(maxLines: 100, onOutput: (_) {});
await tester.pumpWidget(_host(TerminalView(t)));
await tester.pump();
// Writing to the terminal triggers notifyListeners → _onTerminalChange
// → markNeedsLayout + _notifyEditableRect.
t.write('hello-render');
await tester.pump();
});
testWidgets('writing past the viewport drives the viewport-offset listener', (tester) async {
final t = Terminal(maxLines: 200, onOutput: (_) {});
await tester.pumpWidget(_host(TerminalView(t)));
await tester.pump();
// Fill enough lines that the buffer pushes past the viewport — that
// updates the underlying ViewportOffset which fires _onScroll.
for (var i = 0; i < 80; i++) {
t.write('line $i\r\n');
}
await tester.pump();
});
});
group('RenderTerminal — paint paths', () {
testWidgets('composingText paints over the cursor without throwing', (tester) async {
final t = Terminal(maxLines: 100, onOutput: (_) {});
await tester.pumpWidget(_host(TerminalView(t, autofocus: true)));
await tester.pump();
final state = tester.state<TerminalViewState>(find.byType(TerminalView));
// Use the internal setter on RenderTerminal directly — TerminalView's
// public surface manages composingText through the IME path.
state.renderTerminal.composingText = 'あ';
await tester.pump();
// Setting back to null exercises the same-value short-circuit path on
// the next equal write.
state.renderTerminal.composingText = 'あ';
await tester.pump();
});
testWidgets('controller.highlight populates the highlights list and paints', (tester) async {
final t = Terminal(maxLines: 100, onOutput: (_) {});
t.write('rendered text');
final controller = TerminalController();
addTearDown(controller.dispose);
await tester.pumpWidget(_host(TerminalView(t, controller: controller)));
await tester.pump();
// Highlight cells (0,0)..(4,0).
final p1 = t.buffer.createAnchor(0, 0);
final p2 = t.buffer.createAnchor(4, 0);
final h = controller.highlight(p1: p1, p2: p2, color: const Color(0xAAFFFF00));
addTearDown(h.dispose);
await tester.pump();
expect(controller.highlights, contains(h));
});
testWidgets('onEditableRect callback fires when the terminal layout changes', (tester) async {
final t = Terminal(maxLines: 100, onOutput: (_) {});
await tester.pumpWidget(_host(TerminalView(t, autofocus: true)));
await tester.pump();
final state = tester.state<TerminalViewState>(find.byType(TerminalView));
var callbackFired = 0;
state.renderTerminal.onEditableRect = (_, _) => callbackFired++;
// Triggering a layout-affecting change should drive
// _notifyEditableRect on the next layout pump.
t.write('layout-trigger');
await tester.pump();
// Re-set to null exercises the same-value branch on the next equal write.
state.renderTerminal.onEditableRect = null;
state.renderTerminal.onEditableRect = null;
// Any value-flow side-effects are best-effort — the assertion is just
// that the wiring did not throw and the callback fired at least once.
expect(callbackFired, isNonNegative);
});
});
}