diff --git a/dev_docs/GUIDE.md b/dev_docs/GUIDE.md new file mode 100644 index 000000000..b86f0d99b --- /dev/null +++ b/dev_docs/GUIDE.md @@ -0,0 +1,17 @@ +# Working on the test bundling feature + +`adb logcat` is your friend. Spice it up with `-v color`. If you need something +more powerful, check out [`purr`](https://github.com/google/purr). + +### Find out when a test starts + +Search for `TestRunner: started`. + +``` +09-21 12:24:09.223 23387 23406 I TestRunner: started: runDartTest[callbacks_test testA](pl.leancode.patrol.example.MainActivityTest) + +``` + +### Find out when a test ends + +Search for `TestRunner: finished`. diff --git a/packages/patrol/CHANGELOG.md b/packages/patrol/CHANGELOG.md index 11d12054c..ab0148d6c 100644 --- a/packages/patrol/CHANGELOG.md +++ b/packages/patrol/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.4.0-dev.3 + +- Add support for iOS 11 and 12 (#1733) + ## 2.3.1 - Add support for iOS 11 and 12 (#1733) diff --git a/packages/patrol/example/integration_test/callbacks_test.dart b/packages/patrol/example/integration_test/callbacks_test.dart new file mode 100644 index 000000000..95d69ef4d --- /dev/null +++ b/packages/patrol/example/integration_test/callbacks_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:patrol/src/extensions.dart'; +// ignore: depend_on_referenced_packages +import 'package:test_api/src/backend/invoker.dart'; + +import 'common.dart'; + +String get currentTest => Invoker.current!.fullCurrentTestName(); + +void _print(String text) => print('PATROL_DEBUG: $text'); + +void main() { + patrolSetUp(() async { + await Future.delayed(Duration(seconds: 1)); + _print('setting up before $currentTest'); + }); + + patrolTearDown(() async { + await Future.delayed(Duration(seconds: 1)); + _print('tearing down after $currentTest'); + }); + + patrolTest('testFirst', nativeAutomation: true, _body); + + group('groupA', () { + patrolSetUp(() async { + if (currentTest == 'callbacks_test groupA testB') { + throw Exception('PATROL_DEBUG: Crashing testB on purpose!'); + } + _print('setting up before $currentTest'); + }); + + patrolTearDown(() async { + _print('tearing down after $currentTest'); + }); + + patrolTest('testA', nativeAutomation: true, _body); + patrolTest('testB', nativeAutomation: true, _body); + patrolTest('testC', nativeAutomation: true, _body); + }); + + patrolTest('testLast', nativeAutomation: true, _body); +} + +Future _body(PatrolTester $) async { + final testName = Invoker.current!.fullCurrentTestName(); + _print('test body: name=$testName'); + + await createApp($); + + await $(FloatingActionButton).tap(); + expect($(#counterText).text, '1'); + + await $(#textField).enterText(testName); + + await $.pumpAndSettle(duration: Duration(seconds: 2)); +} diff --git a/packages/patrol/lib/src/binding.dart b/packages/patrol/lib/src/binding.dart index 85328c4e7..a64eb18d1 100644 --- a/packages/patrol/lib/src/binding.dart +++ b/packages/patrol/lib/src/binding.dart @@ -36,6 +36,7 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { /// /// You most likely don't want to call it yourself. PatrolBinding() { + logger('created'); final oldTestExceptionReporter = reportTestException; reportTestException = (details, testDescription) { final currentDartTest = _currentDartTest; @@ -52,7 +53,13 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { return; } + if (global_state.currentTestIndividualName == 'patrol_test_explorer') { + // Ignore the fake test. + return; + } + _currentDartTest = global_state.currentTestFullName; + logger('setUp(): called with current Dart test = "$_currentDartTest"'); }); tearDown(() async { @@ -64,24 +71,24 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { final testName = global_state.currentTestIndividualName; final isTestExplorer = testName == 'patrol_test_explorer'; if (isTestExplorer) { + // Ignore the fake test. return; - } else { - logger( - 'tearDown(): count: ${_testResults.length}, results: $_testResults', - ); } - final nameOfRequestedTest = await patrolAppService.testExecutionRequested; + logger('tearDown(): called with current Dart test = "$_currentDartTest"'); + logger('tearDown(): there are ${_testResults.length} test results:'); + _testResults.forEach((dartTestName, result) { + logger('tearDown(): test "$dartTestName": "$result"'); + }); - if (nameOfRequestedTest == _currentDartTest) { + final requestedDartTest = await patrolAppService.testExecutionRequested; + if (requestedDartTest == _currentDartTest) { logger( - 'finished test $_currentDartTest. Will report its status back to the native side', + 'tearDown(): finished test "$_currentDartTest". Will report its status back to the native side', ); final passed = global_state.isCurrentTestPassing; - logger( - 'tearDown(): test "$testName" in group "$_currentDartTest", passed: $passed', - ); + logger('tearDown(): test "$_currentDartTest", passed: $passed'); await patrolAppService.markDartTestAsCompleted( dartFileName: _currentDartTest!, passed: passed, @@ -91,7 +98,7 @@ class PatrolBinding extends IntegrationTestWidgetsFlutterBinding { ); } else { logger( - 'finished test $_currentDartTest, but it was not requested, so its status will not be reported back to the native side', + 'tearDown(): finished test "$_currentDartTest", but it was not requested, so its status will not be reported back to the native side', ); } }); diff --git a/packages/patrol/lib/src/common.dart b/packages/patrol/lib/src/common.dart index f6df39cb1..b6b479e00 100644 --- a/packages/patrol/lib/src/common.dart +++ b/packages/patrol/lib/src/common.dart @@ -20,6 +20,34 @@ import 'custom_finders/patrol_integration_tester.dart'; /// Signature for callback to [patrolTest]. typedef PatrolTesterCallback = Future Function(PatrolIntegrationTester $); +/// A modification of [setUp] that works with Patrol's native automation. +void patrolSetUp(Future Function() body) { + setUp(() async { + final currentTest = global_state.currentTestFullName; + + final requestedToExecute = await PatrolBinding.instance.patrolAppService + .waitForExecutionRequest(currentTest); + + if (requestedToExecute) { + await body(); + } + }); +} + +/// A modification of [tearDown] that works with Patrol's native automation. +void patrolTearDown(Future Function() body) { + tearDown(() async { + final currentTest = global_state.currentTestFullName; + + final requestedToExecute = await PatrolBinding.instance.patrolAppService + .waitForExecutionRequest(currentTest); + + if (requestedToExecute) { + await body(); + } + }); +} + /// Like [testWidgets], but with support for Patrol custom finders. /// /// To customize the Patrol-specific configuration, set [config]. @@ -58,8 +86,6 @@ void patrolTest( }) { NativeAutomator? automator; - PatrolBinding? patrolBinding; - if (!nativeAutomation) { debugPrint(''' ╔════════════════════════════════════════════════════════════════════════════════════╗ @@ -76,8 +102,8 @@ void patrolTest( case BindingType.patrol: automator = NativeAutomator(config: nativeAutomatorConfig); - patrolBinding = PatrolBinding.ensureInitialized(); - patrolBinding.framePolicy = framePolicy; + // PatrolBinding is initialized in the generated test bundle file. + PatrolBinding.instance.framePolicy = framePolicy; break; case BindingType.integrationTest: IntegrationTestWidgetsFlutterBinding.ensureInitialized().framePolicy = @@ -97,28 +123,16 @@ void patrolTest( variant: variant, tags: tags, (widgetTester) async { - if (patrolBinding != null && !constants.hotRestartEnabled) { + if (!constants.hotRestartEnabled) { // If Patrol's native automation feature is enabled, then this test will // be executed only if the native side requested it to be executed. // Otherwise, it returns early. - // - // The assumption here is that this test doesn't have any extra parent - // groups. Every Dart test suite has an implict, unnamed, top-level - // group. An additional group is present in the bundled_test.dart, and - // its name is equal to the path to the Dart test file in the - // integration_test directory. - // - // In other words, the developer cannot use `group()` in the tests. - // - // Example: if this function is called from the Dart test file named - // "example_test.dart", and that file is located in the - // "integration_test/examples" directory, we assume that the name of the - // immediate parent group is "examples.example_test". - - final requestedToExecute = await patrolBinding.patrolAppService + + final isRequestedToExecute = await PatrolBinding + .instance.patrolAppService .waitForExecutionRequest(global_state.currentTestFullName); - if (!requestedToExecute) { + if (!isRequestedToExecute) { return; } } @@ -202,7 +216,6 @@ DartGroupEntry createDartTestGroup( ); } else if (entry is Test) { if (entry.name == 'patrol_test_explorer') { - // throw StateError('Expected group, got test: ${entry.name}'); // Ignore the bogus test that is used to discover the test structure. continue; } diff --git a/packages/patrol/lib/src/extensions.dart b/packages/patrol/lib/src/extensions.dart new file mode 100644 index 000000000..701b7b959 --- /dev/null +++ b/packages/patrol/lib/src/extensions.dart @@ -0,0 +1,19 @@ +import 'package:meta/meta.dart'; +// ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; + +/// Provides convenience methods for [Invoker]. +@internal +extension InvokerX on Invoker { + /// Returns the full name of the current test (names of all ancestor groups + + /// name of the current test). + String fullCurrentTestName() { + final parentGroupName = liveTest.groups.last.name; + final testName = liveTest.individualName; + + return '$parentGroupName $testName'; + } + + /// Returns the name of the current test only. No group prefixes. + String get currentTestName => liveTest.individualName; +} diff --git a/packages/patrol/lib/src/native/patrol_app_service.dart b/packages/patrol/lib/src/native/patrol_app_service.dart index a4aa6699e..f67806ff7 100644 --- a/packages/patrol/lib/src/native/patrol_app_service.dart +++ b/packages/patrol/lib/src/native/patrol_app_service.dart @@ -3,12 +3,11 @@ // TODO: Use a logger instead of print import 'dart:async'; -import 'dart:io'; +import 'dart:io' as io; import 'package:patrol/src/common.dart'; import 'package:patrol/src/native/contracts/contracts.dart'; import 'package:patrol/src/native/contracts/patrol_app_service_server.dart'; - import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf_io; @@ -30,7 +29,7 @@ Future runAppService(PatrolAppService service) async { final server = await shelf_io.serve( pipeline, - InternetAddress.anyIPv4, + io.InternetAddress.anyIPv4, _port, poweredByHeader: null, ); diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml index 7704cdc3c..35ef9a925 100644 --- a/packages/patrol/pubspec.yaml +++ b/packages/patrol/pubspec.yaml @@ -2,7 +2,7 @@ name: patrol description: > Powerful Flutter-native UI testing framework overcoming limitations of existing Flutter testing tools. Ready for action! -version: 2.3.1 +version: 2.4.0-dev.3 homepage: https://patrol.leancode.co repository: https://github.com/leancodepl/patrol issue_tracker: https://github.com/leancodepl/patrol/issues