diff --git a/lib/main.dart b/lib/main.dart index 6c1cec1..164ce51 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,7 +20,6 @@ import 'package:elastic_dashboard/services/log.dart'; import 'package:elastic_dashboard/services/nt_connection.dart'; import 'package:elastic_dashboard/services/nt_widget_builder.dart'; import 'package:elastic_dashboard/services/settings.dart'; -import 'package:elastic_dashboard/services/update_checker.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -217,7 +216,6 @@ class _ElasticState extends State { ntConnection: widget.ntConnection, preferences: widget.preferences, version: widget.version, - updateChecker: UpdateChecker(currentVersion: widget.version), onColorChanged: (color) => setState(() { teamColor = color; widget.preferences.setInt(PrefKeys.teamColor, color.value); diff --git a/lib/pages/dashboard_page.dart b/lib/pages/dashboard_page.dart index f8855b4..de1612c 100644 --- a/lib/pages/dashboard_page.dart +++ b/lib/pages/dashboard_page.dart @@ -44,11 +44,46 @@ import 'package:elastic_dashboard/widgets/settings_dialog.dart'; import 'package:elastic_dashboard/widgets/tab_grid.dart'; import '../widgets/draggable_containers/models/layout_container_model.dart'; +enum LayoutDownloadMode { + overwrite( + name: 'Overwrite', + description: + 'Keeps existing tabs that are not defined in the remote layout. Any tabs that are defined in the remote layout will be overwritten locally.', + ), + merge( + name: 'Merge', + description: + 'Merge the downloaded layout with the existing one. If a new widget cannot be properly placed, it will not be added.', + ), + reload( + name: 'Full Reload', + description: 'Deletes the existing layout and loads the new one.', + ); + + final String name; + final String description; + + const LayoutDownloadMode({required this.name, required this.description}); + + static String get descriptions { + String result = ''; + for (final value in values) { + result += '${value.name}: '; + result += value.description; + + if (value != values.last) { + result += '\n\n'; + } + } + return result; + } +} + class DashboardPage extends StatefulWidget { final String version; final NTConnection ntConnection; final SharedPreferences preferences; - final UpdateChecker updateChecker; + final UpdateChecker? updateChecker; final ElasticLayoutDownloader? layoutDownloader; final Function(Color color)? onColorChanged; final Function(FlexSchemeVariant variant)? onThemeVariantChanged; @@ -58,7 +93,7 @@ class DashboardPage extends StatefulWidget { required this.ntConnection, required this.preferences, required this.version, - required this.updateChecker, + this.updateChecker, this.layoutDownloader, this.onColorChanged, this.onThemeVariantChanged, @@ -71,6 +106,7 @@ class DashboardPage extends StatefulWidget { class _DashboardPageState extends State with WindowListener { SharedPreferences get preferences => widget.preferences; late final ElasticLibListener _robotNotificationListener; + late final UpdateChecker _updateChecker; late final ElasticLayoutDownloader _layoutDownloader; bool _seenShuffleboardWarning = false; @@ -246,11 +282,6 @@ class _DashboardPageState extends State with WindowListener { apiListener.initializeListeners(); }); - if (!isWPILib) { - Future( - () => _checkForUpdates(notifyIfLatest: false, notifyIfError: false)); - } - _robotNotificationListener = ElasticLibListener( ntConnection: widget.ntConnection, onTabSelected: (tabIdentifier) { @@ -301,6 +332,14 @@ class _DashboardPageState extends State with WindowListener { _layoutDownloader = widget.layoutDownloader ?? ElasticLayoutDownloader(Client()); + + _updateChecker = + widget.updateChecker ?? UpdateChecker(currentVersion: widget.version); + + if (!isWPILib) { + Future( + () => _checkForUpdates(notifyIfLatest: false, notifyIfError: false)); + } } @override @@ -397,7 +436,7 @@ class _DashboardPageState extends State with WindowListener { ButtonThemeData buttonTheme = ButtonTheme.of(context); UpdateCheckerResponse updateResponse = - await widget.updateChecker.isUpdateAvailable(); + await _updateChecker.isUpdateAvailable(); if (mounted) { setState(() => lastUpdateResponse = updateResponse); @@ -607,13 +646,20 @@ class _DashboardPageState extends State with WindowListener { return true; } - void _loadLayoutFromJsonData(String jsonString) { + void _clearLayout() { + for (TabData tab in _tabData) { + tab.tabGrid.onDestroy(); + } + _tabData.clear(); + } + + bool _loadLayoutFromJsonData(String jsonString) { logger.info('Loading layout from json'); Map? jsonData = tryCast(jsonDecode(jsonString)); if (!_validateJsonData(jsonData)) { _createDefaultTabs(); - return; + return false; } if (jsonData!.containsKey('grid_size')) { @@ -621,7 +667,7 @@ class _DashboardPageState extends State with WindowListener { preferences.setInt(PrefKeys.gridSize, _gridSize); } - _tabData.clear(); + _clearLayout(); for (Map data in jsonData['tabs']) { _tabData.add( @@ -643,6 +689,8 @@ class _DashboardPageState extends State with WindowListener { if (_currentTabIndex >= _tabData.length) { _currentTabIndex = _tabData.length - 1; } + + return true; } bool _mergeLayoutFromJsonData(String jsonString) { @@ -690,28 +738,120 @@ class _DashboardPageState extends State with WindowListener { return true; } - Future _showRemoteLayoutSelection(List fileNames) async { + void _overwriteLayoutFromJsonData(String jsonString) { + logger.info('Overwriting layout from json'); + + Map? jsonData = tryCast(jsonDecode(jsonString)); + + if (!_validateJsonData(jsonData)) { + return; + } + + int overwritten = 0; + for (Map tabJson in jsonData!['tabs']) { + String tabName = tabJson['name']; + if (!_tabData.any((tab) => tab.name == tabName)) { + _tabData.add( + TabData( + name: tabName, + tabGrid: TabGridModel.fromJson( + ntConnection: widget.ntConnection, + preferences: widget.preferences, + jsonData: tabJson['grid_layout'], + onAddWidgetPressed: _displayAddWidgetDialog, + onJsonLoadingWarning: _showJsonLoadingWarning, + ), + ), + ); + } else { + overwritten++; + TabGridModel existingTab = + _tabData.firstWhere((tab) => tab.name == tabName).tabGrid; + existingTab.onDestroy(); + existingTab.loadFromJson( + jsonData: tabJson['grid_layout'], + onJsonLoadingWarning: _showJsonLoadingWarning, + ); + } + } + + _showInfoNotification( + title: 'Successfully Downloaded Layout', + message: + 'Remote layout has been successfully downloaded, $overwritten tabs were overwritten.', + width: 350, + ); + } + + Future<({String layout, LayoutDownloadMode mode})?> + _showRemoteLayoutSelection(List fileNames) async { if (!mounted) { return null; } - ValueNotifier currentSelection = ValueNotifier(null); - return await showDialog( + ValueNotifier layoutSelection = ValueNotifier(null); + ValueNotifier modeSelection = + ValueNotifier(LayoutDownloadMode.overwrite); + + bool showModes = false; + return await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Select Layout'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ValueListenableBuilder( - valueListenable: currentSelection, - builder: (_, value, child) => DialogDropdownChooser( - choices: fileNames, - initialValue: value, - onSelectionChanged: (selection) => - currentSelection.value = selection, - ), - ) - ], + content: SizedBox( + width: 350, + child: StatefulBuilder( + builder: (context, setState) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Layout File'), + ValueListenableBuilder( + valueListenable: layoutSelection, + builder: (_, value, child) => DialogDropdownChooser( + choices: fileNames, + initialValue: value, + onSelectionChanged: (selection) => + layoutSelection.value = selection, + ), + ), + const SizedBox(height: 20), + const Text('Download Mode'), + Row( + children: [ + Flexible( + child: ValueListenableBuilder( + valueListenable: modeSelection, + builder: (_, value, child) => + DialogDropdownChooser( + choices: LayoutDownloadMode.values, + initialValue: value, + nameMap: (value) => value.name, + onSelectionChanged: (selection) { + if (selection != null) { + modeSelection.value = selection; + } + }, + ), + ), + ), + const SizedBox(width: 5), + TextButton.icon( + label: const Text('Help'), + icon: const Icon(Icons.help_outline), + onPressed: () { + setState(() => showModes = !showModes); + }, + ), + ], + ), + if (showModes) ...[ + const SizedBox(height: 5), + Text(LayoutDownloadMode.descriptions), + ], + ], + ); + }, + ), ), actions: [ TextButton( @@ -719,10 +859,11 @@ class _DashboardPageState extends State with WindowListener { child: const Text('Cancel'), ), ValueListenableBuilder( - valueListenable: currentSelection, + valueListenable: layoutSelection, builder: (_, value, child) => TextButton( onPressed: (value != null) - ? () => Navigator.of(context).pop(value) + ? () => Navigator.of(context) + .pop((layout: value, mode: modeSelection.value)) : null, child: const Text('Download'), ), @@ -763,7 +904,7 @@ class _DashboardPageState extends State with WindowListener { return; } - String? selectedLayout = await _showRemoteLayoutSelection( + final selectedLayout = await _showRemoteLayoutSelection( layoutsResponse.data.sorted((a, b) => a.compareTo(b)), ); @@ -774,7 +915,7 @@ class _DashboardPageState extends State with WindowListener { LayoutDownloadResponse response = await _layoutDownloader.downloadLayout( ntConnection: widget.ntConnection, preferences: preferences, - layoutName: selectedLayout, + layoutName: selectedLayout.layout, ); if (!response.successful) { @@ -786,7 +927,23 @@ class _DashboardPageState extends State with WindowListener { return; } - _mergeLayoutFromJsonData(response.data); + switch (selectedLayout.mode) { + case LayoutDownloadMode.merge: + _mergeLayoutFromJsonData(response.data); + case LayoutDownloadMode.overwrite: + setState(() => _overwriteLayoutFromJsonData(response.data)); + case LayoutDownloadMode.reload: + setState(() { + bool success = _loadLayoutFromJsonData(response.data); + if (success) { + _showInfoNotification( + title: 'Successfully Downloaded Layout', + message: 'Remote layout has been successfully downloaded!', + width: 350, + ); + } + }); + } } void _createDefaultTabs() { diff --git a/lib/widgets/dialog_widgets/dialog_dropdown_chooser.dart b/lib/widgets/dialog_widgets/dialog_dropdown_chooser.dart index cc9d134..2d637d4 100644 --- a/lib/widgets/dialog_widgets/dialog_dropdown_chooser.dart +++ b/lib/widgets/dialog_widgets/dialog_dropdown_chooser.dart @@ -3,13 +3,16 @@ import 'package:flutter/material.dart'; class DialogDropdownChooser extends StatefulWidget { final List? choices; final T? initialValue; - final Function(T?) onSelectionChanged; + final void Function(T?) onSelectionChanged; + final String Function(T value)? nameMap; - const DialogDropdownChooser( - {super.key, - this.choices, - this.initialValue, - required this.onSelectionChanged}); + const DialogDropdownChooser({ + super.key, + this.choices, + this.initialValue, + required this.onSelectionChanged, + this.nameMap, + }); @override State> createState() => @@ -47,7 +50,7 @@ class _DialogDropdownChooserState extends State> { items: widget.choices?.map((T item) { return DropdownMenuItem( value: item, - child: Text(item.toString()), + child: Text(widget.nameMap?.call(item) ?? item.toString()), ); }).toList(), value: selectedValue, diff --git a/lib/widgets/tab_grid.dart b/lib/widgets/tab_grid.dart index 5fb1991..8b58f19 100644 --- a/lib/widgets/tab_grid.dart +++ b/lib/widgets/tab_grid.dart @@ -44,7 +44,17 @@ class TabGridModel extends ChangeNotifier { required this.preferences, required Map jsonData, required this.onAddWidgetPressed, - Function(String message)? onJsonLoadingWarning, + void Function(String message)? onJsonLoadingWarning, + }) { + loadFromJson( + jsonData: jsonData, + onJsonLoadingWarning: onJsonLoadingWarning, + ); + } + + void loadFromJson({ + required Map jsonData, + void Function(String message)? onJsonLoadingWarning, }) { if (jsonData['containers'] != null) { loadContainersFromJson( @@ -56,15 +66,11 @@ class TabGridModel extends ChangeNotifier { if (jsonData['layouts'] != null) { loadLayoutsFromJson(jsonData, onJsonLoadingWarning: onJsonLoadingWarning); } - - for (WidgetContainerModel model in _widgetModels) { - model.addListener(notifyListeners); - } } void mergeFromJson({ required Map jsonData, - Function(String message)? onJsonLoadingWarning, + void Function(String message)? onJsonLoadingWarning, }) { if (jsonData['containers'] != null) { for (Map widgetData in jsonData['containers']) { @@ -171,7 +177,7 @@ class TabGridModel extends ChangeNotifier { void loadContainersFromJson(Map jsonData, {Function(String message)? onJsonLoadingWarning}) { for (Map containerData in jsonData['containers']) { - _widgetModels.add( + addWidget( NTWidgetContainerModel.fromJson( ntConnection: ntConnection, preferences: preferences, @@ -221,7 +227,7 @@ class TabGridModel extends ChangeNotifier { continue; } - _widgetModels.add(widget); + addWidget(widget); } } diff --git a/test/pages/dashboard_page_test.dart b/test/pages/dashboard_page_test.dart index 013b2f7..2476765 100644 --- a/test/pages/dashboard_page_test.dart +++ b/test/pages/dashboard_page_test.dart @@ -18,7 +18,9 @@ import 'package:elastic_dashboard/services/field_images.dart'; import 'package:elastic_dashboard/services/hotkey_manager.dart'; import 'package:elastic_dashboard/services/ip_address_util.dart'; import 'package:elastic_dashboard/services/nt4_client.dart'; +import 'package:elastic_dashboard/services/nt_connection.dart'; import 'package:elastic_dashboard/services/settings.dart'; +import 'package:elastic_dashboard/services/update_checker.dart'; import 'package:elastic_dashboard/widgets/custom_appbar.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_dropdown_chooser.dart'; import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_text_input.dart'; @@ -37,6 +39,30 @@ import '../services/elastic_layout_downloader_test.dart'; import '../test_util.dart'; import '../test_util.mocks.dart'; +Future pumpDashboardPage( + WidgetTester widgetTester, + SharedPreferences preferences, { + NTConnection? ntConnection, + ElasticLayoutDownloader? layoutDownloader, + UpdateChecker? updateChecker, +}) async { + FlutterError.onError = ignoreOverflowErrors; + + await widgetTester.pumpWidget( + MaterialApp( + home: DashboardPage( + ntConnection: ntConnection ?? createMockOfflineNT4(), + preferences: preferences, + version: '0.0.0.0', + updateChecker: updateChecker ?? createMockUpdateChecker(), + layoutDownloader: layoutDownloader, + ), + ), + ); + + await widgetTester.pumpAndSettle(); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -69,20 +95,7 @@ void main() { group('[Loading and Saving]:', () { testWidgets('offline loading', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect( find.textContaining('Network Tables: Disconnected'), findsOneWidget); @@ -95,21 +108,12 @@ void main() { }); testWidgets('online loading', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), ); - await widgetTester.pumpAndSettle(); - expect(find.textContaining('Network Tables: Disconnected'), findsNothing); expect(find.textContaining('Network Tables: Connected'), findsWidgets); expect(find.textContaining('(10.3.53.2)'), findsWidgets); @@ -120,20 +124,7 @@ void main() { }); testWidgets('Save layout (button)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final fileButton = find.widgetWithText(SubmenuButton, 'File'); @@ -153,20 +144,7 @@ void main() { }); testWidgets('Save layout (shortcut)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control); @@ -180,21 +158,12 @@ void main() { group('[Adding Widgets]:', () { testWidgets('Add widget dialog search', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), ); - await widgetTester.pumpAndSettle(); - final addWidget = find.widgetWithText(MenuItemButton, 'Add Widget'); expect(addWidget, findsOneWidget); @@ -279,21 +248,12 @@ void main() { }); testWidgets('Add widget dialog (widgets)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), ); - await widgetTester.pumpAndSettle(); - final addWidget = find.widgetWithText(MenuItemButton, 'Add Widget'); expect(addWidget, findsOneWidget); @@ -345,21 +305,12 @@ void main() { }); testWidgets('Add widget dialog (layouts)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), ); - await widgetTester.pumpAndSettle(); - final addWidget = find.widgetWithText(MenuItemButton, 'Add Widget'); expect(addWidget, findsOneWidget); @@ -403,39 +354,30 @@ void main() { testWidgets('Add widget dialog (list layout sub-table)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4( - virtualTopics: [ - NT4Topic( - name: '/Non-Typed/Value 1', - type: NT4TypeStr.kInt, - properties: {}, - ), - NT4Topic( - name: '/Non-Typed/Value 2', - type: NT4TypeStr.kInt, - properties: {}, - ), - NT4Topic( - name: '/Non-Typed/Value 3', - type: NT4TypeStr.kInt, - properties: {}, - ), - ], + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4( + virtualTopics: [ + NT4Topic( + name: '/Non-Typed/Value 1', + type: NT4TypeStr.kInt, + properties: {}, ), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), + NT4Topic( + name: '/Non-Typed/Value 2', + type: NT4TypeStr.kInt, + properties: {}, + ), + NT4Topic( + name: '/Non-Typed/Value 3', + type: NT4TypeStr.kInt, + properties: {}, + ), + ], ), ); - await widgetTester.pumpAndSettle(); - final addWidget = find.widgetWithText(MenuItemButton, 'Add Widget'); expect(addWidget, findsOneWidget); @@ -471,47 +413,38 @@ void main() { testWidgets('Add widget dialog (unregistered sendable)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4( - virtualTopics: [ - NT4Topic( - name: '/Non-Registered/.type', - type: NT4TypeStr.kString, - properties: {}, - ), - NT4Topic( - name: '/Non-Registered/Value 1', - type: NT4TypeStr.kInt, - properties: {}, - ), - NT4Topic( - name: '/Non-Registered/Value 2', - type: NT4TypeStr.kInt, - properties: {}, - ), - NT4Topic( - name: '/Non-Registered/Value 3', - type: NT4TypeStr.kInt, - properties: {}, - ), - ], - virtualValues: { - '/Non-Registered/.type': 'Non Registered Type', - }, + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4( + virtualTopics: [ + NT4Topic( + name: '/Non-Registered/.type', + type: NT4TypeStr.kString, + properties: {}, ), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), + NT4Topic( + name: '/Non-Registered/Value 1', + type: NT4TypeStr.kInt, + properties: {}, + ), + NT4Topic( + name: '/Non-Registered/Value 2', + type: NT4TypeStr.kInt, + properties: {}, + ), + NT4Topic( + name: '/Non-Registered/Value 3', + type: NT4TypeStr.kInt, + properties: {}, + ), + ], + virtualValues: { + '/Non-Registered/.type': 'Non Registered Type', + }, ), ); - await widgetTester.pumpAndSettle(); - final addWidget = find.widgetWithText(MenuItemButton, 'Add Widget'); expect(addWidget, findsOneWidget); @@ -546,21 +479,12 @@ void main() { }); testWidgets('List Layouts', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), ); - await widgetTester.pumpAndSettle(); - final addWidget = find.widgetWithText(MenuItemButton, 'Add Widget'); expect(addWidget, findsOneWidget); @@ -640,8 +564,6 @@ void main() { group('Shuffleboard API', () { testWidgets('adding widgets', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - List fakeAnnounceCallbacks = []; // A custom mock is set up to reproduce behavior when actually running @@ -700,17 +622,11 @@ void main() { '/Shuffleboard/Test-Tab/Shuffleboard Test Layout/.type')) .thenAnswer((realInvocation) => Future.value('ShuffleboardLayout')); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: mockNT4Connection, - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: mockNT4Connection, ); - await widgetTester.pumpAndSettle(); await widgetTester.runAsync(() async { for (final callback in fakeAnnounceCallbacks) { @@ -779,9 +695,7 @@ void main() { }); testWidgets('switching tabs', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - MockNTConnection ntConnection = createMockOnlineNT4( + NTConnection ntConnection = createMockOnlineNT4( virtualTopics: [ NT4Topic( name: '/Shuffleboard/.metadata/Selected', @@ -791,19 +705,12 @@ void main() { ], ); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: ntConnection, - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: ntConnection, ); - await widgetTester.pumpAndSettle(); - final editableTabBar = find.byType(EditableTabBar); expect(editableTabBar, findsOneWidget); @@ -848,8 +755,6 @@ void main() { }); testWidgets('Shows list of layouts', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - Client mockClient = createHttpClient( mockGetResponses: { 'http://127.0.0.1:5800/?format=json': @@ -860,20 +765,13 @@ void main() { ElasticLayoutDownloader layoutDownloader = ElasticLayoutDownloader(mockClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, ); - await widgetTester.pumpAndSettle(); - expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); await widgetTester.pumpAndSettle(); @@ -893,9 +791,45 @@ void main() { }); group('Download layout', () { - testWidgets('without merges', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; + testWidgets('shows help text', (widgetTester) async { + Client mockClient = createHttpClient( + mockGetResponses: { + 'http://127.0.0.1:5800/?format=json': + Response(jsonEncode(layoutFiles), 200), + }, + ); + + ElasticLayoutDownloader layoutDownloader = + ElasticLayoutDownloader(mockClient); + + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, + ); + + expect(find.text('File'), findsOneWidget); + await widgetTester.tap(find.text('File')); + await widgetTester.pumpAndSettle(); + expect(find.text('Download From Robot'), findsOneWidget); + await widgetTester.tap(find.text('Download From Robot')); + await widgetTester.pumpAndSettle(); + + expect(find.textContaining('Keeps existing tabs'), findsNothing); + + final helpButton = find.byIcon(Icons.help_outline); + + expect(helpButton, findsOneWidget); + await widgetTester.ensureVisible(helpButton); + await widgetTester.tap(helpButton); + await widgetTester.pumpAndSettle(); + + expect(find.textContaining('Keeps existing tabs'), findsOneWidget); + }); + + testWidgets('overwrite mode', (widgetTester) async { Client mockClient = createHttpClient( mockGetResponses: { 'http://127.0.0.1:5800/?format=json': @@ -908,19 +842,44 @@ void main() { ElasticLayoutDownloader layoutDownloader = ElasticLayoutDownloader(mockClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), - ); + SharedPreferences.setMockInitialValues({ + PrefKeys.layout: jsonEncode({ + 'version': 1.0, + 'grid_size': 128.0, + 'tabs': [ + { + 'name': 'Test Tab', + 'grid_layout': { + 'layouts': [], + 'containers': [ + { + 'title': 'Blocking Widget', + 'x': 384.0, + 'y': 128.0, + 'width': 256.0, + 'height': 256.0, + 'type': 'Text Display', + 'properties': { + 'topic': '/Test Tab/Blocking Widget', + 'period': 0.06, + }, + } + ], + }, + }, + ], + }), + PrefKeys.ipAddress: '127.0.0.1', + }); - await widgetTester.pumpAndSettle(); + SharedPreferences preferences = await SharedPreferences.getInstance(); + + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, + ); expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); @@ -941,6 +900,17 @@ void main() { await widgetTester.tap(find.text('elastic-layout 1')); await widgetTester.pumpAndSettle(); + expect(find.text('Download Mode'), findsOneWidget); + expect(find.byType(DialogDropdownChooser), + findsOneWidget); + await widgetTester + .tap(find.byType(DialogDropdownChooser)); + await widgetTester.pumpAndSettle(); + + expect(find.text('Overwrite'), findsNWidgets(2)); + await widgetTester.tap(find.text('Overwrite').last); + await widgetTester.pumpAndSettle(); + expect(find.text('Download'), findsOneWidget); await widgetTester.tap(find.text('Download')); await widgetTester.pump(Duration.zero); @@ -949,6 +919,8 @@ void main() { find.widgetWithText( ElegantNotification, 'Successfully Downloaded Layout'), findsOneWidget); + expect( + find.textContaining('1 tabs were overwritten'), findsOneWidget); await widgetTester.pumpAndSettle(); @@ -956,12 +928,69 @@ void main() { await widgetTester.tap(find.text('Test Tab')); await widgetTester.pumpAndSettle(); + expect(find.text('Blocking Widget'), findsNothing); expect(find.byType(Gyro), findsNWidgets(2)); }); + group('merge mode', () { + testWidgets('without merges', (widgetTester) async { + Client mockClient = createHttpClient( + mockGetResponses: { + 'http://127.0.0.1:5800/?format=json': + Response(jsonEncode(layoutFiles), 200), + 'http://127.0.0.1:5800/${Uri.encodeComponent('elastic-layout 1.json')}': + Response(jsonEncode(layoutOne), 200), + }, + ); - testWidgets('with merges', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; + ElasticLayoutDownloader layoutDownloader = + ElasticLayoutDownloader(mockClient); + + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, + ); + + expect(find.text('File'), findsOneWidget); + await widgetTester.tap(find.text('File')); + await widgetTester.pumpAndSettle(); + + expect(find.text('Download From Robot'), findsOneWidget); + await widgetTester.tap(find.text('Download From Robot')); + await widgetTester.pumpAndSettle(); + + expect(find.text('Select Layout'), findsOneWidget); + expect(find.byType(DialogDropdownChooser), findsOneWidget); + + await widgetTester.tap(find.byType(DialogDropdownChooser)); + await widgetTester.pumpAndSettle(); + expect(find.text('elastic-layout 1'), findsOneWidget); + + await widgetTester.tap(find.text('elastic-layout 1')); + await widgetTester.pumpAndSettle(); + + expect(find.text('Download'), findsOneWidget); + await widgetTester.tap(find.text('Download')); + await widgetTester.pump(Duration.zero); + + expect( + find.widgetWithText( + ElegantNotification, 'Successfully Downloaded Layout'), + findsOneWidget); + + await widgetTester.pumpAndSettle(); + + expect(find.text('Test Tab'), findsOneWidget); + await widgetTester.tap(find.text('Test Tab')); + await widgetTester.pumpAndSettle(); + + expect(find.byType(Gyro), findsNWidgets(2)); + }); + }); + + testWidgets('with merges', (widgetTester) async { Client mockClient = createHttpClient( mockGetResponses: { 'http://127.0.0.1:5800/?format=json': @@ -1006,20 +1035,13 @@ void main() { SharedPreferences preferences = await SharedPreferences.getInstance(); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, ); - await widgetTester.pumpAndSettle(); - expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); await widgetTester.pumpAndSettle(); @@ -1039,6 +1061,17 @@ void main() { await widgetTester.tap(find.text('elastic-layout 1')); await widgetTester.pumpAndSettle(); + expect(find.text('Download Mode'), findsOneWidget); + expect(find.byType(DialogDropdownChooser), + findsOneWidget); + await widgetTester + .tap(find.byType(DialogDropdownChooser)); + await widgetTester.pumpAndSettle(); + + expect(find.text('Merge'), findsOneWidget); + await widgetTester.tap(find.text('Merge')); + await widgetTester.pumpAndSettle(); + expect(find.text('Download'), findsOneWidget); await widgetTester.tap(find.text('Download')); await widgetTester.pump(Duration.zero); @@ -1058,30 +1091,85 @@ void main() { expect(find.byType(Gyro), findsOneWidget); }); }); + testWidgets('reload mode', (widgetTester) async { + Client mockClient = createHttpClient( + mockGetResponses: { + 'http://127.0.0.1:5800/?format=json': + Response(jsonEncode(layoutFiles), 200), + 'http://127.0.0.1:5800/${Uri.encodeComponent('elastic-layout 1.json')}': + Response(jsonEncode(layoutOne), 200), + }, + ); + + ElasticLayoutDownloader layoutDownloader = + ElasticLayoutDownloader(mockClient); + + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, + ); + + expect(find.text('File'), findsOneWidget); + await widgetTester.tap(find.text('File')); + await widgetTester.pumpAndSettle(); + + expect(find.text('Download From Robot'), findsOneWidget); + await widgetTester.tap(find.text('Download From Robot')); + await widgetTester.pumpAndSettle(); + + expect(find.text('Select Layout'), findsOneWidget); + expect(find.byType(DialogDropdownChooser), findsOneWidget); + + await widgetTester.tap(find.byType(DialogDropdownChooser)); + await widgetTester.pumpAndSettle(); + + expect(find.text('elastic-layout 1'), findsOneWidget); + + await widgetTester.tap(find.text('elastic-layout 1')); + await widgetTester.pumpAndSettle(); + + expect(find.text('Download Mode'), findsOneWidget); + expect(find.byType(DialogDropdownChooser), + findsOneWidget); + await widgetTester + .tap(find.byType(DialogDropdownChooser)); + await widgetTester.pumpAndSettle(); + + expect(find.text('Full Reload'), findsOneWidget); + await widgetTester.tap(find.text('Full Reload')); + await widgetTester.pumpAndSettle(); + + expect(find.text('Download'), findsOneWidget); + await widgetTester.tap(find.text('Download')); + await widgetTester.pump(Duration.zero); + + expect( + find.widgetWithText( + ElegantNotification, 'Successfully Downloaded Layout'), + findsOneWidget); + + await widgetTester.pumpAndSettle(); + + expect(find.text('Teleoperated'), findsNothing); + expect(find.text('Autonomous'), findsNothing); + expect(find.text('Test Tab'), findsOneWidget); + }); group('Shows error when', () { testWidgets('network tables is disconnected', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - Client mockClient = createHttpClient(); ElasticLayoutDownloader layoutDownloader = ElasticLayoutDownloader(mockClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + layoutDownloader: layoutDownloader, ); - await widgetTester.pumpAndSettle(); - expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); await widgetTester.pumpAndSettle(); @@ -1100,8 +1188,6 @@ void main() { }); testWidgets('layout fetching is not a json', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - Client mockClient = createHttpClient( mockGetResponses: { 'http://127.0.0.1:5800/?format=json': Response('[1, 2, 3]', 200), @@ -1111,20 +1197,13 @@ void main() { ElasticLayoutDownloader layoutDownloader = ElasticLayoutDownloader(mockClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, ); - await widgetTester.pumpAndSettle(); - expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); await widgetTester.pumpAndSettle(); @@ -1143,8 +1222,6 @@ void main() { }); testWidgets('layout json does not list files', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - Client mockClient = createHttpClient( mockGetResponses: { 'http://127.0.0.1:5800/?format=json': Response('{}', 200), @@ -1154,20 +1231,13 @@ void main() { ElasticLayoutDownloader layoutDownloader = ElasticLayoutDownloader(mockClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, ); - await widgetTester.pumpAndSettle(); - expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); await widgetTester.pumpAndSettle(); @@ -1186,8 +1256,6 @@ void main() { }); testWidgets('layout json has empty files list', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - Client mockClient = createHttpClient( mockGetResponses: { 'http://127.0.0.1:5800/?format=json': @@ -1198,20 +1266,13 @@ void main() { ElasticLayoutDownloader layoutDownloader = ElasticLayoutDownloader(mockClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, ); - await widgetTester.pumpAndSettle(); - expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); await widgetTester.pumpAndSettle(); @@ -1230,8 +1291,6 @@ void main() { }); testWidgets('selected file was not found', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - Client mockClient = createHttpClient( mockGetResponses: { 'http://127.0.0.1:5800/?format=json': @@ -1244,20 +1303,13 @@ void main() { ElasticLayoutDownloader layoutDownloader = ElasticLayoutDownloader(mockClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOnlineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - layoutDownloader: layoutDownloader, - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: createMockOnlineNT4(), + layoutDownloader: layoutDownloader, ); - await widgetTester.pumpAndSettle(); - expect(find.text('File'), findsOneWidget); await widgetTester.tap(find.text('File')); await widgetTester.pumpAndSettle(); @@ -1293,8 +1345,6 @@ void main() { }); group('[Tab Selection]:', () { testWidgets('Passing in tab indexes', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - MockNTConnection ntConnection = createMockOnlineNT4( virtualTopics: [ NT4Topic( @@ -1305,19 +1355,12 @@ void main() { ], ); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: ntConnection, - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: ntConnection, ); - await widgetTester.pumpAndSettle(); - final editableTabBar = find.byType(EditableTabBar); expect(editableTabBar, findsOneWidget); @@ -1341,8 +1384,6 @@ void main() { }); testWidgets('Passing in tab names', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - MockNTConnection ntConnection = createMockOnlineNT4( virtualTopics: [ NT4Topic( @@ -1353,19 +1394,12 @@ void main() { ], ); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: ntConnection, - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: ntConnection, ); - await widgetTester.pumpAndSettle(); - final editableTabBar = find.byType(EditableTabBar); expect(editableTabBar, findsOneWidget); @@ -1393,20 +1427,7 @@ void main() { }); testWidgets('About dialog', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final helpButton = find.widgetWithText(SubmenuButton, 'Help'); @@ -1427,20 +1448,7 @@ void main() { group('[Tab Manipulation]:', () { testWidgets('Changing tabs', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(ComboBoxChooser), findsNothing); @@ -1458,20 +1466,7 @@ void main() { }); testWidgets('Creating new tab', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1489,20 +1484,7 @@ void main() { }); testWidgets('Creating new tab (shortcut)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1516,20 +1498,7 @@ void main() { }); testWidgets('Closing tab', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1561,20 +1530,7 @@ void main() { }); testWidgets('Closing tab (shortcut)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1599,20 +1555,7 @@ void main() { }); testWidgets('Reordering tabs', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1658,20 +1601,7 @@ void main() { }); testWidgets('Reordering tabs (shortcut)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1715,20 +1645,7 @@ void main() { }); testWidgets('Navigate tabs left right (shortcut)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1776,20 +1693,7 @@ void main() { }); testWidgets('Navigate to specific tabs', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); expect(find.byType(TabGrid, skipOffstage: false), findsNWidgets(2)); @@ -1827,20 +1731,7 @@ void main() { }); testWidgets('Renaming tab', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final teleopTab = find.widgetWithText(AnimatedContainer, 'Teleoperated'); @@ -1880,20 +1771,7 @@ void main() { }); testWidgets('Duplicating tab', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final teleopTab = find.widgetWithText(AnimatedContainer, 'Teleoperated'); @@ -1915,20 +1793,7 @@ void main() { group('[Window Manipulation]:', () { testWidgets('Minimizing window', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final minimizeButton = find.ancestor( of: find.byType(DecoratedMinimizeButton), @@ -1940,20 +1805,7 @@ void main() { }); testWidgets('Maximizing/unmaximizing window', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final appBar = find.byType(CustomAppBar); @@ -1973,20 +1825,7 @@ void main() { }); testWidgets('Closing window (All changes saved)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final gyroWidget = find.widgetWithText(WidgetContainer, 'Test Gyro'); @@ -2011,20 +1850,7 @@ void main() { }); testWidgets('Closing window (Unsaved changes)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final gyroWidget = find.widgetWithText(WidgetContainer, 'Test Gyro'); @@ -2058,20 +1884,7 @@ void main() { group('[Misc Shortcuts]:', () { testWidgets('Opening settings', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); final settingsButton = find.widgetWithIcon(MenuItemButton, Icons.settings); @@ -2089,20 +1902,7 @@ void main() { }); testWidgets('Opening settings (shortcut)', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), - ); - - await widgetTester.pumpAndSettle(); + await pumpDashboardPage(widgetTester, preferences); await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control); @@ -2117,8 +1917,6 @@ void main() { }); testWidgets('IP Address shortcuts', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - SharedPreferences.setMockInitialValues({ PrefKeys.ipAddressMode: IPAddressMode.custom.index, PrefKeys.ipAddress: '127.0.0.1', @@ -2130,19 +1928,12 @@ void main() { when(dsClient.lastAnnouncedIP).thenReturn(null); when(ntConnection.dsClient).thenReturn(dsClient); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: ntConnection, - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: ntConnection, ); - await widgetTester.pumpAndSettle(); - await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.control); await widgetTester.sendKeyDownEvent(LogicalKeyboardKey.keyK); @@ -2209,7 +2000,6 @@ void main() { group('[Notifications]:', () { testWidgets('Robot Notifications', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; final Map data = { 'title': 'Robot Notification Title', 'description': 'Robot Notification Description', @@ -2258,19 +2048,14 @@ void main() { final notificationWidget = find.widgetWithText(ElegantNotification, data['title']); - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: connection, - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker(), - ), - ), + await pumpDashboardPage( + widgetTester, + preferences, + ntConnection: connection, ); + expect(notificationWidget, findsNothing); - await widgetTester.pumpAndSettle(); connection .subscribeAll('/Elastic/RobotNotifications', 0.2) .updateValue(jsonEncode(data), 1); @@ -2289,22 +2074,15 @@ void main() { }); testWidgets('Update Notification', (widgetTester) async { - FlutterError.onError = ignoreOverflowErrors; - - await widgetTester.pumpWidget( - MaterialApp( - home: DashboardPage( - ntConnection: createMockOfflineNT4(), - preferences: preferences, - version: '0.0.0.0', - updateChecker: createMockUpdateChecker( - updateAvailable: true, latestVersion: '2025.0.1'), - ), + await pumpDashboardPage( + widgetTester, + preferences, + updateChecker: createMockUpdateChecker( + updateAvailable: true, + latestVersion: '2025.0.1', ), ); - await widgetTester.pumpAndSettle(); - final notificationWidget = find.widgetWithText( ElegantNotification, 'Version 2025.0.1 Available'); final notificationIcon = find.byIcon(Icons.update);