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.
267 lines
9.4 KiB
Dart
267 lines
9.4 KiB
Dart
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:rippr/src/app/providers.dart';
|
|
import 'package:rippr/src/config/config.dart';
|
|
import 'package:rippr/src/hud/hud_metric.dart';
|
|
import 'package:rippr/src/hud/hud_widget_layout.dart';
|
|
import 'package:rippr/src/ui/components/draggable_resizable_hud_widget.dart';
|
|
import 'package:rippr/src/ui/components/hud_edit_overlay.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
/// A representative HUD child -- structurally the same FittedBox(label + value)
|
|
/// shape as `record_screen.dart`'s private `_HudMetricValue`, which can't be
|
|
/// referenced directly from outside its library.
|
|
Widget _representativeHudChild() => const FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text('SPEED', style: TextStyle(fontSize: 10, letterSpacing: 1)),
|
|
SizedBox(height: 4),
|
|
Text('12.3 km/h', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
|
],
|
|
),
|
|
);
|
|
|
|
/// UI-04: drag/toggle behaviour of the customizable HUD, simulated via `TestGesture`
|
|
/// the same way other drag interactions in this codebase are tested (see
|
|
/// `route_planner_screen_test.dart`'s waypoint-drag coverage).
|
|
void main() {
|
|
late Config config;
|
|
|
|
setUp(() async {
|
|
SharedPreferences.setMockInitialValues({});
|
|
config = await Config.load();
|
|
});
|
|
|
|
Widget host() => ProviderScope(
|
|
overrides: [configProvider.overrideWith((ref) => config)],
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox.expand(
|
|
child: HudEditOverlay(
|
|
metricBuilder: (context, metric) => Text(metric.label),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
testWidgets('a tap outside edit mode does not move a widget', (tester) async {
|
|
await tester.pumpWidget(host());
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('hud-edit-done')), findsNothing,
|
|
reason: 'not editing yet');
|
|
|
|
await tester.tap(find.text('Speed'));
|
|
await tester.pump();
|
|
|
|
expect(find.byKey(const Key('hud-edit-done')), findsNothing,
|
|
reason: 'a plain tap on a widget must never enter edit mode or move it');
|
|
});
|
|
|
|
testWidgets('a long-press on empty space enters edit mode, showing resize handles '
|
|
'and Done', (tester) async {
|
|
await tester.pumpWidget(host());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.longPress(find.byKey(const Key('hud-edit-background')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('hud-edit-done')), findsOneWidget);
|
|
expect(find.byKey(const Key('hud-resize-handle')), findsWidgets);
|
|
});
|
|
|
|
testWidgets('a long-press-and-drag on a widget moves it, and Done persists the new '
|
|
'position', (tester) async {
|
|
await tester.pumpWidget(host());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.longPress(find.byKey(const Key('hud-edit-background')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final before = config.hudLayout[HudMetric.speed]!;
|
|
|
|
final speedFinder = find.text('Speed');
|
|
final gesture = await tester.startGesture(tester.getCenter(speedFinder));
|
|
await tester.pump(kLongPressTimeout + kPressTimeout);
|
|
// Large enough to guarantee at least one grid cell of movement in both axes
|
|
// regardless of the test surface's exact pixel size.
|
|
await gesture.moveBy(const Offset(300, 300));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('hud-edit-done')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final after = config.hudLayout[HudMetric.speed]!;
|
|
expect(after.col, isNot(before.col));
|
|
expect(after.row, isNot(before.row));
|
|
});
|
|
|
|
testWidgets('a drag while not editing does not move the widget', (tester) async {
|
|
await tester.pumpWidget(host());
|
|
await tester.pumpAndSettle();
|
|
|
|
final before = config.hudLayout[HudMetric.speed]!;
|
|
final speedFinder = find.text('Speed');
|
|
|
|
final gesture = await tester.startGesture(tester.getCenter(speedFinder));
|
|
await tester.pump(kLongPressTimeout + kPressTimeout);
|
|
await gesture.moveBy(const Offset(40, 60));
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.byKey(const Key('hud-edit-done')), findsNothing);
|
|
final after = config.hudLayout[HudMetric.speed]!;
|
|
expect(after.col, before.col);
|
|
expect(after.row, before.row);
|
|
});
|
|
|
|
testWidgets('only visible metrics render a widget', (tester) async {
|
|
await tester.pumpWidget(host());
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text('Speed'), findsOneWidget, reason: 'visible by default');
|
|
expect(find.text('Max speed'), findsNothing, reason: 'hidden by default');
|
|
});
|
|
|
|
testWidgets('a drag ending over a cell occupied by another visible widget leaves '
|
|
'the dragged widget\'s rendered position unchanged', (tester) async {
|
|
await tester.pumpWidget(host());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.longPress(find.byKey(const Key('hud-edit-background')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final speedFinder = find.byKey(const ValueKey(HudMetric.speed));
|
|
final before = tester.getTopLeft(speedFinder);
|
|
|
|
// Speed (col 0, row 0) dragged exactly one cell right lands squarely on
|
|
// avgSpeed's (col 1, row 0) cell -- both share the same row-0 default rowSpan, so
|
|
// this is a genuine, not merely edge-touching, overlap.
|
|
final gesture = await tester.startGesture(tester.getCenter(find.text('Speed')));
|
|
await tester.pump(kLongPressTimeout + kPressTimeout);
|
|
await gesture.moveBy(Offset(tester.view.physicalSize.width / tester.view.devicePixelRatio / hudGridColumns, 0));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('hud-edit-done')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(config.hudLayout[HudMetric.speed]!.col, 0,
|
|
reason: 'the rejected move must not change the persisted grid cell either');
|
|
expect(tester.getTopLeft(speedFinder), before);
|
|
});
|
|
|
|
testWidgets('a resize past the point of colliding with a sibling leaves the widget '
|
|
'at its pre-gesture size', (tester) async {
|
|
await tester.pumpWidget(host());
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.longPress(find.byKey(const Key('hud-edit-background')));
|
|
await tester.pumpAndSettle();
|
|
|
|
final speedFinder = find.byKey(const ValueKey(HudMetric.speed));
|
|
final beforeSize = tester.getSize(speedFinder);
|
|
final handleFinder = find.descendant(
|
|
of: speedFinder,
|
|
matching: find.byKey(const Key('hud-resize-handle')),
|
|
);
|
|
|
|
// Speed defaults to colSpan 1 at col 0; growing it across the full grid width
|
|
// would swallow avgSpeed/distance/elapsedTime's row-0 slots.
|
|
final cellWidth = tester.view.physicalSize.width / tester.view.devicePixelRatio / hudGridColumns;
|
|
final gesture = await tester.startGesture(tester.getCenter(handleFinder));
|
|
await gesture.moveBy(Offset(cellWidth * hudGridColumns, 0));
|
|
await tester.pump();
|
|
await gesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
await tester.tap(find.byKey(const Key('hud-edit-done')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(config.hudLayout[HudMetric.speed]!.colSpan, 1,
|
|
reason: 'the rejected resize must not change the persisted span either');
|
|
expect(tester.getSize(speedFinder), beforeSize);
|
|
});
|
|
|
|
testWidgets('HUD widget text never overflows at the smallest (1x1) allowed size',
|
|
(tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: 400,
|
|
height: 400,
|
|
child: Stack(
|
|
children: [
|
|
DraggableResizableHudWidget(
|
|
layout: const HudWidgetLayout(
|
|
metric: HudMetric.speed,
|
|
col: 0,
|
|
row: 0,
|
|
colSpan: hudMinSpan,
|
|
rowSpan: hudMinSpan,
|
|
visible: true,
|
|
),
|
|
areaSize: const Size(400, 400),
|
|
editing: false,
|
|
onMoved: (_, _) {},
|
|
onResized: (_, _) {},
|
|
child: _representativeHudChild(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
testWidgets(
|
|
'HUD widget text never overflows at the largest (max col/row span) allowed size',
|
|
(tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: SizedBox(
|
|
width: 400,
|
|
height: 400,
|
|
child: Stack(
|
|
children: [
|
|
DraggableResizableHudWidget(
|
|
layout: const HudWidgetLayout(
|
|
metric: HudMetric.speed,
|
|
col: 0,
|
|
row: 0,
|
|
colSpan: hudMaxColSpan,
|
|
rowSpan: hudMaxRowSpan,
|
|
visible: true,
|
|
),
|
|
areaSize: const Size(400, 400),
|
|
editing: false,
|
|
onMoved: (_, _) {},
|
|
onResized: (_, _) {},
|
|
child: _representativeHudChild(),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
}
|