mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Cover services + app bootstrap toward 100% flutter coverage
Pre-existing coverage gaps, unrelated to the manual-workout feature, blocking the 100% pre-push gate: - widget_test: exercise WorkoutApp's app-lifecycle handler (paused/detached/ resumed/inactive/hidden) via handleAppLifecycleStateChanged. - http_server_service: add a resetForTesting seam and test the bind-in-use SocketException swallow plus the /workout HTTP endpoint (no-data 404, data 200, unknown-path 404). coverage:ignore the /sdcard-file-read branch (only reachable on a real device). - backup_service: coverage:ignore the Android permission wrappers and the defensive readSyncToken catch. - main(): coverage:ignore the platform-channel app bootstrap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PM3auNTifaVgcXWDizZrjH
This commit is contained in:
parent
5b3b15f73c
commit
0d0e6e4481
@ -6,6 +6,10 @@ import 'package:workout_app/services/backup_service.dart';
|
||||
import 'package:workout_app/services/http_server_service.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
|
||||
// coverage:ignore-start
|
||||
// App bootstrap: permission request, DB init, and a real listening socket —
|
||||
// platform-channel work that can't run on the CI test host. The pieces it wires
|
||||
// up are unit-tested individually; the entry point itself is not.
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await BackupService.instance.requestStoragePermission();
|
||||
@ -14,6 +18,7 @@ void main() async {
|
||||
await HttpServerService.instance.start();
|
||||
runApp(const WorkoutApp());
|
||||
}
|
||||
// coverage:ignore-end
|
||||
|
||||
/// Root widget that bootstraps the app with Material 3 dark theming.
|
||||
///
|
||||
|
||||
@ -55,6 +55,10 @@ class BackupService {
|
||||
|
||||
// ── Permission ─────────────────────────────────────────────────────────────
|
||||
|
||||
// coverage:ignore-start
|
||||
// Thin wrappers over the permission_handler platform channel — Android
|
||||
// runtime permissions that can't be meaningfully exercised on the test host.
|
||||
|
||||
/// Returns true if the app has MANAGE_EXTERNAL_STORAGE.
|
||||
Future<bool> hasStoragePermission() async {
|
||||
return Permission.manageExternalStorage.isGranted;
|
||||
@ -64,10 +68,10 @@ class BackupService {
|
||||
///
|
||||
/// Returns true once granted.
|
||||
Future<bool> requestStoragePermission() async {
|
||||
final status =
|
||||
await Permission.manageExternalStorage.request();
|
||||
final status = await Permission.manageExternalStorage.request();
|
||||
return status.isGranted;
|
||||
}
|
||||
// coverage:ignore-end
|
||||
|
||||
// ── Export ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -133,8 +137,12 @@ class BackupService {
|
||||
if (!f.existsSync()) return null;
|
||||
final token = await f.readAsString();
|
||||
return token.isEmpty ? null : token;
|
||||
// coverage:ignore-start
|
||||
// Defensive: a present, readable token file is the norm; a portable way
|
||||
// to make readAsString throw here (unreadable file) isn't available.
|
||||
} on Exception {
|
||||
return null;
|
||||
}
|
||||
// coverage:ignore-end
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:workout_app/services/sync_service.dart';
|
||||
|
||||
@ -84,6 +85,9 @@ class HttpServerService {
|
||||
}
|
||||
for (final path in candidates) {
|
||||
final file = File(path);
|
||||
// coverage:ignore-start
|
||||
// Only reachable when a candidate file exists — i.e. on a real device
|
||||
// with /sdcard (or Android external storage). Not present on the host.
|
||||
if (file.existsSync()) {
|
||||
try {
|
||||
_latestJson = await file.readAsString();
|
||||
@ -92,9 +96,15 @@ class HttpServerService {
|
||||
// Try next path.
|
||||
}
|
||||
}
|
||||
// coverage:ignore-end
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the cached workout JSON (test seam so the "no data yet" HTTP
|
||||
/// response can be exercised after other tests have set it).
|
||||
@visibleForTesting
|
||||
void resetForTesting() => _latestJson = null;
|
||||
|
||||
Future<void> _serve() async {
|
||||
final server = _server;
|
||||
if (server == null) return;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:workout_app/services/http_server_service.dart';
|
||||
|
||||
@ -40,5 +42,34 @@ void main() {
|
||||
test('kWorkoutServerPort has expected value', () {
|
||||
expect(kWorkoutServerPort, 8765);
|
||||
});
|
||||
|
||||
test('start swallows a bind failure when the port is taken', () async {
|
||||
final blocker = await ServerSocket.bind(
|
||||
InternetAddress.anyIPv4,
|
||||
kWorkoutServerPort,
|
||||
);
|
||||
// Binding fails (SocketException) but start() must not throw.
|
||||
await HttpServerService.instance.start();
|
||||
await blocker.close();
|
||||
});
|
||||
|
||||
test('serves the latest workout over HTTP', () async {
|
||||
HttpServerService.instance.resetForTesting();
|
||||
await HttpServerService.instance.start();
|
||||
final client = HttpClient();
|
||||
|
||||
Future<int> get(String path) async {
|
||||
final req = await client.get('localhost', kWorkoutServerPort, path);
|
||||
final resp = await req.close();
|
||||
await resp.drain<void>();
|
||||
return resp.statusCode;
|
||||
}
|
||||
|
||||
expect(await get('/workout'), 404); // no data yet
|
||||
HttpServerService.instance.latestWorkout = '{"workout_type":"A"}';
|
||||
expect(await get('/workout'), 200);
|
||||
expect(await get('/other'), 404); // unknown path
|
||||
client.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
|
||||
import 'package:workout_app/main.dart';
|
||||
import 'package:workout_app/services/http_server_service.dart';
|
||||
import 'package:workout_app/services/storage_service.dart';
|
||||
|
||||
void main() {
|
||||
@ -23,4 +24,24 @@ void main() {
|
||||
await tester.pump();
|
||||
expect(find.text('Workout Tracker'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('app-lifecycle changes drive the HTTP server', (tester) async {
|
||||
await tester.runAsync(() async {
|
||||
await tester.pumpWidget(const WorkoutApp());
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
// A valid forward transition path that visits every handled state.
|
||||
for (final state in const [
|
||||
AppLifecycleState.resumed,
|
||||
AppLifecycleState.inactive,
|
||||
AppLifecycleState.hidden,
|
||||
AppLifecycleState.paused,
|
||||
AppLifecycleState.detached,
|
||||
]) {
|
||||
tester.binding.handleAppLifecycleStateChanged(state);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 5));
|
||||
}
|
||||
await HttpServerService.instance.stop();
|
||||
});
|
||||
await tester.pump();
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user