206 lines
6.6 KiB
Dart
206 lines
6.6 KiB
Dart
import 'package:drift/native.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:glp1_companion/app/router/app_router.dart';
|
|
import 'package:glp1_companion/core/database/app_database.dart';
|
|
import 'package:glp1_companion/core/platform/notification_service.dart';
|
|
import 'package:glp1_companion/features/auth/presentation/controllers/dev_auth_bypass.dart';
|
|
import 'package:glp1_companion/features/auth/presentation/screens/login_screen.dart';
|
|
import 'package:glp1_companion/features/medication/presentation/controllers/medication_controller.dart';
|
|
import 'package:glp1_companion/main.dart';
|
|
|
|
/// Shell-level integration tests: the adaptive scaffold must route between
|
|
/// every core destination — `/medication`, `/nutrition`, `/meal-planner`,
|
|
/// `/pantry` — under both an authenticated session and the debug dev bypass,
|
|
/// on desktop (navigation rail) and mobile (bottom navigation bar) layouts.
|
|
///
|
|
/// Supabase is never booted: the auth guard reads `isAuthenticatedProvider`
|
|
/// (overridden per scenario) and the drift database is an in-memory
|
|
/// stand-in, so the whole app graph runs offline and deterministically.
|
|
|
|
/// No-op reminders: `flutter_local_notifications` has no test host.
|
|
class _NoopNotificationService implements INotificationService {
|
|
@override
|
|
Future<void> initialize() async {}
|
|
|
|
@override
|
|
Future<bool> requestPermissions() async => false;
|
|
|
|
@override
|
|
Future<bool> scheduleDoseReminder({
|
|
required DateTime scheduledAt,
|
|
required String title,
|
|
required String body,
|
|
int id = kDoseReminderNotificationId,
|
|
String? payload,
|
|
}) async =>
|
|
false;
|
|
|
|
@override
|
|
Future<void> cancelReminder(int id) async {}
|
|
|
|
@override
|
|
Future<void> cancelAllReminders() async {}
|
|
}
|
|
|
|
void main() {
|
|
late AppDatabase database;
|
|
|
|
setUp(() {
|
|
database = AppDatabase.forTesting(NativeDatabase.memory());
|
|
});
|
|
|
|
tearDown(() => database.close());
|
|
|
|
/// Core overrides backing every scenario with offline doubles.
|
|
List<Override> baseOverrides() => [
|
|
appDatabaseProvider.overrideWithValue(database),
|
|
notificationServiceProvider
|
|
.overrideWithValue(_NoopNotificationService()),
|
|
];
|
|
|
|
Future<void> pumpApp(
|
|
WidgetTester tester, {
|
|
Size viewport = const Size(1280, 1400),
|
|
List<Override> extraOverrides = const [],
|
|
}) async {
|
|
tester.view.physicalSize = viewport;
|
|
tester.view.devicePixelRatio = 1.0;
|
|
addTearDown(tester.view.resetPhysicalSize);
|
|
addTearDown(tester.view.resetDevicePixelRatio);
|
|
|
|
await tester.pumpWidget(
|
|
ProviderScope(
|
|
overrides: [...baseOverrides(), ...extraOverrides],
|
|
child: const GLP1CompanionApp(),
|
|
),
|
|
);
|
|
await tester.pumpAndSettle();
|
|
}
|
|
|
|
/// Unmounts the tree and flushes the zero-duration timers drift schedules
|
|
/// when its query streams are cancelled during container disposal, which
|
|
/// would otherwise trip the test framework's pending-timers invariant.
|
|
Future<void> unmountAndFlushTimers(WidgetTester tester) async {
|
|
await tester.pumpWidget(const SizedBox());
|
|
await tester.pump(const Duration(milliseconds: 1));
|
|
}
|
|
|
|
/// Taps the rail destination [railLabel] and asserts the shell switched to
|
|
/// the screen titled [title] with [index] selected.
|
|
Future<void> expectRailHop(
|
|
WidgetTester tester, {
|
|
required String railLabel,
|
|
required int index,
|
|
required String title,
|
|
}) async {
|
|
await tester.tap(find.text(railLabel));
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(find.text(title), findsOneWidget);
|
|
expect(
|
|
tester.widget<NavigationRail>(find.byType(NavigationRail)).selectedIndex,
|
|
index,
|
|
);
|
|
}
|
|
|
|
Future<void> verifyAllRailRoutes(WidgetTester tester) async {
|
|
await expectRailHop(
|
|
tester,
|
|
railLabel: 'Nutrition',
|
|
index: 1,
|
|
title: 'Nutrition & Hydration',
|
|
);
|
|
await expectRailHop(
|
|
tester,
|
|
railLabel: 'Meal Plan',
|
|
index: 2,
|
|
title: 'Meal Planner & Import',
|
|
);
|
|
await expectRailHop(
|
|
tester,
|
|
railLabel: 'Pantry',
|
|
index: 3,
|
|
title: 'Pantry & Shopping Delta',
|
|
);
|
|
await expectRailHop(
|
|
tester,
|
|
railLabel: 'Medication',
|
|
index: 0,
|
|
title: 'Medication Tracker',
|
|
);
|
|
}
|
|
|
|
testWidgets(
|
|
'authenticated sessions start on /medication and the rail switches '
|
|
'every core route', (tester) async {
|
|
await pumpApp(
|
|
tester,
|
|
extraOverrides: [isAuthenticatedProvider.overrideWithValue(true)],
|
|
);
|
|
|
|
// Guard admits the session without visiting /login.
|
|
expect(find.byType(LoginScreen), findsNothing);
|
|
expect(find.text('Development mode - not signed in'), findsNothing);
|
|
expect(find.byType(NavigationRail), findsOneWidget);
|
|
expect(find.text('Medication Tracker'), findsOneWidget);
|
|
|
|
await verifyAllRailRoutes(tester);
|
|
await unmountAndFlushTimers(tester);
|
|
});
|
|
|
|
testWidgets('dev-bypass sessions reach every route and wear the dev banner',
|
|
(tester) async {
|
|
await pumpApp(
|
|
tester,
|
|
extraOverrides: [devAuthBypassProvider.overrideWith((ref) => true)],
|
|
);
|
|
|
|
expect(find.byType(LoginScreen), findsNothing);
|
|
expect(find.text('Development mode - not signed in'), findsOneWidget);
|
|
|
|
await verifyAllRailRoutes(tester);
|
|
// The banner persists across route switches.
|
|
expect(find.text('Development mode - not signed in'), findsOneWidget);
|
|
await unmountAndFlushTimers(tester);
|
|
});
|
|
|
|
testWidgets('mobile widths use the bottom navigation bar for every route',
|
|
(tester) async {
|
|
await pumpApp(
|
|
tester,
|
|
viewport: const Size(390, 2400),
|
|
extraOverrides: [devAuthBypassProvider.overrideWith((ref) => true)],
|
|
);
|
|
|
|
expect(find.byType(NavigationBar), findsOneWidget);
|
|
expect(find.byType(NavigationRail), findsNothing);
|
|
|
|
Future<void> tapBar(String label, int index, String title) async {
|
|
await tester.tap(find.text(label));
|
|
await tester.pumpAndSettle();
|
|
expect(find.text(title), findsOneWidget);
|
|
expect(
|
|
tester.widget<NavigationBar>(find.byType(NavigationBar)).selectedIndex,
|
|
index,
|
|
);
|
|
}
|
|
|
|
await tapBar('Food', 1, 'Nutrition & Hydration');
|
|
await tapBar('Plan', 2, 'Meal Planner & Import');
|
|
await tapBar('Pantry', 3, 'Pantry & Shopping Delta');
|
|
await tapBar('Meds', 0, 'Medication Tracker');
|
|
await unmountAndFlushTimers(tester);
|
|
});
|
|
|
|
testWidgets('unauthenticated sessions without bypass are bounced to /login',
|
|
(tester) async {
|
|
await pumpApp(tester);
|
|
|
|
expect(find.byType(LoginScreen), findsOneWidget);
|
|
expect(find.byType(NavigationRail), findsNothing);
|
|
await unmountAndFlushTimers(tester);
|
|
});
|
|
}
|