diff --git a/frontend/integration_test/simple_test.dart b/frontend/integration_test/simple_test.dart index 6b66abcc..14ea0d64 100644 --- a/frontend/integration_test/simple_test.dart +++ b/frontend/integration_test/simple_test.dart @@ -21,6 +21,7 @@ @Tags(['api']) library; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:logging/logging.dart'; import 'package:shrapnel/main.dart'; @@ -28,6 +29,7 @@ import 'package:shrapnel/midi_mapping/model/models.dart'; import '../test/firmware_api_test/util.dart'; import '../test/home_page_object.dart'; +import '../test/util.dart'; // preset test // start up @@ -82,8 +84,7 @@ const networkSsid = String.fromEnvironment('NETWORK_SSID'); const networkPassphrase = String.fromEnvironment('NETWORK_PASSPHRASE'); // ignore: do_not_use_environment const firmwareBinaryPath = String.fromEnvironment('FIRMWARE_BINARY_PATH'); -// ignore: do_not_use_environment -const useFastProvisioning = bool.fromEnvironment('FAST_PROVISIONING'); +const port = '/dev/ttyUSB0'; final _log = Logger('test'); @@ -96,136 +97,179 @@ void main() { setUpAll(() async { macAddress = await flashFirmware( + port: port, appPartitionAddress: 0x10000, path: firmwareBinaryPath, ); }); - setUp(() async { - await nvsErase(); + testWidgets('wifi provisioning', (tester) async { + await nvsErase(port: port); + + uart = await ShrapnelUart.open(port); + addTearDown(() => uart.dispose()); + + await connectToDutAccessPoint(macAddress); + + // mDNS seems to be slow, use the IP address directly for faster testing. + await tester.pumpWidget( + App( + normalHost: dutIpAddress, + provisioningHost: '192.168.4.1', + ), + ); + + final homePage = HomePageObject(tester); + final provisioningPage = await homePage.openWifiProvisioningPage(); + + await provisioningPage.startProvisioning(); + + await pumpWaitingFor( + tester: tester, + predicate: () => + provisioningPage.findScanCompletePage.evaluate().isNotEmpty, + timeout: const Duration(seconds: 10), + ); + + await provisioningPage.openAdvancedSetup(); + + await provisioningPage.enterSsid(networkSsid); + await provisioningPage.enterPassword(networkPassphrase); + + await provisioningPage.submitAdvanced(); + + await pumpWaitingFor( + tester: tester, + predicate: () => provisioningPage.findSuccessPage.evaluate().isNotEmpty, + timeout: const Duration(seconds: 10), + ); + + await tester.tap(find.byType(BackButton)); + + await pumpWaitingFor( + tester: tester, + predicate: () => homePage.findHomePage.evaluate().isNotEmpty, + timeout: const Duration(seconds: 10), + ); + + await homePage.waitUntilConnected(); + }); + + group('wifi already set up', () { + setUp(() async { + await nvsErase(port: port); - uart = await ShrapnelUart.open('/dev/ttyUSB0'); - // TODO move this into the uart class, so it logs by itself even if there - // are no external log listeners - // ensure at least one listener so logging side effect always runs - uart.log.listen((_) {}); - addTearDown(uart.dispose); + uart = await ShrapnelUart.open(port); + addTearDown(uart.dispose); - if (useFastProvisioning) { _log.warning('Bypassing Wi-Fi provisioning to speed up test execution'); await uart.provisionWifi(ssid: networkSsid, password: networkPassphrase); - } else { - // TODO we can still speed up tests where provisioning must be tested: - // - export NVS partition to file after first time wifi provisioning is used - // - reload it in the setup for each following test. This resets the NVS - // partition without requiring a fresh wifi provisioning run for each - // test. - // - // Alternatively, create a new test group that doesn't run the fast wifi - // provisioning setup, and instead runs the slow wifi setup. - await connectToDutAccessPoint(macAddress); - await setUpWiFi(ssid: networkSsid, password: networkPassphrase); - } + // Wait for firmware to start server after getting provisioned + await Future.delayed(const Duration(seconds: 10)); + }); - // Wait for firmware to start server after getting provisioned - await Future.delayed(const Duration(seconds: 10)); - }); + testWidgets('uart console smoke', (tester) async { + await tester.runAsync(() async { + // At least one response line has to arrive within 1 second + final responseLogs = uart.log.first.timeout(const Duration(seconds: 1)); + + await uart.sendMidiMessage( + const MidiMessage.controlChange( + channel: 0, + control: 1, + value: 0x00, + ), + ); + + await expectLater(responseLogs, completes); + }); + }); - testWidgets('uart console smoke', (tester) async { - await tester.runAsync(() async { - // At least one response line has to arrive within 1 second - final responseLogs = uart.log.first.timeout(const Duration(seconds: 1)); + testWidgets('audio parameters', (tester) async { + await tester.runAsync(() async { + // audio parameter basic test + // factory reset + // all parameters loaded correctly + // update a parameter + // force save to NVS + // reboot + // check parameter reloaded + }); + }); - await uart.sendMidiMessage( - const MidiMessage.controlChange( - channel: 0, - control: 1, - value: 0x00, + testWidgets('simple test', (tester) async { + // mDNS seems to be slow, use the IP address directly for faster testing. + await tester.pumpWidget( + App( + normalHost: dutIpAddress, + provisioningHost: '192.168.4.1', ), ); - await expectLater(responseLogs, completes); - }); - }); + // wait until connected + // poll connection status widget until ready with timeout + final homePage = HomePageObject(tester); + await homePage.waitUntilConnected(); - testWidgets('audio parameters', (tester) async { - await tester.runAsync(() async { - // audio parameter basic test - // factory reset - // all parameters loaded correctly - // update a parameter - // force save to NVS - // reboot - // check parameter reloaded - }); - }); + await homePage.createPreset('Preset 1'); - testWidgets('simple test', (tester) async { - await tester.pumpWidget(App()); + final midiMappingPage = await homePage.openMidiMapping(); - // wait until connected - // poll connection status widget until ready with timeout - final homePage = HomePageObject(tester); - await homePage.waitUntilConnected(); + expect(midiMappingPage.findPage(), findsOneWidget); + expect(midiMappingPage.findMappingRows(), findsNothing); - await homePage.createPreset('Preset 1'); - - final midiMappingPage = await homePage.openMidiMapping(); - - expect(midiMappingPage.findPage(), findsOneWidget); - expect(midiMappingPage.findMappingRows(), findsNothing); - - // create a mapping - final midiMappingCreatePage = await midiMappingPage.openCreateDialog(); - await midiMappingCreatePage.selectMidiChannel(1); - await midiMappingCreatePage.selectCcNumber(2); - await midiMappingCreatePage.selectMode(MidiMappingMode.parameter); - // XXX: There is a bug in flutter where the DropdownButton's popup menu is - // unreliable during tests: https://github.com/flutter/flutter/issues/82908 - // - // Pick an arbitrary parameter here. The only criteria for selection is that - // it actually works during the test. This is more likely if something is - // picked from the top of the list. - await midiMappingCreatePage.selectParameter('Chorus: DEPTH'); - await midiMappingCreatePage.submitCreateDialog(); - - await tester.pump(const Duration(seconds: 1)); - await tester.pumpAndSettle(); - - // Expect new mapping visible in UI - expect(midiMappingPage.findMappingRows(), findsOneWidget); - - await midiMappingPage.openCreateDialog(); - await midiMappingCreatePage.selectMidiChannel(2); - await midiMappingCreatePage.selectCcNumber(3); - await midiMappingCreatePage.selectMode(MidiMappingMode.button); - await midiMappingCreatePage.selectPreset('Preset 1'); - await midiMappingCreatePage.submitCreateDialog(); - - await tester.pump(const Duration(seconds: 1)); - await tester.pumpAndSettle(); - - // Expect new mapping visible in UI - expect(midiMappingPage.findMappingRows(), findsNWidgets(2)); - - await midiMappingPage.openCreateDialog(); - await midiMappingCreatePage.selectMidiChannel(3); - await midiMappingCreatePage.selectCcNumber(4); - await midiMappingCreatePage.selectMode(MidiMappingMode.toggle); - // XXX: There is a bug in flutter where the DropdownButton's popup menu is - // unreliable during tests: https://github.com/flutter/flutter/issues/82908 - // - // Pick an arbitrary parameter here. The only criteria for selection is that - // it actually works during the test. This is more likely if something is - // picked from the top of the list. - await midiMappingCreatePage.selectParameter('Chorus: RATE'); - await midiMappingCreatePage.submitCreateDialog(); - - await tester.pump(const Duration(seconds: 1)); - await tester.pumpAndSettle(); - - // Expect new mapping visible in UI - expect(midiMappingPage.findMappingRows(), findsNWidgets(3)); + // create a mapping + final midiMappingCreatePage = await midiMappingPage.openCreateDialog(); + await midiMappingCreatePage.selectMidiChannel(1); + await midiMappingCreatePage.selectCcNumber(2); + await midiMappingCreatePage.selectMode(MidiMappingMode.parameter); + // XXX: There is a bug in flutter where the DropdownButton's popup menu is + // unreliable during tests: https://github.com/flutter/flutter/issues/82908 + // + // Pick an arbitrary parameter here. The only criteria for selection is that + // it actually works during the test. This is more likely if something is + // picked from the top of the list. + await midiMappingCreatePage.selectParameter('Chorus: DEPTH'); + await midiMappingCreatePage.submitCreateDialog(); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + // Expect new mapping visible in UI + expect(midiMappingPage.findMappingRows(), findsOneWidget); + + await midiMappingPage.openCreateDialog(); + await midiMappingCreatePage.selectMidiChannel(2); + await midiMappingCreatePage.selectCcNumber(3); + await midiMappingCreatePage.selectMode(MidiMappingMode.button); + await midiMappingCreatePage.selectPreset('Preset 1'); + await midiMappingCreatePage.submitCreateDialog(); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + // Expect new mapping visible in UI + expect(midiMappingPage.findMappingRows(), findsNWidgets(2)); + + await midiMappingPage.openCreateDialog(); + await midiMappingCreatePage.selectMidiChannel(3); + await midiMappingCreatePage.selectCcNumber(4); + await midiMappingCreatePage.selectMode(MidiMappingMode.toggle); + // XXX: There is a bug in flutter where the DropdownButton's popup menu is + // unreliable during tests: https://github.com/flutter/flutter/issues/82908 + // + // Pick an arbitrary parameter here. The only criteria for selection is that + // it actually works during the test. This is more likely if something is + // picked from the top of the list. + await midiMappingCreatePage.selectParameter('Chorus: RATE'); + await midiMappingCreatePage.submitCreateDialog(); + + await tester.pump(const Duration(seconds: 1)); + await tester.pumpAndSettle(); + + // Expect new mapping visible in UI + expect(midiMappingPage.findMappingRows(), findsNWidgets(3)); + }); }); } diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 22c47215..9ddbccbc 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -108,9 +108,15 @@ class App extends StatelessWidget { PresetsRepositoryBase? presetsRepository, ParameterService? parameterService, SelectedPresetRepositoryBase? selectedPresetRepository, + String? normalHost, + String? provisioningHost, }) { + provisioningHost ??= 'guitar-dsp.local'; + normalHost ??= 'guitar-dsp.local'; + websocket ??= RobustWebsocket( - uri: Uri.parse('http://guitar-dsp.local:8080/websocket'), + uri: Uri.parse('http://guitar-dsp.local:8080/websocket') + .replace(host: normalHost), ); _websocket = websocket; apiWebsocket ??= ApiWebsocket(websocket: websocket); @@ -125,7 +131,7 @@ class App extends StatelessWidget { _log.info('Creating provisioning connection'); return Provisioning( security: Security1(pop: 'abcd1234'), - transport: TransportHTTP('guitar-dsp.local'), + transport: TransportHTTP(provisioningHost!), ); }, ); diff --git a/frontend/lib/robust_websocket.dart b/frontend/lib/robust_websocket.dart index 9a0bda6b..b1b4e84e 100644 --- a/frontend/lib/robust_websocket.dart +++ b/frontend/lib/robust_websocket.dart @@ -93,7 +93,7 @@ class RobustWebsocket extends ChangeNotifier while (socket == null) { try { final client = HttpClient(); - client.connectionTimeout = const Duration(seconds: 15); + client.connectionTimeout = const Duration(seconds: 5); final request = await client.openUrl('GET', uri); final nonce = []; diff --git a/frontend/lib/wifi_provisioning.dart b/frontend/lib/wifi_provisioning.dart index a7cf3fc4..486d6fd4 100644 --- a/frontend/lib/wifi_provisioning.dart +++ b/frontend/lib/wifi_provisioning.dart @@ -309,6 +309,7 @@ class _WifiScanningScreenState extends State<_WifiScanningScreen> { child = Padding( padding: const EdgeInsets.all(8), child: Column( + key: const Key('wifi scan complete page'), children: [ Expanded( child: ListView.builder( diff --git a/frontend/test/firmware_api_test/firmware_api_test.dart b/frontend/test/firmware_api_test/firmware_api_test.dart index 4af77c39..2b8e14fc 100644 --- a/frontend/test/firmware_api_test/firmware_api_test.dart +++ b/frontend/test/firmware_api_test/firmware_api_test.dart @@ -94,10 +94,7 @@ const networkSsid = String.fromEnvironment('NETWORK_SSID'); const networkPassphrase = String.fromEnvironment('NETWORK_PASSPHRASE'); // ignore: do_not_use_environment const firmwareBinaryPath = String.fromEnvironment('FIRMWARE_BINARY_PATH'); -// ignore: do_not_use_environment -const useFastProvisioning = bool.fromEnvironment('FAST_PROVISIONING'); - -final _log = Logger('test'); +const port = '/dev/ttyUSB0'; void main() { assert(dutIpAddress.isNotEmpty, 'DUT_IP_ADDRESS must be set'); @@ -109,66 +106,57 @@ void main() { setUpAll(() async { macAddress = await flashFirmware( + port: port, appPartitionAddress: 0x10000, path: firmwareBinaryPath, ); }); - setUp(() async { - await nvsErase(); - - uart = await ShrapnelUart.open('/dev/ttyUSB0'); - // TODO move this into the uart class, so it logs by itself even if there - // are no external log listeners - // ensure at least one listener so logging side effect always runs - uart.log.listen((_) {}); - addTearDown(uart.dispose); - - if (useFastProvisioning) { - _log.warning('Bypassing Wi-Fi provisioning to speed up test execution'); - await uart.provisionWifi(ssid: networkSsid, password: networkPassphrase); - } else { - // TODO we can still speed up tests where provisioning must be tested: - // - export NVS partition to file after first time wifi provisioning is used - // - reload it in the setup for each following test. This resets the NVS - // partition without requiring a fresh wifi provisioning run for each - // test. - // - // Alternatively, create a new test group that doesn't run the fast wifi - // provisioning setup, and instead runs the slow wifi setup. - - await connectToDutAccessPoint(macAddress); - await setUpWiFi(ssid: networkSsid, password: networkPassphrase); - } - - // Wait for firmware to start server after getting provisioned - await Future.delayed(const Duration(seconds: 10)); - - const uri = 'ws://$dutIpAddress:8080/websocket'; - // closed by the WebSocketTransport - // ignore: close_sinks - final webSocket = await WebSocket.connect(uri); - final webSocketTransport = WebSocketTransportAdapter(websocket: webSocket); - api = ApiWebsocket(websocket: webSocketTransport); - addTearDown(webSocketTransport.dispose); + test('wifi provisioning', () async { + await nvsErase(port: port); + await connectToDutAccessPoint(macAddress); + await setUpWiFi(ssid: networkSsid, password: networkPassphrase); }); - test('simple test', () async { - final transport = ParameterTransport(websocket: api); - final service = ParameterService(transport: transport); + group('wifi already set up', () { + setUp(() async { + await nvsErase(port: port); - final ampGain = AudioParameterDoubleModel( - groupName: 'test', - id: 'ampGain', - name: 'Amp Gain', - parameterService: service, - ); + uart = await ShrapnelUart.open('/dev/ttyUSB0'); + addTearDown(uart.dispose); + + await uart.provisionWifi(ssid: networkSsid, password: networkPassphrase); - // Skip 1, because it's the default injected by the parameter model. We - // are waiting for the first update from the firmware here. - final update = await ampGain.value.skip(1).first; - // Expect notification including the default value - expect(update, closeTo(0.5, 0.000001)); + // Wait for firmware to start server after getting provisioned + await Future.delayed(const Duration(seconds: 10)); + + const uri = 'ws://$dutIpAddress:8080/websocket'; + // closed by the WebSocketTransport + // ignore: close_sinks + final webSocket = await WebSocket.connect(uri); + final webSocketTransport = + WebSocketTransportAdapter(websocket: webSocket); + api = ApiWebsocket(websocket: webSocketTransport); + addTearDown(webSocketTransport.dispose); + }); + + test('simple test', () async { + final transport = ParameterTransport(websocket: api); + final service = ParameterService(transport: transport); + + final ampGain = AudioParameterDoubleModel( + groupName: 'test', + id: 'ampGain', + name: 'Amp Gain', + parameterService: service, + ); + + // Skip 1, because it's the default injected by the parameter model. We + // are waiting for the first update from the firmware here. + final update = await ampGain.value.skip(1).first; + // Expect notification including the default value + expect(update, closeTo(0.5, 0.000001)); + }); }); } diff --git a/frontend/test/firmware_api_test/util.dart b/frontend/test/firmware_api_test/util.dart index df9d6608..7f8c7833 100644 --- a/frontend/test/firmware_api_test/util.dart +++ b/frontend/test/firmware_api_test/util.dart @@ -26,7 +26,6 @@ import 'package:flutter_libserialport/flutter_libserialport.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as p; import 'package:shrapnel/api/proto_extension.dart'; -import 'package:shrapnel/core/stream_extensions.dart'; import 'package:shrapnel/midi_mapping/model/models.dart'; import 'package:shrapnel/wifi_provisioning.dart'; @@ -56,7 +55,7 @@ Future setUpWiFi({required String ssid, required String password}) async { final provisioning = await _retry(() async { final provisioning = Provisioning( security: Security1(pop: 'abcd1234'), - transport: TransportHTTP('guitar-dsp.local'), + transport: TransportHTTP('192.168.4.1'), ); final success = await provisioning.establishSession(); @@ -84,13 +83,13 @@ Future setUpWiFi({required String ssid, required String password}) async { } } -Future _eraseFlash() async { +Future _eraseFlash({required String port}) async { _log.info('Flashing firmware'); // pip can be used to install esptool into the global environment: // https://docs.espressif.com/projects/esptool/en/latest/esp32/#quick-start const command = 'esptool.py'; - final args = ['erase_flash']; + final args = ['-p', port, 'erase_flash']; final result = await Process.run(command, args); @@ -111,6 +110,7 @@ List _getMacAddressFromEsptoolStdout(String espToolStdout) { /// /// If the firmware is already loaded, then does nothing and quickly returns. Future flashFirmware({ + required String port, required int appPartitionAddress, required String path, }) async { @@ -120,13 +120,14 @@ Future flashFirmware({ // takes about 1 second. final (isAlreadyFlashed, macAddress) = await _checkIfAlreadyFlashed( + port: port, appPartitionAddress: appPartitionAddress, binaryFilePath: p.join(path, 'esp32-dsp.bin'), ); if (!isAlreadyFlashed) { - await _eraseFlash(); - await _flashFirmware(path); + await _eraseFlash(port: port); + await _flashFirmware(port: port, path); } else { _log.info('Firmware appears to be already flashed, skip flashing'); } @@ -135,6 +136,7 @@ Future flashFirmware({ } Future<(bool, MacAddress)> _checkIfAlreadyFlashed({ + required String port, required int appPartitionAddress, required String binaryFilePath, }) async { @@ -169,7 +171,8 @@ Future<(bool, MacAddress)> _checkIfAlreadyFlashed({ // pip can be used to install esptool into the global environment: // https://docs.espressif.com/projects/esptool/en/latest/esp32/#quick-start const command = 'esptool.py'; - final args = '-b 2000000 ' + final args = '-p $port ' + '-b 2000000 ' '--before default_reset --after hard_reset ' '--chip esp32 ' 'verify_flash 0x10000 $expectedHeaderFilePath' @@ -196,13 +199,14 @@ Future<(bool, MacAddress)> _checkIfAlreadyFlashed({ ); } -Future _flashFirmware(String path) async { +Future _flashFirmware(String path, {required String port}) async { _log.info('Flashing firmware'); // pip can be used to install esptool into the global environment: // https://docs.espressif.com/projects/esptool/en/latest/esp32/#quick-start const command = 'esptool.py'; - final args = '-b 2000000 ' + final args = '-p $port ' + '-b 2000000 ' '--before default_reset --after hard_reset ' '--chip esp32 ' 'write_flash --flash_mode dio --flash_size 4MB --flash_freq 80m ' @@ -219,13 +223,14 @@ Future _flashFirmware(String path) async { _log.info(result.stdout); } -Future nvsErase() async { +Future nvsErase({required String port}) async { _log.info('Erasing NVS partition'); // pip can be used to install esptool into the global environment: // https://docs.espressif.com/projects/esptool/en/latest/esp32/#quick-start const command = 'esptool.py'; - final args = '-b 2000000 ' + final args = '-p $port ' + '-b 2000000 ' '--before default_reset --after hard_reset ' '--chip esp32 ' 'erase_region 0x9000 0x6000' @@ -239,13 +244,14 @@ Future nvsErase() async { _log.info(result.stdout); } -Future nvsLoad(String binaryPath) async { +Future nvsLoad(String binaryPath, {required String port}) async { _log.info('Flashing NVS partition'); // pip can be used to install esptool into the global environment: // https://docs.espressif.com/projects/esptool/en/latest/esp32/#quick-start const command = 'esptool.py'; - final args = '-b 2000000 ' + final args = '-p $port ' + '-b 2000000 ' '--before default_reset --after hard_reset ' '--chip esp32 ' 'write_flash --flash_mode dio --flash_size 4MB --flash_freq 80m ' @@ -262,7 +268,9 @@ Future nvsLoad(String binaryPath) async { /// UART driver class ShrapnelUart { - ShrapnelUart._(this.port, this.reader); + ShrapnelUart._(this.port, this.reader) { + log.listen(_logger.info); + } static final _logger = Logger('ShrapnelUart'); @@ -324,7 +332,6 @@ class ShrapnelUart { .cast>() .transform(utf8.decoder) .transform(const LineSplitter()) - .logInfo(_logger, (event) => event) .asBroadcastStream(); Stream get log => _log; diff --git a/frontend/test/home_page_object.dart b/frontend/test/home_page_object.dart index b4f11910..a4c7d582 100644 --- a/frontend/test/home_page_object.dart +++ b/frontend/test/home_page_object.dart @@ -27,6 +27,8 @@ import 'package:shrapnel/knob.dart'; import 'midi_mapping/midi_mapping_page_object.dart'; import 'presets/presets_page_object.dart'; +import 'util.dart'; +import 'wifi_provisioning_page_object.dart'; final _log = Logger('home_page_object'); @@ -35,6 +37,10 @@ class HomePageObject { final WidgetTester tester; + Finder get findHomePage => _findMidiLearnButton; + + Finder get _findMidiLearnButton => find.byKey(const Key('midi-learn-button')); + Finder findKnob(String parameterId) => find.byKey(Key('knob-$parameterId')); Finder findMidiLearnWaitingForParameterMessage() => @@ -50,7 +56,7 @@ class HomePageObject { find.byKey(const Key('undo-remove-duplicate-mappings')); Future startMidiLearn() async { - await tester.tap(find.byKey(const Key('midi-learn-button'))); + await tester.tap(_findMidiLearnButton); await tester.pumpAndSettle(); } @@ -108,22 +114,18 @@ class HomePageObject { } Future waitUntilConnected({ - Duration timeout = const Duration(seconds: 10), + Duration timeout = const Duration(seconds: 30), }) async { - var keepGoing = true; - final timer = Timer(timeout, () { + final success = await pumpWaitingFor( + tester: tester, + predicate: () => isConnected, + timeout: timeout, + ); + + if (!success) { _log.warning('Connection to provisioned access point timed out'); - keepGoing = false; - }); - while (keepGoing) { - if (isConnected) { - timer.cancel(); - return; - } - await tester.pump(const Duration(seconds: 1)); + throw TimeoutException('Connection timed out'); } - - throw TimeoutException('Connection timed out'); } Future createPreset(String name) async { @@ -133,6 +135,12 @@ class HomePageObject { final createPresetPage = CreatePresetPageObject(tester); await createPresetPage.submitName(name); } + + Future openWifiProvisioningPage() async { + await tester.tap(find.byKey(const Key('wifi provisioning button'))); + await tester.pumpAndSettle(); + return WifiProvisioningPageObject(tester); + } } class CreatePresetPageObject { diff --git a/frontend/test/util.dart b/frontend/test/util.dart new file mode 100644 index 00000000..4e385fc6 --- /dev/null +++ b/frontend/test/util.dart @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Barabas Raffai + * + * This file is part of ShrapnelDSP. + * + * ShrapnelDSP is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * ShrapnelDSP is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * ShrapnelDSP. If not, see . + */ + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; + +Future pumpWaitingFor({ + required WidgetTester tester, + required bool Function() predicate, + required Duration timeout, +}) async { + var keepGoing = true; + final timer = Timer(timeout, () { + keepGoing = false; + }); + while (keepGoing) { + if (predicate()) { + timer.cancel(); + return true; + } + await tester.pump(const Duration(seconds: 1)); + } + + return false; +} diff --git a/frontend/test/wifi_provisioning_page_object.dart b/frontend/test/wifi_provisioning_page_object.dart new file mode 100644 index 00000000..82668fae --- /dev/null +++ b/frontend/test/wifi_provisioning_page_object.dart @@ -0,0 +1,60 @@ +/* + * Copyright 2022 Barabas Raffai + * + * This file is part of ShrapnelDSP. + * + * ShrapnelDSP is free software: you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free + * Software Foundation, either version 3 of the License, or (at your option) + * any later version. + * + * ShrapnelDSP is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * ShrapnelDSP. If not, see . + */ + +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class WifiProvisioningPageObject { + WifiProvisioningPageObject(this.tester); + + final WidgetTester tester; + + Finder get findSuccessPage => find.textContaining('success'); + + Finder get findScanCompletePage => + find.byKey(const Key('wifi scan complete page')); + + Future startProvisioning() async { + await tester.tap(find.byKey(const Key('wifi provisioning start'))); + await tester.pumpAndSettle(); + } + + Future openAdvancedSetup() async { + await tester.tap(find.textContaining('Advanced')); + await tester.pumpAndSettle(); + } + + Future enterSsid(String ssid) async { + final ssidField = find.byKey(const Key('ssid text field')); + await tester.enterText(ssidField, ssid); + } + + Future enterPassword(String password) async { + final passwordField = find.byKey(const Key('password text field')); + await tester.enterText(passwordField, password); + } + + Future submitPassword() async { + await tester.tap(find.byKey(const Key('password submit button'))); + } + + Future submitAdvanced() async { + await tester.tap(find.byKey(const Key('advanced submit button'))); + } +} diff --git a/frontend/test/wifi_provisioning_test.dart b/frontend/test/wifi_provisioning_test.dart index cb993b08..c13439ee 100644 --- a/frontend/test/wifi_provisioning_test.dart +++ b/frontend/test/wifi_provisioning_test.dart @@ -27,6 +27,7 @@ import 'package:shrapnel/main.dart'; import 'package:shrapnel/robust_websocket.dart'; import 'package:shrapnel/wifi_provisioning.dart'; +import 'home_page_object.dart'; import 'wifi_provisioning_test.mocks.dart'; Map _createFakeWifi({ @@ -85,8 +86,8 @@ void main() { (tester) async { await tester.pumpWidget(sut); - await tester.tap(find.byKey(const Key('wifi provisioning button'))); - await tester.pumpAndSettle(); + final homePage = HomePageObject(tester); + final provisioningPage = await homePage.openWifiProvisioningPage(); provisioningFactory = () { mockProvisioning = MockProvisioning(); @@ -113,8 +114,7 @@ void main() { ); }; - await tester.tap(find.byKey(const Key('wifi provisioning start'))); - await tester.pumpAndSettle(); + await provisioningPage.startProvisioning(); await tester.pump(const Duration(seconds: 1)); @@ -146,9 +146,8 @@ void main() { ), ); - final passwordField = find.byKey(const Key('password text field')); - await tester.enterText(passwordField, 'password'); - await tester.tap(find.byKey(const Key('password submit button'))); + await provisioningPage.enterPassword('password'); + await provisioningPage.submitPassword(); await tester.pump(const Duration(milliseconds: 500)); @@ -156,7 +155,7 @@ void main() { await tester.pump(const Duration(seconds: 1)); - expect(find.textContaining('success'), findsOneWidget); + expect(provisioningPage.findSuccessPage, findsOneWidget); }, ); @@ -164,8 +163,8 @@ void main() { (tester) async { await tester.pumpWidget(sut); - await tester.tap(find.byKey(const Key('wifi provisioning button'))); - await tester.pumpAndSettle(); + final homePage = HomePageObject(tester); + final provisioningPage = await homePage.openWifiProvisioningPage(); provisioningFactory = () { mockProvisioning = MockProvisioning(); @@ -173,8 +172,7 @@ void main() { .thenAnswer((_) => Future.value(false)); }; - await tester.tap(find.byKey(const Key('wifi provisioning start'))); - await tester.pumpAndSettle(); + await provisioningPage.startProvisioning(); expect(find.textContaining('failed'), findsOneWidget); }); @@ -183,8 +181,8 @@ void main() { (tester) async { await tester.pumpWidget(sut); - await tester.tap(find.byKey(const Key('wifi provisioning button'))); - await tester.pumpAndSettle(); + final homePage = HomePageObject(tester); + final provisioningPage = await homePage.openWifiProvisioningPage(); provisioningFactory = () { mockProvisioning = MockProvisioning(); @@ -211,8 +209,7 @@ void main() { ); }; - await tester.tap(find.byKey(const Key('wifi provisioning start'))); - await tester.pumpAndSettle(); + await provisioningPage.startProvisioning(); await tester.pump(const Duration(seconds: 1)); @@ -242,9 +239,8 @@ void main() { ), ); - final passwordField = find.byKey(const Key('password text field')); - await tester.enterText(passwordField, 'password'); - await tester.tap(find.byKey(const Key('password submit button'))); + await provisioningPage.enterPassword('password'); + await provisioningPage.submitPassword(); await tester.pump(const Duration(milliseconds: 500)); @@ -259,8 +255,8 @@ void main() { (tester) async { await tester.pumpWidget(sut); - await tester.tap(find.byKey(const Key('wifi provisioning button'))); - await tester.pumpAndSettle(); + final homePage = HomePageObject(tester); + final provisioningPage = await homePage.openWifiProvisioningPage(); provisioningFactory = () { mockProvisioning = MockProvisioning(); @@ -287,8 +283,7 @@ void main() { ); }; - await tester.tap(find.byKey(const Key('wifi provisioning start'))); - await tester.pumpAndSettle(); + await provisioningPage.startProvisioning(); await tester.pump(const Duration(seconds: 1)); @@ -318,9 +313,8 @@ void main() { ), ); - final passwordField = find.byKey(const Key('password text field')); - await tester.enterText(passwordField, 'password'); - await tester.tap(find.byKey(const Key('password submit button'))); + await provisioningPage.enterPassword('password'); + await provisioningPage.submitPassword(); await tester.pump(const Duration(milliseconds: 500)); @@ -335,8 +329,8 @@ void main() { (tester) async { await tester.pumpWidget(sut); - await tester.tap(find.byKey(const Key('wifi provisioning button'))); - await tester.pumpAndSettle(); + final homePage = HomePageObject(tester); + final provisioningPage = await homePage.openWifiProvisioningPage(); provisioningFactory = () { mockProvisioning = MockProvisioning(); @@ -363,8 +357,7 @@ void main() { ); }; - await tester.tap(find.byKey(const Key('wifi provisioning start'))); - await tester.pumpAndSettle(); + await provisioningPage.startProvisioning(); await tester.pump(const Duration(seconds: 1)); @@ -374,12 +367,9 @@ void main() { await tester.tap(ssidCard.first); await tester.pumpAndSettle(); - final passwordField = find.byKey(const Key('password text field')); - final submitButton = find.byKey(const Key('password submit button')); - Future submitPassword(String password) async { - await tester.enterText(passwordField, password); - await tester.tap(submitButton); + await provisioningPage.enterPassword(password); + await provisioningPage.submitPassword(); await tester.pump(); } @@ -400,8 +390,8 @@ void main() { (tester) async { await tester.pumpWidget(sut); - await tester.tap(find.byKey(const Key('wifi provisioning button'))); - await tester.pumpAndSettle(); + final homePage = HomePageObject(tester); + final provisioningPage = await homePage.openWifiProvisioningPage(); provisioningFactory = () { mockProvisioning = MockProvisioning(); @@ -428,16 +418,14 @@ void main() { ); }; - await tester.tap(find.byKey(const Key('wifi provisioning start'))); - await tester.pumpAndSettle(); + await provisioningPage.startProvisioning(); await tester.pump(const Duration(seconds: 1)); final ssidCard = find.textContaining('test SSID'); expect(ssidCard, findsOneWidget); - await tester.tap(find.textContaining('Advanced')); - await tester.pumpAndSettle(); + await provisioningPage.openAdvancedSetup(); when( mockProvisioning.sendWifiConfig( @@ -459,12 +447,10 @@ void main() { ), ); - final ssidField = find.byKey(const Key('ssid text field')); - await tester.enterText(ssidField, 'hidden SSID'); + await provisioningPage.enterSsid('hidden SSID'); - final passwordField = find.byKey(const Key('password text field')); - await tester.enterText(passwordField, 'password'); - await tester.tap(find.byKey(const Key('advanced submit button'))); + await provisioningPage.enterPassword('password'); + await provisioningPage.submitAdvanced(); await tester.pump(const Duration(milliseconds: 500)); @@ -472,6 +458,6 @@ void main() { await tester.pump(const Duration(seconds: 1)); - expect(find.textContaining('success'), findsOneWidget); + expect(provisioningPage.findSuccessPage, findsOneWidget); }); }