diet-guard/app/test/screens/settings_screen_test.dart
Krzysztof kuhy Rudnicki c0ed6154ac Split the remaining seven Dart test files
Eleventh batch of the 250-line sweep (over-cap count 12 -> 5). Every test file
in the repo, Python and Dart, is now under cap; the five left are all in
app/lib/.

  wrapper_server_test.dart        349 -> 145  (+ proxy, account siblings)
  app_settings_service_test.dart  321 -> 162  (+ sync sibling)
  average_service_test.dart       299 -> 158  (+ periods sibling)
  settings_screen_test.dart       274 -> 189  (+ battery sibling)
  day_status_service_test.dart    270 -> 161  (+ streaks sibling)
  edit_entry_screen_test.dart     269 -> 159  (+ foodbank sibling)
  day_status_calendar_test.dart   256 -> 237  (+ _test_support sibling)

day_status_calendar_test is a single group whose tests close over group-local
state, so it could not be split by block. Its ~25 lines of file-level fixtures
moved to `day_status_calendar_test_support.dart` instead, which required the
same publicisation as the sync support file (`_theme` -> `appTheme`, `_wrap` ->
`wrapInApp`, `_colorOfDay` -> `colorOfDay`) with doc comments.

Block counts were compared against HEAD per family rather than trusting the
suite total, since the duplication bug in the previous batch passed green at an
inflated count. All seven families match exactly (29/26/34/11/34/8/14).

Verified: flutter test 555 (unchanged), flutter analyze clean, pre-commit
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
2026-08-15 20:53:55 +02:00

190 lines
6.9 KiB
Dart

import 'dart:io';
import 'package:crdt_sync/crdt_sync.dart';
import 'package:diet_guard_app/screens/settings_screen.dart';
import 'package:diet_guard_app/services/document_store_io.dart';
import 'package:diet_guard_app/services/app_settings_service.dart';
import 'package:diet_guard_app/services/budget_history_service.dart';
import 'package:diet_guard_app/services/firebase_backend.dart';
import 'package:diet_guard_app/services/foodbank_service.dart';
import 'package:diet_guard_app/services/log_storage_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../fake_secure_storage.dart';
void main() {
late Directory tempDir;
setUp(() async {
tempDir = await Directory.systemTemp.createTemp('diet_guard_settings_');
LogStorageService.resetForTesting(store: FileDocumentStore(tempDir));
FoodBankService.resetForTesting(store: FileDocumentStore(tempDir));
AppSettingsService.resetForTesting(store: FileDocumentStore(tempDir));
BudgetHistoryService.resetForTesting(
store: FileDocumentStore(tempDir),
);
SharedPreferences.setMockInitialValues({});
installFakeSecureStorage();
});
tearDown(() async {
LogStorageService.resetForTesting();
FoodBankService.resetForTesting();
AppSettingsService.resetForTesting();
BudgetHistoryService.resetForTesting();
await tempDir.delete(recursive: true);
});
// SettingsScreen loads its settings via real (non-fake-clocked) I/O
// through FileDocumentStore, so every test body runs under
// tester.runAsync() -- without it, those Futures never get a chance to
// complete inside TestWidgetsFlutterBinding's fake-clocked zone, and
// pumpAndSettle spins forever. Also grows the test viewport: the
// Notifications section pushes earlier fields/buttons below the default
// 800x600 fold, making them unreachable to tester.tap otherwise.
Future<void> settle(WidgetTester tester) async {
tester.view.physicalSize = const Size(800, 1400);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
await Future<void>.delayed(const Duration(milliseconds: 200));
await tester.pumpAndSettle();
}
testWidgets('shows the current kcal goal on load', (tester) async {
await tester.runAsync(() async {
await AppSettingsService.instance.saveDailyKcalGoal(1800);
await tester.pumpWidget(const MaterialApp(home: SettingsScreen()));
await settle(tester);
expect(find.widgetWithText(TextField, '1800'), findsOneWidget);
});
});
testWidgets('typing a kcal goal debounces the save', (tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(const MaterialApp(home: SettingsScreen()));
await settle(tester);
await tester.enterText(
find.widgetWithText(TextField, 'Daily kcal goal'),
'2100',
);
// Immediately after typing, the debounce has not fired yet.
expect(AppSettingsService.dailyKcalGoal, isNot(2100));
await Future<void>.delayed(const Duration(milliseconds: 700));
await tester.pump();
expect(AppSettingsService.dailyKcalGoal, 2100);
});
});
testWidgets('an invalid kcal goal is not saved', (tester) async {
await tester.runAsync(() async {
final initial = AppSettingsService.dailyKcalGoal;
await tester.pumpWidget(const MaterialApp(home: SettingsScreen()));
await settle(tester);
await tester.enterText(
find.widgetWithText(TextField, 'Daily kcal goal'),
'0',
);
await Future<void>.delayed(const Duration(milliseconds: 700));
await tester.pump();
expect(AppSettingsService.dailyKcalGoal, initial);
});
});
testWidgets('disposing mid-debounce still flushes the pending goal', (
tester,
) async {
await tester.runAsync(() async {
await tester.pumpWidget(const MaterialApp(home: SettingsScreen()));
await settle(tester);
await tester.enterText(
find.widgetWithText(TextField, 'Daily kcal goal'),
'1950',
);
// Leave before the 600ms debounce fires. dispose()'s flush fires the
// persist unawaited (the framework cannot await dispose()), so the
// in-memory value updates synchronously but the disk write is still a
// real pending Future -- give it a moment to finish before tearDown
// deletes the temp dir out from under it.
await tester.pumpWidget(const MaterialApp(home: SizedBox()));
await tester.pump();
await Future<void>.delayed(const Duration(milliseconds: 50));
expect(AppSettingsService.dailyKcalGoal, 1950);
});
});
testWidgets('tapping Sync settings opens the shared SyncSettingsScreen', (
tester,
) async {
await tester.runAsync(() async {
await tester.pumpWidget(
MaterialApp(
home: SettingsScreen(
accountLoader: () async => null,
sessionProbe: () async => false,
googleAvailable: false,
),
),
);
await settle(tester);
await tester.tap(find.text('Sync settings'));
await settle(tester);
expect(find.text('Firebase sync'), findsOneWidget);
});
});
testWidgets(
'Sync settings wires storedAccount, not loadAccount, as the '
'default accountLoader',
(tester) async {
// Verifies the read-back fix documented on SettingsScreen.accountLoader:
// on a device with a stored account and a live session, the shared
// screen must show it as connected using the injected default, with no
// fake required to prove the wiring compiles and runs end-to-end.
await tester.runAsync(() async {
await saveAccount(
const FirebaseAccount(email: 'sync@example.com', password: 'pw'),
);
await credentialStore().save(
FirebaseCredentials(
idToken: 'id',
refreshToken: 'refresh',
expiresAt: DateTime.now().toUtc().add(const Duration(hours: 1)),
),
);
await tester.pumpWidget(const MaterialApp(home: SettingsScreen()));
await settle(tester);
await tester.tap(find.text('Sync settings'));
await settle(tester);
expect(find.text('sync@example.com'), findsOneWidget);
expect(find.text('Disconnect'), findsOneWidget);
});
},
);
testWidgets(
'tapping Advanced sync (GitHub) opens the local GitHubMirrorScreen',
(tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(const MaterialApp(home: SettingsScreen()));
await settle(tester);
await tester.tap(find.text('Advanced sync (GitHub)'));
await settle(tester);
expect(find.text('Advanced sync (GitHub)'), findsWidgets);
expect(find.text('Connect GitHub'), findsOneWidget);
});
},
);
}