175 lines
6.0 KiB
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);
|
|
});
|
|
}
|