From f39a22668e51bffdee1eef2abb5b8f1a94158a00 Mon Sep 17 00:00:00 2001 From: just_stuff_tm <133525672+just-stuff-tm@users.noreply.github.com> Date: Mon, 2 Mar 2026 05:28:40 -0500 Subject: [PATCH] Add initial load scheduling and tests for USB screen and frame codec functionality --- lib/screens/usb_screen.dart | 6 +- test/screens/usb_flow_test.dart | 125 ++++++++++++++++++ .../services/usb_serial_frame_codec_test.dart | 39 ++++++ test/utils/usb_port_labels_test.dart | 13 +- 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 test/screens/usb_flow_test.dart diff --git a/lib/screens/usb_screen.dart b/lib/screens/usb_screen.dart index 160f463..c99e10f 100644 --- a/lib/screens/usb_screen.dart +++ b/lib/screens/usb_screen.dart @@ -21,6 +21,7 @@ class _UsbScreenState extends State { bool _isLoadingPorts = true; bool _isConnecting = false; bool _navigatedToContacts = false; + bool _didScheduleInitialLoad = false; String? _selectedPort; String? _errorText; late final MeshCoreConnector _connector; @@ -58,13 +59,16 @@ class _UsbScreenState extends State { } }; _connector.addListener(_connectionListener); - unawaited(_loadPorts()); } @override void didChangeDependencies() { super.didChangeDependencies(); _connector.setUsbRequestPortLabel(context.l10n.usbScreenStatus); + if (!_didScheduleInitialLoad) { + _didScheduleInitialLoad = true; + unawaited(_loadPorts()); + } } @override diff --git a/test/screens/usb_flow_test.dart b/test/screens/usb_flow_test.dart new file mode 100644 index 0000000..499f894 --- /dev/null +++ b/test/screens/usb_flow_test.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +import 'package:meshcore_open/connector/meshcore_connector.dart'; +import 'package:meshcore_open/l10n/app_localizations.dart'; +import 'package:meshcore_open/screens/connection_choice_screen.dart'; +import 'package:meshcore_open/screens/usb_screen.dart'; +import 'package:meshcore_open/utils/platform_info.dart'; + +class _FakeMeshCoreConnector extends MeshCoreConnector { + _FakeMeshCoreConnector({ + this.initialState = MeshCoreConnectionState.disconnected, + List? ports, + }) : _ports = ports ?? []; + + final MeshCoreConnectionState initialState; + final List _ports; + + String? requestPortLabel; + int connectUsbCalls = 0; + String? lastConnectPortName; + String? fakeActiveUsbPort; + bool fakeUsbTransportConnected = false; + + @override + MeshCoreConnectionState get state => initialState; + + @override + String? get activeUsbPort => fakeActiveUsbPort; + + @override + bool get isUsbTransportConnected => fakeUsbTransportConnected; + + @override + Future> listUsbPorts() async => List.from(_ports); + + @override + Future connectUsb({ + required String portName, + int baudRate = 115200, + }) async { + connectUsbCalls += 1; + lastConnectPortName = portName; + } + + @override + void setUsbRequestPortLabel(String label) { + requestPortLabel = label; + } +} + +Widget _buildTestApp({ + required MeshCoreConnector connector, + required Widget child, +}) { + return ChangeNotifierProvider.value( + value: connector, + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: child, + ), + ); +} + +void main() { + testWidgets('UsbScreen passes localized chooser label to connector', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + expect(connector.requestPortLabel, 'Select a USB device'); + }); + + testWidgets( + 'UsbScreen does not call connectUsb when connector is not disconnected', + (tester) async { + final connector = _FakeMeshCoreConnector( + initialState: MeshCoreConnectionState.connected, + ports: ['COM6 - USB Serial Device (COM6)'], + ); + + await tester.pumpWidget( + _buildTestApp(connector: connector, child: const UsbScreen()), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pump(); + + expect(connector.connectUsbCalls, 0); + expect(find.byType(CircularProgressIndicator), findsNothing); + }, + ); + + testWidgets('ConnectionChoiceScreen USB button reflects platform support', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector(); + + await tester.pumpWidget( + _buildTestApp( + connector: connector, + child: const ConnectionChoiceScreen(), + ), + ); + await tester.pumpAndSettle(); + + final usbButton = tester.widget( + find.widgetWithText(ElevatedButton, 'USB'), + ); + + if (PlatformInfo.supportsUsbSerial) { + expect(usbButton.onPressed, isNotNull); + } else { + expect(usbButton.onPressed, isNull); + } + }); +} diff --git a/test/services/usb_serial_frame_codec_test.dart b/test/services/usb_serial_frame_codec_test.dart index f0ce186..be4497e 100644 --- a/test/services/usb_serial_frame_codec_test.dart +++ b/test/services/usb_serial_frame_codec_test.dart @@ -13,6 +13,21 @@ void main() { ); }); + test('wrapUsbSerialTxFrame rejects payloads above protocol maximum', () { + final payload = Uint8List(usbSerialMaxPayloadLength + 1); + + expect( + () => wrapUsbSerialTxFrame(payload), + throwsA( + isA().having( + (error) => error.name, + 'name', + 'payload.length', + ), + ), + ); + }); + test('UsbSerialFrameDecoder buffers partial frames until complete', () { final decoder = UsbSerialFrameDecoder(); @@ -81,4 +96,28 @@ void main() { expect(packets[1].payload, orderedEquals([0x33])); }, ); + + test( + 'UsbSerialFrameDecoder drops oversized frames and resyncs on the next valid packet', + () { + final decoder = UsbSerialFrameDecoder(); + + final packets = decoder.ingest( + Uint8List.fromList([ + usbSerialRxFrameStart, + 0xAD, + 0x00, + 0x99, + usbSerialRxFrameStart, + 0x01, + 0x00, + 0x44, + ]), + ); + + expect(packets, hasLength(1)); + expect(packets.single.isRxFrame, isTrue); + expect(packets.single.payload, orderedEquals([0x44])); + }, + ); } diff --git a/test/utils/usb_port_labels_test.dart b/test/utils/usb_port_labels_test.dart index 4fef509..e375005 100644 --- a/test/utils/usb_port_labels_test.dart +++ b/test/utils/usb_port_labels_test.dart @@ -50,7 +50,18 @@ void main() { test('describeWebUsbPort returns chooser label when no usb ids exist', () { expect( describeWebUsbPort(vendorId: null, productId: null), - usbRequestPortLabel, + 'Choose USB Device', + ); + }); + + test('describeWebUsbPort uses caller-provided chooser label', () { + expect( + describeWebUsbPort( + vendorId: null, + productId: null, + requestPortLabel: 'Select a USB device', + ), + 'Select a USB device', ); });