import "dart:async"; import "package:flutter/material.dart"; import "package:flutter_test/flutter_test.dart"; import "package:integration_test/integration_test.dart"; import "benign_errors.dart"; abstract class BaseRobot { BaseRobot(this.tester); final WidgetTester tester; Future isDisplayed(); void drainBackgroundErrors() { final Object? pending = tester.takeException(); if (pending == null) { return; } final kind = benignErrorKind(pending.toString()); if (kind != null) { tester.printToConsole("Ignoring benign $kind background error: $pending"); return; } throw TestFailure("Unhandled background error: $pending"); } Future _pumpStep(Duration step) async { await tester.pump(step); drainBackgroundErrors(); } Future pumpUntilFound( Finder finder, { Duration timeout = const Duration(seconds: 30), Duration step = const Duration(milliseconds: 100), }) async { final endTime = DateTime.now().add(timeout); while (DateTime.now().isBefore(endTime)) { await _pumpStep(step); if (tester.any(finder)) { return; } } await takeScreenshot("${runtimeType}_pump_until_found_timeout"); throw TestFailure("Widget not found after ${timeout.inSeconds}s: $finder"); } Future pumpUntilGone( Finder finder, { Duration timeout = const Duration(seconds: 30), Duration step = const Duration(milliseconds: 100), }) async { final endTime = DateTime.now().add(timeout); while (DateTime.now().isBefore(endTime)) { await _pumpStep(step); if (!tester.any(finder)) { return; } } await takeScreenshot("${runtimeType}_pump_until_gone_timeout"); throw TestFailure("Widget still present after ${timeout.inSeconds}s: $finder"); } Future tapByKey(String key, {Duration timeout = const Duration(seconds: 30)}) async { final finder = find.byKey(ValueKey(key)); await pumpUntilFound(finder, timeout: timeout); // A tap during a route transition lands on the animating overlay instead of the widget await settle(max: const Duration(seconds: 2)); await tester.tap(finder.first, warnIfMissed: false); await tester.pump(const Duration(milliseconds: 300)); } Future tapWhenVisible( Finder finder, { Duration timeout = const Duration(seconds: 30), }) async { await pumpUntilFound(finder, timeout: timeout); await settle(max: const Duration(seconds: 2)); await tester.tap(finder.first, warnIfMissed: false); await tester.pump(const Duration(milliseconds: 300)); } Future enterTextByKey(String key, String text) async { final finder = find.byKey(ValueKey(key)); await pumpUntilFound(finder); await tester.enterText(finder.first, text); await tester.pump(const Duration(milliseconds: 300)); } Future pumpUntil( bool Function() condition, { Duration timeout = const Duration(seconds: 30), Duration step = const Duration(milliseconds: 100), }) async { final endTime = DateTime.now().add(timeout); while (DateTime.now().isBefore(endTime)) { await _pumpStep(step); if (condition()) { return true; } } return false; } Future goBack() async { final navigator = tester.state(find.byType(Navigator).last); navigator.pop(); await tester.pump(const Duration(milliseconds: 500)); } Future dismissModal() async { final navigator = tester.state(find.byType(Navigator).first); navigator.pop(); await tester.pump(const Duration(milliseconds: 500)); } // We make this bounded instead of pumpAndSettle Future settle({Duration max = const Duration(seconds: 3)}) async { final endTime = DateTime.now().add(max); while (DateTime.now().isBefore(endTime)) { await _pumpStep(const Duration(milliseconds: 100)); if (!tester.binding.hasScheduledFrame) { return; } } } Future isSpecificPage({Duration timeout = const Duration(seconds: 30)}) async { await pumpUntilFound(find.byType(T), timeout: timeout); await settle(); hasType(); } bool isKeyPresent(String key) => tester.any(find.byKey(ValueKey(key))); String describeScreen() { final names = tester.allWidgets .map((widget) => widget.runtimeType.toString()) .where((name) => name.endsWith("Page") || name.endsWith("Sheet") || name.endsWith("Screen")) .toSet() .toList() ..sort(); return names.join(", "); } void hasType() { expect(find.byType(T), findsOneWidget); } void hasText(String text) { expect(find.text(text), findsOneWidget); } void hasTextAtLeastOnce(String text) { expect(find.text(text), findsAny); } String? textByKey(String key) { final finder = find.byKey(ValueKey(key)); if (!tester.any(finder)) { return null; } return tester.widget(finder.first).data; } static bool _surfaceConverted = false; Future takeScreenshot(String name) async { try { final binding = tester.binding; if (binding is! IntegrationTestWidgetsFlutterBinding) { return; } if (!_surfaceConverted) { await binding.convertFlutterSurfaceToImage(); _surfaceConverted = true; } var finished = false; unawaited( binding.takeScreenshot("${name}_${DateTime.now().millisecondsSinceEpoch}").then( (_) => finished = true, onError: (Object error) { finished = true; tester.printToConsole("Screenshot $name failed: $error"); }, ), ); final deadline = DateTime.now().add(const Duration(seconds: 10)); while (!finished && DateTime.now().isBefore(deadline)) { await tester.pump(const Duration(milliseconds: 100)); } if (!finished) { tester.printToConsole("Screenshot $name gave up after 10s"); } } catch (e) { tester.printToConsole("Screenshot $name failed: $e"); } } }