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>
354 lines
14 KiB
Dart
354 lines
14 KiB
Dart
/// Pure-Dart tests for the BufferRange family (CellOffset, BufferSegment,
|
|
/// BufferRangeLine, BufferRangeBlock). No Flutter dependency.
|
|
library;
|
|
|
|
import 'package:clide/src/terminal/src/core/buffer/cell_offset.dart';
|
|
import 'package:clide/src/terminal/src/core/buffer/range.dart';
|
|
import 'package:clide/src/terminal/src/core/buffer/range_block.dart';
|
|
import 'package:clide/src/terminal/src/core/buffer/range_line.dart';
|
|
import 'package:clide/src/terminal/src/core/buffer/segment.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
group('CellOffset', () {
|
|
test('equality + hashCode for same coordinates', () {
|
|
const a = CellOffset(3, 5);
|
|
const b = CellOffset(3, 5);
|
|
expect(a, b);
|
|
expect(a.hashCode, b.hashCode);
|
|
expect(a.isEqual(b), isTrue);
|
|
expect(identical(a, a), isTrue); // hits the identity branch in ==
|
|
const Object notACellOffset = 'CellOffset(3, 5)';
|
|
expect(a == notACellOffset, isFalse); // hits the type-mismatch branch
|
|
});
|
|
|
|
test('inequality for different coordinates', () {
|
|
expect(const CellOffset(3, 5), isNot(const CellOffset(4, 5)));
|
|
expect(const CellOffset(3, 5).isEqual(const CellOffset(3, 6)), isFalse);
|
|
});
|
|
|
|
test('isBefore / isAfter — different rows', () {
|
|
const top = CellOffset(9, 1);
|
|
const bottom = CellOffset(0, 2);
|
|
expect(top.isBefore(bottom), isTrue);
|
|
expect(bottom.isAfter(top), isTrue);
|
|
expect(top.isAfter(bottom), isFalse);
|
|
expect(bottom.isBefore(top), isFalse);
|
|
});
|
|
|
|
test('isBefore / isAfter — same row, different columns', () {
|
|
const left = CellOffset(2, 4);
|
|
const right = CellOffset(7, 4);
|
|
expect(left.isBefore(right), isTrue);
|
|
expect(right.isAfter(left), isTrue);
|
|
expect(left.isAfter(right), isFalse);
|
|
expect(right.isBefore(left), isFalse);
|
|
});
|
|
|
|
test('isBefore / isAfter — equal positions are neither', () {
|
|
const a = CellOffset(2, 2);
|
|
const b = CellOffset(2, 2);
|
|
expect(a.isBefore(b), isFalse);
|
|
expect(a.isAfter(b), isFalse);
|
|
});
|
|
|
|
test('isBeforeOrSame / isAfterOrSame include equal positions', () {
|
|
const a = CellOffset(3, 3);
|
|
const b = CellOffset(3, 3);
|
|
const earlier = CellOffset(2, 3);
|
|
const later = CellOffset(4, 3);
|
|
expect(a.isBeforeOrSame(b), isTrue);
|
|
expect(a.isAfterOrSame(b), isTrue);
|
|
expect(earlier.isBeforeOrSame(a), isTrue);
|
|
expect(later.isAfterOrSame(a), isTrue);
|
|
expect(later.isBeforeOrSame(a), isFalse);
|
|
expect(earlier.isAfterOrSame(a), isFalse);
|
|
});
|
|
|
|
test('isAtSameRow / isAtSameColumn', () {
|
|
expect(const CellOffset(2, 5).isAtSameRow(const CellOffset(9, 5)), isTrue);
|
|
expect(const CellOffset(2, 5).isAtSameRow(const CellOffset(2, 6)), isFalse);
|
|
expect(const CellOffset(4, 1).isAtSameColumn(const CellOffset(4, 9)), isTrue);
|
|
expect(const CellOffset(4, 1).isAtSameColumn(const CellOffset(5, 9)), isFalse);
|
|
});
|
|
|
|
test('isWithin delegates to BufferRange.contains', () {
|
|
final range = BufferRangeLine(const CellOffset(0, 0), const CellOffset(5, 0));
|
|
expect(const CellOffset(3, 0).isWithin(range), isTrue);
|
|
expect(const CellOffset(3, 1).isWithin(range), isFalse);
|
|
});
|
|
|
|
test('toString shape', () {
|
|
expect(const CellOffset(2, 3).toString(), 'CellOffset(2, 3)');
|
|
});
|
|
});
|
|
|
|
group('BufferSegment', () {
|
|
test('isWithin — wrong line is never within', () {
|
|
const seg = BufferSegment(_dummyRange, 4, 2, 8);
|
|
expect(seg.isWithin(const CellOffset(5, 3)), isFalse);
|
|
expect(seg.isWithin(const CellOffset(5, 5)), isFalse);
|
|
});
|
|
|
|
test('isWithin — bounded segment respects start and end', () {
|
|
const seg = BufferSegment(_dummyRange, 4, 2, 8);
|
|
expect(seg.isWithin(const CellOffset(2, 4)), isTrue); // at start
|
|
expect(seg.isWithin(const CellOffset(5, 4)), isTrue); // middle
|
|
expect(seg.isWithin(const CellOffset(8, 4)), isTrue); // at end
|
|
expect(seg.isWithin(const CellOffset(1, 4)), isFalse); // before start
|
|
expect(seg.isWithin(const CellOffset(9, 4)), isFalse); // after end
|
|
});
|
|
|
|
test('isWithin — null start means line beginning', () {
|
|
const seg = BufferSegment(_dummyRange, 0, null, 5);
|
|
expect(seg.isWithin(const CellOffset(0, 0)), isTrue);
|
|
expect(seg.isWithin(const CellOffset(5, 0)), isTrue);
|
|
expect(seg.isWithin(const CellOffset(6, 0)), isFalse);
|
|
});
|
|
|
|
test('isWithin — null end means line end', () {
|
|
const seg = BufferSegment(_dummyRange, 0, 3, null);
|
|
expect(seg.isWithin(const CellOffset(3, 0)), isTrue);
|
|
expect(seg.isWithin(const CellOffset(999, 0)), isTrue);
|
|
expect(seg.isWithin(const CellOffset(2, 0)), isFalse);
|
|
});
|
|
|
|
test('equality + hashCode', () {
|
|
const a = BufferSegment(_dummyRange, 1, 2, 3);
|
|
const b = BufferSegment(_dummyRange, 1, 2, 3);
|
|
const c = BufferSegment(_dummyRange, 1, 2, 4);
|
|
expect(a, b);
|
|
expect(a.hashCode, b.hashCode);
|
|
expect(a, isNot(c));
|
|
const Object notASegment = 'segment';
|
|
expect(a == notASegment, isFalse);
|
|
expect(identical(a, a), isTrue);
|
|
});
|
|
|
|
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)');
|
|
});
|
|
});
|
|
|
|
group('BufferRangeLine', () {
|
|
test('collapsed range has equal begin and end', () {
|
|
final r = BufferRangeLine.collapsed(const CellOffset(2, 3));
|
|
expect(r.isCollapsed, isTrue);
|
|
expect(r.begin, const CellOffset(2, 3));
|
|
expect(r.end, const CellOffset(2, 3));
|
|
});
|
|
|
|
test('isNormalized true when begin <= end', () {
|
|
final r = BufferRangeLine(const CellOffset(0, 0), const CellOffset(3, 1));
|
|
expect(r.isNormalized, isTrue);
|
|
});
|
|
|
|
test('normalized swaps begin/end when reversed', () {
|
|
final r = BufferRangeLine(const CellOffset(3, 1), const CellOffset(0, 0));
|
|
expect(r.isNormalized, isFalse);
|
|
final n = r.normalized;
|
|
expect(n.begin, const CellOffset(0, 0));
|
|
expect(n.end, const CellOffset(3, 1));
|
|
expect(n.normalized, n); // already normalized → identity
|
|
});
|
|
|
|
test('toSegments — single line returns one bounded segment', () {
|
|
final r = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 0));
|
|
final segs = r.toSegments().toList();
|
|
expect(segs, hasLength(1));
|
|
expect(segs[0].line, 0);
|
|
expect(segs[0].start, 2);
|
|
expect(segs[0].end, 5);
|
|
});
|
|
|
|
test('toSegments — multiple lines: first bounded-left, last bounded-right, middle unbounded', () {
|
|
final r = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 2));
|
|
final segs = r.toSegments().toList();
|
|
expect(segs.map((s) => s.line), [0, 1, 2]);
|
|
expect(segs[0].start, 2);
|
|
expect(segs[0].end, isNull);
|
|
expect(segs[1].start, isNull);
|
|
expect(segs[1].end, isNull);
|
|
expect(segs[2].start, isNull);
|
|
expect(segs[2].end, 5);
|
|
});
|
|
|
|
test('contains positions inside the range, including endpoints', () {
|
|
final r = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 2));
|
|
expect(r.contains(const CellOffset(2, 0)), isTrue);
|
|
expect(r.contains(const CellOffset(0, 1)), isTrue); // mid line
|
|
expect(r.contains(const CellOffset(5, 2)), isTrue);
|
|
expect(r.contains(const CellOffset(1, 0)), isFalse);
|
|
expect(r.contains(const CellOffset(6, 2)), isFalse);
|
|
});
|
|
|
|
test('contains works on a denormalized range too (normalizes internally)', () {
|
|
final r = BufferRangeLine(const CellOffset(5, 2), const CellOffset(2, 0));
|
|
expect(r.contains(const CellOffset(0, 1)), isTrue);
|
|
});
|
|
|
|
test('merge picks earliest begin and latest end', () {
|
|
final a = BufferRangeLine(const CellOffset(2, 0), const CellOffset(5, 0));
|
|
final b = BufferRangeLine(const CellOffset(0, 0), const CellOffset(9, 0));
|
|
final m = a.merge(b);
|
|
expect(m.begin, const CellOffset(0, 0));
|
|
expect(m.end, const CellOffset(9, 0));
|
|
});
|
|
|
|
test('extend grows the range to cover an outside position', () {
|
|
final r = BufferRangeLine(const CellOffset(2, 1), const CellOffset(5, 1));
|
|
final earlier = r.extend(const CellOffset(0, 0));
|
|
expect(earlier.begin, const CellOffset(0, 0));
|
|
expect(earlier.end, const CellOffset(5, 1));
|
|
final later = r.extend(const CellOffset(9, 3));
|
|
expect(later.begin, const CellOffset(2, 1));
|
|
expect(later.end, const CellOffset(9, 3));
|
|
});
|
|
|
|
test('equality + hashCode + toString', () {
|
|
final a = BufferRangeLine(const CellOffset(0, 0), const CellOffset(1, 1));
|
|
final b = BufferRangeLine(const CellOffset(0, 0), const CellOffset(1, 1));
|
|
final c = BufferRangeLine(const CellOffset(0, 0), const CellOffset(1, 2));
|
|
expect(a, b);
|
|
expect(a.hashCode, b.hashCode);
|
|
expect(a, isNot(c));
|
|
const Object notALineRange = 'line range';
|
|
expect(a == notALineRange, isFalse);
|
|
expect(identical(a, a), isTrue);
|
|
expect(a.toString(), startsWith('Line Range('));
|
|
});
|
|
});
|
|
|
|
group('BufferRangeBlock', () {
|
|
test('isNormalized requires top-left + bottom-right corners', () {
|
|
final tlBr = BufferRangeBlock(const CellOffset(2, 1), const CellOffset(5, 4));
|
|
expect(tlBr.isNormalized, isTrue);
|
|
|
|
final trBl = BufferRangeBlock(const CellOffset(5, 1), const CellOffset(2, 4));
|
|
expect(trBl.isNormalized, isFalse);
|
|
});
|
|
|
|
test('normalized produces top-left / bottom-right corners', () {
|
|
final r = BufferRangeBlock(const CellOffset(5, 1), const CellOffset(2, 4));
|
|
final n = r.normalized;
|
|
expect(n.begin, const CellOffset(2, 1));
|
|
expect(n.end, const CellOffset(5, 4));
|
|
expect(n.normalized, n); // already normalized branch
|
|
});
|
|
|
|
test('toSegments — bounded on every line', () {
|
|
final r = BufferRangeBlock(const CellOffset(2, 0), const CellOffset(5, 2));
|
|
final segs = r.toSegments().toList();
|
|
expect(segs.map((s) => s.line), [0, 1, 2]);
|
|
for (final s in segs) {
|
|
expect(s.start, 2);
|
|
expect(s.end, 5);
|
|
}
|
|
});
|
|
|
|
test('toSegments — denormalized input still yields normalized output', () {
|
|
final r = BufferRangeBlock(const CellOffset(5, 2), const CellOffset(2, 0));
|
|
final segs = r.toSegments().toList();
|
|
// After internal normalization the segment range is x:[2..5] over y:[0..2].
|
|
expect(segs.map((s) => s.line), [0, 1, 2]);
|
|
for (final s in segs) {
|
|
expect(s.start, 2);
|
|
expect(s.end, 5);
|
|
}
|
|
});
|
|
|
|
test('contains — inside, on edge, outside', () {
|
|
final r = BufferRangeBlock(const CellOffset(2, 0), const CellOffset(5, 2));
|
|
expect(r.contains(const CellOffset(2, 0)), isTrue); // tl
|
|
expect(r.contains(const CellOffset(5, 2)), isTrue); // br
|
|
expect(r.contains(const CellOffset(3, 1)), isTrue); // inside
|
|
expect(r.contains(const CellOffset(1, 1)), isFalse); // left of block
|
|
expect(r.contains(const CellOffset(6, 1)), isFalse); // right of block
|
|
expect(r.contains(const CellOffset(3, 3)), isFalse); // below block
|
|
});
|
|
|
|
test('contains works on denormalized blocks', () {
|
|
final r = BufferRangeBlock(const CellOffset(5, 2), const CellOffset(2, 0));
|
|
expect(r.contains(const CellOffset(3, 1)), isTrue);
|
|
});
|
|
|
|
test('extend — position inside is identity', () {
|
|
final r = BufferRangeBlock(const CellOffset(2, 0), const CellOffset(5, 2));
|
|
final same = r.extend(const CellOffset(3, 1));
|
|
expect(same, r);
|
|
});
|
|
|
|
test('extend — position outside grows the block', () {
|
|
final r = BufferRangeBlock(const CellOffset(2, 1), const CellOffset(5, 3));
|
|
final out = r.extend(const CellOffset(0, 0));
|
|
expect(out.begin, const CellOffset(0, 0));
|
|
expect(out.end, const CellOffset(5, 3));
|
|
});
|
|
|
|
test('merge — combines two blocks into the smallest enclosing block', () {
|
|
final a = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(2, 2));
|
|
final b = BufferRangeBlock(const CellOffset(5, 5), const CellOffset(7, 7));
|
|
final m = a.merge(b);
|
|
expect(m.begin, const CellOffset(0, 0));
|
|
expect(m.end, const CellOffset(7, 7));
|
|
});
|
|
|
|
test('equality + hashCode + toString', () {
|
|
final a = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(1, 1));
|
|
final b = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(1, 1));
|
|
final c = BufferRangeBlock(const CellOffset(0, 0), const CellOffset(1, 2));
|
|
expect(a, b);
|
|
expect(a.hashCode, b.hashCode);
|
|
expect(a, isNot(c));
|
|
const Object notABlockRange = 'block range';
|
|
expect(a == notABlockRange, isFalse);
|
|
expect(identical(a, a), isTrue);
|
|
expect(a.toString(), startsWith('Block Range('));
|
|
});
|
|
});
|
|
|
|
group('BufferRange (abstract base) — collapsed + isCollapsed + isNormalized', () {
|
|
test('collapsed range is normalized and collapsed', () {
|
|
final r = BufferRangeLine.collapsed(const CellOffset(3, 3));
|
|
expect(r.isCollapsed, isTrue);
|
|
expect(r.isNormalized, isTrue);
|
|
});
|
|
|
|
test('block collapsed constructor', () {
|
|
final r = BufferRangeBlock.collapsed(const CellOffset(2, 2));
|
|
expect(r.isCollapsed, isTrue);
|
|
expect(r.begin, r.end);
|
|
});
|
|
|
|
test('base ==/hashCode/toString via direct subclass that does not override them', () {
|
|
const a = _StubRange(CellOffset(0, 0), CellOffset(1, 1));
|
|
const b = _StubRange(CellOffset(0, 0), CellOffset(1, 1));
|
|
const c = _StubRange(CellOffset(0, 0), CellOffset(2, 2));
|
|
expect(a, b); // same begin + end via base ==
|
|
expect(a.hashCode, b.hashCode);
|
|
expect(a, isNot(c));
|
|
const Object notARange = 'not a range';
|
|
expect(a == notARange, isFalse); // type-mismatch branch
|
|
expect(identical(a, a), isTrue);
|
|
expect(a.toString(), 'Range(CellOffset(0, 0), CellOffset(1, 1))');
|
|
});
|
|
});
|
|
}
|
|
|
|
const _dummyRange = _StubRange(CellOffset(0, 0), CellOffset(0, 0));
|
|
|
|
class _StubRange extends BufferRange {
|
|
const _StubRange(super.begin, super.end);
|
|
@override
|
|
BufferRange get normalized => this;
|
|
@override
|
|
Iterable<BufferSegment> toSegments() => const [];
|
|
@override
|
|
bool contains(CellOffset position) => false;
|
|
@override
|
|
BufferRange merge(BufferRange range) => this;
|
|
@override
|
|
BufferRange extend(CellOffset position) => this;
|
|
}
|