MotorSignal-App/test/hybrid_v1_phase1_test.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);
});
});
}