mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-12 00:40:28 +00:00
feat: Enhance device configuration and location tracking features
- Refetch device info after updating settings in device_config_screen.dart. - Update map_tab.dart to use singleton instance of LocationTrackingService and streamline location tracking callbacks. - Modify ble_response_handler.dart to handle contact not found errors and improve error callback structure. - Enhance location_tracking_service.dart with retry logic for GPS position acquisition and initial position setting without broadcasting. - Update meshcore_ble_service.dart to track last contact for auto-recovery on errors. - Improve tile_cache_service.dart error messages and streamline tile download logic. - Add current GPS location insertion feature in direct_message_sheet.dart with permission checks. - Update pubspec.lock and pubspec.yaml to include integration_test dependency. - Add screenshot automation script for iOS and Android devices. - Create integration test driver for screenshot capturing.
This commit is contained in:
217
integration_test/app_screenshots_test.dart
Normal file
217
integration_test/app_screenshots_test.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'package:meshcore_sar_app/main.dart';
|
||||
import 'package:meshcore_sar_app/providers/connection_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/contacts_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/drawing_provider.dart';
|
||||
import 'package:meshcore_sar_app/providers/map_provider.dart';
|
||||
|
||||
import 'helpers/screenshot_helper.dart';
|
||||
import 'helpers/mock_data.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('App Screenshots', () {
|
||||
late ScreenshotHelper screenshotHelper;
|
||||
|
||||
testWidgets('Capture all app screens with mock data', (tester) async {
|
||||
// Initialize screenshot helper
|
||||
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
screenshotHelper = ScreenshotHelper(binding);
|
||||
|
||||
// Pump the app
|
||||
await tester.pumpWidget(const MeshCoreSarApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// ===================================================================
|
||||
// 1. DISCONNECTED STATE - Home Screen
|
||||
// ===================================================================
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'home_disconnected',
|
||||
wait: const Duration(seconds: 1),
|
||||
);
|
||||
|
||||
// ===================================================================
|
||||
// 2. SIMULATED CONNECTED STATE (inject mock data into providers)
|
||||
// ===================================================================
|
||||
// Note: Since we can't actually connect to a BLE device in tests,
|
||||
// we'll need to manually inject mock data into the providers.
|
||||
// This requires accessing the providers through the context.
|
||||
|
||||
final context = tester.element(find.byType(MaterialApp));
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
|
||||
// Inject mock contacts
|
||||
final mockContacts = MockData.getMockContacts();
|
||||
for (final contact in mockContacts) {
|
||||
contactsProvider.addOrUpdateContact(contact);
|
||||
}
|
||||
|
||||
// Inject mock messages
|
||||
final mockMessages = MockData.getMockMessages();
|
||||
for (final message in mockMessages) {
|
||||
messagesProvider.receiveMessage(message);
|
||||
}
|
||||
|
||||
// Inject mock SAR markers
|
||||
final mockSarMarkers = MockData.getMockSarMarkers();
|
||||
for (final marker in mockSarMarkers) {
|
||||
messagesProvider.addSarMarker(marker);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// ===================================================================
|
||||
// 3. MESSAGES TAB WITH DATA
|
||||
// ===================================================================
|
||||
// The app should default to Messages tab (index 0)
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'messages_list_with_sar_markers',
|
||||
);
|
||||
|
||||
// Try to find and tap a SAR marker to show detail
|
||||
final sarMarkerFinder = find.byWidgetPredicate(
|
||||
(widget) => widget.runtimeType.toString().contains('SarMarker'),
|
||||
);
|
||||
if (sarMarkerFinder.hasFound) {
|
||||
await tester.tapAndSettle(sarMarkerFinder.first);
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'messages_sar_marker_detail',
|
||||
);
|
||||
// Go back
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 4. CONTACTS TAB
|
||||
// ===================================================================
|
||||
// Find and tap the Contacts tab
|
||||
final contactsTab = find.text('Contacts');
|
||||
if (contactsTab.hasFound) {
|
||||
await tester.tapAndSettle(contactsTab);
|
||||
} else {
|
||||
// Try icon-based navigation
|
||||
final tabBar = find.byType(TabBar);
|
||||
if (tabBar.hasFound) {
|
||||
final contactsIcon = find.descendant(
|
||||
of: tabBar,
|
||||
matching: find.byIcon(Icons.contacts),
|
||||
);
|
||||
await tester.tapAndSettle(contactsIcon);
|
||||
}
|
||||
}
|
||||
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'contacts_list_with_teams',
|
||||
);
|
||||
|
||||
// Try to find and tap a contact to show detail
|
||||
final contactListTile = find.byType(ListTile).first;
|
||||
if (contactListTile.hasFound) {
|
||||
await tester.tapAndSettle(contactListTile);
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'contacts_detail_dialog',
|
||||
);
|
||||
// Close dialog (tap outside or back button)
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 5. MAP TAB
|
||||
// ===================================================================
|
||||
// Find and tap the Map tab
|
||||
final mapTab = find.text('Map');
|
||||
if (mapTab.hasFound) {
|
||||
await tester.tapAndSettle(mapTab);
|
||||
} else {
|
||||
// Try icon-based navigation
|
||||
final tabBar = find.byType(TabBar);
|
||||
if (tabBar.hasFound) {
|
||||
final mapIcon = find.descendant(
|
||||
of: tabBar,
|
||||
matching: find.byIcon(Icons.map),
|
||||
);
|
||||
await tester.tapAndSettle(mapIcon);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for map to load
|
||||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'map_with_markers_and_sar',
|
||||
);
|
||||
|
||||
// Try to find and open the map legend
|
||||
final legendFinder = find.byWidgetPredicate(
|
||||
(widget) => widget.runtimeType.toString().contains('Legend'),
|
||||
);
|
||||
if (legendFinder.hasFound) {
|
||||
await tester.tapAndSettle(legendFinder.first);
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'map_legend_expanded',
|
||||
);
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 6. SETTINGS SCREEN (via menu)
|
||||
// ===================================================================
|
||||
// Go back to Messages tab
|
||||
final messagesTab = find.text('Messages');
|
||||
if (messagesTab.hasFound) {
|
||||
await tester.tapAndSettle(messagesTab);
|
||||
}
|
||||
|
||||
// Find and tap the menu button
|
||||
final menuButton = find.byIcon(Icons.more_vert);
|
||||
if (menuButton.hasFound) {
|
||||
await tester.tapAndSettle(menuButton);
|
||||
|
||||
// Find and tap Settings in the popup menu
|
||||
final settingsMenuItem = find.text('Settings');
|
||||
if (settingsMenuItem.hasFound) {
|
||||
await tester.tapAndSettle(settingsMenuItem);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
await screenshotHelper.takeScreenshot(
|
||||
tester,
|
||||
'settings_screen',
|
||||
);
|
||||
|
||||
// Go back
|
||||
await tester.pageBack();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// 7. SIMULATED DEVICE CONNECTION DIALOG
|
||||
// ===================================================================
|
||||
// NOTE: This is challenging without actual BLE, but we can try to
|
||||
// trigger the connection dialog
|
||||
// For now, we'll skip this as it requires disconnecting first
|
||||
|
||||
// ===================================================================
|
||||
// SUMMARY
|
||||
// ===================================================================
|
||||
print('\n✅ Screenshot capture complete!');
|
||||
print('📸 Total screenshots taken: ${screenshotHelper.screenshotCount}');
|
||||
print('\nScreenshots are saved in the default integration test output directory.');
|
||||
print('To view them, check your flutter drive output folder.\n');
|
||||
});
|
||||
});
|
||||
}
|
||||
230
integration_test/helpers/mock_data.dart
Normal file
230
integration_test/helpers/mock_data.dart
Normal file
@@ -0,0 +1,230 @@
|
||||
import 'package:meshcore_sar_app/models/contact.dart';
|
||||
import 'package:meshcore_sar_app/models/message.dart';
|
||||
import 'package:meshcore_sar_app/models/sar_marker.dart';
|
||||
|
||||
/// Mock data for integration tests and screenshots
|
||||
class MockData {
|
||||
/// Generate mock contacts with predictable data
|
||||
static List<Contact> getMockContacts() {
|
||||
return [
|
||||
Contact(
|
||||
publicKey: '0x1111111111111111111111111111111111111111111111111111111111111111',
|
||||
name: 'Alpha Team Lead',
|
||||
contactType: ContactType.chat,
|
||||
lastAdvertisement: DateTime.now().subtract(const Duration(minutes: 2)),
|
||||
lastLocation: const ContactLocation(
|
||||
latitude: 46.0569,
|
||||
longitude: 14.5058,
|
||||
altitude: 295.0,
|
||||
),
|
||||
batteryMillivolts: 3850,
|
||||
hopCount: 1,
|
||||
rssi: -45,
|
||||
),
|
||||
Contact(
|
||||
publicKey: '0x2222222222222222222222222222222222222222222222222222222222222222',
|
||||
name: 'Bravo Scout',
|
||||
contactType: ContactType.chat,
|
||||
lastAdvertisement: DateTime.now().subtract(const Duration(minutes: 5)),
|
||||
lastLocation: const ContactLocation(
|
||||
latitude: 46.0589,
|
||||
longitude: 14.5078,
|
||||
altitude: 310.0,
|
||||
),
|
||||
batteryMillivolts: 3700,
|
||||
hopCount: 2,
|
||||
rssi: -68,
|
||||
),
|
||||
Contact(
|
||||
publicKey: '0x3333333333333333333333333333333333333333333333333333333333333333',
|
||||
name: 'Charlie Base',
|
||||
contactType: ContactType.chat,
|
||||
lastAdvertisement: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||
lastLocation: const ContactLocation(
|
||||
latitude: 46.0549,
|
||||
longitude: 14.5038,
|
||||
altitude: 285.0,
|
||||
),
|
||||
batteryMillivolts: 4100,
|
||||
hopCount: 0,
|
||||
rssi: -35,
|
||||
),
|
||||
Contact(
|
||||
publicKey: '0x4444444444444444444444444444444444444444444444444444444444444444',
|
||||
name: 'Delta Medic',
|
||||
contactType: ContactType.chat,
|
||||
lastAdvertisement: DateTime.now().subtract(const Duration(minutes: 8)),
|
||||
lastLocation: const ContactLocation(
|
||||
latitude: 46.0609,
|
||||
longitude: 14.5098,
|
||||
altitude: 320.0,
|
||||
),
|
||||
batteryMillivolts: 3600,
|
||||
hopCount: 3,
|
||||
rssi: -75,
|
||||
),
|
||||
Contact(
|
||||
publicKey: '0x5555555555555555555555555555555555555555555555555555555555555555',
|
||||
name: 'Mountain Repeater 1',
|
||||
contactType: ContactType.repeater,
|
||||
lastAdvertisement: DateTime.now().subtract(const Duration(minutes: 1)),
|
||||
lastLocation: const ContactLocation(
|
||||
latitude: 46.0650,
|
||||
longitude: 14.5150,
|
||||
altitude: 450.0,
|
||||
),
|
||||
batteryMillivolts: 4150,
|
||||
hopCount: 0,
|
||||
rssi: -40,
|
||||
),
|
||||
Contact(
|
||||
publicKey: '0x6666666666666666666666666666666666666666666666666666666666666666',
|
||||
name: 'SAR Command Room',
|
||||
contactType: ContactType.room,
|
||||
lastAdvertisement: DateTime.now().subtract(const Duration(minutes: 30)),
|
||||
hopCount: 1,
|
||||
rssi: -50,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Generate mock messages with SAR markers
|
||||
static List<Message> getMockMessages() {
|
||||
final now = DateTime.now();
|
||||
return [
|
||||
Message(
|
||||
id: 'msg1',
|
||||
sender: '0x1111111111111111111111111111111111111111111111111111111111111111',
|
||||
senderName: 'Alpha Team Lead',
|
||||
content: 'Team Alpha in position, beginning sweep of sector 3',
|
||||
timestamp: now.subtract(const Duration(minutes: 15)),
|
||||
isSent: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
Message(
|
||||
id: 'msg2',
|
||||
sender: '0x2222222222222222222222222222222222222222222222222222222222222222',
|
||||
senderName: 'Bravo Scout',
|
||||
content: 'S:🧑:46.0589,14.5078:Found injured hiker near trail marker 7',
|
||||
timestamp: now.subtract(const Duration(minutes: 12)),
|
||||
isSent: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
Message(
|
||||
id: 'msg3',
|
||||
sender: 'self',
|
||||
senderName: 'You',
|
||||
content: 'Copy that Bravo, sending medic to your location',
|
||||
timestamp: now.subtract(const Duration(minutes: 11)),
|
||||
isSent: true,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
Message(
|
||||
id: 'msg4',
|
||||
sender: '0x4444444444444444444444444444444444444444444444444444444444444444',
|
||||
senderName: 'Delta Medic',
|
||||
content: 'En route to Bravo position, ETA 5 minutes',
|
||||
timestamp: now.subtract(const Duration(minutes: 10)),
|
||||
isSent: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
Message(
|
||||
id: 'msg5',
|
||||
sender: '0x3333333333333333333333333333333333333333333333333333333333333333',
|
||||
senderName: 'Charlie Base',
|
||||
content: 'S:🏕️:46.0549,14.5038:Staging area established, supplies available',
|
||||
timestamp: now.subtract(const Duration(minutes: 8)),
|
||||
isSent: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
Message(
|
||||
id: 'msg6',
|
||||
sender: '0x1111111111111111111111111111111111111111111111111111111111111111',
|
||||
senderName: 'Alpha Team Lead',
|
||||
content: 'S:🔥:46.0620,14.5120:Small campfire spotted in sector 4, monitoring',
|
||||
timestamp: now.subtract(const Duration(minutes: 5)),
|
||||
isSent: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
Message(
|
||||
id: 'msg7',
|
||||
sender: '0x2222222222222222222222222222222222222222222222222222222222222222',
|
||||
senderName: 'Bravo Scout',
|
||||
content: 'Patient stabilized, waiting for extraction',
|
||||
timestamp: now.subtract(const Duration(minutes: 3)),
|
||||
isSent: false,
|
||||
isPublicChannel: false,
|
||||
),
|
||||
Message(
|
||||
id: 'msg8',
|
||||
sender: 'self',
|
||||
senderName: 'You',
|
||||
content: 'All teams: Weather window closing in 2 hours, prepare to RTB',
|
||||
timestamp: now.subtract(const Duration(minutes: 1)),
|
||||
isSent: true,
|
||||
isPublicChannel: true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Generate mock SAR markers from messages
|
||||
static List<SarMarker> getMockSarMarkers() {
|
||||
final now = DateTime.now();
|
||||
return [
|
||||
SarMarker(
|
||||
id: 'sar1',
|
||||
type: SarMarkerType.foundPerson,
|
||||
latitude: 46.0589,
|
||||
longitude: 14.5078,
|
||||
message: 'Found injured hiker near trail marker 7',
|
||||
timestamp: now.subtract(const Duration(minutes: 12)),
|
||||
sender: '0x2222222222222222222222222222222222222222222222222222222222222222',
|
||||
senderName: 'Bravo Scout',
|
||||
),
|
||||
SarMarker(
|
||||
id: 'sar2',
|
||||
type: SarMarkerType.stagingArea,
|
||||
latitude: 46.0549,
|
||||
longitude: 14.5038,
|
||||
message: 'Staging area established, supplies available',
|
||||
timestamp: now.subtract(const Duration(minutes: 8)),
|
||||
sender: '0x3333333333333333333333333333333333333333333333333333333333333333',
|
||||
senderName: 'Charlie Base',
|
||||
),
|
||||
SarMarker(
|
||||
id: 'sar3',
|
||||
type: SarMarkerType.fireLocation,
|
||||
latitude: 46.0620,
|
||||
longitude: 14.5120,
|
||||
message: 'Small campfire spotted in sector 4, monitoring',
|
||||
timestamp: now.subtract(const Duration(minutes: 5)),
|
||||
sender: '0x1111111111111111111111111111111111111111111111111111111111111111',
|
||||
senderName: 'Alpha Team Lead',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Mock device info for connection status
|
||||
static Map<String, dynamic> getMockDeviceInfo() {
|
||||
return {
|
||||
'deviceName': 'MeshCore-SAR-DEMO',
|
||||
'firmwareVersion': '2.1.0',
|
||||
'hardwareVersion': 'v3',
|
||||
'publicKey': '0xAABBCCDDEEFF00112233445566778899AABBCCDDEEFF00112233445566778899',
|
||||
'batteryMillivolts': 3950,
|
||||
'storageUsed': 1024 * 512, // 512 KB
|
||||
'storageTotal': 1024 * 1024 * 4, // 4 MB
|
||||
};
|
||||
}
|
||||
|
||||
/// Mock radio parameters
|
||||
static Map<String, dynamic> getMockRadioParams() {
|
||||
return {
|
||||
'frequency': 915.0,
|
||||
'bandwidth': 125.0,
|
||||
'spreadingFactor': 9,
|
||||
'codingRate': 7,
|
||||
'txPower': 20,
|
||||
};
|
||||
}
|
||||
}
|
||||
116
integration_test/helpers/screenshot_helper.dart
Normal file
116
integration_test/helpers/screenshot_helper.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
/// Helper class for taking screenshots during integration tests
|
||||
class ScreenshotHelper {
|
||||
final IntegrationTestWidgetsFlutterBinding binding;
|
||||
final String outputDir;
|
||||
int _screenshotCounter = 0;
|
||||
|
||||
ScreenshotHelper(this.binding, {this.outputDir = 'screenshots'});
|
||||
|
||||
/// Take a screenshot with automatic numbering and description
|
||||
Future<void> takeScreenshot(
|
||||
WidgetTester tester,
|
||||
String description, {
|
||||
Duration? wait,
|
||||
}) async {
|
||||
// Wait for UI to settle
|
||||
await tester.pumpAndSettle(wait ?? const Duration(milliseconds: 500));
|
||||
|
||||
// Add extra delay for animations
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
// Increment counter
|
||||
_screenshotCounter++;
|
||||
|
||||
// Format filename: 01_description.png
|
||||
final filename =
|
||||
'${_screenshotCounter.toString().padLeft(2, '0')}_${_sanitizeFilename(description)}.png';
|
||||
|
||||
if (kDebugMode) {
|
||||
print('📸 Taking screenshot: $filename');
|
||||
}
|
||||
|
||||
// Take screenshot
|
||||
await binding.takeScreenshot(filename);
|
||||
}
|
||||
|
||||
/// Sanitize filename by removing special characters
|
||||
String _sanitizeFilename(String input) {
|
||||
return input
|
||||
.toLowerCase()
|
||||
.replaceAll(RegExp(r'[^\w\s-]'), '')
|
||||
.replaceAll(RegExp(r'[\s_]+'), '_')
|
||||
.replaceAll(RegExp(r'^-+|-+$'), '');
|
||||
}
|
||||
|
||||
/// Reset counter (useful for multiple test runs)
|
||||
void resetCounter() {
|
||||
_screenshotCounter = 0;
|
||||
}
|
||||
|
||||
/// Get current screenshot count
|
||||
int get screenshotCount => _screenshotCounter;
|
||||
|
||||
/// Create output directory if it doesn't exist
|
||||
static Future<void> ensureOutputDir(String path) async {
|
||||
final dir = Directory(path);
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension methods for easier screenshot taking
|
||||
extension ScreenshotTestExtension on WidgetTester {
|
||||
/// Wait for a specific widget to appear
|
||||
Future<void> waitFor(
|
||||
Finder finder, {
|
||||
Duration timeout = const Duration(seconds: 10),
|
||||
}) async {
|
||||
final end = DateTime.now().add(timeout);
|
||||
while (DateTime.now().isBefore(end)) {
|
||||
await pump();
|
||||
if (finder.evaluate().isNotEmpty) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
throw Exception('Widget not found: $finder');
|
||||
}
|
||||
|
||||
/// Tap and wait for navigation
|
||||
Future<void> tapAndSettle(Finder finder, {Duration? settleDuration}) async {
|
||||
await tap(finder);
|
||||
await pumpAndSettle(settleDuration ?? const Duration(milliseconds: 500));
|
||||
}
|
||||
|
||||
/// Scroll until widget is visible
|
||||
Future<void> scrollUntilVisible(
|
||||
Finder finder,
|
||||
Finder scrollable, {
|
||||
double delta = 100,
|
||||
int maxScrolls = 50,
|
||||
}) async {
|
||||
int scrollCount = 0;
|
||||
while (finder.evaluate().isEmpty && scrollCount < maxScrolls) {
|
||||
await drag(scrollable, Offset(0, -delta));
|
||||
await pump(const Duration(milliseconds: 100));
|
||||
scrollCount++;
|
||||
}
|
||||
if (finder.evaluate().isEmpty) {
|
||||
throw Exception('Could not scroll to widget: $finder');
|
||||
}
|
||||
}
|
||||
|
||||
/// Fill text field and dismiss keyboard
|
||||
Future<void> enterTextAndDismiss(Finder finder, String text) async {
|
||||
await enterText(finder, text);
|
||||
await pump();
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user