142 lines
4.7 KiB
Dart
142 lines
4.7 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/features/entitlements/widgets/pro_lock_badge.dart';
|
|
import 'package:motorcycle_dash/features/rides/ride_state_machine.dart';
|
|
import 'package:motorcycle_dash/services/billing/product_catalog.dart';
|
|
import 'package:motorcycle_dash/services/entitlements/entitlement_state.dart';
|
|
import 'package:motorcycle_dash/services/entitlements/feature_access_service.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
group('Rider Safety & Entitlement Separation Tests', () {
|
|
testWidgets(
|
|
'suppresses modal upgrade sheet and shows passive snackbar during active ride',
|
|
(tester) async {
|
|
final activeRidingSession = RideSession(
|
|
state: RideState.riding,
|
|
tripDistanceMiles: 5.2,
|
|
);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith(
|
|
(ref) => _FakeRideStateMachine(activeRidingSession),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Consumer(
|
|
builder: (context, ref, _) {
|
|
return ElevatedButton(
|
|
onPressed: () => showProUpgradeSheet(
|
|
context,
|
|
ref,
|
|
feature: MotorSignalFeature.csvExport,
|
|
),
|
|
child: const Text('TRIGGER UPGRADE'),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Tap the button that triggers upgrade sheet
|
|
await tester.tap(find.text('TRIGGER UPGRADE'));
|
|
await tester.pump();
|
|
|
|
// Ensure NO modal bottom sheet appeared
|
|
expect(find.byType(BottomSheet), findsNothing);
|
|
|
|
// Ensure non-blocking passive snackbar appeared
|
|
expect(
|
|
find.text('Pro feature locked. Upgrade available when stopped.'),
|
|
findsOneWidget,
|
|
);
|
|
},
|
|
);
|
|
|
|
testWidgets('allows modal upgrade sheet when rider is idle / stopped', (
|
|
tester,
|
|
) async {
|
|
final idleSession = RideSession(state: RideState.idle);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [
|
|
rideStateMachineProvider.overrideWith(
|
|
(ref) => _FakeRideStateMachine(idleSession),
|
|
),
|
|
],
|
|
child: MaterialApp(
|
|
home: Scaffold(
|
|
body: Consumer(
|
|
builder: (context, ref, _) {
|
|
return ElevatedButton(
|
|
onPressed: () => showProUpgradeSheet(
|
|
context,
|
|
ref,
|
|
feature: MotorSignalFeature.csvExport,
|
|
),
|
|
child: const Text('TRIGGER UPGRADE'),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Tap the button that triggers upgrade sheet
|
|
await tester.tap(find.text('TRIGGER UPGRADE'));
|
|
await tester.pumpAndSettle();
|
|
|
|
// Ensure modal bottom sheet DID open
|
|
expect(find.byType(BottomSheet), findsOneWidget);
|
|
expect(find.text('Forensic CSV & PDF Export'), findsOneWidget);
|
|
expect(find.text('VIEW MOTORSIGNAL PRO'), findsOneWidget);
|
|
expect(find.text('NOT NOW'), findsOneWidget);
|
|
});
|
|
|
|
test('Entitlement state changes do not disrupt RideSession or RideState', () {
|
|
final activeSession = RideSession(
|
|
state: RideState.riding,
|
|
tripDistanceMiles: 12.4,
|
|
topSpeedMph: 64.0,
|
|
);
|
|
|
|
// Verify that changing EntitlementState from Free to Pro to Expired is independent of RideSession
|
|
var entitlement = const EntitlementState.free();
|
|
expect(entitlement.isPro, isFalse);
|
|
expect(activeSession.state, RideState.riding);
|
|
|
|
entitlement = EntitlementState.proMonthlyActive(
|
|
productId: ProductCatalog.proMonthly,
|
|
);
|
|
expect(entitlement.isPro, isTrue);
|
|
expect(activeSession.state, RideState.riding);
|
|
|
|
entitlement = EntitlementState.normalized(
|
|
plan: MotorSignalPlan.proMonthly,
|
|
status: MotorSignalEntitlementStatus.proMonthlyExpired,
|
|
verificationStatus: EntitlementVerificationStatus.expired,
|
|
);
|
|
expect(entitlement.isPro, isFalse);
|
|
expect(activeSession.state, RideState.riding);
|
|
expect(activeSession.tripDistanceMiles, 12.4);
|
|
});
|
|
});
|
|
}
|
|
|
|
class _FakeRideStateMachine extends StateNotifier<RideSession>
|
|
implements RideStateMachine {
|
|
_FakeRideStateMachine(super.initialState);
|
|
|
|
@override
|
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
|
}
|