| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- import 'package:cardash_launcher/data/models/bluetooth_device_info.dart';
- import 'package:cardash_launcher/data/models/bluetooth_power_state.dart';
- import 'package:cardash_launcher/data/models/gps_speed.dart';
- import 'package:cardash_launcher/data/models/location_reading.dart';
- import 'package:cardash_launcher/data/models/obd_state.dart';
- import 'package:cardash_launcher/data/repositories/settings_repository.dart';
- import 'package:cardash_launcher/data/services/telemetry_bridge.dart';
- import 'package:cardash_launcher/features/home/obd_controller.dart';
- import 'package:flutter_test/flutter_test.dart';
- import 'package:shared_preferences/shared_preferences.dart';
- const _obdAddress = 'AA:BB:CC:DD:EE:01';
- const _phoneAddress = 'AA:BB:CC:DD:EE:02';
- const _obdAdapter = BluetoothDeviceInfo(
- name: 'ELM327 v1.5',
- address: _obdAddress,
- deviceClass: 0x1F00,
- );
- const _phone = BluetoothDeviceInfo(
- name: 'Pixel 8',
- address: _phoneAddress,
- deviceClass: 0x0200,
- );
- class _FakeTelemetry implements TelemetryPlatform {
- BluetoothPowerState state = BluetoothPowerState.on;
- List<BluetoothDeviceInfo> bonded = const [];
- bool permissionGranted = true;
- TelemetryException? connectError;
- ObdState obdState = const ObdState();
- int listCalls = 0;
- int stateCalls = 0;
- int connectCalls = 0;
- String? connectedAddress;
- @override
- Future<List<BluetoothDeviceInfo>> listBondedDevices() async {
- listCalls++;
- return bonded;
- }
- @override
- Future<BluetoothPowerState> bluetoothState() async {
- stateCalls++;
- return state;
- }
- @override
- Future<bool> ensureBluetoothPermission() async => permissionGranted;
- @override
- Future<bool> openBluetoothSettings() async => true;
- @override
- Future<bool> ensureLocationPermission() async => true;
- @override
- Future<LocationReading> getLocation() async =>
- const LocationReading(latitude: 0, longitude: 0, place: 'Home');
- @override
- Future<void> connectObd(String address) async {
- connectCalls++;
- connectedAddress = address;
- final problem = connectError;
- if (problem != null) throw problem;
- }
- @override
- Future<void> disconnectObd() async {}
- @override
- Future<ObdState> getObdState() async => obdState;
- @override
- Future<bool> startGpsSpeed() async => false;
- @override
- Future<void> stopGpsSpeed() async {}
- @override
- Future<GpsSpeed> getGpsSpeed() async => const GpsSpeed();
- }
- Future<SettingsRepository> _settingsWithAdapter(String? address) async {
- SharedPreferences.setMockInitialValues({});
- final prefs = await SharedPreferences.getInstance();
- final settings = SettingsRepository(prefs);
- await settings.setObdAdapterAddress(address);
- return settings;
- }
- void main() {
- TestWidgetsFlutterBinding.ensureInitialized();
- test('classifies OBD adapters and non-OBD devices', () {
- expect(_obdAdapter.isLikelyObd, isTrue);
- expect(_phone.isLikelyObd, isFalse);
- const headset = BluetoothDeviceInfo(
- name: 'Car audio',
- address: 'AA:BB:CC:DD:EE:03',
- deviceClass: 0x0400,
- );
- expect(headset.isLikelyObd, isFalse);
- const sppDevice = BluetoothDeviceInfo(
- name: 'Unknown',
- address: 'AA:BB:CC:DD:EE:04',
- deviceClass: 0x0200,
- uuids: ['00001101-0000-1000-8000-00805f9b34fb'],
- );
- expect(sppDevice.isLikelyObd, isTrue);
- const unnamedClass = BluetoothDeviceInfo(
- name: 'Unknown',
- address: 'AA:BB:CC:DD:EE:05',
- );
- expect(unnamedClass.isLikelyObd, isTrue);
- const parsed = BluetoothDeviceInfo(
- name: 'Phone',
- address: _phoneAddress,
- deviceClass: 0x0200,
- );
- expect(parsed.majorClass, 0x0200);
- });
- test('splits paired devices into OBD and other lists', () async {
- final settings = await _settingsWithAdapter(null);
- final telemetry = _FakeTelemetry()..bonded = const [_obdAdapter, _phone];
- final controller = ObdController(telemetry, settings);
- addTearDown(controller.dispose);
- expect(await controller.refreshDevices(force: true), isTrue);
- expect(controller.devices, hasLength(2));
- expect(controller.obdDevices.single.address, _obdAddress);
- expect(controller.otherDevices.single.address, _phoneAddress);
- });
- test('resume checks power state but does not relist known devices', () async {
- var now = DateTime(2026);
- final settings = await _settingsWithAdapter(null);
- final telemetry = _FakeTelemetry()..bonded = const [_obdAdapter];
- final controller = ObdController(telemetry, settings, clock: () => now);
- addTearDown(controller.dispose);
- expect(await controller.refreshDevices(force: true), isTrue);
- expect(telemetry.listCalls, 1);
- expect(telemetry.stateCalls, 1);
- now = now.add(const Duration(seconds: 5));
- expect(await controller.refreshDevices(dueToResume: true), isTrue);
- expect(telemetry.listCalls, 1);
- expect(telemetry.stateCalls, 2);
- });
- test('turning on is reported without listing devices', () async {
- final settings = await _settingsWithAdapter(null);
- final telemetry = _FakeTelemetry()
- ..state = BluetoothPowerState.turningOn
- ..bonded = const [_obdAdapter];
- final controller = ObdController(telemetry, settings);
- addTearDown(controller.dispose);
- expect(await controller.refreshDevices(force: true), isFalse);
- expect(controller.bluetoothState, BluetoothPowerState.turningOn);
- expect(telemetry.listCalls, 0);
- expect(controller.statusLabel, 'Bluetooth is starting…');
- });
- test('denied Bluetooth permission clears the loading state', () async {
- final settings = await _settingsWithAdapter(null);
- final telemetry = _FakeTelemetry()..permissionGranted = false;
- final controller = ObdController(telemetry, settings);
- addTearDown(controller.dispose);
- expect(
- await controller.refreshDevices(requestPermission: true, force: true),
- isFalse,
- );
- expect(controller.loadingDevices, isFalse);
- expect(controller.needsBluetoothPermission, isTrue);
- expect(telemetry.listCalls, 0);
- });
- test('start does not auto-connect to a saved non-OBD device', () async {
- final settings = await _settingsWithAdapter(_phoneAddress);
- final telemetry = _FakeTelemetry()..bonded = const [_phone];
- final controller = ObdController(telemetry, settings)..initialize();
- addTearDown(controller.dispose);
- await controller.start();
- expect(telemetry.connectCalls, 0);
- expect(controller.error, contains('does not look like an OBD-II'));
- expect(controller.canRetry, isTrue);
- await controller.retryConnection();
- controller.stop();
- expect(telemetry.connectCalls, 1);
- });
- test('start connects a saved OBD adapter', () async {
- final settings = await _settingsWithAdapter(_obdAddress);
- final telemetry = _FakeTelemetry()..bonded = const [_obdAdapter];
- final controller = ObdController(telemetry, settings)..initialize();
- addTearDown(controller.dispose);
- await controller.start();
- controller.stop();
- expect(telemetry.connectCalls, 1);
- expect(telemetry.connectedAddress, _obdAddress);
- });
- test('connect timeout stops polling and exposes retry', () async {
- final settings = await _settingsWithAdapter(_obdAddress);
- final telemetry = _FakeTelemetry()
- ..bonded = const [_obdAdapter]
- ..connectError = TelemetryException(
- 'Bluetooth is not responding.',
- code: 'timeout',
- );
- final controller = ObdController(telemetry, settings)..initialize();
- addTearDown(controller.dispose);
- await controller.start();
- controller.stop();
- expect(telemetry.connectCalls, 1);
- expect(controller.canRetry, isTrue);
- expect(controller.statusLabel, 'Not connected — adapter not responding');
- });
- test('parses retryExhausted from the native snapshot', () {
- final state = ObdState.fromMap({
- 'connected': false,
- 'connecting': false,
- 'error': 'Could not connect.',
- 'retryExhausted': true,
- });
- expect(state.retryExhausted, isTrue);
- });
- test('parses the ECU fuel rate from the native snapshot', () {
- final state = ObdState.fromMap({
- 'connected': true,
- 'speedKmh': 60,
- 'maf': 12.0,
- 'fuelRateLph': 4.25,
- });
- expect(state.fuelRateLph, 4.25);
- });
- }
|