Cover settings_screen.dart to 100%

Add tests for the threshold selectors (success + fail persistence), the
debounced weight write, and three device-flow branches that were untested:
token-save failure (throwing secure storage), verification failure, and
the transient pending "verifying" status tile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWkAKQ48Jhg5iSPkzh8NsH
This commit is contained in:
Krzysztof Rudnicki 2026-07-13 14:03:21 +02:00
parent 5fe4222dfd
commit e14cdf9997

View File

@ -377,4 +377,212 @@ void main() {
await tester.pump();
});
});
testWidgets('changing a success threshold persists it', (tester) async {
await _pump(tester, _wrap());
final label = find.text('↑ Increase after N successes').first;
await tester.scrollUntilVisible(
label,
300,
scrollable: find.byType(Scrollable),
);
// The circles (1-5) live in the same Row as the label; tap the "5" circle.
final row = find.ancestor(of: label, matching: find.byType(Row)).first;
final five = find.descendant(of: row, matching: find.text('5')).first;
// The row can sit below the 800x600 test fold — bring the circle fully in.
await tester.ensureVisible(five);
await tester.pumpAndSettle();
// _onThresholdChanged awaits a DB write, so drive it on the real loop.
await tester.runAsync(() async {
await tester.tap(five);
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.pump();
final states = await tester.runAsync(
() => StorageService.instance.getAllExerciseStates(),
);
expect(states!.any((s) => s.successThreshold == 5), isTrue);
// Also exercise the fail-threshold path (onFailChanged closure).
final failLabel = find.text('↓ Decrease after N failures').first;
await tester.ensureVisible(failLabel);
await tester.pumpAndSettle();
final failRow =
find.ancestor(of: failLabel, matching: find.byType(Row)).first;
final failFour = find.descendant(of: failRow, matching: find.text('4')).first;
await tester.ensureVisible(failFour);
await tester.pumpAndSettle();
await tester.runAsync(() async {
await tester.tap(failFour);
await Future<void>.delayed(const Duration(milliseconds: 300));
});
await tester.pump();
final states2 = await tester.runAsync(
() => StorageService.instance.getAllExerciseStates(),
);
expect(states2!.any((s) => s.failThreshold == 4), isTrue);
});
testWidgets('weight change is debounced then written to storage', (
tester,
) async {
await _pump(tester, _wrap());
final name = workoutA.first.name;
final before = (await tester.runAsync(
() => StorageService.instance.getExerciseState(name),
))!.weight;
// Tapping "+" schedules a 600ms debounce Timer; running the tap and the
// wait on the real loop lets the timer fire and setExerciseWeight complete.
await tester.runAsync(() async {
await tester.tap(find.byIcon(Icons.add).first);
await Future<void>.delayed(const Duration(milliseconds: 800));
});
await tester.pump();
final after = (await tester.runAsync(
() => StorageService.instance.getExerciseState(name),
))!.weight;
expect(after, before + kWeightIncrement);
});
testWidgets('device flow: shows a message when the token cannot be saved', (
tester,
) async {
// Throwing secure storage makes SyncSettings.save() fail.
installFakeSecureStorage(throwing: true);
final mock = MockClient((req) async {
if (req.url.path.contains('device/code')) {
return http.Response(
jsonEncode({
'device_code': 'dev123',
'user_code': 'WXYZ-1234',
'verification_uri': 'https://github.com/login/device',
'interval': 0,
'expires_in': 900,
}),
200,
);
}
if (req.url.path.contains('oauth/access_token')) {
return http.Response(jsonEncode({'access_token': 'gho_test'}), 200);
}
return http.Response(
jsonEncode({'content': base64Encode(utf8.encode('{}'))}),
200,
);
});
await tester.runAsync(() async {
await tester.pumpWidget(_wrap(httpClient: mock));
await Future<void>.delayed(const Duration(milliseconds: 300));
await tester.pump();
await _scrollToGitHubSync(tester);
await tester.tap(find.text('Connect GitHub'));
await _pumpUntil(
tester,
() =>
find.textContaining('could not save the token').evaluate().isNotEmpty,
);
expect(
find.textContaining('could not save the token'),
findsOneWidget,
);
});
});
testWidgets('device flow: shows a message when verification fails', (
tester,
) async {
final mock = MockClient((req) async {
if (req.url.path.contains('device/code')) {
return http.Response(
jsonEncode({
'device_code': 'dev123',
'user_code': 'WXYZ-1234',
'verification_uri': 'https://github.com/login/device',
'interval': 0,
'expires_in': 900,
}),
200,
);
}
if (req.url.path.contains('oauth/access_token')) {
return http.Response(jsonEncode({'access_token': 'gho_test'}), 200);
}
// The post-connect verification GET fails → GitHubSyncError.
return http.Response('boom', 500);
});
await tester.runAsync(() async {
await tester.pumpWidget(_wrap(httpClient: mock));
await Future<void>.delayed(const Duration(milliseconds: 300));
await tester.pump();
await _scrollToGitHubSync(tester);
await tester.tap(find.text('Connect GitHub'));
await _pumpUntil(
tester,
() => find.textContaining('could not verify').evaluate().isNotEmpty,
);
expect(find.textContaining('could not verify'), findsOneWidget);
});
});
testWidgets('device flow: renders the pending "verifying" status', (
tester,
) async {
final mock = MockClient((req) async {
if (req.url.path.contains('device/code')) {
return http.Response(
jsonEncode({
'device_code': 'dev123',
'user_code': 'WXYZ-1234',
'verification_uri': 'https://github.com/login/device',
'interval': 0,
'expires_in': 900,
}),
200,
);
}
if (req.url.path.contains('oauth/access_token')) {
return http.Response(jsonEncode({'access_token': 'gho_test'}), 200);
}
// Hold the verification in-flight so the pending status tile is
// observable before it resolves to success.
await Future<void>.delayed(const Duration(milliseconds: 600));
return http.Response(
jsonEncode({'content': base64Encode(utf8.encode('{}'))}),
200,
);
});
await tester.runAsync(() async {
await tester.pumpWidget(_wrap(httpClient: mock));
await Future<void>.delayed(const Duration(milliseconds: 300));
await tester.pump();
await _scrollToGitHubSync(tester);
await tester.tap(find.text('Connect GitHub'));
await _pumpUntil(
tester,
() => find.textContaining('verifying').evaluate().isNotEmpty,
);
// Pending tile (spinner + white70 status colour) is now on screen.
expect(find.textContaining('verifying'), findsOneWidget);
await _pumpUntil(
tester,
() =>
find
.textContaining('Connected and verified')
.evaluate()
.isNotEmpty,
);
expect(find.textContaining('Connected and verified'), findsOneWidget);
});
});
}