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>
This commit is contained in:
2026-06-11 12:11:53 +02:00
co-authored by Claude Opus 4.8
parent bcea5f15b7
commit 6d0ebab721
444 changed files with 7587 additions and 12849 deletions
+2 -14
View File
@@ -14,14 +14,7 @@ import 'package:clide/src/terminal/src/core/state.dart';
import 'package:test/test.dart';
class _State implements TerminalState {
_State({
this.viewWidth = 10,
this.viewHeight = 5,
this.autoWrapMode = true,
this.lineFeedMode = false,
this.originMode = false,
this.reflowEnabled = false,
});
_State({this.viewWidth = 10, this.viewHeight = 5, this.autoWrapMode = true, this.lineFeedMode = false, this.originMode = false, this.reflowEnabled = false});
@override
int viewWidth;
@@ -83,12 +76,7 @@ Buffer _newBuffer({
originMode: originMode,
reflowEnabled: reflowEnabled,
);
return Buffer(
state,
maxLines: maxLines,
isAltBuffer: isAltBuffer,
wordSeparators: wordSeparators,
);
return Buffer(state, maxLines: maxLines, isAltBuffer: isAltBuffer, wordSeparators: wordSeparators);
}
void main() {
+2 -8
View File
@@ -126,14 +126,8 @@ void main() {
});
test('toString shape — bounded and unbounded', () {
expect(
const BufferSegment(_dummyRange, 4, 2, 8).toString(),
'Segment(4, 2 -> 8)',
);
expect(
const BufferSegment(_dummyRange, 0, null, null).toString(),
'Segment(0, start -> end)',
);
expect(const BufferSegment(_dummyRange, 4, 2, 8).toString(), 'Segment(4, 2 -> 8)');
expect(const BufferSegment(_dummyRange, 0, null, null).toString(), 'Segment(0, start -> end)');
});
});
+9 -8
View File
@@ -39,10 +39,7 @@ void main() {
test('toString shape is the documented debug format', () {
final c = CellData(foreground: 1, background: 2, flags: 4, content: 8);
expect(
c.toString(),
'CellData{foreground: 1, background: 2, flags: 4, content: 8}',
);
expect(c.toString(), 'CellData{foreground: 1, background: 2, flags: 4, content: 8}');
});
});
@@ -374,10 +371,14 @@ void main() {
final src = line(12, 'short');
final endAnchor = src.createAnchor(12); // viewWidth, past trimmedLength
final out = reflow(ring([src]), 12, 4);
expect(out.contains(endAnchor.line), isTrue,
reason: 'end anchor must land on a line that survives '
'lines.replaceWith — otherwise selection.attached is false '
'and the selection silently vanishes after resize.');
expect(
out.contains(endAnchor.line),
isTrue,
reason:
'end anchor must land on a line that survives '
'lines.replaceWith — otherwise selection.attached is false '
'and the selection silently vanishes after resize.',
);
});
test('inner wide-char clamp during _addPart leaves the wide cell to the next iteration', () {
+1 -5
View File
@@ -49,11 +49,7 @@ void main() {
// CJK ideograph — terminal stores it as a width-2 cell, so paintLine
// advances `i` twice and the wide-char branch fires.
t.write('漢字');
final p = TerminalPainter(
theme: TerminalThemes.defaultTheme,
textStyle: const TerminalStyle(),
textScaler: const TextScaler.linear(1.0),
);
final p = TerminalPainter(theme: TerminalThemes.defaultTheme, textStyle: const TerminalStyle(), textScaler: const TextScaler.linear(1.0));
p.paintLine(Canvas(PictureRecorder()), Offset.zero, t.buffer.lines[0]);
});
}
+10 -16
View File
@@ -19,12 +19,12 @@ class _Recorder {
}
Widget _host(Widget child) => Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Center(child: SizedBox(width: 200, height: 100, child: child)),
),
);
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Center(child: SizedBox(width: 200, height: 100, child: child)),
),
);
CustomTextEdit _build({
required FocusNode focusNode,
@@ -43,7 +43,8 @@ CustomTextEdit _build({
onDelete: () => r.deletes++,
onComposing: r.composings.add,
onAction: r.actions.add,
onKeyEvent: onKeyEvent ??
onKeyEvent:
onKeyEvent ??
(node, event) {
r.keys.add(event);
return KeyEventResult.ignored;
@@ -230,10 +231,7 @@ void main() {
await tester.pump();
final state = tester.state<CustomTextEditState>(find.byType(CustomTextEdit));
// Send a value shorter than the init state (2 chars) directly.
state.updateEditingValue(const TextEditingValue(
text: '',
selection: TextSelection.collapsed(offset: 0),
));
state.updateEditingValue(const TextEditingValue(text: '', selection: TextSelection.collapsed(offset: 0)));
expect(r.deletes, 1);
});
@@ -244,11 +242,7 @@ void main() {
await tester.pumpWidget(_host(_build(focusNode: focus, r: r, autofocus: true)));
await tester.pump();
final state = tester.state<CustomTextEditState>(find.byType(CustomTextEdit));
state.updateEditingValue(const TextEditingValue(
text: 'hi',
selection: TextSelection.collapsed(offset: 2),
composing: TextRange(start: 0, end: 2),
));
state.updateEditingValue(const TextEditingValue(text: 'hi', selection: TextSelection.collapsed(offset: 2), composing: TextRange(start: 0, end: 2)));
expect(r.composings, contains('hi'));
});
+10 -52
View File
@@ -238,15 +238,8 @@ void main() {
test('writes printable chars one-by-one to writeChar', () {
final f = _newParser();
f.parser.write('Hi!');
expect(
f.h.calls.map((c) => c.name).toList(),
['writeChar', 'writeChar', 'writeChar'],
);
expect(f.h.calls.map((c) => c.args.first).toList(), [
'H'.codeUnitAt(0),
'i'.codeUnitAt(0),
'!'.codeUnitAt(0),
]);
expect(f.h.calls.map((c) => c.name).toList(), ['writeChar', 'writeChar', 'writeChar']);
expect(f.h.calls.map((c) => c.args.first).toList(), ['H'.codeUnitAt(0), 'i'.codeUnitAt(0), '!'.codeUnitAt(0)]);
});
test('chars beyond the SBC table go straight to writeChar', () {
@@ -289,21 +282,13 @@ void main() {
test('ESC 7 / ESC 8 → save / restore cursor', () {
final f = _newParser();
f.parser.write('\x1b7\x1b8');
expect(f.h.calls.map((c) => c.name).toList(), [
'saveCursor',
'restoreCursor',
]);
expect(f.h.calls.map((c) => c.name).toList(), ['saveCursor', 'restoreCursor']);
});
test('ESC D / E / H / M', () {
final f = _newParser();
f.parser.write('\x1bD\x1bE\x1bH\x1bM');
expect(f.h.calls.map((c) => c.name).toList(), [
'index',
'nextLine',
'setTapStop',
'reverseIndex',
]);
expect(f.h.calls.map((c) => c.name).toList(), ['index', 'nextLine', 'setTapStop', 'reverseIndex']);
});
test('ESC ( / ESC ) designate G0 / G1 charset', () {
@@ -320,10 +305,7 @@ void main() {
final f = _newParser();
// ESC = enables app keypad mode, ESC > disables it.
f.parser.write('\x1b=\x1b>');
expect(
f.h.named('setAppKeypadMode').map((c) => c.args.first).toList(),
[true, false],
);
expect(f.h.named('setAppKeypadMode').map((c) => c.args.first).toList(), [true, false]);
});
test('unknown ESC byte → unkownEscape', () {
@@ -354,12 +336,7 @@ void main() {
test('A / B / C / D — default amount = 1, missing param uses default', () {
final f = _newParser();
f.parser.write('\x1b[A\x1b[B\x1b[C\x1b[D');
expect(f.h.calls.map((c) => '${c.name}/${c.args}').toList(), [
'moveCursorY/[-1]',
'moveCursorY/[1]',
'moveCursorX/[1]',
'moveCursorX/[-1]',
]);
expect(f.h.calls.map((c) => '${c.name}/${c.args}').toList(), ['moveCursorY/[-1]', 'moveCursorY/[1]', 'moveCursorX/[1]', 'moveCursorX/[-1]']);
});
test('A / B / C / D — explicit amount + 0-as-1 fallback', () {
@@ -412,12 +389,7 @@ void main() {
test('J 0/1/2/3 → erase display below/above/all/scrollback', () {
final f = _newParser();
f.parser.write('\x1b[J\x1b[1J\x1b[2J\x1b[3J');
expect(f.h.calls.map((c) => c.name).toList(), [
'eraseDisplayBelow',
'eraseDisplayAbove',
'eraseDisplay',
'eraseScrollbackOnly',
]);
expect(f.h.calls.map((c) => c.name).toList(), ['eraseDisplayBelow', 'eraseDisplayAbove', 'eraseDisplay', 'eraseScrollbackOnly']);
});
test('K 0/1/2 → erase line right / left / all', () {
@@ -483,11 +455,7 @@ void main() {
test('c / >c / =c — primary, secondary, tertiary device attributes', () {
final f = _newParser();
f.parser.write('\x1b[c\x1b[>c\x1b[=c');
expect(f.h.calls.map((c) => c.name).toList(), [
'sendPrimaryDeviceAttributes',
'sendSecondaryDeviceAttributes',
'sendTertiaryDeviceAttributes',
]);
expect(f.h.calls.map((c) => c.name).toList(), ['sendPrimaryDeviceAttributes', 'sendSecondaryDeviceAttributes', 'sendTertiaryDeviceAttributes']);
});
test('n 5 → operating status; n 6 → cursor position', () {
@@ -595,12 +563,7 @@ void main() {
final f = _newParser();
f.parser.write('\x1b[?9h\x1b[?1000h\x1b[?1002h\x1b[?1003h');
final modes = f.h.named('setMouseMode').map((c) => c.args.first).toList();
expect(modes, [
MouseMode.clickOnly,
MouseMode.upDownScroll,
MouseMode.upDownScrollDrag,
MouseMode.upDownScrollMove,
]);
expect(modes, [MouseMode.clickOnly, MouseMode.upDownScroll, MouseMode.upDownScrollDrag, MouseMode.upDownScrollMove]);
});
test('CSI ?9 / ?1000 disabled → setMouseMode(none)', () {
@@ -614,12 +577,7 @@ void main() {
final f = _newParser();
f.parser.write('\x1b[?1005h\x1b[?1006h\x1b[?1015h\x1b[?1006l');
final modes = f.h.named('setMouseReportMode').map((c) => c.args.first).toList();
expect(modes, [
MouseReportMode.utf,
MouseReportMode.sgr,
MouseReportMode.urxvt,
MouseReportMode.normal,
]);
expect(modes, [MouseReportMode.utf, MouseReportMode.sgr, MouseReportMode.urxvt, MouseReportMode.normal]);
});
test('CSI ?3 / ?5 / ?6 / ?12 / ?66 / ?1004 / ?1007 — column/reverse/origin/blink/keypad/focus/altScroll', () {
+22 -93
View File
@@ -19,10 +19,7 @@ import 'package:clide/src/terminal/src/core/state.dart';
import 'package:test/test.dart';
class _State implements TerminalState {
_State({
this.lineFeedMode = false,
this.appKeypadMode = false,
});
_State({this.lineFeedMode = false, this.appKeypadMode = false});
@override
bool lineFeedMode;
@@ -78,10 +75,7 @@ TerminalKeyboardEvent _evt(
shift: shift,
ctrl: ctrl,
alt: alt,
state: _State(
lineFeedMode: lineFeedMode,
appKeypadMode: appKeypadMode,
),
state: _State(lineFeedMode: lineFeedMode, appKeypadMode: appKeypadMode),
altBuffer: altBuffer,
platform: platform,
);
@@ -161,10 +155,7 @@ void main() {
group('tokenize', () {
test('parses a keyboard-name line', () {
final tokens = tokenize('keyboard "Default"').toList();
expect(tokens.map((t) => t.type).toList(), [
KeytabTokenType.keyboard,
KeytabTokenType.input,
]);
expect(tokens.map((t) => t.type).toList(), [KeytabTokenType.keyboard, KeytabTokenType.input]);
expect(tokens.last.value, 'Default');
});
@@ -200,24 +191,15 @@ key Tab : "\\t" # trailing comment
''';
final tokens = tokenize(source).toList();
// Trailing-comment stripping leaves no broken tokens.
expect(
tokens.map((t) => t.type).toList(),
contains(KeytabTokenType.colon),
);
expect(tokens.map((t) => t.type).toList(), contains(KeytabTokenType.colon));
});
test('tokenize throws TokenizeError on a malformed key line missing colon', () {
expect(
() => tokenize('key Tab "\\t"').toList(),
throwsA(isA<TokenizeError>()),
);
expect(() => tokenize('key Tab "\\t"').toList(), throwsA(isA<TokenizeError>()));
});
test('tokenize throws TokenizeError on unterminated keyboard line', () {
expect(
() => tokenize('keyboard X').toList(),
throwsA(isA<TokenizeError>()),
);
expect(() => tokenize('keyboard X').toList(), throwsA(isA<TokenizeError>()));
});
test('KeytabToken toString reflects type + value', () {
@@ -291,14 +273,8 @@ key Up +Bogus : "x"
group('KeytabRecord / KeytabAction toString', () {
test('action toString quotes input and bare-prints shortcut', () {
expect(
KeytabAction(KeytabActionType.input, r'\E[A').toString(),
'"\\E[A"',
);
expect(
KeytabAction(KeytabActionType.shortcut, 'scrollUp').toString(),
'scrollUp',
);
expect(KeytabAction(KeytabActionType.input, r'\E[A').toString(), '"\\E[A"');
expect(KeytabAction(KeytabActionType.shortcut, 'scrollUp').toString(), 'scrollUp');
});
test('record toString writes +Mode for true and -Mode for false flags', () {
@@ -341,10 +317,7 @@ key Up +Shift : scrollLineUp
test('action unescapedValue returns the raw value for shortcut actions', () {
// Input actions go through keytabUnescape; shortcut actions don't.
expect(
KeytabAction(KeytabActionType.shortcut, r'\Ehello').unescapedValue(),
r'\Ehello',
);
expect(KeytabAction(KeytabActionType.shortcut, r'\Ehello').unescapedValue(), r'\Ehello');
});
test('record toString covers every supported mode flag when set', () {
@@ -384,34 +357,20 @@ key Up +Shift : scrollLineUp
group('KeytabParser — defensive error paths through hand-crafted tokens', () {
test('addTokens throws ParseError on a stray non-keyboard / non-keyDefine token', () {
final parser = KeytabParser();
expect(
() => parser.addTokens([
KeytabToken(KeytabTokenType.colon, ':'),
]),
throwsA(isA<ParseError>()),
);
expect(() => parser.addTokens([KeytabToken(KeytabTokenType.colon, ':')]), throwsA(isA<ParseError>()));
});
test('_parseName throws when the second token is not an input token', () {
final parser = KeytabParser();
expect(
() => parser.addTokens([
KeytabToken(KeytabTokenType.keyboard, 'keyboard'),
KeytabToken(KeytabTokenType.keyName, 'Up'),
]),
() => parser.addTokens([KeytabToken(KeytabTokenType.keyboard, 'keyboard'), KeytabToken(KeytabTokenType.keyName, 'Up')]),
throwsA(isA<ParseError>()),
);
});
test('_parseKeyDefine throws when the second token is not a keyName', () {
final parser = KeytabParser();
expect(
() => parser.addTokens([
KeytabToken(KeytabTokenType.keyDefine, 'key'),
KeytabToken(KeytabTokenType.colon, ':'),
]),
throwsA(isA<ParseError>()),
);
expect(() => parser.addTokens([KeytabToken(KeytabTokenType.keyDefine, 'key'), KeytabToken(KeytabTokenType.colon, ':')]), throwsA(isA<ParseError>()));
});
test('_parseKeyDefine throws on an unrecognised modeStatus value', () {
@@ -470,10 +429,7 @@ key Up +Shift : scrollLineUp
test('exact match: -Shift on a record with shift=null and no anyModifier', () {
final t = build('key Up -Shift : "\\E[A"');
expect(
t.find(TerminalKey.arrowUp, shift: false)?.action.value,
r'\E[A',
);
expect(t.find(TerminalKey.arrowUp, shift: false)?.action.value, r'\E[A');
expect(t.find(TerminalKey.arrowUp, shift: true), isNull);
});
@@ -495,15 +451,9 @@ key Up +NewLine+AppKeyPad+AppScreen+Mac : "match"
key Up : "fallback"
''');
// Wrong newLine mode falls through to fallback.
expect(
t.find(TerminalKey.arrowUp, newLineMode: false, appKeyPad: true, appScreen: true, macos: true)?.action.value,
'fallback',
);
expect(t.find(TerminalKey.arrowUp, newLineMode: false, appKeyPad: true, appScreen: true, macos: true)?.action.value, 'fallback');
// All matching → primary record.
expect(
t.find(TerminalKey.arrowUp, newLineMode: true, appKeyPad: true, appScreen: true, macos: true)?.action.value,
'match',
);
expect(t.find(TerminalKey.arrowUp, newLineMode: true, appKeyPad: true, appScreen: true, macos: true)?.action.value, 'match');
});
test('-Ansi records are skipped (VT52 not supported yet)', () {
@@ -519,10 +469,7 @@ key Up : "fallback"
test('appCursorKeys + keyPad gates also filter', () {
final t = build('key Up +AppCuKeys+KeyPad : "\\EOA"\nkey Up : "fallback"');
expect(t.find(TerminalKey.arrowUp)?.action.value, 'fallback');
expect(
t.find(TerminalKey.arrowUp, appCursorKeys: true, keyPad: true)?.action.value,
r'\EOA',
);
expect(t.find(TerminalKey.arrowUp, appCursorKeys: true, keyPad: true)?.action.value, r'\EOA');
});
});
@@ -591,23 +538,11 @@ key Up : "fallback"
(_evt(TerminalKey.arrowUp, alt: true), '\x1b[1;3A'), // alt → 3
(_evt(TerminalKey.arrowUp, ctrl: true), '\x1b[1;5A'), // ctrl → 5
// Pair codes.
(
_evt(TerminalKey.arrowUp, shift: true, alt: true),
'\x1b[1;4A',
),
(
_evt(TerminalKey.arrowUp, shift: true, ctrl: true),
'\x1b[1;6A',
),
(
_evt(TerminalKey.arrowUp, ctrl: true, alt: true),
'\x1b[1;7A',
),
(_evt(TerminalKey.arrowUp, shift: true, alt: true), '\x1b[1;4A'),
(_evt(TerminalKey.arrowUp, shift: true, ctrl: true), '\x1b[1;6A'),
(_evt(TerminalKey.arrowUp, ctrl: true, alt: true), '\x1b[1;7A'),
// Triple.
(
_evt(TerminalKey.arrowUp, shift: true, alt: true, ctrl: true),
'\x1b[1;8A',
),
(_evt(TerminalKey.arrowUp, shift: true, alt: true, ctrl: true), '\x1b[1;8A'),
];
for (final c in cases) {
expect(h(c.$1), c.$2, reason: '$c');
@@ -662,10 +597,7 @@ key Up : "fallback"
});
test('returns null on macOS (Alt is reserved for char composition)', () {
expect(
h(_evt(TerminalKey.keyA, alt: true, platform: TerminalTargetPlatform.macos)),
isNull,
);
expect(h(_evt(TerminalKey.keyA, alt: true, platform: TerminalTargetPlatform.macos)), isNull);
});
test('returns null for non-letter keys', () {
@@ -680,10 +612,7 @@ key Up : "fallback"
test('routes Ctrl+C to 0x03 via CtrlInputHandler when keytab misses', () {
// Default keytab has no entry for Ctrl+keyC, so CtrlInputHandler runs.
expect(
defaultInputHandler(_evt(TerminalKey.keyC, ctrl: true)),
'\x03',
);
expect(defaultInputHandler(_evt(TerminalKey.keyC, ctrl: true)), '\x03');
});
});
}
+17 -78
View File
@@ -13,10 +13,7 @@ import 'package:clide/src/terminal/src/core/state.dart';
import 'package:test/test.dart';
class _State implements TerminalState {
_State({
this.mouseMode = MouseMode.none,
this.mouseReportMode = MouseReportMode.normal,
});
_State({this.mouseMode = MouseMode.none, this.mouseReportMode = MouseReportMode.normal});
@override
MouseMode mouseMode;
@@ -91,12 +88,7 @@ void main() {
expect(TerminalMouseButton.wheelDown.id, 64 + 5);
expect(TerminalMouseButton.wheelLeft.id, 64 + 6);
expect(TerminalMouseButton.wheelRight.id, 64 + 7);
for (final b in [
TerminalMouseButton.wheelUp,
TerminalMouseButton.wheelDown,
TerminalMouseButton.wheelLeft,
TerminalMouseButton.wheelRight,
]) {
for (final b in [TerminalMouseButton.wheelUp, TerminalMouseButton.wheelDown, TerminalMouseButton.wheelLeft, TerminalMouseButton.wheelRight]) {
expect(b.isWheel, isTrue, reason: '$b');
}
});
@@ -134,14 +126,8 @@ void main() {
group('MouseReporter (sgr mode)', () {
test('M for press, m for release, with raw 1-based coords', () {
expect(
MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.down, const CellOffset(10, 20), MouseReportMode.sgr),
'\x1b[<1;11;21M',
);
expect(
MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.up, const CellOffset(10, 20), MouseReportMode.sgr),
'\x1b[<1;11;21m',
);
expect(MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.down, const CellOffset(10, 20), MouseReportMode.sgr), '\x1b[<1;11;21M');
expect(MouseReporter.report(TerminalMouseButton.middle, TerminalMouseButtonState.up, const CellOffset(10, 20), MouseReportMode.sgr), '\x1b[<1;11;21m');
});
});
@@ -171,21 +157,11 @@ void main() {
group('CascadeMouseHandler', () {
test('returns the first non-null result; null when all return null', () {
const cascade = CascadeMouseHandler([
_NullHandler(),
_ConstHandler('first'),
_ConstHandler('second'),
]);
expect(
cascade(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)),
'first',
);
const cascade = CascadeMouseHandler([_NullHandler(), _ConstHandler('first'), _ConstHandler('second')]);
expect(cascade(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)), 'first');
const allNull = CascadeMouseHandler([_NullHandler(), _NullHandler()]);
expect(
allNull(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)),
isNull,
);
expect(allNull(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down)), isNull);
});
});
@@ -198,31 +174,16 @@ void main() {
});
test('clickOnly mode + up → null (only down events report)', () {
expect(
h(_evt(TerminalMouseButton.middle, TerminalMouseButtonState.up, mode: MouseMode.clickOnly)),
isNull,
);
expect(h(_evt(TerminalMouseButton.middle, TerminalMouseButtonState.up, mode: MouseMode.clickOnly)), isNull);
});
test('clickOnly mode + button id >= 3 (wheel) → null', () {
expect(
h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.down, mode: MouseMode.clickOnly)),
isNull,
);
expect(h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.down, mode: MouseMode.clickOnly)), isNull);
});
test('non-clickOnly modes always return null', () {
for (final m in [
MouseMode.none,
MouseMode.upDownScroll,
MouseMode.upDownScrollDrag,
MouseMode.upDownScrollMove,
]) {
expect(
h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)),
isNull,
reason: '$m',
);
for (final m in [MouseMode.none, MouseMode.upDownScroll, MouseMode.upDownScrollDrag, MouseMode.upDownScrollMove]) {
expect(h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)), isNull, reason: '$m');
}
});
});
@@ -232,39 +193,22 @@ void main() {
test('none / clickOnly modes return null', () {
for (final m in [MouseMode.none, MouseMode.clickOnly]) {
expect(
h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)),
isNull,
);
expect(h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)), isNull);
}
});
test('upDownScroll modes report regular button events', () {
for (final m in [
MouseMode.upDownScroll,
MouseMode.upDownScrollDrag,
MouseMode.upDownScrollMove,
]) {
expect(
h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)),
isNotNull,
reason: '$m',
);
for (final m in [MouseMode.upDownScroll, MouseMode.upDownScrollDrag, MouseMode.upDownScrollMove]) {
expect(h(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: m)), isNotNull, reason: '$m');
}
});
test('wheel up events are silently dropped (no report on wheel release)', () {
expect(
h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.up, mode: MouseMode.upDownScroll)),
isNull,
);
expect(h(_evt(TerminalMouseButton.wheelUp, TerminalMouseButtonState.up, mode: MouseMode.upDownScroll)), isNull);
});
test('wheel down events do report (one click per scroll tick)', () {
expect(
h(_evt(TerminalMouseButton.wheelDown, TerminalMouseButtonState.down, mode: MouseMode.upDownScroll)),
isNotNull,
);
expect(h(_evt(TerminalMouseButton.wheelDown, TerminalMouseButtonState.down, mode: MouseMode.upDownScroll)), isNotNull);
});
});
@@ -280,12 +224,7 @@ void main() {
});
test('mode=none produces null (no handler reports)', () {
expect(
defaultMouseHandler(
_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: MouseMode.none),
),
isNull,
);
expect(defaultMouseHandler(_evt(TerminalMouseButton.left, TerminalMouseButtonState.down, mode: MouseMode.none)), isNull);
});
});
}
+9 -12
View File
@@ -31,22 +31,19 @@ void main() {
test('JetBrainsMono Bold advance width matches Regular (cell drift = 0)', () {
final regularWidth = _measure(FontWeight.normal);
final boldWidth = _measure(FontWeight.bold);
expect(boldWidth, regularWidth,
reason: 'Bold advance width must equal Regular at $_fontSize px '
'or the terminal cell grid drifts when bold flips on.');
expect(
boldWidth,
regularWidth,
reason:
'Bold advance width must equal Regular at $_fontSize px '
'or the terminal cell grid drifts when bold flips on.',
);
});
}
double _measure(FontWeight weight) {
final builder = ui.ParagraphBuilder(ui.ParagraphStyle(
fontFamily: _family,
fontSize: _fontSize,
))
..pushStyle(ui.TextStyle(
fontFamily: _family,
fontWeight: weight,
fontSize: _fontSize,
))
final builder = ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: _family, fontSize: _fontSize))
..pushStyle(ui.TextStyle(fontFamily: _family, fontWeight: weight, fontSize: _fontSize))
..addText(_sample);
final paragraph = builder.build()..layout(const ui.ParagraphConstraints(width: double.infinity));
final width = paragraph.maxIntrinsicWidth;
+31 -93
View File
@@ -18,16 +18,8 @@ import 'package:clide/src/terminal/src/ui/themes.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
TerminalPainter _make({
TerminalTheme? theme,
TerminalStyle? style,
TextScaler? scaler,
}) =>
TerminalPainter(
theme: theme ?? TerminalThemes.defaultTheme,
textStyle: style ?? const TerminalStyle(),
textScaler: scaler ?? const TextScaler.linear(1.0),
);
TerminalPainter _make({TerminalTheme? theme, TerminalStyle? style, TextScaler? scaler}) =>
TerminalPainter(theme: theme ?? TerminalThemes.defaultTheme, textStyle: style ?? const TerminalStyle(), textScaler: scaler ?? const TextScaler.linear(1.0));
Canvas _canvas() => Canvas(PictureRecorder());
@@ -101,11 +93,7 @@ void main() {
});
group('TerminalPainter — paintCursor', () {
final cases = <TerminalCursorType>{
TerminalCursorType.block,
TerminalCursorType.underline,
TerminalCursorType.verticalBar,
};
final cases = <TerminalCursorType>{TerminalCursorType.block, TerminalCursorType.underline, TerminalCursorType.verticalBar};
for (final c in cases) {
test('focused $c paints without throwing', () {
_make().paintCursor(_canvas(), const Offset(0, 0), cursorType: c);
@@ -114,12 +102,7 @@ void main() {
test('unfocused (hasFocus=false) draws a stroked rect regardless of cursor type', () {
// Should hit the early-return-with-stroke branch, not the switch.
_make().paintCursor(
_canvas(),
const Offset(0, 0),
cursorType: TerminalCursorType.verticalBar,
hasFocus: false,
);
_make().paintCursor(_canvas(), const Offset(0, 0), cursorType: TerminalCursorType.verticalBar, hasFocus: false);
});
});
@@ -149,12 +132,7 @@ void main() {
test('paintCellForeground caches the layout — second call hits the cache', () {
final p = _make();
final cell = CellData(
foreground: CellColor.normal,
background: CellColor.normal,
flags: 0,
content: 'A'.codeUnitAt(0),
);
final cell = CellData(foreground: CellColor.normal, background: CellColor.normal, flags: 0, content: 'A'.codeUnitAt(0));
// Two paints with the same cell → second one resolves through the cache.
p.paintCellForeground(_canvas(), Offset.zero, cell);
p.paintCellForeground(_canvas(), Offset.zero, cell);
@@ -164,88 +142,48 @@ void main() {
final p = _make();
// faint
p.paintCellForeground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.normal,
background: CellColor.normal,
flags: CellFlags.faint,
content: 'B'.codeUnitAt(0),
));
_canvas(),
Offset.zero,
CellData(foreground: CellColor.normal, background: CellColor.normal, flags: CellFlags.faint, content: 'B'.codeUnitAt(0)),
);
// inverse → uses background as foreground
p.paintCellForeground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.normal,
background: CellColor.named | 2,
flags: CellFlags.inverse,
content: 'C'.codeUnitAt(0),
));
_canvas(),
Offset.zero,
CellData(foreground: CellColor.normal, background: CellColor.named | 2, flags: CellFlags.inverse, content: 'C'.codeUnitAt(0)),
);
// underline + space → swaps to U+00A0 internally
p.paintCellForeground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.normal,
background: CellColor.normal,
flags: CellFlags.underline,
content: 0x20,
));
_canvas(),
Offset.zero,
CellData(foreground: CellColor.normal, background: CellColor.normal, flags: CellFlags.underline, content: 0x20),
);
// bold + italic — exercises the flag-driven TextStyle path.
p.paintCellForeground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.normal,
background: CellColor.normal,
flags: CellFlags.bold | CellFlags.italic,
content: 'D'.codeUnitAt(0),
));
_canvas(),
Offset.zero,
CellData(foreground: CellColor.normal, background: CellColor.normal, flags: CellFlags.bold | CellFlags.italic, content: 'D'.codeUnitAt(0)),
);
});
test('paintCellBackground covers normal early-return, inverse, and named/palette paths', () {
final p = _make();
// normal + no inverse → early return, no draw.
p.paintCellBackground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.normal,
background: CellColor.normal,
flags: 0,
content: 0,
));
p.paintCellBackground(_canvas(), Offset.zero, CellData(foreground: CellColor.normal, background: CellColor.normal, flags: 0, content: 0));
// inverse → resolves foreground colour and draws.
p.paintCellBackground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.named | 1,
background: CellColor.normal,
flags: CellFlags.inverse,
content: 0,
));
_canvas(),
Offset.zero,
CellData(foreground: CellColor.named | 1, background: CellColor.normal, flags: CellFlags.inverse, content: 0),
);
// explicit background colour → draws.
p.paintCellBackground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.normal,
background: CellColor.palette | 5,
flags: 0,
content: 0,
));
p.paintCellBackground(_canvas(), Offset.zero, CellData(foreground: CellColor.normal, background: CellColor.palette | 5, flags: 0, content: 0));
// double-width cell → exercises the widthScale=2 branch.
p.paintCellBackground(
_canvas(),
Offset.zero,
CellData(
foreground: CellColor.normal,
background: CellColor.named | 4,
flags: 0,
content: 'A'.codeUnitAt(0) | (2 << CellContent.widthShift),
));
_canvas(),
Offset.zero,
CellData(foreground: CellColor.normal, background: CellColor.named | 4, flags: 0, content: 'A'.codeUnitAt(0) | (2 << CellContent.widthShift)),
);
});
});
+6 -6
View File
@@ -40,12 +40,12 @@ class _TestViewportOffset extends ViewportOffset {
}
Widget _host(Widget child) => Directionality(
textDirection: TextDirection.ltr,
child: MediaQuery(
data: const MediaQueryData(),
child: Center(child: SizedBox(width: 400, height: 200, child: child)),
),
);
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', () {
+21 -17
View File
@@ -15,12 +15,12 @@ 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)),
),
);
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', () {
@@ -31,16 +31,20 @@ void main() {
// 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.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);
@@ -160,7 +164,7 @@ void main() {
await tester.pump();
final state = tester.state<TerminalViewState>(find.byType(TerminalView));
var callbackFired = 0;
state.renderTerminal.onEditableRect = (_, __) => callbackFired++;
state.renderTerminal.onEditableRect = (_, _) => callbackFired++;
// Triggering a layout-affecting change should drive
// _notifyEditableRect on the next layout pump.
t.write('layout-trigger');
+77 -76
View File
@@ -49,12 +49,7 @@ void main() {
tearDown(() => debugDefaultTargetPlatformOverride = null);
test('Linux/Windows/Android/Fuchsia use Ctrl-modified bindings', () {
for (final p in [
TargetPlatform.linux,
TargetPlatform.windows,
TargetPlatform.android,
TargetPlatform.fuchsia,
]) {
for (final p in [TargetPlatform.linux, TargetPlatform.windows, TargetPlatform.android, TargetPlatform.fuchsia]) {
debugDefaultTargetPlatformOverride = p;
final map = defaultTerminalShortcuts;
// Three entries: copy / paste / select-all.
@@ -83,10 +78,7 @@ void main() {
});
group('TerminalActions', () {
Widget host(Widget child) => Directionality(
textDirection: TextDirection.ltr,
child: child,
);
Widget host(Widget child) => Directionality(textDirection: TextDirection.ltr, child: child);
testWidgets('CopySelectionTextIntent writes selection text to the clipboard', (tester) async {
final outputs = <String>[];
@@ -95,32 +87,32 @@ void main() {
final controller = TerminalController();
addTearDown(controller.dispose);
// Set a selection covering 'hello'.
controller.setSelection(
terminal.buffer.createAnchor(0, 0),
terminal.buffer.createAnchor(5, 0),
);
controller.setSelection(terminal.buffer.createAnchor(0, 0), terminal.buffer.createAnchor(5, 0));
String? clipboardText;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
if (call.method == 'Clipboard.setData') {
final args = call.arguments as Map;
clipboardText = args['text'] as String?;
}
return null;
},
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') {
final args = call.arguments as Map;
clipboardText = args['text'] as String?;
}
return null;
});
late BuildContext capturedContext;
await tester.pumpWidget(host(TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(builder: (context) {
capturedContext = context;
return const SizedBox();
}),
)));
await tester.pumpWidget(
host(
TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(
builder: (context) {
capturedContext = context;
return const SizedBox();
},
),
),
),
);
Actions.invoke(capturedContext, CopySelectionTextIntent.copy);
await tester.pump();
@@ -132,23 +124,26 @@ void main() {
final controller = TerminalController();
addTearDown(controller.dispose);
var setDataCalls = 0;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
if (call.method == 'Clipboard.setData') setDataCalls++;
return null;
},
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.setData') setDataCalls++;
return null;
});
late BuildContext ctx;
await tester.pumpWidget(host(TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(builder: (c) {
ctx = c;
return const SizedBox();
}),
)));
await tester.pumpWidget(
host(
TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(
builder: (c) {
ctx = c;
return const SizedBox();
},
),
),
),
);
Actions.invoke(ctx, CopySelectionTextIntent.copy);
await tester.pump();
@@ -160,31 +155,31 @@ void main() {
final terminal = Terminal(maxLines: 100, onOutput: outputs.add);
final controller = TerminalController();
addTearDown(controller.dispose);
controller.setSelection(
terminal.buffer.createAnchor(0, 0),
terminal.buffer.createAnchor(2, 0),
);
controller.setSelection(terminal.buffer.createAnchor(0, 0), terminal.buffer.createAnchor(2, 0));
expect(controller.selection, isNotNull);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.platform,
(call) async {
if (call.method == 'Clipboard.getData') {
return <String, dynamic>{'text': 'pasted-payload'};
}
return null;
},
);
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'Clipboard.getData') {
return <String, dynamic>{'text': 'pasted-payload'};
}
return null;
});
late BuildContext ctx;
await tester.pumpWidget(host(TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(builder: (c) {
ctx = c;
return const SizedBox();
}),
)));
await tester.pumpWidget(
host(
TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(
builder: (c) {
ctx = c;
return const SizedBox();
},
),
),
),
);
Actions.invoke(ctx, const PasteTextIntent(SelectionChangedCause.keyboard));
await tester.pumpAndSettle();
@@ -201,14 +196,20 @@ void main() {
expect(controller.selection, isNull);
late BuildContext ctx;
await tester.pumpWidget(host(TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(builder: (c) {
ctx = c;
return const SizedBox();
}),
)));
await tester.pumpWidget(
host(
TerminalActions(
terminal: terminal,
controller: controller,
child: Builder(
builder: (c) {
ctx = c;
return const SizedBox();
},
),
),
),
);
Actions.invoke(ctx, const SelectAllTextIntent(SelectionChangedCause.keyboard));
await tester.pump();
+1 -5
View File
@@ -24,11 +24,7 @@ class _Recorder {
String? lastOscPs;
List<String>? lastOscArgs;
Terminal build({
int maxLines = 100,
TerminalTargetPlatform platform = TerminalTargetPlatform.linux,
bool reflow = true,
}) {
Terminal build({int maxLines = 100, TerminalTargetPlatform platform = TerminalTargetPlatform.linux, bool reflow = true}) {
return Terminal(
maxLines: maxLines,
platform: platform,
+31 -57
View File
@@ -53,11 +53,7 @@ void main() {
focus.dispose();
});
await tester.pumpWidget(_host(TerminalView(
t,
controller: controller,
focusNode: focus,
)));
await tester.pumpWidget(_host(TerminalView(t, controller: controller, focusNode: focus)));
// Tear down via removing the widget; supplied controllers should NOT
// throw on later use (the state didn't dispose them).
await tester.pumpWidget(_host(const SizedBox()));
@@ -89,10 +85,7 @@ void main() {
await testGesture.addPointer(location: viewCenter);
// A scroll event with a positive dy → page-down keys; a negative
// dy → page-up. Use a magnitude that comfortably rounds to 1+ lines.
await tester.sendEventToBinding(PointerScrollEvent(
position: viewCenter,
scrollDelta: const Offset(0, 100),
));
await tester.sendEventToBinding(PointerScrollEvent(position: viewCenter, scrollDelta: const Offset(0, 100)));
await tester.pump();
// Output should contain at least one PgDown escape.
expect(r.outputs, isNotEmpty);
@@ -108,10 +101,7 @@ void main() {
final viewCenter = tester.getCenter(find.byType(TerminalView));
final testGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
await testGesture.addPointer(location: viewCenter);
await tester.sendEventToBinding(PointerScrollEvent(
position: viewCenter,
scrollDelta: const Offset(0, 100),
));
await tester.sendEventToBinding(PointerScrollEvent(position: viewCenter, scrollDelta: const Offset(0, 100)));
await tester.pump();
expect(r.outputs, isNotEmpty);
// Wheel-down id is 64+5=69; normal-mode reporter encodes button
@@ -131,10 +121,7 @@ void main() {
final viewCenter = tester.getCenter(find.byType(TerminalView));
// PointerScaleEvent is a sibling of PointerScrollEvent; the handler
// ignores anything other than PointerScrollEvent.
await tester.sendEventToBinding(PointerScaleEvent(
position: viewCenter,
scale: 1.2,
));
await tester.sendEventToBinding(PointerScaleEvent(position: viewCenter, scale: 1.2));
await tester.pump();
expect(r.outputs, isEmpty);
});
@@ -188,11 +175,15 @@ void main() {
testWidgets('hardware key event with a non-modifier key reaches Terminal.keyInput', (tester) async {
final r = _OutputRecorder();
final t = r.build();
await tester.pumpWidget(_host(TerminalView(
t,
autofocus: true,
// hardwareKeyboardOnly: true,
)));
await tester.pumpWidget(
_host(
TerminalView(
t,
autofocus: true,
// hardwareKeyboardOnly: true,
),
),
);
// Wait one frame for autofocus to settle.
await tester.pump();
// Send a Down arrow keypress through the binding.
@@ -241,14 +232,18 @@ void main() {
final r = _OutputRecorder();
final t = r.build();
var overrideCalls = 0;
await tester.pumpWidget(_host(TerminalView(
t,
autofocus: true,
onKeyEvent: (focusNode, event) {
overrideCalls++;
return KeyEventResult.handled;
},
)));
await tester.pumpWidget(
_host(
TerminalView(
t,
autofocus: true,
onKeyEvent: (focusNode, event) {
overrideCalls++;
return KeyEventResult.handled;
},
),
),
);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
@@ -259,11 +254,7 @@ void main() {
testWidgets('onKeyEvent returning ignored falls through to the default handler', (tester) async {
final r = _OutputRecorder();
final t = r.build();
await tester.pumpWidget(_host(TerminalView(
t,
autofocus: true,
onKeyEvent: (_, __) => KeyEventResult.ignored,
)));
await tester.pumpWidget(_host(TerminalView(t, autofocus: true, onKeyEvent: (_, _) => KeyEventResult.ignored)));
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
@@ -308,10 +299,7 @@ void main() {
final r = _OutputRecorder();
final t = r.build();
var tappedAt = 0;
await tester.pumpWidget(_host(TerminalView(
t,
onTapUp: (_, __) => tappedAt++,
)));
await tester.pumpWidget(_host(TerminalView(t, onTapUp: (_, _) => tappedAt++)));
await tester.tapAt(tester.getCenter(find.byType(TerminalView)));
// The gesture detector arms a 300 ms double-tap timer after the tap;
// explicit duration flushes it (pumpAndSettle waits on animations,
@@ -324,10 +312,7 @@ void main() {
final t = _OutputRecorder().build();
final controller = TerminalController();
addTearDown(controller.dispose);
controller.setSelection(
t.buffer.createAnchor(0, 0),
t.buffer.createAnchor(2, 0),
);
controller.setSelection(t.buffer.createAnchor(0, 0), t.buffer.createAnchor(2, 0));
expect(controller.selection, isNotNull);
await tester.pumpWidget(_host(TerminalView(t, controller: controller)));
await tester.tapAt(tester.getCenter(find.byType(TerminalView)));
@@ -340,11 +325,7 @@ void main() {
final t = r.build();
var downCalls = 0;
var upCalls = 0;
await tester.pumpWidget(_host(TerminalView(
t,
onSecondaryTapDown: (_, __) => downCalls++,
onSecondaryTapUp: (_, __) => upCalls++,
)));
await tester.pumpWidget(_host(TerminalView(t, onSecondaryTapDown: (_, _) => downCalls++, onSecondaryTapUp: (_, _) => upCalls++)));
final pos = tester.getCenter(find.byType(TerminalView));
final gesture = await tester.startGesture(pos, buttons: kSecondaryButton);
await gesture.up();
@@ -448,10 +429,7 @@ void main() {
// deleteDetection seeds the editing value with two spaces; shrinking
// it below that length triggers CustomTextEdit.onDelete, which fires
// _scrollToBottom + Terminal.keyInput(backspace) in TerminalView.
tester.testTextInput.updateEditingValue(const TextEditingValue(
text: '',
selection: TextSelection.collapsed(offset: 0),
));
tester.testTextInput.updateEditingValue(const TextEditingValue(text: '', selection: TextSelection.collapsed(offset: 0)));
await tester.pump();
// Backspace produces a non-empty escape sequence (^?).
expect(r.outputs, isNotEmpty);
@@ -461,11 +439,7 @@ void main() {
final t = _OutputRecorder().build();
final focus = FocusNode();
addTearDown(focus.dispose);
await tester.pumpWidget(_host(TerminalView(
t,
hardwareKeyboardOnly: true,
focusNode: focus,
)));
await tester.pumpWidget(_host(TerminalView(t, hardwareKeyboardOnly: true, focusNode: focus)));
// Tap with no selection-clear short-circuit — the else-branch of
// _onTapUp routes through _focusNode.requestFocus when
// hardwareKeyboardOnly is set (no CustomTextEdit to delegate to).
+3 -14
View File
@@ -142,9 +142,7 @@ void main() {
});
test('fromTextStyle picks fontSize / height / fontFamilyFallback through to TerminalStyle', () {
final s = TerminalStyle.fromTextStyle(
const TextStyle(fontSize: 18, height: 1.5, fontFamilyFallback: ['Mono']),
);
final s = TerminalStyle.fromTextStyle(const TextStyle(fontSize: 18, height: 1.5, fontFamilyFallback: ['Mono']));
expect(s.fontSize, 18);
expect(s.height, 1.5);
expect(s.fontFamilyFallback, ['Mono']);
@@ -152,12 +150,7 @@ void main() {
test('toTextStyle threads attribute flags into the resulting TextStyle', () {
const s = TerminalStyle(fontSize: 16);
final t = s.toTextStyle(
color: const Color(0xFF112233),
bold: true,
italic: true,
underline: true,
);
final t = s.toTextStyle(color: const Color(0xFF112233), bold: true, italic: true, underline: true);
expect(t.fontSize, 16);
expect(t.fontWeight, FontWeight.bold);
expect(t.fontStyle, FontStyle.italic);
@@ -244,11 +237,7 @@ void main() {
group('SelectionMode + TerminalCursorType enums', () {
test('all enum values exist and are distinct', () {
expect(SelectionMode.values, [SelectionMode.line, SelectionMode.block]);
expect(TerminalCursorType.values, [
TerminalCursorType.block,
TerminalCursorType.underline,
TerminalCursorType.verticalBar,
]);
expect(TerminalCursorType.values, [TerminalCursorType.block, TerminalCursorType.underline, TerminalCursorType.verticalBar]);
});
});
+33 -27
View File
@@ -28,16 +28,18 @@ void main() {
final focus = FocusNode();
addTearDown(focus.dispose);
await tester.pumpWidget(_host(
CustomKeyboardListener(
focusNode: focus,
autofocus: true,
onInsert: inserts.add,
onComposing: composings.add,
onKeyEvent: (_, __) => KeyEventResult.ignored,
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
await tester.pumpWidget(
_host(
CustomKeyboardListener(
focusNode: focus,
autofocus: true,
onInsert: inserts.add,
onComposing: composings.add,
onKeyEvent: (_, _) => KeyEventResult.ignored,
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
),
),
));
);
await tester.pump();
// A character key with non-empty `character` triggers the insert path.
await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a');
@@ -50,16 +52,18 @@ void main() {
final focus = FocusNode();
addTearDown(focus.dispose);
await tester.pumpWidget(_host(
CustomKeyboardListener(
focusNode: focus,
autofocus: true,
onInsert: inserts.add,
onComposing: (_) {},
onKeyEvent: (_, __) => KeyEventResult.handled,
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
await tester.pumpWidget(
_host(
CustomKeyboardListener(
focusNode: focus,
autofocus: true,
onInsert: inserts.add,
onComposing: (_) {},
onKeyEvent: (_, _) => KeyEventResult.handled,
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
),
),
));
);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.keyA, character: 'a');
await tester.pump();
@@ -71,16 +75,18 @@ void main() {
final focus = FocusNode();
addTearDown(focus.dispose);
await tester.pumpWidget(_host(
CustomKeyboardListener(
focusNode: focus,
autofocus: true,
onInsert: inserts.add,
onComposing: (_) {},
onKeyEvent: (_, __) => KeyEventResult.ignored,
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
await tester.pumpWidget(
_host(
CustomKeyboardListener(
focusNode: focus,
autofocus: true,
onInsert: inserts.add,
onComposing: (_) {},
onKeyEvent: (_, _) => KeyEventResult.ignored,
child: const ColoredBox(color: Color(0xFF000000), child: SizedBox.expand()),
),
),
));
);
await tester.pump();
// ArrowUp has no `character` — ignored result + no character means
// _onKeyEvent returns ignored without firing onInsert.