From a6922b6a27e105f2ea7467edcdf2ff72f54d82a4 Mon Sep 17 00:00:00 2001 From: Irene Date: Mon, 24 Aug 2026 06:43:37 +0900 Subject: [PATCH] feat: add TestRunViewModel with state management --- .../test_run/test_run_view_model.dart | 58 ++++++++++++++++++ .../test_run/test_run_view_model_test.dart | 61 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 lib/ui/features/test_run/test_run_view_model.dart create mode 100644 test/ui/features/test_run/test_run_view_model_test.dart diff --git a/lib/ui/features/test_run/test_run_view_model.dart b/lib/ui/features/test_run/test_run_view_model.dart new file mode 100644 index 0000000..402bd61 --- /dev/null +++ b/lib/ui/features/test_run/test_run_view_model.dart @@ -0,0 +1,58 @@ +import 'package:flutter/foundation.dart'; +import 'package:iperf3gui/data/services/iperf3_service.dart'; +import 'package:iperf3gui/domain/models/test_configuration.dart'; +import 'package:iperf3gui/domain/models/iperf3_result.dart'; + +/// State of the test run. +enum TestRunState { idle, running, completed, error } + +/// ViewModel for managing test run state. +class TestRunViewModel extends ChangeNotifier { + final Iperf3Service _iperf3Service; + + TestRunState _state = TestRunState.idle; + TestRunState get state => _state; + + Iperf3Result? _result; + Iperf3Result? get result => _result; + + TestConfiguration? _currentConfig; + TestConfiguration? get currentConfig => _currentConfig; + + TestRunViewModel({required Iperf3Service iperf3Service}) + : _iperf3Service = iperf3Service; + + /// Start a test with the given configuration. + Future runTest(TestConfiguration config) async { + if (!config.isValid) { + _state = TestRunState.error; + notifyListeners(); + return; + } + + _state = TestRunState.running; + _currentConfig = config; + _result = null; + notifyListeners(); + + try { + final result = await _iperf3Service.run(config); + _result = result; + _state = result.exitCode == 0 + ? TestRunState.completed + : TestRunState.error; + } catch (e) { + _state = TestRunState.error; + } finally { + notifyListeners(); + } + } + + /// Reset to idle state. + void reset() { + _state = TestRunState.idle; + _result = null; + _currentConfig = null; + notifyListeners(); + } +} diff --git a/test/ui/features/test_run/test_run_view_model_test.dart b/test/ui/features/test_run/test_run_view_model_test.dart new file mode 100644 index 0000000..5062020 --- /dev/null +++ b/test/ui/features/test_run/test_run_view_model_test.dart @@ -0,0 +1,61 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:iperf3gui/data/services/iperf3_service.dart'; +import 'package:iperf3gui/domain/models/iperf3_result.dart'; +import 'package:iperf3gui/domain/models/test_configuration.dart'; +import 'package:iperf3gui/ui/features/test_run/test_run_view_model.dart'; + +void main() { + group('TestRunViewModel', () { + test('starts in idle state', () { + final vm = TestRunViewModel( + iperf3Service: _MockIperf3Service(), + ); + expect(vm.state, equals(TestRunState.idle)); + expect(vm.result, isNull); + expect(vm.currentConfig, isNull); + }); + + test('can be reset', () { + final vm = TestRunViewModel( + iperf3Service: _MockIperf3Service(), + ); + vm.reset(); + expect(vm.state, equals(TestRunState.idle)); + }); + + test('invalid config sets error state without running', () async { + final vm = TestRunViewModel( + iperf3Service: _MockIperf3Service(), + ); + const config = TestConfiguration(server: ''); + await vm.runTest(config); + expect(vm.state, equals(TestRunState.error)); + }); + + test('runTest with valid config calls service', () async { + final mock = _MockIperf3Service( + onRun: (config) async { + expect(config.server, equals('192.168.1.1')); + return Iperf3Result(rawOutput: 'done', exitCode: 0); + }, + ); + final vm = TestRunViewModel(iperf3Service: mock); + const config = TestConfiguration(server: '192.168.1.1'); + await vm.runTest(config); + expect(vm.state, equals(TestRunState.completed)); + expect(vm.result, isNotNull); + }); + }); +} + +class _MockIperf3Service implements Iperf3Service { + _MockIperf3Service({this.onRun}); + + final Future Function(TestConfiguration)? onRun; + + @override + Future run(TestConfiguration config) async { + if (onRun != null) return onRun!(config); + return Iperf3Result(rawOutput: '', exitCode: 0); + } +}