glp-companion/test/features/settings/settings_screen_test.dart

175 lines
6.0 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/core/database/app_database.dart';
import 'package:glp1_companion/core/platform/health_gateway.dart';
import 'package:glp1_companion/features/health_sync/presentation/controllers/biometric_controller.dart';
import 'package:glp1_companion/features/settings/presentation/screens/settings_screen.dart';
/// Phase 3B: the Settings screen must render the Health Integrations section
/// with desktop-safe disabled states when native mobile providers are
/// unavailable (Windows / Web fallback driver), and run a full connect /
/// disconnect round trip through the UI when they are.
/// Fallback driver stand-in: no native SDK anywhere.
class _UnsupportedHealthGateway extends IHealthGateway {
@override
bool get supportsNativeHealth => false;
@override
NativeHealthPlatform? get nativePlatform => null;
@override
Future<bool> requestPermissions() async => false;
@override
Future<List<HealthMetric>> fetchWeightHistory({
required DateTime start,
required DateTime end,
}) async =>
const [];
@override
Future<List<HealthMetric>> fetchActiveEnergyBurned({
required DateTime start,
required DateTime end,
}) async =>
const [];
@override
Future<bool> writeWeight({
required double weightLbs,
required DateTime timestamp,
}) async =>
false;
}
/// Android-backed stand-in: Health Connect answers status and grants on
/// request; Apple Health stays unsupported.
class _HealthConnectGateway extends _UnsupportedHealthGateway {
HealthConnectionStatus status = HealthConnectionStatus.notConfigured;
@override
bool get supportsNativeHealth => true;
@override
NativeHealthPlatform? get nativePlatform => NativeHealthPlatform.healthConnect;
@override
Future<HealthConnectionStatus> checkStatus(
HealthProviderType provider,
) async {
if (provider == HealthProviderType.manualFallback) {
return HealthConnectionStatus.connected;
}
if (provider == HealthProviderType.enterpriseFhir) {
return HealthConnectionStatus.notConfigured;
}
if (provider == HealthProviderType.healthConnect) return status;
return HealthConnectionStatus.unsupportedPlatform;
}
@override
Future<bool> requestAuthorization(HealthProviderType provider) async {
if (provider == HealthProviderType.healthConnect) {
status = HealthConnectionStatus.connected;
return true;
}
return false;
}
@override
Future<void> disconnect(HealthProviderType provider) async {
if (provider == HealthProviderType.healthConnect) {
status = HealthConnectionStatus.notConfigured;
}
}
}
void main() {
late AppDatabase database;
setUp(() {
database = AppDatabase.forTesting(NativeDatabase.memory());
});
tearDown(() => database.close());
Widget buildScreen(IHealthGateway gateway) => ProviderScope(
overrides: [
appDatabaseProvider.overrideWithValue(database),
healthGatewayProvider.overrideWithValue(gateway),
],
child: const MaterialApp(home: SettingsScreen()),
);
// A tall phone-like viewport keeps every settings card on-screen (the
// ListView builds lazily beyond its cache extent).
void useTallViewport(WidgetTester tester) {
tester.view.physicalSize = const Size(1080, 2400);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.reset);
}
testWidgets(
'desktop fallback renders integrations with disabled Connect buttons '
'and Unsupported badges', (tester) async {
useTallViewport(tester);
await tester.pumpWidget(buildScreen(_UnsupportedHealthGateway()));
await tester.pumpAndSettle();
expect(find.text('Health Integrations'), findsOneWidget);
expect(find.text('Apple Health'), findsOneWidget);
expect(find.text('Health Connect (Samsung & Pixel)'), findsOneWidget);
expect(find.text('Google Health'), findsOneWidget);
expect(find.text('SMART on FHIR'), findsOneWidget);
// Native providers and the (web-stubbed) Google Health connector are
// unsupported on desktop; the FHIR connector is not provisioned yet —
// every Connect affordance is disabled.
expect(find.text('Unsupported'), findsNWidgets(3));
final connectButtons = find.widgetWithText(FilledButton, 'Connect');
expect(connectButtons, findsNWidgets(4));
for (final button in tester.widgetList<FilledButton>(connectButtons)) {
expect(button.onPressed, isNull);
}
// Encrypted storage card and the Supabase sign-out action are present.
expect(find.text('SQLCipher AES-256 active'), findsOneWidget);
expect(find.widgetWithText(FilledButton, 'Sign Out'), findsOneWidget);
});
testWidgets(
'mobile gateway runs the connect / disconnect round trip with live '
'status badges', (tester) async {
useTallViewport(tester);
final gateway = _HealthConnectGateway();
await tester.pumpWidget(buildScreen(gateway));
await tester.pumpAndSettle();
// Health Connect row is available with an enabled Connect button (the
// FHIR row also reads "Not connected" until its connector ships).
expect(find.text('Not connected'), findsNWidgets(2));
final connectFinder = find.widgetWithText(FilledButton, 'Connect');
final allConnect = connectFinder.evaluate().toList();
final enabledIndex = allConnect
.indexWhere((element) => (element.widget as FilledButton).onPressed != null);
expect(enabledIndex, greaterThanOrEqualTo(0));
await tester.tap(connectFinder.at(enabledIndex));
await tester.pumpAndSettle();
// Connected: badge flips and the button becomes Disconnect.
expect(find.text('Connected'), findsOneWidget);
final disconnect = find.widgetWithText(OutlinedButton, 'Disconnect');
expect(disconnect, findsOneWidget);
await tester.tap(disconnect);
await tester.pumpAndSettle();
expect(find.text('Not connected'), findsNWidgets(2));
expect(find.text('Connected'), findsNothing);
});
}