mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Introduce a headless `flutter test` suite (83 tests) covering models, utilities, every API endpoint, and the five screens, with shared helpers and fixtures so new tests stay terse. API endpoint methods are statically-dispatched extensions on the BackendAPI singleton, so they cannot be mocked via `implements`. Mock at the Dio HTTP-adapter layer (http_mock_adapter) instead, enabled by two small @visibleForTesting seams: BackendAPI.dioForTesting swaps the singleton's Dio, and BackendReachability.forceOnlineForTesting() forces a deterministic online state so polling widgets load. Add frontend/run_tests.sh and document it in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
3.1 KiB
Dart
107 lines
3.1 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'package:dio/dio.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'oott_api.dart';
|
|
|
|
class BackendReachability extends ChangeNotifier {
|
|
BackendReachability._internal() {
|
|
_subscription = Connectivity().onConnectivityChanged.listen(
|
|
_handleConnectivityChange,
|
|
);
|
|
Connectivity().checkConnectivity().then(_handleConnectivityChange);
|
|
}
|
|
|
|
static final BackendReachability instance = BackendReachability._internal();
|
|
|
|
StreamSubscription<List<ConnectivityResult>>? _subscription;
|
|
Future<void> Function()? _prober;
|
|
|
|
bool _deviceHasNetwork = true;
|
|
bool _isBackendReachable = true;
|
|
DateTime? _lastSuccessAt;
|
|
DateTime? _lastFailureAt;
|
|
String? _lastErrorMessage;
|
|
bool _probing = false;
|
|
|
|
bool get deviceHasNetwork => _deviceHasNetwork;
|
|
bool get isBackendReachable => _isBackendReachable;
|
|
bool get isOnline => _deviceHasNetwork && _isBackendReachable;
|
|
bool get isProbing => _probing;
|
|
DateTime? get lastSuccessAt => _lastSuccessAt;
|
|
DateTime? get lastFailureAt => _lastFailureAt;
|
|
String? get lastErrorMessage => _lastErrorMessage;
|
|
|
|
void setProber(Future<void> Function() prober) {
|
|
_prober = prober;
|
|
}
|
|
|
|
/// Test-only hook to force a deterministic online state so polling widgets
|
|
/// load during tests (where connectivity_plus has no platform backing).
|
|
@visibleForTesting
|
|
void forceOnlineForTesting() {
|
|
_deviceHasNetwork = true;
|
|
_isBackendReachable = true;
|
|
_lastErrorMessage = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
void recordSuccess() {
|
|
_lastSuccessAt = DateTime.now();
|
|
_lastErrorMessage = null;
|
|
final wasReachable = _isBackendReachable;
|
|
_isBackendReachable = true;
|
|
if (!wasReachable) notifyListeners();
|
|
}
|
|
|
|
void recordFailure(DioException error) {
|
|
if (!_isConnectionLevel(error)) return;
|
|
_lastFailureAt = DateTime.now();
|
|
_lastErrorMessage = dioErrorToUserMessage(error);
|
|
final wasReachable = _isBackendReachable;
|
|
_isBackendReachable = false;
|
|
if (wasReachable) notifyListeners();
|
|
}
|
|
|
|
Future<void> probe() async {
|
|
final prober = _prober;
|
|
if (prober == null || _probing) return;
|
|
_probing = true;
|
|
notifyListeners();
|
|
try {
|
|
await prober();
|
|
} catch (_) {
|
|
// Interceptor records the outcome; nothing else to do here.
|
|
} finally {
|
|
_probing = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void _handleConnectivityChange(List<ConnectivityResult> results) {
|
|
final hasNetwork = results.any((r) => r != ConnectivityResult.none);
|
|
if (hasNetwork == _deviceHasNetwork) return;
|
|
_deviceHasNetwork = hasNetwork;
|
|
notifyListeners();
|
|
if (hasNetwork) {
|
|
unawaited(probe());
|
|
}
|
|
}
|
|
|
|
bool _isConnectionLevel(DioException error) {
|
|
if (error.type == DioExceptionType.cancel) return false;
|
|
// Anything without a response from the server is a connection-level
|
|
// failure (timeouts, connection refused, DNS, TLS, web fetch errors
|
|
// surfaced as DioExceptionType.unknown, etc.).
|
|
return error.response == null;
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_subscription?.cancel();
|
|
super.dispose();
|
|
}
|
|
}
|