886 lines
32 KiB
Dart
886 lines
32 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:motorcycle_dash/core/theme/app_colors.dart';
|
|
import 'package:motorcycle_dash/core/theme/app_spacing.dart';
|
|
import 'package:motorcycle_dash/core/theme/app_typography.dart';
|
|
import 'package:motorcycle_dash/core/theme/theme.dart';
|
|
import 'package:motorcycle_dash/core/theme/theme_extensions.dart';
|
|
import 'package:motorcycle_dash/core/widgets/hybrid_card.dart';
|
|
import 'package:motorcycle_dash/core/widgets/hybrid_metric_tile.dart';
|
|
import 'package:motorcycle_dash/core/widgets/hybrid_riding_action_bar.dart';
|
|
import 'package:motorcycle_dash/core/widgets/hybrid_static_rec_badge.dart';
|
|
import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
|
|
|
|
/// Test double for [RideStateMachine] allowing inspection of invoked transitions.
|
|
class _FakeRideStateMachine extends StateNotifier<RideSession>
|
|
implements RideStateMachine {
|
|
_FakeRideStateMachine([RideSession? initial])
|
|
: super(
|
|
initial ??
|
|
RideSession(
|
|
state: RideState.idle,
|
|
startTime: DateTime.now(),
|
|
),
|
|
);
|
|
|
|
bool startRideCalled = false;
|
|
bool pauseRideCalled = false;
|
|
bool resumeRideCalled = false;
|
|
bool finishRideCalled = false;
|
|
|
|
@override
|
|
Future<void> startRide() async {
|
|
startRideCalled = true;
|
|
state = RideSession(state: RideState.riding, startTime: DateTime.now());
|
|
}
|
|
|
|
@override
|
|
void pauseRide() {
|
|
pauseRideCalled = true;
|
|
state = RideSession(state: RideState.paused, startTime: DateTime.now());
|
|
}
|
|
|
|
@override
|
|
void resumeRide() {
|
|
resumeRideCalled = true;
|
|
state = RideSession(state: RideState.riding, startTime: DateTime.now());
|
|
}
|
|
|
|
@override
|
|
Future<void> finishRide() async {
|
|
finishRideCalled = true;
|
|
state = RideSession(state: RideState.finished, startTime: DateTime.now());
|
|
}
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
// ===========================================================================
|
|
// SECTION 1: Canonical Tokens in AppColors and MotorSignalThemeColors
|
|
// ===========================================================================
|
|
group('1. Canonical Tokens in AppColors and MotorSignalThemeColors', () {
|
|
test('AppColors defines canonical hybrid canvases', () {
|
|
expect(AppColors.hybridNightCanvas, const Color(0xFF0B1326));
|
|
expect(AppColors.hybridDayCanvas, const Color(0xFFF8FAFC));
|
|
});
|
|
|
|
test('MotorSignalThemeColors surfaceCanvas matches canonical canvases', () {
|
|
expect(
|
|
MotorSignalThemeColors.dark.surfaceCanvas,
|
|
const Color(0xFF0B1326),
|
|
);
|
|
expect(
|
|
MotorSignalThemeColors.light.surfaceCanvas,
|
|
const Color(0xFFF8FAFC),
|
|
);
|
|
});
|
|
|
|
test('AppColors defines canonical hybrid core state tokens', () {
|
|
// Night tokens
|
|
expect(AppColors.hybridNightBrandCyan, const Color(0xFF06B6D4));
|
|
expect(AppColors.hybridNightRecGreen, const Color(0xFF10B981));
|
|
expect(AppColors.hybridNightCautionAmber, const Color(0xFFF59E0B));
|
|
expect(AppColors.hybridNightDangerRed, const Color(0xFFEF4444));
|
|
|
|
// Day tokens
|
|
expect(AppColors.hybridDayBrandCyan, const Color(0xFF0284C7));
|
|
expect(AppColors.hybridDayRecGreen, const Color(0xFF10B981));
|
|
expect(AppColors.hybridDayCautionAmber, const Color(0xFFD97706));
|
|
expect(AppColors.hybridDayDangerRed, const Color(0xFFDC2626));
|
|
});
|
|
|
|
test('MotorSignalThemeColors presets map brandCyan, recStaticGreen, cautionAmber, and dangerRed', () {
|
|
// Dark mode preset
|
|
final dark = MotorSignalThemeColors.dark;
|
|
expect(dark.brandCyan, AppColors.hybridNightBrandCyan);
|
|
expect(dark.brandCyan, const Color(0xFF06B6D4));
|
|
expect(dark.recStaticGreen, AppColors.hybridNightRecGreen);
|
|
expect(dark.recStaticGreen, const Color(0xFF10B981));
|
|
expect(dark.cautionAmber, AppColors.hybridNightCautionAmber);
|
|
expect(dark.cautionAmber, const Color(0xFFF59E0B));
|
|
expect(dark.dangerRed, AppColors.hybridNightDangerRed);
|
|
expect(dark.dangerRed, const Color(0xFFEF4444));
|
|
|
|
// Light mode preset
|
|
final light = MotorSignalThemeColors.light;
|
|
expect(light.brandCyan, AppColors.hybridDayBrandCyan);
|
|
expect(light.brandCyan, const Color(0xFF0284C7));
|
|
expect(light.recStaticGreen, AppColors.hybridDayRecGreen);
|
|
expect(light.recStaticGreen, const Color(0xFF10B981));
|
|
expect(light.cautionAmber, AppColors.hybridDayCautionAmber);
|
|
expect(light.cautionAmber, const Color(0xFFD97706));
|
|
expect(light.dangerRed, AppColors.hybridDayDangerRed);
|
|
expect(light.dangerRed, const Color(0xFFDC2626));
|
|
});
|
|
|
|
test('MotorSignalThemeColors.copyWith supports all 15 hybrid tokens', () {
|
|
const dark = MotorSignalThemeColors.dark;
|
|
|
|
final modified = dark.copyWith(
|
|
surfaceCanvas: const Color(0xFF100001),
|
|
surfaceCardPrimary: const Color(0xFF100002),
|
|
surfaceCardSecondary: const Color(0xFF100003),
|
|
surfaceCardLowest: const Color(0xFF100004),
|
|
surfaceBorder: const Color(0xFF100005),
|
|
surfaceBorderSubtle: const Color(0xFF100006),
|
|
textPrimary: const Color(0xFF100007),
|
|
textMuted: const Color(0xFF100008),
|
|
brandCyan: const Color(0xFF100009),
|
|
brandCyanBright: const Color(0xFF10000A),
|
|
brandCyanContainer: const Color(0xFF10000B),
|
|
recStaticGreen: const Color(0xFF10000C),
|
|
recStaticGreenBg: const Color(0xFF10000D),
|
|
cautionAmber: const Color(0xFF10000E),
|
|
dangerRed: const Color(0xFF10000F),
|
|
);
|
|
|
|
expect(modified.surfaceCanvas, const Color(0xFF100001));
|
|
expect(modified.surfaceCardPrimary, const Color(0xFF100002));
|
|
expect(modified.surfaceCardSecondary, const Color(0xFF100003));
|
|
expect(modified.surfaceCardLowest, const Color(0xFF100004));
|
|
expect(modified.surfaceBorder, const Color(0xFF100005));
|
|
expect(modified.surfaceBorderSubtle, const Color(0xFF100006));
|
|
expect(modified.textPrimary, const Color(0xFF100007));
|
|
expect(modified.textMuted, const Color(0xFF100008));
|
|
expect(modified.brandCyan, const Color(0xFF100009));
|
|
expect(modified.brandCyanBright, const Color(0xFF10000A));
|
|
expect(modified.brandCyanContainer, const Color(0xFF10000B));
|
|
expect(modified.recStaticGreen, const Color(0xFF10000C));
|
|
expect(modified.recStaticGreenBg, const Color(0xFF10000D));
|
|
expect(modified.cautionAmber, const Color(0xFF10000E));
|
|
expect(modified.dangerRed, const Color(0xFF10000F));
|
|
|
|
// Existing non-hybrid tokens remain preserved
|
|
expect(modified.cyberCyan, dark.cyberCyan);
|
|
expect(modified.velocityBlue, dark.velocityBlue);
|
|
expect(modified.electricCyan, dark.electricCyan);
|
|
|
|
// Unmodified copy retains all original values
|
|
final unchanged = dark.copyWith();
|
|
expect(unchanged.surfaceCanvas, dark.surfaceCanvas);
|
|
expect(unchanged.brandCyan, dark.brandCyan);
|
|
expect(unchanged.recStaticGreen, dark.recStaticGreen);
|
|
expect(unchanged.cautionAmber, dark.cautionAmber);
|
|
expect(unchanged.dangerRed, dark.dangerRed);
|
|
});
|
|
|
|
test('MotorSignalThemeColors.lerp interpolates all 15 hybrid tokens', () {
|
|
const dark = MotorSignalThemeColors.dark;
|
|
const light = MotorSignalThemeColors.light;
|
|
|
|
final lerped = dark.lerp(light, 0.5);
|
|
expect(lerped, isA<MotorSignalThemeColors>());
|
|
|
|
expect(lerped.surfaceCanvas, Color.lerp(dark.surfaceCanvas, light.surfaceCanvas, 0.5));
|
|
expect(lerped.surfaceCardPrimary, Color.lerp(dark.surfaceCardPrimary, light.surfaceCardPrimary, 0.5));
|
|
expect(lerped.surfaceCardSecondary, Color.lerp(dark.surfaceCardSecondary, light.surfaceCardSecondary, 0.5));
|
|
expect(lerped.surfaceCardLowest, Color.lerp(dark.surfaceCardLowest, light.surfaceCardLowest, 0.5));
|
|
expect(lerped.surfaceBorder, Color.lerp(dark.surfaceBorder, light.surfaceBorder, 0.5));
|
|
expect(lerped.surfaceBorderSubtle, Color.lerp(dark.surfaceBorderSubtle, light.surfaceBorderSubtle, 0.5));
|
|
expect(lerped.textPrimary, Color.lerp(dark.textPrimary, light.textPrimary, 0.5));
|
|
expect(lerped.textMuted, Color.lerp(dark.textMuted, light.textMuted, 0.5));
|
|
expect(lerped.brandCyan, Color.lerp(dark.brandCyan, light.brandCyan, 0.5));
|
|
expect(lerped.brandCyanBright, Color.lerp(dark.brandCyanBright, light.brandCyanBright, 0.5));
|
|
expect(lerped.brandCyanContainer, Color.lerp(dark.brandCyanContainer, light.brandCyanContainer, 0.5));
|
|
expect(lerped.recStaticGreen, Color.lerp(dark.recStaticGreen, light.recStaticGreen, 0.5));
|
|
expect(lerped.recStaticGreenBg, Color.lerp(dark.recStaticGreenBg, light.recStaticGreenBg, 0.5));
|
|
expect(lerped.cautionAmber, Color.lerp(dark.cautionAmber, light.cautionAmber, 0.5));
|
|
expect(lerped.dangerRed, Color.lerp(dark.dangerRed, light.dangerRed, 0.5));
|
|
|
|
// Null or non-matching other returns this
|
|
expect(dark.lerp(null, 0.5), same(dark));
|
|
});
|
|
});
|
|
|
|
// ===========================================================================
|
|
// SECTION 2: Reusable Components in lib/core/widgets/
|
|
// ===========================================================================
|
|
group('2. Reusable Components', () {
|
|
group('HybridCard', () {
|
|
testWidgets('renders with 1px border, primary surface color, and padding', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridCard(
|
|
child: Text('Card Content'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final containerFinder = find.descendant(
|
|
of: find.byType(HybridCard),
|
|
matching: find.byType(Container),
|
|
).first;
|
|
final containerWidget = tester.widget<Container>(containerFinder);
|
|
final decoration = containerWidget.decoration as ShapeDecoration;
|
|
final shape = decoration.shape as RoundedRectangleBorder;
|
|
|
|
// 1px border verification
|
|
expect(shape.side.width, 1.0);
|
|
expect(shape.side.color, AppColors.hybridNightBorder);
|
|
|
|
// Surface color: primary by default
|
|
expect(decoration.color, AppColors.hybridNightSurfaceCardPrimary);
|
|
|
|
// Padding: default AppSpacing.md (12.0)
|
|
final paddingFinder = find.ancestor(
|
|
of: find.text('Card Content'),
|
|
matching: find.byType(Padding),
|
|
);
|
|
final paddingWidget = tester.widget<Padding>(paddingFinder.first);
|
|
expect(paddingWidget.padding, const EdgeInsets.all(AppSpacing.md));
|
|
|
|
// Child content present
|
|
expect(find.text('Card Content'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('renders secondary surface color and custom border color when specified', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.lightTheme,
|
|
home: const Scaffold(
|
|
body: HybridCard(
|
|
secondary: true,
|
|
borderColor: Color(0xFF123456),
|
|
padding: EdgeInsets.all(20.0),
|
|
child: Text('Secondary Card'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final containerFinder = find.descendant(
|
|
of: find.byType(HybridCard),
|
|
matching: find.byType(Container),
|
|
).first;
|
|
final containerWidget = tester.widget<Container>(containerFinder);
|
|
final decoration = containerWidget.decoration as ShapeDecoration;
|
|
final shape = decoration.shape as RoundedRectangleBorder;
|
|
|
|
expect(shape.side.width, 1.0);
|
|
expect(shape.side.color, const Color(0xFF123456));
|
|
expect(decoration.color, AppColors.hybridDaySurfaceCardSecondary);
|
|
|
|
final paddingFinder = find.ancestor(
|
|
of: find.text('Secondary Card'),
|
|
matching: find.byType(Padding),
|
|
);
|
|
final paddingWidget = tester.widget<Padding>(paddingFinder.first);
|
|
expect(paddingWidget.padding, const EdgeInsets.all(20.0));
|
|
});
|
|
|
|
testWidgets('handles onTap callback and InkWell presence', (tester) async {
|
|
bool tapped = false;
|
|
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: Scaffold(
|
|
body: HybridCard(
|
|
onTap: () {
|
|
tapped = true;
|
|
},
|
|
child: const Text('Tappable Card'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(InkWell), findsOneWidget);
|
|
|
|
await tester.tap(find.text('Tappable Card'));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(tapped, isTrue);
|
|
});
|
|
|
|
testWidgets('omits InkWell when onTap is null', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridCard(
|
|
child: Text('Non-tappable Card'),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(InkWell), findsNothing);
|
|
});
|
|
});
|
|
|
|
group('HybridStaticRecBadge', () {
|
|
testWidgets('renders isRecording true badge (REC • LIVE, green)', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridStaticRecBadge(isRecording: true),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('REC • LIVE'), findsOneWidget);
|
|
|
|
final textWidget = tester.widget<Text>(find.text('REC • LIVE'));
|
|
expect(textWidget.style?.color, AppColors.hybridNightRecGreen);
|
|
|
|
final outerContainer = tester.widget<Container>(find.byType(Container).first);
|
|
final boxDec = outerContainer.decoration as BoxDecoration;
|
|
expect(boxDec.color, AppColors.hybridNightRecGreenBg);
|
|
});
|
|
|
|
testWidgets('renders isPaused true badge (PAUSED, amber)', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridStaticRecBadge(
|
|
isRecording: false,
|
|
isPaused: true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('PAUSED'), findsOneWidget);
|
|
|
|
final textWidget = tester.widget<Text>(find.text('PAUSED'));
|
|
expect(textWidget.style?.color, AppColors.hybridNightCautionAmber);
|
|
});
|
|
|
|
testWidgets('renders standby badge (STANDBY) when neither recording nor paused', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridStaticRecBadge(
|
|
isRecording: false,
|
|
isPaused: false,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('STANDBY'), findsOneWidget);
|
|
|
|
final textWidget = tester.widget<Text>(find.text('STANDBY'));
|
|
expect(textWidget.style?.color, AppColors.hybridNightTextMuted);
|
|
});
|
|
|
|
testWidgets('verifies zero animation controllers or ticker providers are used', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridStaticRecBadge(isRecording: true),
|
|
),
|
|
),
|
|
);
|
|
|
|
// HybridStaticRecBadge is strictly a StatelessWidget
|
|
final badgeWidget = tester.widget(find.byType(HybridStaticRecBadge));
|
|
expect(badgeWidget, isA<StatelessWidget>());
|
|
|
|
// Zero StatefulWidgets or TickerProviders anywhere in the badge tree
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(HybridStaticRecBadge),
|
|
matching: find.byWidgetPredicate((w) => w is StatefulWidget),
|
|
),
|
|
findsNothing,
|
|
);
|
|
|
|
// Zero AnimatedWidget descendants
|
|
expect(
|
|
find.descendant(
|
|
of: find.byType(HybridStaticRecBadge),
|
|
matching: find.byType(AnimatedWidget),
|
|
),
|
|
findsNothing,
|
|
);
|
|
|
|
// Zero active transient animation callbacks
|
|
expect(tester.binding.transientCallbackCount, 0);
|
|
});
|
|
});
|
|
|
|
group('HybridMetricTile', () {
|
|
testWidgets('renders tabular value, unit, and label with uppercase tracking', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridMetricTile(
|
|
label: 'Speed',
|
|
value: '72',
|
|
unit: 'mph',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Label: uppercase tracking
|
|
expect(find.text('SPEED'), findsOneWidget);
|
|
final labelText = tester.widget<Text>(find.text('SPEED'));
|
|
expect(labelText.style?.fontFamily, AppTypography.fontHeadline);
|
|
expect(labelText.style?.fontSize, 11);
|
|
expect(labelText.style?.fontWeight, FontWeight.w700);
|
|
expect(labelText.style?.letterSpacing, 1.0);
|
|
expect(labelText.style?.color, AppColors.hybridNightTextMuted);
|
|
|
|
// Value: tabular JetBrains Mono font
|
|
expect(find.text('72'), findsOneWidget);
|
|
final valueText = tester.widget<Text>(find.text('72'));
|
|
expect(valueText.style?.fontFamily, AppTypography.fontTelemetry);
|
|
expect(valueText.style?.fontSize, 26);
|
|
expect(valueText.style?.fontWeight, FontWeight.w700);
|
|
expect(valueText.style?.color, AppColors.hybridNightTextPrimary);
|
|
|
|
// Unit: uppercase tracking
|
|
expect(find.text('MPH'), findsOneWidget);
|
|
final unitText = tester.widget<Text>(find.text('MPH'));
|
|
expect(unitText.style?.fontFamily, AppTypography.fontTelemetry);
|
|
expect(unitText.style?.fontSize, 11);
|
|
expect(unitText.style?.letterSpacing, 0.5);
|
|
});
|
|
|
|
testWidgets('renders compact mode with scaled font sizes and icon', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridMetricTile(
|
|
label: 'Lean',
|
|
value: '42°',
|
|
unit: 'deg',
|
|
icon: Icons.navigation_rounded,
|
|
compact: true,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.text('LEAN'), findsOneWidget);
|
|
final labelText = tester.widget<Text>(find.text('LEAN'));
|
|
expect(labelText.style?.fontSize, 10);
|
|
|
|
expect(find.text('42°'), findsOneWidget);
|
|
final valueText = tester.widget<Text>(find.text('42°'));
|
|
expect(valueText.style?.fontSize, 22);
|
|
|
|
expect(find.text('DEG'), findsOneWidget);
|
|
final unitText = tester.widget<Text>(find.text('DEG'));
|
|
expect(unitText.style?.fontSize, 10);
|
|
|
|
final iconWidget = tester.widget<Icon>(find.byType(Icon));
|
|
expect(iconWidget.size, 12);
|
|
});
|
|
});
|
|
|
|
group('HybridRidingActionBar', () {
|
|
testWidgets('verifies 56dp height and 48dp min touch bounds in standard mode', (tester) async {
|
|
final fakeMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.ready, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => fakeMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridRidingActionBar(compact: false),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final buttonFinder = find.byKey(const ValueKey('hybrid_start_ride_button'));
|
|
expect(buttonFinder, findsOneWidget);
|
|
|
|
final size = tester.getSize(buttonFinder);
|
|
expect(size.height, AppSpacing.primaryRidingCtaHeight); // 56dp
|
|
expect(size.height, greaterThanOrEqualTo(AppSpacing.minTouchTarget)); // >= 48dp
|
|
});
|
|
|
|
testWidgets('verifies 48dp compact height and min touch bounds', (tester) async {
|
|
final fakeMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.ready, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => fakeMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridRidingActionBar(compact: true),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final buttonFinder = find.byKey(const ValueKey('hybrid_start_ride_button'));
|
|
expect(buttonFinder, findsOneWidget);
|
|
|
|
final size = tester.getSize(buttonFinder);
|
|
expect(size.height, AppSpacing.minTouchTarget); // 48dp
|
|
expect(size.height, greaterThanOrEqualTo(AppSpacing.minTouchTarget)); // >= 48dp
|
|
});
|
|
|
|
testWidgets('renders start button when ready / standby and invokes startRide', (tester) async {
|
|
final fakeMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.ready, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => fakeMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridRidingActionBar(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byKey(const ValueKey('hybrid_start_ride_button')), findsOneWidget);
|
|
expect(find.text('Start Session'), findsOneWidget);
|
|
expect(find.byKey(const ValueKey('hybrid_pause_resume_ride_button')), findsNothing);
|
|
expect(find.byKey(const ValueKey('hybrid_finish_ride_button')), findsNothing);
|
|
|
|
await tester.tap(find.byKey(const ValueKey('hybrid_start_ride_button')));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(fakeMachine.startRideCalled, isTrue);
|
|
});
|
|
|
|
testWidgets('renders Pause Ride and Finish Ride buttons when riding', (tester) async {
|
|
final fakeMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.riding, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => fakeMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridRidingActionBar(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byKey(const ValueKey('hybrid_start_ride_button')), findsNothing);
|
|
|
|
final pauseFinder = find.byKey(const ValueKey('hybrid_pause_resume_ride_button'));
|
|
final finishFinder = find.byKey(const ValueKey('hybrid_finish_ride_button'));
|
|
|
|
expect(pauseFinder, findsOneWidget);
|
|
expect(finishFinder, findsOneWidget);
|
|
expect(find.text('Pause Ride'), findsOneWidget);
|
|
expect(find.text('Finish Ride'), findsOneWidget);
|
|
|
|
// Tap Pause
|
|
await tester.tap(pauseFinder);
|
|
await tester.pumpAndSettle();
|
|
expect(fakeMachine.pauseRideCalled, isTrue);
|
|
|
|
// Tap Finish
|
|
await tester.tap(finishFinder);
|
|
await tester.pumpAndSettle();
|
|
expect(fakeMachine.finishRideCalled, isTrue);
|
|
});
|
|
|
|
testWidgets('renders Resume button when paused and invokes resumeRide', (tester) async {
|
|
final fakeMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.paused, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => fakeMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const Scaffold(
|
|
body: HybridRidingActionBar(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final resumeFinder = find.byKey(const ValueKey('hybrid_pause_resume_ride_button'));
|
|
expect(resumeFinder, findsOneWidget);
|
|
expect(find.text('Resume'), findsOneWidget);
|
|
expect(find.text('Pause Ride'), findsNothing);
|
|
|
|
await tester.tap(resumeFinder);
|
|
await tester.pumpAndSettle();
|
|
expect(fakeMachine.resumeRideCalled, isTrue);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===========================================================================
|
|
// SECTION 3: Large Text Accessibility (200% Text Scale)
|
|
// ===========================================================================
|
|
group('3. Large Text Accessibility (200% text scale)', () {
|
|
const largeTextMedia = MediaQueryData(
|
|
size: Size(390, 844),
|
|
textScaler: TextScaler.linear(2.0),
|
|
);
|
|
|
|
testWidgets('HybridCard renders under 200% text scale without RenderFlex overflow', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const MediaQuery(
|
|
data: largeTextMedia,
|
|
child: Scaffold(
|
|
body: HybridCard(
|
|
child: Text('Hybrid Card under large text scaling mode'),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.text('Hybrid Card under large text scaling mode'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('HybridMetricTile renders under 200% text scale without RenderFlex overflow', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const MediaQuery(
|
|
data: largeTextMedia,
|
|
child: Scaffold(
|
|
body: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
HybridMetricTile(
|
|
label: 'Speed',
|
|
value: '120',
|
|
unit: 'mph',
|
|
icon: Icons.speed,
|
|
),
|
|
SizedBox(height: 16),
|
|
HybridMetricTile(
|
|
label: 'Max Lean Angle Left',
|
|
value: '48.5°',
|
|
unit: 'deg',
|
|
compact: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.text('SPEED'), findsOneWidget);
|
|
expect(find.text('120'), findsOneWidget);
|
|
expect(find.text('MAX LEAN ANGLE LEFT'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('HybridStaticRecBadge renders under 200% text scale without RenderFlex overflow', (tester) async {
|
|
await tester.pumpWidget(
|
|
MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const MediaQuery(
|
|
data: largeTextMedia,
|
|
child: Scaffold(
|
|
body: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
HybridStaticRecBadge(isRecording: true),
|
|
SizedBox(height: 8),
|
|
HybridStaticRecBadge(isRecording: false, isPaused: true),
|
|
SizedBox(height: 8),
|
|
HybridStaticRecBadge(isRecording: false, isPaused: false),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.text('REC • LIVE'), findsOneWidget);
|
|
expect(find.text('PAUSED'), findsOneWidget);
|
|
expect(find.text('STANDBY'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('HybridRidingActionBar riding mode renders under 200% text scale without RenderFlex overflow', (tester) async {
|
|
final ridingMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.riding, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => ridingMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const MediaQuery(
|
|
data: largeTextMedia,
|
|
child: Scaffold(
|
|
body: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: HybridRidingActionBar(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.text('Pause Ride'), findsOneWidget);
|
|
expect(find.text('Finish Ride'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('HybridRidingActionBar paused mode renders under 200% text scale without RenderFlex overflow', (tester) async {
|
|
final pausedMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.paused, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => pausedMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const MediaQuery(
|
|
data: largeTextMedia,
|
|
child: Scaffold(
|
|
body: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: HybridRidingActionBar(compact: true),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.text('Resume'), findsOneWidget);
|
|
expect(find.text('Finish Ride'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('HybridRidingActionBar standby/ready mode renders under 200% text scale without RenderFlex overflow', (tester) async {
|
|
final readyMachine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.ready, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => readyMachine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const MediaQuery(
|
|
data: largeTextMedia,
|
|
child: Scaffold(
|
|
body: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: HybridRidingActionBar(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.text('Start Session'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Composite hybrid cockpit dashboard renders under 200% text scale without RenderFlex overflow', (tester) async {
|
|
final machine = _FakeRideStateMachine(
|
|
RideSession(state: RideState.riding, startTime: DateTime.now()),
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith((ref) => machine),
|
|
],
|
|
child: MaterialApp(
|
|
theme: AppTheme.darkTheme,
|
|
home: const MediaQuery(
|
|
data: largeTextMedia,
|
|
child: Scaffold(
|
|
body: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: EdgeInsets.all(16.0),
|
|
child: Column(
|
|
children: [
|
|
HybridStaticRecBadge(isRecording: true),
|
|
SizedBox(height: 16),
|
|
HybridCard(
|
|
child: Column(
|
|
children: [
|
|
HybridMetricTile(
|
|
label: 'Speed',
|
|
value: '65',
|
|
unit: 'mph',
|
|
),
|
|
SizedBox(height: 12),
|
|
HybridMetricTile(
|
|
label: 'Lean Angle',
|
|
value: '38°',
|
|
unit: 'deg',
|
|
compact: true,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 16),
|
|
HybridRidingActionBar(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.takeException(), isNull);
|
|
expect(find.byType(HybridStaticRecBadge), findsOneWidget);
|
|
expect(find.byType(HybridCard), findsOneWidget);
|
|
expect(find.byType(HybridMetricTile), findsNWidgets(2));
|
|
expect(find.byType(HybridRidingActionBar), findsOneWidget);
|
|
});
|
|
});
|
|
}
|