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.
137 lines
5.7 KiB
Dart
137 lines
5.7 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:rippr/src/config/config.dart';
|
|
import 'package:rippr/src/hud/hud_layout_controller.dart';
|
|
import 'package:rippr/src/hud/hud_metric.dart';
|
|
import 'package:rippr/src/hud/hud_widget_layout.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
void main() {
|
|
Future<Config> freshConfig() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
return Config.load();
|
|
}
|
|
|
|
group('HudLayoutController', () {
|
|
test('updatePosition updates only the given metric, clamped to the grid', () async {
|
|
final controller = HudLayoutController(await freshConfig());
|
|
final before = controller.state[HudMetric.distance]!;
|
|
|
|
// Row 4 is unused by any default layout, so this can't collide.
|
|
controller.updatePosition(HudMetric.speed, 0, 4);
|
|
|
|
expect(controller.state[HudMetric.speed]!.row, 4);
|
|
expect(controller.state[HudMetric.distance]!.col, before.col,
|
|
reason: 'moving one metric must not disturb another');
|
|
expect(controller.state[HudMetric.distance]!.row, before.row);
|
|
});
|
|
|
|
test('updatePosition clamps a candidate that lands outside the grid', () async {
|
|
final controller = HudLayoutController(await freshConfig());
|
|
|
|
controller.updatePosition(HudMetric.speed, 99, 99);
|
|
|
|
final speed = controller.state[HudMetric.speed]!;
|
|
expect(speed.col, lessThanOrEqualTo(hudGridColumns - speed.colSpan));
|
|
expect(speed.row, lessThanOrEqualTo(hudGridRows - speed.rowSpan));
|
|
});
|
|
|
|
test('updateSize clamps to the legibility floor/ceiling', () async {
|
|
final controller = HudLayoutController(await freshConfig());
|
|
|
|
controller.updateSize(HudMetric.speed, 0, 0);
|
|
expect(controller.state[HudMetric.speed]!.colSpan, hudMinSpan);
|
|
expect(controller.state[HudMetric.speed]!.rowSpan, hudMinSpan);
|
|
});
|
|
|
|
test('updatePosition onto a cell occupied by another visible metric is a no-op', () async {
|
|
final controller = HudLayoutController(await freshConfig());
|
|
final before = controller.state[HudMetric.speed]!;
|
|
final avgSpeed = controller.state[HudMetric.avgSpeed]!;
|
|
|
|
// avgSpeed is visible by default -- try to drag speed directly on top of it.
|
|
controller.updatePosition(HudMetric.speed, avgSpeed.col, avgSpeed.row);
|
|
|
|
expect(controller.state[HudMetric.speed]!.col, before.col);
|
|
expect(controller.state[HudMetric.speed]!.row, before.row);
|
|
});
|
|
|
|
test('updateSize that would overlap a sibling is a no-op', () async {
|
|
final controller = HudLayoutController(await freshConfig());
|
|
final before = controller.state[HudMetric.speed]!;
|
|
|
|
// Growing speed (default col 0, row 0) to span all 4 columns would swallow
|
|
// avgSpeed/distance/elapsedTime's row-0 slots.
|
|
controller.updateSize(HudMetric.speed, hudGridColumns, before.rowSpan);
|
|
|
|
expect(controller.state[HudMetric.speed]!.colSpan, before.colSpan);
|
|
expect(controller.state[HudMetric.speed]!.rowSpan, before.rowSpan);
|
|
});
|
|
|
|
test('setVisible(false) then setVisible(true) restores the last saved position, '
|
|
'when that position is still free', () async {
|
|
final controller = HudLayoutController(await freshConfig());
|
|
// Row 4 is unused by any default layout, so this can't collide with anything.
|
|
controller.updatePosition(HudMetric.avgSpeed, 0, 4);
|
|
final moved = controller.state[HudMetric.avgSpeed]!;
|
|
|
|
controller.setVisible(HudMetric.avgSpeed, false);
|
|
expect(controller.state[HudMetric.avgSpeed]!.visible, isFalse);
|
|
|
|
controller.setVisible(HudMetric.avgSpeed, true);
|
|
expect(controller.state[HudMetric.avgSpeed]!.visible, isTrue);
|
|
expect(controller.state[HudMetric.avgSpeed]!.col, moved.col,
|
|
reason: 'a metric with a real saved position keeps it when re-enabled');
|
|
expect(controller.state[HudMetric.avgSpeed]!.row, moved.row);
|
|
});
|
|
|
|
test('setVisible(true) when the metric\'s last position now collides with '
|
|
'another visible widget places it somewhere else instead', () async {
|
|
final controller = HudLayoutController(await freshConfig());
|
|
final maxSpeedDefault = controller.state[HudMetric.maxSpeed]!;
|
|
|
|
// maxSpeed starts hidden at its default slot; move a currently-visible metric
|
|
// onto that exact slot so it's occupied by the time maxSpeed is re-enabled.
|
|
controller.updatePosition(
|
|
HudMetric.speed,
|
|
maxSpeedDefault.col,
|
|
maxSpeedDefault.row,
|
|
);
|
|
|
|
controller.setVisible(HudMetric.maxSpeed, true);
|
|
|
|
final maxSpeed = controller.state[HudMetric.maxSpeed]!;
|
|
expect(maxSpeed.visible, isTrue);
|
|
final others = controller.state.values.where(
|
|
(l) => l.visible && l.metric != HudMetric.maxSpeed,
|
|
);
|
|
for (final other in others) {
|
|
expect(hudRectsOverlap(maxSpeed, other), isFalse,
|
|
reason: 'the re-enabled metric must not stack on any other visible widget');
|
|
}
|
|
});
|
|
|
|
test('persist writes the current state to Config', () async {
|
|
final config = await freshConfig();
|
|
final controller = HudLayoutController(config);
|
|
controller.updatePosition(HudMetric.speed, 0, 4);
|
|
|
|
await controller.persist();
|
|
|
|
final reloaded = config.hudLayout;
|
|
expect(reloaded[HudMetric.speed]!.row, 4);
|
|
});
|
|
|
|
test('edits before persist are not visible to a fresh read of Config', () async {
|
|
final config = await freshConfig();
|
|
final controller = HudLayoutController(config);
|
|
controller.updatePosition(HudMetric.speed, 0, 4);
|
|
|
|
// No persist() call yet.
|
|
final reloaded = config.hudLayout;
|
|
expect(reloaded[HudMetric.speed]!.row, isNot(4),
|
|
reason: 'a drag in progress must only touch in-memory state -- '
|
|
'persisting every frame is the ticket\'s own named risk');
|
|
});
|
|
});
|
|
}
|