Replace free-form fractional HUD widget positioning with a 4x8 int grid (col/row/colSpan/rowSpan), reject drag/resize moves that would overlap another visible widget, and re-place a re-enabled metric into the next free grid slot instead of stacking it on an occupier. Drag/resize now track a local pixel offset during the gesture and only snap/commit once at gesture end. HUD widget text is wrapped in FittedBox(scaleDown) so it never overflows and stays centered at any grid size.
226 lines
8.1 KiB
Dart
226 lines
8.1 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:rippr/src/hud/hud_metric.dart';
|
|
import 'package:rippr/src/hud/hud_widget_layout.dart';
|
|
|
|
/// FB-03: the JSON round-trip, grid-clamp math, overlap check, and free-slot search
|
|
/// are all exercised directly with fixed inputs -- no real gestures needed to test
|
|
/// any of these, per the ticket's own Tests section.
|
|
void main() {
|
|
group('JSON round trip', () {
|
|
test('encodes and decodes exactly', () {
|
|
const original = HudWidgetLayout(
|
|
metric: HudMetric.speed,
|
|
col: 1,
|
|
row: 2,
|
|
colSpan: 3,
|
|
rowSpan: 2,
|
|
visible: true,
|
|
);
|
|
|
|
final decoded = HudWidgetLayout.fromJson(HudMetric.speed, original.toJson());
|
|
|
|
expect(decoded.col, original.col);
|
|
expect(decoded.row, original.row);
|
|
expect(decoded.colSpan, original.colSpan);
|
|
expect(decoded.rowSpan, original.rowSpan);
|
|
expect(decoded.visible, original.visible);
|
|
});
|
|
|
|
test('a malformed entry falls back to the default rather than crashing', () {
|
|
final decoded = HudWidgetLayout.fromJson(HudMetric.distance, {'col': 'not a number'});
|
|
final default_ = HudWidgetLayout.defaultFor(HudMetric.distance);
|
|
|
|
expect(decoded.col, default_.col);
|
|
expect(decoded.colSpan, default_.colSpan);
|
|
});
|
|
|
|
test('a missing field falls back to the default rather than crashing', () {
|
|
final decoded = HudWidgetLayout.fromJson(HudMetric.maxSpeed, {'col': 1, 'row': 1});
|
|
final default_ = HudWidgetLayout.defaultFor(HudMetric.maxSpeed);
|
|
|
|
expect(decoded.col, default_.col);
|
|
});
|
|
|
|
test('an old-shaped (pre-grid) saved layout falls back to the default rather '
|
|
'than crashing', () {
|
|
final decoded = HudWidgetLayout.fromJson(
|
|
HudMetric.speed,
|
|
{'x': 0.1, 'y': 0.2, 'width': 0.3, 'height': 0.15, 'visible': true},
|
|
);
|
|
final default_ = HudWidgetLayout.defaultFor(HudMetric.speed);
|
|
|
|
expect(decoded.col, default_.col);
|
|
expect(decoded.row, default_.row);
|
|
expect(decoded.colSpan, default_.colSpan);
|
|
expect(decoded.rowSpan, default_.rowSpan);
|
|
});
|
|
});
|
|
|
|
group('defaultFor', () {
|
|
test('every metric gets a distinct, non-overlapping default rect', () {
|
|
final layouts = HudMetric.values.map(HudWidgetLayout.defaultFor).toList();
|
|
for (var i = 0; i < layouts.length; i++) {
|
|
for (var j = i + 1; j < layouts.length; j++) {
|
|
expect(hudRectsOverlap(layouts[i], layouts[j]), isFalse,
|
|
reason: 'no two metrics should default to overlapping rects');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('the first four metrics (the Map HUD mockup\'s fixed row: Speed/Avg Speed/'
|
|
'Dist/Time) start visible', () {
|
|
expect(HudWidgetLayout.defaultFor(HudMetric.speed).visible, isTrue);
|
|
expect(HudWidgetLayout.defaultFor(HudMetric.avgSpeed).visible, isTrue);
|
|
expect(HudWidgetLayout.defaultFor(HudMetric.distance).visible, isTrue);
|
|
expect(HudWidgetLayout.defaultFor(HudMetric.elapsedTime).visible, isTrue);
|
|
});
|
|
|
|
test('metrics beyond the fixed row start hidden', () {
|
|
expect(HudWidgetLayout.defaultFor(HudMetric.maxSpeed).visible, isFalse);
|
|
expect(HudWidgetLayout.defaultFor(HudMetric.elevationGain).visible, isFalse);
|
|
});
|
|
|
|
test('every default is already within the valid grid bounds', () {
|
|
for (final metric in HudMetric.values) {
|
|
final layout = HudWidgetLayout.defaultFor(metric);
|
|
expect(layout.clampedToGrid().col, layout.col);
|
|
expect(layout.clampedToGrid().row, layout.row);
|
|
expect(layout.clampedToGrid().colSpan, layout.colSpan);
|
|
expect(layout.clampedToGrid().rowSpan, layout.rowSpan);
|
|
}
|
|
});
|
|
});
|
|
|
|
group('clampedToGrid', () {
|
|
const base = HudWidgetLayout(
|
|
metric: HudMetric.speed,
|
|
col: 1,
|
|
row: 1,
|
|
colSpan: 2,
|
|
rowSpan: 2,
|
|
visible: true,
|
|
);
|
|
|
|
test('a position dragged past the right/bottom edge is pulled back onto the '
|
|
'grid', () {
|
|
final result = base.copyWith(col: 20, row: 20).clampedToGrid();
|
|
expect(result.col, hudGridColumns - base.colSpan);
|
|
expect(result.row, hudGridRows - base.rowSpan);
|
|
});
|
|
|
|
test('a position dragged past the left/top edge is pulled back onto the grid', () {
|
|
final result = base.copyWith(col: -5, row: -5).clampedToGrid();
|
|
expect(result.col, 0);
|
|
expect(result.row, 0);
|
|
});
|
|
|
|
test('a resize below the legibility floor is corrected up to the minimum', () {
|
|
final result = base.copyWith(colSpan: 0, rowSpan: 0).clampedToGrid();
|
|
expect(result.colSpan, hudMinSpan);
|
|
expect(result.rowSpan, hudMinSpan);
|
|
});
|
|
|
|
test('a resize above the sane ceiling is corrected down to the maximum', () {
|
|
final result = base.copyWith(colSpan: 99, rowSpan: 99).clampedToGrid();
|
|
expect(result.colSpan, hudMaxColSpan);
|
|
expect(result.rowSpan, hudMaxRowSpan);
|
|
});
|
|
|
|
test('shrinking to fit happens before repositioning, so a widget resized past '
|
|
'the edge shrinks rather than silently relocates', () {
|
|
final result = base.copyWith(col: 3, colSpan: 4).clampedToGrid();
|
|
expect(result.colSpan, hudMaxColSpan);
|
|
// col itself is still clamped against the (now-bounded) span so the widget
|
|
// never sits even partially off the grid either.
|
|
expect(result.col + result.colSpan, lessThanOrEqualTo(hudGridColumns));
|
|
});
|
|
|
|
test('an already-valid layout is unchanged', () {
|
|
expect(base.clampedToGrid().col, base.col);
|
|
expect(base.clampedToGrid().row, base.row);
|
|
expect(base.clampedToGrid().colSpan, base.colSpan);
|
|
expect(base.clampedToGrid().rowSpan, base.rowSpan);
|
|
});
|
|
});
|
|
|
|
group('hudRectsOverlap', () {
|
|
const a = HudWidgetLayout(
|
|
metric: HudMetric.speed,
|
|
col: 1,
|
|
row: 1,
|
|
colSpan: 2,
|
|
rowSpan: 2,
|
|
visible: true,
|
|
);
|
|
|
|
test('two identical rects overlap', () {
|
|
expect(hudRectsOverlap(a, a), isTrue);
|
|
});
|
|
|
|
test('two rects sharing only an edge (touching, not overlapping) do not overlap', () {
|
|
final rightNeighbor = a.copyWith(col: a.col + a.colSpan);
|
|
expect(hudRectsOverlap(a, rightNeighbor), isFalse);
|
|
|
|
final belowNeighbor = a.copyWith(row: a.row + a.rowSpan);
|
|
expect(hudRectsOverlap(a, belowNeighbor), isFalse);
|
|
});
|
|
|
|
test('two disjoint rects do not overlap', () {
|
|
final far = a.copyWith(col: a.col + a.colSpan + 3, row: a.row + a.rowSpan + 3);
|
|
expect(hudRectsOverlap(a, far), isFalse);
|
|
});
|
|
|
|
test('a rect fully containing another overlaps', () {
|
|
const big = HudWidgetLayout(
|
|
metric: HudMetric.distance,
|
|
col: 0,
|
|
row: 0,
|
|
colSpan: 4,
|
|
rowSpan: 4,
|
|
visible: true,
|
|
);
|
|
expect(hudRectsOverlap(big, a), isTrue);
|
|
});
|
|
|
|
test('two partially-overlapping rects overlap', () {
|
|
final overlapping = a.copyWith(col: a.col + 1, row: a.row + 1);
|
|
expect(hudRectsOverlap(a, overlapping), isTrue);
|
|
});
|
|
});
|
|
|
|
group('nextFreeSlot', () {
|
|
test('returns a rect that does not collide with any occupied visible layout, '
|
|
'when the metric\'s own default position is taken', () {
|
|
final occupier = HudWidgetLayout.defaultFor(HudMetric.speed);
|
|
final occupied = {HudMetric.speed: occupier};
|
|
|
|
final result = HudWidgetLayout.nextFreeSlot(
|
|
HudMetric.avgSpeed,
|
|
occupied: occupied,
|
|
colSpan: occupier.colSpan,
|
|
rowSpan: occupier.rowSpan,
|
|
);
|
|
|
|
expect(hudRectsOverlap(result, occupier), isFalse);
|
|
});
|
|
|
|
test('returns the same rect defaultFor would have (or another valid one) when '
|
|
'occupied is empty', () {
|
|
final result = HudWidgetLayout.nextFreeSlot(HudMetric.speed, occupied: {});
|
|
expect(result.col, 0);
|
|
expect(result.row, 0);
|
|
});
|
|
|
|
test('returns the same rect defaultFor would have (or another valid one) when '
|
|
'occupied is all-hidden', () {
|
|
final hidden = HudWidgetLayout.defaultFor(HudMetric.speed).copyWith(visible: false);
|
|
final result = HudWidgetLayout.nextFreeSlot(
|
|
HudMetric.avgSpeed,
|
|
occupied: {HudMetric.speed: hidden},
|
|
);
|
|
expect(result.col, 0);
|
|
expect(result.row, 0);
|
|
});
|
|
});
|
|
}
|