>() {}.getType();
+ String json = gson.toJson(data, typeOfHashMap);
+ outputStream.write(json.getBytes());
+ outputStream.write("\n".getBytes());
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to write state file", e);
+ }
+ }
+
+ static String convertStreamToString(InputStream inputStream) {
+ Scanner s = new Scanner(inputStream).useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
+
+ /**
+ * Sets the state of lifecycle callbacks in the app.
+ *
+ * This is required because the app is launched in a new process for each test.
+ */
+ public void setLifecycleCallbacksState() {
+ final String TAG = "PatrolJUnitRunner.setLifecycleCallbacksStateInApp(): ";
+
+ try {
+ patrolAppServiceClient.setLifecycleCallbacksState(readStateFile());
+ } catch (PatrolAppServiceClientException e) {
+ Logger.INSTANCE.e(TAG + "Failed to set lifecycle callbacks state in app: ", e);
+ throw new RuntimeException(e);
+ }
+ }
+
/**
* Requests execution of a Dart test and waits for it to finish.
* Throws AssertionError if the test fails.
*/
public RunDartTestResponse runDartTest(String name) {
- final String TAG = "PatrolJUnitRunner.runDartTest(" + name + "): ";
+ final String TAG = "PatrolJUnitRunner.runDartTest(\"" + name + "\"): ";
try {
Logger.INSTANCE.i(TAG + "Requested execution");
diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt
index 998420419..c930fd0ec 100644
--- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt
+++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/PatrolPlugin.kt
@@ -1,24 +1,52 @@
package pl.leancode.patrol
import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
-class PatrolPlugin : FlutterPlugin, MethodCallHandler {
+class PatrolPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
private lateinit var channel: MethodChannel
+ private var isInitialRun: Boolean? = null
+
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "pl.leancode.patrol/main")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
- result.notImplemented()
+ when (call.method) {
+ "isInitialRun" -> result.success(isInitialRun)
+ else -> result.notImplemented()
+ }
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
+
+ override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+ val intent = binding.activity.intent
+ if (!intent.hasExtra("isInitialRun")) {
+ throw IllegalStateException("PatrolPlugin must be initialized with intent having isInitialRun boolean")
+ }
+
+ isInitialRun = intent.getBooleanExtra("isInitialRun", false)
+ }
+
+ override fun onDetachedFromActivityForConfigChanges() {
+ // Do nothing
+ }
+
+ override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+ // Do nothing
+ }
+
+ override fun onDetachedFromActivity() {
+ // Do nothing
+ }
}
diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt
index 83b6daa3b..10b1cbe3b 100644
--- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt
+++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/Contracts.kt
@@ -33,6 +33,10 @@ class Contracts {
fine,
}
+ class Empty (
+
+ )
+
data class DartGroupEntry (
val name: String,
val type: GroupEntryType,
@@ -43,6 +47,11 @@ class Contracts {
val group: DartGroupEntry
)
+ data class ListDartLifecycleCallbacksResponse (
+ val setUpAlls: List,
+ val tearDownAlls: List
+ )
+
data class RunDartTestRequest (
val name: String
)
@@ -56,6 +65,10 @@ class Contracts {
}
}
+ data class SetLifecycleCallbacksStateRequest (
+ val state: Map
+ )
+
data class ConfigureRequest (
val findTimeoutMillis: Long
)
@@ -260,4 +273,8 @@ class Contracts {
val locationAccuracy: SetLocationAccuracyRequestLocationAccuracy
)
+ data class MarkLifecycleCallbackExecutedRequest (
+ val name: String
+ )
+
}
diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt
index c553b00be..9e76ffe0c 100644
--- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt
+++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/NativeAutomatorServer.kt
@@ -48,6 +48,7 @@ abstract class NativeAutomatorServer {
abstract fun setLocationAccuracy(request: Contracts.SetLocationAccuracyRequest)
abstract fun debug()
abstract fun markPatrolAppServiceReady()
+ abstract fun markLifecycleCallbackExecuted(request: Contracts.MarkLifecycleCallbackExecutedRequest)
val router = routes(
"initialize" bind POST to {
@@ -206,6 +207,11 @@ abstract class NativeAutomatorServer {
"markPatrolAppServiceReady" bind POST to {
markPatrolAppServiceReady()
Response(OK)
+ },
+ "markLifecycleCallbackExecuted" bind POST to {
+ val body = json.fromJson(it.bodyString(), Contracts.MarkLifecycleCallbackExecutedRequest::class.java)
+ markLifecycleCallbackExecuted(body)
+ Response(OK)
}
)
diff --git a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt
index 3896e9f77..12bdc9bf8 100644
--- a/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt
+++ b/packages/patrol/android/src/main/kotlin/pl/leancode/patrol/contracts/PatrolAppServiceClient.kt
@@ -19,6 +19,16 @@ class PatrolAppServiceClient(address: String, port: Int, private val timeout: Lo
return json.fromJson(response, Contracts.ListDartTestsResponse::class.java)
}
+ fun listDartLifecycleCallbacks(): Contracts.ListDartLifecycleCallbacksResponse {
+ val response = performRequest("listDartLifecycleCallbacks")
+ return json.fromJson(response, Contracts.ListDartLifecycleCallbacksResponse::class.java)
+ }
+
+ fun setLifecycleCallbacksState(request: Contracts.SetLifecycleCallbacksStateRequest): Contracts.Empty {
+ val response = performRequest("setLifecycleCallbacksState", json.toJson(request))
+ return json.fromJson(response, Contracts.Empty::class.java)
+ }
+
fun runDartTest(request: Contracts.RunDartTestRequest): Contracts.RunDartTestResponse {
val response = performRequest("runDartTest", json.toJson(request))
return json.fromJson(response, Contracts.RunDartTestResponse::class.java)
diff --git a/packages/patrol/example/integration_test/internal/callbacks_all_test.dart b/packages/patrol/example/integration_test/internal/callbacks_all_test.dart
new file mode 100644
index 000000000..b927ff425
--- /dev/null
+++ b/packages/patrol/example/integration_test/internal/callbacks_all_test.dart
@@ -0,0 +1,40 @@
+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('TEST_DEBUG: $text');
+
+void main() {
+ group('parent', () {
+ patrolSetUpAll(() async {
+ await Future.delayed(Duration(seconds: 1));
+ _print('ran patrolSetUpAll (1) before "$currentTest"');
+ });
+
+ patrolSetUpAll(() async {
+ await Future.delayed(Duration(seconds: 1));
+ _print('ran patrolSetUpAll (2) before "$currentTest"');
+ });
+
+ patrolTest('testA', _body);
+ patrolTest('testB', _body);
+ patrolTest('testC', _body);
+ });
+}
+
+Future _body(PatrolIntegrationTester $) async {
+ final testName = Invoker.current!.fullCurrentTestName();
+ _print('ran body of test "$testName"');
+
+ await createApp($);
+
+ await $(FloatingActionButton).tap();
+ expect($(#counterText).text, '1');
+
+ await $(#textField).enterText(testName);
+}
diff --git a/packages/patrol/example/integration_test/internal/callbacks_test.dart b/packages/patrol/example/integration_test/internal/callbacks_test.dart
new file mode 100644
index 000000000..a1cd8183d
--- /dev/null
+++ b/packages/patrol/example/integration_test/internal/callbacks_test.dart
@@ -0,0 +1,58 @@
+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('TEST_DEBUG: $text');
+
+void main() {
+ patrolSetUp(() async {
+ await Future.delayed(Duration(seconds: 1));
+ _print('ran patrolSetUp (1) up before "$currentTest"');
+ });
+
+ patrolTearDown(() async {
+ await Future.delayed(Duration(seconds: 1));
+ _print('ran patrolTearDown (1) after "$currentTest"');
+ });
+
+ patrolTest('testFirst', _body);
+
+ group('groupA', () {
+ patrolSetUp(() async {
+ if (currentTest == 'internal.callbacks_test groupA testB') {
+ throw Exception('TEST_DEBUG: "$currentTest" crashed on purpose');
+ }
+
+ _print('ran patrolSetUp (2) before "$currentTest"');
+ });
+
+ patrolTearDown(() async {
+ _print('ran patrolTearDown (2) after "$currentTest"');
+ });
+
+ patrolTest('testA', _body);
+ patrolTest('testB', _body);
+ patrolTest('testC', _body);
+ });
+
+ patrolTest('testLast', _body);
+}
+
+Future _body(PatrolIntegrationTester $) async {
+ final testName = Invoker.current!.fullCurrentTestName();
+ _print('ran body of test "$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/ios/Classes/AutomatorServer/AutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift
index ea8f5df94..d90b65ef1 100644
--- a/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift
+++ b/packages/patrol/ios/Classes/AutomatorServer/AutomatorServer.swift
@@ -5,11 +5,18 @@
final class AutomatorServer: NativeAutomatorServer {
private let automator: Automator
- private let onAppReady: (Bool) -> Void
+ private let onAppReady: () -> Void
- init(automator: Automator, onAppReady: @escaping (Bool) -> Void) {
+ private let onDartLifecycleCallbackExecuted: (String) -> Void
+
+ init(
+ automator: Automator,
+ onAppReady: @escaping () -> Void,
+ onDartLifecycleCallbackExecuted: @escaping (String) -> Void
+ ) {
self.automator = automator
self.onAppReady = onAppReady
+ self.onDartLifecycleCallbackExecuted = onDartLifecycleCallbackExecuted
}
func initialize() throws {}
@@ -304,7 +311,11 @@
}
func markPatrolAppServiceReady() throws {
- onAppReady(true)
+ onAppReady()
+ }
+
+ func markLifecycleCallbackExecuted(request: MarkLifecycleCallbackExecutedRequest) throws {
+ onDartLifecycleCallbackExecuted(request.name)
}
}
diff --git a/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift b/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift
index 3b575e968..cd4857e14 100644
--- a/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift
+++ b/packages/patrol/ios/Classes/AutomatorServer/Contracts.swift
@@ -32,6 +32,10 @@ enum SetLocationAccuracyRequestLocationAccuracy: String, Codable {
case fine
}
+struct Empty: Codable {
+
+}
+
struct DartGroupEntry: Codable {
var name: String
var type: GroupEntryType
@@ -42,6 +46,11 @@ struct ListDartTestsResponse: Codable {
var group: DartGroupEntry
}
+struct ListDartLifecycleCallbacksResponse: Codable {
+ var setUpAlls: [String]
+ var tearDownAlls: [String]
+}
+
struct RunDartTestRequest: Codable {
var name: String
}
@@ -51,6 +60,10 @@ struct RunDartTestResponse: Codable {
var details: String?
}
+struct SetLifecycleCallbacksStateRequest: Codable {
+ var state: [String: Bool]
+}
+
struct ConfigureRequest: Codable {
var findTimeoutMillis: Int
}
@@ -174,3 +187,7 @@ struct SetLocationAccuracyRequest: Codable {
var locationAccuracy: SetLocationAccuracyRequestLocationAccuracy
}
+struct MarkLifecycleCallbackExecutedRequest: Codable {
+ var name: String
+}
+
diff --git a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift
index 9e40b9d9d..35afaa05a 100644
--- a/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift
+++ b/packages/patrol/ios/Classes/AutomatorServer/NativeAutomatorServer.swift
@@ -43,6 +43,7 @@ protocol NativeAutomatorServer {
func setLocationAccuracy(request: SetLocationAccuracyRequest) throws
func debug() throws
func markPatrolAppServiceReady() throws
+ func markLifecycleCallbackExecuted(request: MarkLifecycleCallbackExecutedRequest) throws
}
extension NativeAutomatorServer {
@@ -241,6 +242,12 @@ extension NativeAutomatorServer {
try markPatrolAppServiceReady()
return HTTPResponse(.ok)
}
+
+ private func markLifecycleCallbackExecutedHandler(request: HTTPRequest) throws -> HTTPResponse {
+ let requestArg = try JSONDecoder().decode(MarkLifecycleCallbackExecutedRequest.self, from: request.body)
+ try markLifecycleCallbackExecuted(request: requestArg)
+ return HTTPResponse(.ok)
+ }
}
extension NativeAutomatorServer {
@@ -420,6 +427,11 @@ extension NativeAutomatorServer {
request: request,
handler: markPatrolAppServiceReadyHandler)
}
+ server.route(.POST, "markLifecycleCallbackExecuted") {
+ request in handleRequest(
+ request: request,
+ handler: markLifecycleCallbackExecutedHandler)
+ }
}
}
diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift
index f48791431..66ae8b542 100644
--- a/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift
+++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolAppServiceClient.swift
@@ -20,6 +20,19 @@ class PatrolAppServiceClient {
performRequestWithResult(requestName: "listDartTests", completion: completion)
}
+ func listDartLifecycleCallbacks(completion: @escaping (Result) -> Void) {
+ performRequestWithResult(requestName: "listDartLifecycleCallbacks", completion: completion)
+ }
+
+ func setLifecycleCallbacksState(request: SetLifecycleCallbacksStateRequest, completion: @escaping (Result) -> Void) {
+ do {
+ let body = try JSONEncoder().encode(request)
+ performRequestWithResult(requestName: "setLifecycleCallbacksState", body: body, completion: completion)
+ } catch let err {
+ completion(.failure(err))
+ }
+ }
+
func runDartTest(request: RunDartTestRequest, completion: @escaping (Result) -> Void) {
do {
let body = try JSONEncoder().encode(request)
diff --git a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift
index 9c9b28d61..8841ae504 100644
--- a/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift
+++ b/packages/patrol/ios/Classes/AutomatorServer/PatrolServer.swift
@@ -12,8 +12,9 @@ import Telegraph
private let server: Server
#endif
- @objc
- public private(set) var appReady = false
+ private let onAppReady: () -> Void
+
+ private let onDartLifecycleCallbackExecuted: (String) -> Void
private var passedPort: Int = {
guard let portStr = ProcessInfo.processInfo.environment[envPortKey] else {
@@ -31,7 +32,12 @@ import Telegraph
return portInt
}()
- @objc public override init() {
+ @objc public init(
+ withOnAppReadyCallback onAppReady: @escaping () -> Void,
+ onDartLifecycleCallbackExecuted: @escaping (String) -> Void
+ ) {
+ self.onDartLifecycleCallbackExecuted = onDartLifecycleCallbackExecuted
+ self.onAppReady = onAppReady
Logger.shared.i("PatrolServer constructor called")
#if PATROL_ENABLED
@@ -48,10 +54,18 @@ import Telegraph
#if PATROL_ENABLED
Logger.shared.i("Starting server...")
- let provider = AutomatorServer(automator: automator) { appReady in
- Logger.shared.i("App reported that it is ready")
- self.appReady = appReady
- }
+ let provider = AutomatorServer(
+ automator: automator,
+ onAppReady: {
+ Logger.shared.i("App reported that it is ready")
+ self.onAppReady()
+ },
+ onDartLifecycleCallbackExecuted: { callback in
+ Logger.shared.i(
+ "App reported that Dart lifecycle callback \(format: callback) was executed")
+ self.onDartLifecycleCallbackExecuted(callback)
+ }
+ )
provider.setupRoutes(server: server)
diff --git a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift
index 9b2b3290a..1c5c20c02 100644
--- a/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift
+++ b/packages/patrol/ios/Classes/ObjCPatrolAppServiceClient.swift
@@ -45,28 +45,53 @@
}
}
+ @objc public func listDartLifecycleCallbacks(
+ completion: @escaping ([String]?, [String]?, Error?) -> Void
+ ) {
+ NSLog("PatrolAppService.listDartLifecycleCallbacks()")
+
+ client.listDartLifecycleCallbacks { result in
+ switch result {
+ case .success(let result):
+ completion(result.setUpAlls, result.tearDownAlls, nil)
+ case .failure(let error):
+ completion(nil, nil, error)
+ }
+ }
+ }
+
+ @objc public func setDartLifecycleCallbacksState(
+ _ state: [String: Bool], completion: @escaping (Error?) -> Void
+ ) {
+ NSLog("PatrolAppService.setDartLifecycleCallbacksState(\(state)")
+
+ let request = SetLifecycleCallbacksStateRequest(state: state)
+ self.client.setLifecycleCallbacksState(request: request) { result in
+ switch result {
+ case .success(_):
+ completion(nil)
+ case .failure(let error):
+ completion(error)
+ }
+ }
+ }
+
@objc public func runDartTest(
- name: String, completion: @escaping (ObjCRunDartTestResponse?, Error?) -> Void
+ _ name: String, completion: @escaping (ObjCRunDartTestResponse?, Error?) -> Void
) {
- // TODO: simple workaround - patrolAppService starts running too slowly.
- // We should wait for appReady in the dynamically created test case method,
- // before calling runDartTest() (in PATROL_INTEGRATION_TEST_IOS_MACRO)
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
- NSLog("PatrolAppServiceClient.runDartTest(\(name))")
-
- let request = RunDartTestRequest(name: name)
- self.client.runDartTest(request: request) { result in
- switch result {
- case .success(let result):
- let testRespone = ObjCRunDartTestResponse(
- passed: result.result == .success,
- details: result.details
- )
- completion(testRespone, nil)
- case .failure(let error):
- completion(nil, error)
- }
+ NSLog("PatrolAppServiceClient.runDartTest(\(format: name))")
+ let request = RunDartTestRequest(name: name)
+ self.client.runDartTest(request: request) { result in
+ switch result {
+ case .success(let result):
+ let testRespone = ObjCRunDartTestResponse(
+ passed: result.result == .success,
+ details: result.details
+ )
+ completion(testRespone, nil)
+ case .failure(let error):
+ completion(nil, error)
}
}
}
@@ -85,7 +110,7 @@ extension DartGroupEntry {
// This case is invalid, because every test will have at least
// 1 named group - its filename.
- fatalError("Invariant violated: test \(test.name) has no named parent group")
+ fatalError("Invariant violated: test \(format: test.name) has no named parent group")
}
test.name = "\(parentGroupName) \(test.name)"
diff --git a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h
index d7f6c0b05..14e7a6fc5 100644
--- a/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h
+++ b/packages/patrol/ios/Classes/PatrolIntegrationTestRunner.h
@@ -1,9 +1,9 @@
// This file is a one giant macro to make the setup as easy as possible for the developer.
// To edit it:
-// 1. Remove the trailing backslashes: $ sed 's/\\$//' ios/Classes/PatrolIntegrationTestRunner.h
+// 1. Remove the trailing backslashes: $ sed 's/\\$//' PatrolIntegrationTestRunner.h
// 2. Paste its contents into the RunnerUITests.m in the RunnerUITests target
// 3. Make the changes, make sure it works
-// 4. Re-add trailing backslashes: $ sed 's/$/\\/' ios/Classes/PatrolIntegrationTestRunner.h
+// 4. Re-add trailing backslashes: $ sed 's/$/\\/' RunnerUITests.m
// 5. Copy the contents from RunnerUITests.m back here
// 6. Go back to using a macro in RunnerTests.m
@@ -16,8 +16,20 @@
@implementation __test_class \
\
+(NSArray *)testInvocations { \
+ __block NSMutableDictionary *callbacksState = NULL; \
+ __block NSArray *dartTests = NULL; \
+ __block BOOL appReady = NO; \
+ \
/* Start native automation server */ \
- PatrolServer *server = [[PatrolServer alloc] init]; \
+ PatrolServer *server = [[PatrolServer alloc] \
+ initWithOnAppReadyCallback:^{ \
+ appReady = YES; \
+ } \
+ onDartLifecycleCallbackExecuted:^(NSString *_Nonnull callbackName) { \
+ /* callbacksState dictionary will have been already initialized when this callback is executed */ \
+ NSLog(@"onLifecycleCallbackExecuted for %@", callbackName); \
+ [callbacksState setObject:@YES forKey:callbackName]; \
+ }]; \
\
NSError *_Nullable __autoreleasing *_Nullable err = NULL; \
[server startAndReturnError:err]; \
@@ -35,15 +47,45 @@
[systemAlerts.buttons[@"Allow"] tap]; \
} \
\
+ NSLog(@"Performing initial run of the app..."); \
+ \
+ /* MARK: Start initial run */ \
/* Run the app for the first time to gather Dart tests */ \
- [[[XCUIApplication alloc] init] launch]; \
+ XCUIApplication *app = [[XCUIApplication alloc] init]; \
+ NSDictionary *args = @{@"PATROL_INITIAL_RUN" : @"true"}; \
+ app.launchEnvironment = args; \
+ [app launch]; \
\
- /* Spin the runloop waiting until the app reports that it is ready to report Dart tests */ \
- while (!server.appReady) { \
+ NSLog(@"Waiting for PatrolAppService (running in the app under test) to report its readiness..."); \
+ \
+ /* Spin the runloop waiting until the app reports that PatrolAppService is up */ \
+ while (!appReady) { \
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \
} \
\
- __block NSArray *dartTests = NULL; \
+ /* MARK: List Dart lifecycle callbacks */ \
+ \
+ [appServiceClient listDartLifecycleCallbacksWithCompletion:^(NSArray *_Nullable setUpAlls, \
+ NSArray *_Nullable tearDownAlls, \
+ NSError *_Nullable err) { \
+ if (err != NULL) { \
+ NSLog(@"listDartLifecycleCallbacks(): failed, err: %@", err); \
+ } \
+ \
+ callbacksState = [[NSMutableDictionary alloc] init]; \
+ for (NSString * setUpAll in setUpAlls) { \
+ [callbacksState setObject:@NO forKey:setUpAll]; \
+ } \
+ }]; \
+ \
+ /* Spin the runloop waiting until the app reports the Dart lifecycle callbacks it contains */ \
+ while (!callbacksState) { \
+ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \
+ } \
+ NSLog(@"Got %lu Dart lifecycle callbacks: %@", callbacksState.count, callbacksState); \
+ \
+ /* MARK: List Dart tests */ \
+ \
[appServiceClient listDartTestsWithCompletion:^(NSArray *_Nullable tests, NSError *_Nullable err) { \
if (err != NULL) { \
NSLog(@"listDartTests(): failed, err: %@", err); \
@@ -56,12 +98,14 @@
while (!dartTests) { \
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \
} \
- \
NSLog(@"Got %lu Dart tests: %@", dartTests.count, dartTests); \
\
+ /* MARK: Create tests at runtime */ \
+ \
NSMutableArray *invocations = [[NSMutableArray alloc] init]; \
\
/** \
+ * \
* Once Dart tests are available, we: \
* \
* Step 1. Dynamically add test case methods that request execution of an individual Dart test file. \
@@ -73,25 +117,63 @@
/* Step 1 - dynamically create test cases */ \
\
IMP implementation = imp_implementationWithBlock(^(id _self) { \
- [[[XCUIApplication alloc] init] launch]; \
+ /* Reset server's appReady state, because new app process will be started */ \
+ appReady = NO; \
\
- __block ObjCRunDartTestResponse *response = NULL; \
- [appServiceClient runDartTestWithName:dartTest \
- completion:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \
- if (err != NULL) { \
- NSLog(@"runDartTestWithName(%@): failed, err: %@", dartTest, err); \
- } \
+ XCUIApplication *app = [[XCUIApplication alloc] init]; \
+ NSDictionary *args = @{@"PATROL_INITIAL_RUN" : @"false"}; \
+ [app setLaunchEnvironment:args]; \
+ [app launch]; \
+ \
+ /* Spin the runloop waiting until the app reports that PatrolAppService is up */ \
+ while (!appReady) { \
+ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \
+ } \
\
- response = r; \
- }]; \
+ __block BOOL callbacksSet = NO; \
+ [appServiceClient \
+ setDartLifecycleCallbacksState:callbacksState \
+ completion:^(NSError *_Nullable err) { \
+ if (err != NULL) { \
+ NSLog(@"setDartLifecycleCallbacksState(): call failed, err: %@", err); \
+ } \
\
- /* Wait until Dart test finishes */ \
- while (!response) { \
+ NSLog(@"setDartLifecycleCallbacksState(): call succeeded"); \
+ callbacksSet = YES; \
+ }]; \
+ \
+ /*Wait until lifecycle callbacks are set*/ \
+ while (!callbacksSet) { \
[NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \
} \
\
- XCTAssertTrue(response.passed, @"%@", response.details); \
+ __block ObjCRunDartTestResponse *response = NULL; \
+ __block NSError *error; \
+ [appServiceClient runDartTest:dartTest \
+ completion:^(ObjCRunDartTestResponse *_Nullable r, NSError *_Nullable err) { \
+ NSString *status; \
+ if (err != NULL) { \
+ error = err; \
+ status = @"CRASHED"; \
+ } else { \
+ response = r; \
+ status = response.passed ? @"PASSED" : @"FAILED"; \
+ } \
+ \
+ NSLog(@"runDartTest(\"%@\"): call finished, test result: %@", dartTest, status); \
+ }]; \
+ \
+ /* Wait until Dart test finishes (either fails or passes) or crashes */ \
+ while (!response && !error) { \
+ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]]; \
+ } \
+ \
+ BOOL passed = response ? response.passed : NO; \
+ NSString *details = response ? response.details : @"(no details - app likely crashed)"; \
+ \
+ XCTAssertTrue(passed, @"%@", details); \
}); \
+ \
NSString *selectorName = [PatrolUtils createMethodNameFromPatrolGeneratedGroup:dartTest]; \
SEL selector = NSSelectorFromString(selectorName); \
class_addMethod(self, selector, implementation, "v@:"); \
diff --git a/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift b/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift
index c07ab9da1..0e99b7fff 100644
--- a/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift
+++ b/packages/patrol/ios/Classes/SwiftPatrolPlugin.swift
@@ -2,10 +2,9 @@ import Flutter
import UIKit
let kChannelName = "pl.leancode.patrol/main"
-let kMethodAllTestsFinished = "allTestsFinished"
-let kErrorCreateChannelFailed = "create_channel_failed"
-let kErrorCreateChannelFailedMsg = "Failed to create GRPC channel"
+let kErrorInvalidValue = "invalid value"
+let kErrorInvalidValueMsg = "isInitialRun env var is not a bool"
/// A Flutter plugin that was responsible for communicating the test results back
/// to iOS XCUITest.
@@ -23,6 +22,22 @@ public class SwiftPatrolPlugin: NSObject, FlutterPlugin {
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
- result(FlutterMethodNotImplemented)
+ switch call.method {
+ case "isInitialRun":
+ let rawInitialRun = ProcessInfo.processInfo.environment["PATROL_INITIAL_RUN"]
+ let initialRun = Bool(rawInitialRun ?? "invalid")
+ if initialRun == nil {
+ result(
+ FlutterError(
+ code: kErrorInvalidValue,
+ message: "PATROL_INITIAL_RUN value is invalid: \(String(describing: rawInitialRun))",
+ details: nil)
+ )
+ } else {
+ result(initialRun)
+ }
+ default:
+ result(FlutterMethodNotImplemented)
+ }
}
}
diff --git a/packages/patrol/lib/src/binding.dart b/packages/patrol/lib/src/binding.dart
index 0511a6ad0..df4dd2ef7 100644
--- a/packages/patrol/lib/src/binding.dart
+++ b/packages/patrol/lib/src/binding.dart
@@ -9,8 +9,6 @@ import 'package:patrol/src/devtools_service_extensions/devtools_service_extensio
// ignore: implementation_imports, depend_on_referenced_packages
import 'package:patrol/src/global_state.dart' as global_state;
-import 'constants.dart' as constants;
-
const _success = 'success';
void _defaultPrintLogger(String message) {
@@ -38,56 +36,74 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding {
/// Creates a new [PatrolBinding].
///
/// You most likely don't want to call it yourself.
- PatrolBinding(NativeAutomatorConfig config)
- : _serviceExtensions = DevtoolsServiceExtensions(config) {
+
+ PatrolBinding(
+ this.patrolAppService,
+ this.nativeAutomator,
+ NativeAutomatorConfig config,
+ ) : _serviceExtensions = DevtoolsServiceExtensions(config) {
+ logger('created');
shouldPropagateDevicePointerEvents = true;
final oldTestExceptionReporter = reportTestException;
reportTestException = (details, testDescription) {
final currentDartTest = _currentDartTest;
if (currentDartTest != null) {
- assert(!constants.hotRestartEnabled);
+ assert(!global_state.hotRestartEnabled);
_testResults[currentDartTest] = Failure(testDescription, '$details');
}
oldTestExceptionReporter(details, testDescription);
};
setUp(() {
- if (constants.hotRestartEnabled) {
+ if (global_state.hotRestartEnabled) {
// Sending results ends the test, which we don't want for Hot Restart
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 {
- if (constants.hotRestartEnabled) {
+ if (global_state.hotRestartEnabled) {
// Sending results ends the test, which we don't want for Hot Restart
return;
}
+ if (await global_state.isInitialRun) {
+ // If this is the initial run, then no test has been requested to
+ // execute. Return to avoid blocking on didRequestTestExecution below.
+ return;
+ }
+
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"');
+ });
+
+ final requestedDartTest = await patrolAppService.didRequestTestExecution;
- if (nameOfRequestedTest == _currentDartTest) {
+ 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,
@@ -97,7 +113,7 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding {
);
} 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',
);
}
});
@@ -107,9 +123,13 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding {
/// if necessary.
///
/// This method is idempotent.
- factory PatrolBinding.ensureInitialized(NativeAutomatorConfig config) {
+ factory PatrolBinding.ensureInitialized(
+ PatrolAppService patrolAppService,
+ NativeAutomator nativeAutomator,
+ NativeAutomatorConfig config,
+ ) {
if (_instance == null) {
- PatrolBinding(config);
+ PatrolBinding(patrolAppService, nativeAutomator, config);
}
return _instance!;
}
@@ -128,7 +148,11 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding {
///
/// It's only for test reporting purposes and should not be used for anything
/// else.
- late PatrolAppService patrolAppService;
+ final PatrolAppService patrolAppService;
+
+ /// The [NativeAutomator] used by this binding to interact with the native
+ /// side.
+ final NativeAutomator nativeAutomator;
/// The singleton instance of this object.
///
@@ -200,12 +224,12 @@ class PatrolBinding extends LiveTestWidgetsFlutterBinding {
@override
Widget wrapWithDefaultView(Widget rootWidget) {
assert(
- (_currentDartTest != null) != (constants.hotRestartEnabled),
+ (_currentDartTest != null) != (global_state.hotRestartEnabled),
'_currentDartTest can be null if and only if Hot Restart is enabled',
);
const testLabelEnabled = bool.fromEnvironment('PATROL_TEST_LABEL_ENABLED');
- if (!testLabelEnabled || constants.hotRestartEnabled) {
+ if (!testLabelEnabled || global_state.hotRestartEnabled) {
return super.wrapWithDefaultView(RepaintBoundary(child: rootWidget));
} else {
return super.wrapWithDefaultView(
diff --git a/packages/patrol/lib/src/common.dart b/packages/patrol/lib/src/common.dart
index 16a8c9a97..c9beb87dd 100644
--- a/packages/patrol/lib/src/common.dart
+++ b/packages/patrol/lib/src/common.dart
@@ -5,6 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
import 'package:patrol/src/binding.dart';
import 'package:patrol/src/global_state.dart' as global_state;
+import 'package:patrol/src/logs.dart';
import 'package:patrol/src/native/contracts/contracts.dart';
import 'package:patrol/src/native/native.dart';
import 'package:patrol_finders/patrol_finders.dart' as finders;
@@ -13,13 +14,108 @@ import 'package:test_api/src/backend/group.dart';
// ignore: implementation_imports
import 'package:test_api/src/backend/test.dart';
-import 'constants.dart' as constants;
import 'custom_finders/patrol_integration_tester.dart';
/// Signature for callback to [patrolTest].
typedef PatrolTesterCallback = Future Function(PatrolIntegrationTester $);
-/// Like [testWidgets], but with support for Patrol custom finders.
+/// A modification of [setUp] that works with Patrol's native automation.
+void patrolSetUp(dynamic Function() body) {
+ setUp(() async {
+ if (global_state.hotRestartEnabled) {
+ await body();
+ return;
+ }
+
+ if (await global_state.isInitialRun) {
+ // Skip calling body if we're in test discovery phase
+ return;
+ }
+
+ 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(dynamic Function() body) {
+ tearDown(() async {
+ if (global_state.hotRestartEnabled) {
+ await body();
+ return;
+ }
+
+ if (await global_state.isInitialRun) {
+ // Skip calling body if we're in test discovery phase
+ return;
+ }
+
+ final currentTest = global_state.currentTestFullName;
+
+ final requestedToExecute = await PatrolBinding.instance.patrolAppService
+ .waitForExecutionRequest(currentTest);
+
+ if (requestedToExecute) {
+ await body();
+ }
+ });
+}
+
+/// A modification of [setUpAll] that works with Patrol's native automation.
+///
+/// It keeps track of calls made to setUpAll.
+void patrolSetUpAll(dynamic Function() body) {
+ setUpAll(() async {
+ final patrolAppService = PatrolBinding.instance.patrolAppService;
+ final parentGroupsName = global_state.currentGroupFullName;
+ final setUpAllName = patrolAppService.addSetUpAll(parentGroupsName);
+
+ if (await global_state.isInitialRun) {
+ // Skip calling body if we're in test discovery phase
+ patrolDebug(
+ 'skipping setUpAll "$setUpAllName" because we are in the initial run',
+ );
+ return;
+ }
+
+ patrolDebug('Waiting for lifecycle callbacks state...');
+ final callbacksState =
+ await patrolAppService.didReceiveLifecycleCallbacksState;
+
+ assert(
+ callbacksState[setUpAllName] != null,
+ 'setUpAll "$setUpAllName" was not registered in PatrolAppService. This looks very nasty.',
+ );
+
+ if (callbacksState[setUpAllName] ?? false) {
+ // Skip calling body if this setUpAll was already executed
+ patrolDebug('skipping setUpAll "$setUpAllName" because it already ran');
+ return;
+ }
+
+ final requestedTest = await patrolAppService.didRequestTestExecution;
+
+ // Skip calling if parentGroupName is not a substring of requestedTestName
+ if (!requestedTest.startsWith(parentGroupsName)) {
+ // This is not exhaustive.
+ return;
+ }
+
+ // Mark this setUpAll as executed
+ final nativeAutomator = PatrolBinding.instance.nativeAutomator;
+ await nativeAutomator.markLifecycleCallbackExecuted(setUpAllName);
+
+ await body();
+ });
+}
+
+/// Like [testWidgets], but with support for Patrol custom fiders.
///
/// To customize the Patrol-specific configuration, set [config].
///
@@ -50,14 +146,8 @@ void patrolTest(
LiveTestWidgetsFlutterBindingFramePolicy framePolicy =
LiveTestWidgetsFlutterBindingFramePolicy.fadePointers,
}) {
- NativeAutomator automator;
-
- PatrolBinding? patrolBinding;
-
- automator = NativeAutomator(config: nativeAutomatorConfig);
- final binding =
- patrolBinding = PatrolBinding.ensureInitialized(nativeAutomatorConfig)
- ..framePolicy = framePolicy;
+ final automator = NativeAutomator(config: nativeAutomatorConfig);
+ final binding = PatrolBinding.instance..framePolicy = framePolicy;
testWidgets(
description,
@@ -67,31 +157,30 @@ void patrolTest(
variant: variant,
tags: tags,
(widgetTester) async {
- if (patrolBinding != null && !constants.hotRestartEnabled) {
+ if (!global_state.hotRestartEnabled) {
+ if (await global_state.isInitialRun) {
+ patrolDebug(
+ 'skippng test "${global_state.currentTestFullName}" because this is the initial run',
+ );
+ // Fall through tests during the initial run that discovers tests.
+ //
+ // This is required to be able to find all setUpAll callbacks.
+ return;
+ }
+
// 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;
}
}
+
if (!kIsWeb && io.Platform.isIOS) {
widgetTester.binding.platformDispatcher.onSemanticsEnabledChanged = () {
// This callback is empty on purpose. It's a workaround for tests
@@ -104,7 +193,7 @@ void patrolTest(
final patrolTester = PatrolIntegrationTester(
tester: widgetTester,
- nativeAutomator: automator,
+ nativeAutomator: PatrolBinding.instance.nativeAutomator,
config: config,
);
await callback(patrolTester);
@@ -175,7 +264,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/constants.dart b/packages/patrol/lib/src/constants.dart
deleted file mode 100644
index 01440e6ce..000000000
--- a/packages/patrol/lib/src/constants.dart
+++ /dev/null
@@ -1,5 +0,0 @@
-import 'package:meta/meta.dart';
-
-/// Whether Hot Restart is enabled.
-@internal
-const bool hotRestartEnabled = bool.fromEnvironment('PATROL_HOT_RESTART');
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/global_state.dart b/packages/patrol/lib/src/global_state.dart
index 318574d3e..a4479ce1a 100644
--- a/packages/patrol/lib/src/global_state.dart
+++ b/packages/patrol/lib/src/global_state.dart
@@ -1,3 +1,5 @@
+import 'package:flutter/services.dart';
+import 'package:meta/meta.dart';
// ignore: implementation_imports
import 'package:test_api/src/backend/invoker.dart';
@@ -6,6 +8,10 @@ import 'package:test_api/src/backend/invoker.dart';
/// See https://github.com/leancodepl/patrol/issues/1725
const maxTestLength = 190;
+/// Whether Hot Restart is enabled.
+@internal
+const bool hotRestartEnabled = bool.fromEnvironment('PATROL_HOT_RESTART');
+
/// This file wraps the [Invoker] API, which is internal to package:test. We
/// want to minimize the usage of internal APIs to a minimum.
@@ -24,7 +30,16 @@ String get currentTestFullName {
return nameCandidate;
}
-/// Returns the individual name of the current test. Omits all ancestor groups.
+/// Returns the name of the current group.
+///
+/// Includes all ancestor groups.
+String get currentGroupFullName {
+ return Invoker.current!.liveTest.groups.last.name;
+}
+
+/// Returns the individual name of the current test.
+///
+/// Omits all ancestor groups.
String get currentTestIndividualName {
return Invoker.current!.liveTest.individualName;
}
@@ -33,3 +48,11 @@ String get currentTestIndividualName {
bool get isCurrentTestPassing {
return Invoker.current!.liveTest.state.result.isPassing;
}
+
+const _channel = MethodChannel('pl.leancode.patrol/main');
+
+/// Returns whether this is the first run of the app under test during which
+/// test discovery happens.
+Future get isInitialRun async {
+ return await _channel.invokeMethod('isInitialRun') as bool;
+}
diff --git a/packages/patrol/lib/src/logs.dart b/packages/patrol/lib/src/logs.dart
new file mode 100644
index 000000000..474b7d31f
--- /dev/null
+++ b/packages/patrol/lib/src/logs.dart
@@ -0,0 +1,14 @@
+import 'package:meta/meta.dart';
+
+final _runKey = Object().hashCode.toRadixString(16).padLeft(4, '0');
+
+/// Logs a message with a tag that's constant in the single app process
+/// instance.
+///
+/// Helps to differentiate same logs that happen in different app process runs.
+@internal
+void patrolDebug(String message) {
+ // TODO: Enable only if debug dart define is passed
+ // ignore: avoid_print
+ print('PATROL_DEBUG($_runKey): $message');
+}
diff --git a/packages/patrol/lib/src/native/contracts/contracts.dart b/packages/patrol/lib/src/native/contracts/contracts.dart
index 599d77066..3723967c4 100644
--- a/packages/patrol/lib/src/native/contracts/contracts.dart
+++ b/packages/patrol/lib/src/native/contracts/contracts.dart
@@ -48,6 +48,18 @@ enum SetLocationAccuracyRequestLocationAccuracy {
fine
}
+@JsonSerializable()
+class Empty with EquatableMixin {
+ Empty();
+
+ factory Empty.fromJson(Map json) => _$EmptyFromJson(json);
+
+ Map toJson() => _$EmptyToJson(this);
+
+ @override
+ List get props => const [];
+}
+
@JsonSerializable()
class DartGroupEntry with EquatableMixin {
DartGroupEntry({
@@ -92,6 +104,31 @@ class ListDartTestsResponse with EquatableMixin {
];
}
+@JsonSerializable()
+class ListDartLifecycleCallbacksResponse with EquatableMixin {
+ ListDartLifecycleCallbacksResponse({
+ required this.setUpAlls,
+ required this.tearDownAlls,
+ });
+
+ factory ListDartLifecycleCallbacksResponse.fromJson(
+ Map json,
+ ) =>
+ _$ListDartLifecycleCallbacksResponseFromJson(json);
+
+ final List setUpAlls;
+ final List tearDownAlls;
+
+ Map toJson() =>
+ _$ListDartLifecycleCallbacksResponseToJson(this);
+
+ @override
+ List get props => [
+ setUpAlls,
+ tearDownAlls,
+ ];
+}
+
@JsonSerializable()
class RunDartTestRequest with EquatableMixin {
RunDartTestRequest({
@@ -133,6 +170,28 @@ class RunDartTestResponse with EquatableMixin {
];
}
+@JsonSerializable()
+class SetLifecycleCallbacksStateRequest with EquatableMixin {
+ SetLifecycleCallbacksStateRequest({
+ required this.state,
+ });
+
+ factory SetLifecycleCallbacksStateRequest.fromJson(
+ Map json,
+ ) =>
+ _$SetLifecycleCallbacksStateRequestFromJson(json);
+
+ final Map state;
+
+ Map toJson() =>
+ _$SetLifecycleCallbacksStateRequestToJson(this);
+
+ @override
+ List get props => [
+ state,
+ ];
+}
+
@JsonSerializable()
class ConfigureRequest with EquatableMixin {
ConfigureRequest({
@@ -644,3 +703,25 @@ class SetLocationAccuracyRequest with EquatableMixin {
locationAccuracy,
];
}
+
+@JsonSerializable()
+class MarkLifecycleCallbackExecutedRequest with EquatableMixin {
+ MarkLifecycleCallbackExecutedRequest({
+ required this.name,
+ });
+
+ factory MarkLifecycleCallbackExecutedRequest.fromJson(
+ Map json,
+ ) =>
+ _$MarkLifecycleCallbackExecutedRequestFromJson(json);
+
+ final String name;
+
+ Map toJson() =>
+ _$MarkLifecycleCallbackExecutedRequestToJson(this);
+
+ @override
+ List get props => [
+ name,
+ ];
+}
diff --git a/packages/patrol/lib/src/native/contracts/contracts.g.dart b/packages/patrol/lib/src/native/contracts/contracts.g.dart
index 62436ad5e..5cd8550f8 100644
--- a/packages/patrol/lib/src/native/contracts/contracts.g.dart
+++ b/packages/patrol/lib/src/native/contracts/contracts.g.dart
@@ -6,6 +6,10 @@ part of 'contracts.dart';
// JsonSerializableGenerator
// **************************************************************************
+Empty _$EmptyFromJson(Map json) => Empty();
+
+Map _$EmptyToJson(Empty instance) => {};
+
DartGroupEntry _$DartGroupEntryFromJson(Map json) =>
DartGroupEntry(
name: json['name'] as String,
@@ -39,6 +43,23 @@ Map _$ListDartTestsResponseToJson(
'group': instance.group,
};
+ListDartLifecycleCallbacksResponse _$ListDartLifecycleCallbacksResponseFromJson(
+ Map json) =>
+ ListDartLifecycleCallbacksResponse(
+ setUpAlls:
+ (json['setUpAlls'] as List).map((e) => e as String).toList(),
+ tearDownAlls: (json['tearDownAlls'] as List)
+ .map((e) => e as String)
+ .toList(),
+ );
+
+Map _$ListDartLifecycleCallbacksResponseToJson(
+ ListDartLifecycleCallbacksResponse instance) =>
+ {
+ 'setUpAlls': instance.setUpAlls,
+ 'tearDownAlls': instance.tearDownAlls,
+ };
+
RunDartTestRequest _$RunDartTestRequestFromJson(Map json) =>
RunDartTestRequest(
name: json['name'] as String,
@@ -68,6 +89,18 @@ const _$RunDartTestResponseResultEnumMap = {
RunDartTestResponseResult.failure: 'failure',
};
+SetLifecycleCallbacksStateRequest _$SetLifecycleCallbacksStateRequestFromJson(
+ Map json) =>
+ SetLifecycleCallbacksStateRequest(
+ state: Map.from(json['state'] as Map),
+ );
+
+Map _$SetLifecycleCallbacksStateRequestToJson(
+ SetLifecycleCallbacksStateRequest instance) =>
+ {
+ 'state': instance.state,
+ };
+
ConfigureRequest _$ConfigureRequestFromJson(Map json) =>
ConfigureRequest(
findTimeoutMillis: json['findTimeoutMillis'] as int,
@@ -404,3 +437,15 @@ const _$SetLocationAccuracyRequestLocationAccuracyEnumMap = {
SetLocationAccuracyRequestLocationAccuracy.coarse: 'coarse',
SetLocationAccuracyRequestLocationAccuracy.fine: 'fine',
};
+
+MarkLifecycleCallbackExecutedRequest
+ _$MarkLifecycleCallbackExecutedRequestFromJson(Map json) =>
+ MarkLifecycleCallbackExecutedRequest(
+ name: json['name'] as String,
+ );
+
+Map _$MarkLifecycleCallbackExecutedRequestToJson(
+ MarkLifecycleCallbackExecutedRequest instance) =>
+ {
+ 'name': instance.name,
+ };
diff --git a/packages/patrol/lib/src/native/contracts/native_automator_client.dart b/packages/patrol/lib/src/native/contracts/native_automator_client.dart
index aa0433a40..53e06ed8c 100644
--- a/packages/patrol/lib/src/native/contracts/native_automator_client.dart
+++ b/packages/patrol/lib/src/native/contracts/native_automator_client.dart
@@ -304,6 +304,15 @@ class NativeAutomatorClient {
);
}
+ Future markLifecycleCallbackExecuted(
+ MarkLifecycleCallbackExecutedRequest request,
+ ) {
+ return _sendRequest(
+ 'markLifecycleCallbackExecuted',
+ request.toJson(),
+ );
+ }
+
Future> _sendRequest(
String requestName, [
Map? request,
diff --git a/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart b/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart
index dca57118d..55b631b60 100644
--- a/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart
+++ b/packages/patrol/lib/src/native/contracts/patrol_app_service_server.dart
@@ -16,6 +16,22 @@ abstract class PatrolAppServiceServer {
if ('listDartTests' == request.url.path) {
final result = await listDartTests();
+ final body = jsonEncode(result.toJson());
+ return Response.ok(body);
+ } else if ('listDartLifecycleCallbacks' == request.url.path) {
+ final result = await listDartLifecycleCallbacks();
+
+ final body = jsonEncode(result.toJson());
+ return Response.ok(body);
+ } else if ('setLifecycleCallbacksState' == request.url.path) {
+ final stringContent = await request.readAsString(utf8);
+ final json = jsonDecode(stringContent);
+ final requestObj = SetLifecycleCallbacksStateRequest.fromJson(
+ json as Map,
+ );
+
+ final result = await setLifecycleCallbacksState(requestObj);
+
final body = jsonEncode(result.toJson());
return Response.ok(body);
} else if ('runDartTest' == request.url.path) {
@@ -34,5 +50,9 @@ abstract class PatrolAppServiceServer {
}
Future listDartTests();
+ Future listDartLifecycleCallbacks();
+ Future setLifecycleCallbacksState(
+ SetLifecycleCallbacksStateRequest request,
+ );
Future runDartTest(RunDartTestRequest request);
}
diff --git a/packages/patrol/lib/src/native/native_automator.dart b/packages/patrol/lib/src/native/native_automator.dart
index a4ea2d865..d095f3929 100644
--- a/packages/patrol/lib/src/native/native_automator.dart
+++ b/packages/patrol/lib/src/native/native_automator.dart
@@ -769,7 +769,7 @@ class NativeAutomator {
);
}
- /// Tells the AndroidJUnitRunner that PatrolAppService is ready to answer
+ /// Tells the native test runner that PatrolAppService is ready to answer
/// requests about the structure of Dart tests.
@internal
Future markPatrolAppServiceReady() async {
@@ -778,4 +778,17 @@ class NativeAutomator {
_client.markPatrolAppServiceReady,
);
}
+
+ /// Tells the native test runner that callback [name] has been executed.
+ ///
+ /// The native test runner persists that information.
+ @internal
+ Future markLifecycleCallbackExecuted(String name) async {
+ await _wrapRequest(
+ 'markLifecycleCallbackExecuted',
+ () => _client.markLifecycleCallbackExecuted(
+ MarkLifecycleCallbackExecutedRequest(name: name),
+ ),
+ );
+ }
}
diff --git a/packages/patrol/lib/src/native/patrol_app_service.dart b/packages/patrol/lib/src/native/patrol_app_service.dart
index a4aa6699e..eb8a83f31 100644
--- a/packages/patrol/lib/src/native/patrol_app_service.dart
+++ b/packages/patrol/lib/src/native/patrol_app_service.dart
@@ -3,14 +3,15 @@
// 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;
+// ignore: implementation_imports
+import 'package:test_api/src/backend/live_test.dart';
const _port = 8082;
const _idleTimeout = Duration(hours: 2);
@@ -30,7 +31,7 @@ Future runAppService(PatrolAppService service) async {
final server = await shelf_io.serve(
pipeline,
- InternetAddress.anyIPv4,
+ io.InternetAddress.anyIPv4,
_port,
poweredByHeader: null,
);
@@ -44,34 +45,78 @@ Future runAppService(PatrolAppService service) async {
);
}
-/// Implements a stateful gRPC service for querying and executing Dart tests.
+/// Implements a stateful HTTP service for querying and executing Dart tests.
///
/// This is an internal class and you don't want to use it. It's public so that
/// the generated code can access it.
class PatrolAppService extends PatrolAppServiceServer {
/// Creates a new [PatrolAppService].
- PatrolAppService({required this.topLevelDartTestGroup});
+ PatrolAppService();
/// The ambient test group that wraps all the other groups and tests in the
/// bundled Dart test file.
- final DartGroupEntry topLevelDartTestGroup;
+ DartGroupEntry? topLevelDartTestGroup;
+
+ /// The names of all setUpAll callbacks.
+ ///
+ /// setUpAlls, unlike setUps, aren't executed in the [LiveTest] context.
+ /// Because of this, we can't depend on the [LiveTest]'s name, so we identify
+ /// them by the parent group ID instead.
+ ///
+ /// This is a list of groups containing setUpAllCallbacks with an index
+ /// appended.
+ List setUpAlls = [];
+
+ final _didReceiveLifecycleCallbacksState = Completer>();
+
+ /// A completer that completes when the native side sets the state of
+ /// lifecycle callbacks.
+ ///
+ /// Completes with a map that knows which lifecycle callbacks have been
+ /// already executed.
+ Future> get didReceiveLifecycleCallbacksState {
+ return _didReceiveLifecycleCallbacksState.future;
+ }
/// A completer that completes with the name of the Dart test file that was
/// requested to execute by the native side.
- final _testExecutionRequested = Completer();
+ final _didRequestTestExecution = Completer();
/// A future that completes with the name of the Dart test file that was
/// requested to execute by the native side.
- Future get testExecutionRequested => _testExecutionRequested.future;
+ Future get didRequestTestExecution => _didRequestTestExecution.future;
- final _testExecutionCompleted = Completer<_TestExecutionResult>();
+ final _didCompleteTestExecution = Completer<_TestExecutionResult>();
/// A future that completes when the Dart test file (whose execution was
/// requested by the native side) completes.
///
/// Returns true if the test passed, false otherwise.
- Future<_TestExecutionResult> get testExecutionCompleted {
- return _testExecutionCompleted.future;
+ Future<_TestExecutionResult> get didCompleteTestExecution {
+ return _didCompleteTestExecution.future;
+ }
+
+ /// Adds a setUpAll callback to the list of all setUpAll callbacks.
+ ///
+ /// Returns the name under which this setUpAll callback was added.
+ String addSetUpAll(String group) {
+ // Not optimal, but good enough for now.
+
+ // Go over all groups, checking if the group is already in the list.
+ var groupIndex = 0;
+ for (final setUpAll in setUpAlls) {
+ final parts = setUpAll.split(' ');
+ final groupName = parts.sublist(0, parts.length - 1).join(' ');
+
+ if (groupName == group) {
+ groupIndex++;
+ }
+ }
+
+ final name = '$group $groupIndex';
+
+ setUpAlls.add(name);
+ return name;
}
/// Marks [dartFileName] as completed with the given [passed] status.
@@ -84,19 +129,20 @@ class PatrolAppService extends PatrolAppServiceServer {
required String? details,
}) async {
print('PatrolAppService.markDartTestAsCompleted(): $dartFileName');
+
assert(
- _testExecutionRequested.isCompleted,
+ _didRequestTestExecution.isCompleted,
'Tried to mark a test as completed, but no tests were requested to run',
);
- final requestedDartTestName = await testExecutionRequested;
+ final requestedDartTestName = await didRequestTestExecution;
assert(
requestedDartTestName == dartFileName,
'Tried to mark test $dartFileName as completed, but the test '
'that was most recently requested to run was $requestedDartTestName',
);
- _testExecutionCompleted.complete(
+ _didCompleteTestExecution.complete(
_TestExecutionResult(passed: passed, details: details),
);
}
@@ -113,7 +159,7 @@ class PatrolAppService extends PatrolAppServiceServer {
Future waitForExecutionRequest(String dartTest) async {
print('PatrolAppService: registered "$dartTest"');
- final requestedDartTest = await testExecutionRequested;
+ final requestedDartTest = await didRequestTestExecution;
if (requestedDartTest != dartTest) {
// If the requested Dart test is not the one we're waiting for now, it
// means that dartTest was already executed. Return false so that callers
@@ -134,18 +180,26 @@ class PatrolAppService extends PatrolAppServiceServer {
@override
Future listDartTests() async {
print('PatrolAppService.listDartTests() called');
+
+ final topLevelDartTestGroup = this.topLevelDartTestGroup;
+ if (topLevelDartTestGroup == null) {
+ throw StateError(
+ 'PatrolAppService.listDartTests(): tests not discovered yet',
+ );
+ }
+
return ListDartTestsResponse(group: topLevelDartTestGroup);
}
@override
Future runDartTest(RunDartTestRequest request) async {
- assert(_testExecutionCompleted.isCompleted == false);
- // patrolTest() always calls this method.
-
print('PatrolAppService.runDartTest(${request.name}) called');
- _testExecutionRequested.complete(request.name);
- final testExecutionResult = await testExecutionCompleted;
+ assert(_didCompleteTestExecution.isCompleted == false);
+
+ _didRequestTestExecution.complete(request.name);
+
+ final testExecutionResult = await didCompleteTestExecution;
return RunDartTestResponse(
result: testExecutionResult.passed
? RunDartTestResponseResult.success
@@ -153,4 +207,32 @@ class PatrolAppService extends PatrolAppServiceServer {
details: testExecutionResult.details,
);
}
+
+ @override
+ Future
+ listDartLifecycleCallbacks() async {
+ print('PatrolAppService.listDartLifecycleCallbacks() called');
+
+ assert(_didRequestTestExecution.isCompleted == false);
+ assert(_didCompleteTestExecution.isCompleted == false);
+ assert(_didReceiveLifecycleCallbacksState.isCompleted == false);
+
+ return ListDartLifecycleCallbacksResponse(
+ setUpAlls: setUpAlls,
+ tearDownAlls: [],
+ );
+ }
+
+ @override
+ Future setLifecycleCallbacksState(
+ SetLifecycleCallbacksStateRequest request,
+ ) async {
+ print('PatrolAppService.setLifecycleCallbacksState() called');
+ assert(_didRequestTestExecution.isCompleted == false);
+ assert(_didCompleteTestExecution.isCompleted == false);
+ assert(_didReceiveLifecycleCallbacksState.isCompleted == false);
+
+ _didReceiveLifecycleCallbacksState.complete(request.state);
+ return Empty();
+ }
}
diff --git a/packages/patrol/pubspec.yaml b/packages/patrol/pubspec.yaml
index 6581c5d41..acd1c3c74 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: 3.0.3
+version: 3.1.0-dev.1
homepage: https://patrol.leancode.co
repository: https://github.com/leancodepl/patrol/tree/master/packages/patrol
issue_tracker: https://github.com/leancodepl/patrol/issues
diff --git a/packages/patrol_cli/CHANGELOG.md b/packages/patrol_cli/CHANGELOG.md
index a788f2ed6..b5efa5c88 100644
--- a/packages/patrol_cli/CHANGELOG.md
+++ b/packages/patrol_cli/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.4.0-dev.1
+
+- Add support for `patrolSetUpAll` (#1751)
+
+This version requires version 3.1.0-dev1 of `patrol` package.
+
## 2.3.1+1
- Add screenshots to `pubspec.yaml` (#1917)
diff --git a/packages/patrol_cli/lib/src/base/constants.dart b/packages/patrol_cli/lib/src/base/constants.dart
index 7ddfcd114..e199caacb 100644
--- a/packages/patrol_cli/lib/src/base/constants.dart
+++ b/packages/patrol_cli/lib/src/base/constants.dart
@@ -1,2 +1,2 @@
/// Version of Patrol CLI. Must be kept in sync with pubspec.yaml.
-const version = '2.3.1+1';
+const version = '2.4.0-dev.1';
diff --git a/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart b/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart
index 0fd7ababc..65277fae6 100644
--- a/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart
+++ b/packages/patrol_cli/lib/src/runner/patrol_command_runner.dart
@@ -10,7 +10,7 @@ import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:patrol_cli/src/analytics/analytics.dart';
import 'package:patrol_cli/src/android/android_test_backend.dart';
-import 'package:patrol_cli/src/base/constants.dart';
+import 'package:patrol_cli/src/base/constants.dart' as constants;
import 'package:patrol_cli/src/base/exceptions.dart';
import 'package:patrol_cli/src/base/logger.dart';
import 'package:patrol_cli/src/base/process.dart';
@@ -86,6 +86,7 @@ class PatrolCommandRunner extends CompletionCommandRunner {
required Analytics analytics,
required Logger logger,
required bool isCI,
+ String version = constants.version,
}) : _platform = platform,
_pubUpdater = pubUpdater,
_fs = fs,
@@ -94,6 +95,7 @@ class PatrolCommandRunner extends CompletionCommandRunner {
_disposeScope = DisposeScope(),
_logger = logger,
_isCI = isCI,
+ _version = version,
super(
'patrol',
'Tool for running Flutter-native UI tests with superpowers',
@@ -220,6 +222,7 @@ class PatrolCommandRunner extends CompletionCommandRunner {
final DisposeScope _disposeScope;
final Logger _logger;
final bool _isCI;
+ final String _version;
Future dispose() async {
try {
@@ -312,7 +315,7 @@ Ask questions, get support at https://github.com/leancodepl/patrol/discussions''
final int? exitCode;
if (topLevelResults['version'] == true) {
- _logger.info('patrol_cli v$version');
+ _logger.info('patrol_cli v$_version');
exitCode = 0;
} else {
exitCode = await super.runCommand(topLevelResults);
@@ -372,7 +375,7 @@ Ask questions, get support at https://github.com/leancodepl/patrol/discussions''
}
final latestVersion = await _pubUpdater.getLatestVersion('patrol_cli');
- final isUpToDate = version == latestVersion;
+ final isUpToDate = _version == latestVersion;
if (isUpToDate) {
return;
@@ -382,7 +385,7 @@ Ask questions, get support at https://github.com/leancodepl/patrol/discussions''
..info('')
..info(
'''
-${lightYellow.wrap('Update available!')} ${lightCyan.wrap(version)} \u2192 ${lightCyan.wrap(latestVersion)}
+${lightYellow.wrap('Update available!')} ${lightCyan.wrap(_version)} \u2192 ${lightCyan.wrap(latestVersion)}
Run ${lightCyan.wrap('patrol update')} to update''',
)
..info('');
diff --git a/packages/patrol_cli/lib/src/test_bundler.dart b/packages/patrol_cli/lib/src/test_bundler.dart
index bc8674c7a..8e39d0184 100644
--- a/packages/patrol_cli/lib/src/test_bundler.dart
+++ b/packages/patrol_cli/lib/src/test_bundler.dart
@@ -28,6 +28,7 @@ import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
+import 'package:patrol/src/global_state.dart' as global_state;
import 'package:patrol/src/native/contracts/contracts.dart';
import 'package:test_api/src/backend/invoker.dart';
@@ -70,8 +71,15 @@ Future main() async {
final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig());
await nativeAutomator.initialize();
- final binding = PatrolBinding.ensureInitialized(NativeAutomatorConfig());
- final testExplorationCompleter = Completer();
+
+ final appService = PatrolAppService();
+ await runAppService(appService);
+
+ final config = NativeAutomatorConfig();
+ PatrolBinding.ensureInitialized(appService, nativeAutomator, config);
+
+ final didExploreTests = Completer();
+ final didExploreLifecycleCallbacks = Completer();
// A special test to explore the hierarchy of groups and tests. This is a hack
// around https://github.com/dart-lang/test/issues/1998.
@@ -83,7 +91,7 @@ Future main() async {
// to group() below.
final topLevelGroup = Invoker.current!.liveTest.groups.first;
final dartTestGroup = createDartTestGroup(topLevelGroup);
- testExplorationCompleter.complete(dartTestGroup);
+ didExploreTests.complete(dartTestGroup);
print('patrol_test_explorer: obtained Dart-side test hierarchy:');
printGroupStructure(dartTestGroup);
});
@@ -92,17 +100,25 @@ Future main() async {
${generateGroupsCode(testFilePaths).split('\n').map((e) => ' $e').join('\n')}
// END: GENERATED TEST GROUPS
- final dartTestGroup = await testExplorationCompleter.future;
- final appService = PatrolAppService(topLevelDartTestGroup: dartTestGroup);
- binding.patrolAppService = appService;
- await runAppService(appService);
+ // An additional callback to discover setUpAlls.
+ tearDownAll(() async {
+ if (await global_state.isInitialRun) {
+ didExploreLifecycleCallbacks.complete();
+ }
+ });
+
+ appService.topLevelDartTestGroup = await didExploreTests.future;
+
+ if (await global_state.isInitialRun) {
+ await didExploreLifecycleCallbacks.future;
+ }
// Until now, the native test runner was waiting for us, the Dart side, to
// come alive. Now that we did, let's tell it that we're ready to be asked
// about Dart tests.
await nativeAutomator.markPatrolAppServiceReady();
- await appService.testExecutionCompleted;
+ await appService.didCompleteTestExecution;
}
''';
@@ -139,7 +155,12 @@ ${generateImports([testFilePath])}
Future main() async {
final nativeAutomator = NativeAutomator(config: NativeAutomatorConfig());
await nativeAutomator.initialize();
- PatrolBinding.ensureInitialized(NativeAutomatorConfig())
+
+ final appService = PatrolAppService();
+ await runAppService(appService);
+
+ final config = NativeAutomatorConfig();
+ PatrolBinding.ensureInitialized(appService, nativeAutomator, config)
..workaroundDebugDefaultTargetPlatformOverride =
debugDefaultTargetPlatformOverride;
diff --git a/packages/patrol_cli/lib/src/test_runner.dart b/packages/patrol_cli/lib/src/test_runner.dart
index cf96acd44..74e3df8d5 100644
--- a/packages/patrol_cli/lib/src/test_runner.dart
+++ b/packages/patrol_cli/lib/src/test_runner.dart
@@ -3,7 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:path/path.dart' show basename;
import 'package:patrol_cli/src/devices.dart';
-// TODO(bartekpacia): Lots of this code is not needed after #1004 is done.
+// TODO: Lots of this code is not needed after #1004 is done.
enum TargetRunStatus { failedToBuild, failedToExecute, passed, canceled }
diff --git a/packages/patrol_cli/pubspec.yaml b/packages/patrol_cli/pubspec.yaml
index 902d17ee4..1bb759525 100644
--- a/packages/patrol_cli/pubspec.yaml
+++ b/packages/patrol_cli/pubspec.yaml
@@ -1,7 +1,7 @@
name: patrol_cli
description: >
Command-line tool for Patrol, a powerful Flutter-native UI testing framework.
-version: 2.3.1+1 # Must be kept in sync with constants.dart
+version: 2.4.0-dev.1 # Must be kept in sync with constants.dart
homepage: https://patrol.leancode.co
repository: https://github.com/leancodepl/patrol/tree/master/packages/patrol_cli
issue_tracker: https://github.com/leancodepl/patrol/issues?q=is%3Aopen+is%3Aissue+label%3A%22package%3A+patrol_cli%22
diff --git a/packages/patrol_cli/test/commands/patrol_command_runner_test.dart b/packages/patrol_cli/test/commands/patrol_command_runner_test.dart
index 790303d49..19014a7ad 100644
--- a/packages/patrol_cli/test/commands/patrol_command_runner_test.dart
+++ b/packages/patrol_cli/test/commands/patrol_command_runner_test.dart
@@ -1,7 +1,6 @@
import 'package:args/command_runner.dart';
import 'package:file/memory.dart';
import 'package:mocktail/mocktail.dart';
-import 'package:patrol_cli/src/base/constants.dart';
import 'package:patrol_cli/src/base/extensions/command_runner.dart';
import 'package:patrol_cli/src/base/logger.dart';
import 'package:patrol_cli/src/runner/patrol_command_runner.dart';
@@ -12,10 +11,11 @@ import 'package:test/test.dart';
import '../ios/ios_test_backend_test.dart';
import '../src/mocks.dart';
-const latestVersion = '0.0.0';
+const currentVersion = '1.2.0';
+const latestVersion = '1.3.0';
final updatePrompt = '''
-${lightYellow.wrap('Update available!')} ${lightCyan.wrap(version)} \u2192 ${lightCyan.wrap(latestVersion)}
+${lightYellow.wrap('Update available!')} ${lightCyan.wrap(currentVersion)} \u2192 ${lightCyan.wrap(latestVersion)}
Run ${lightCyan.wrap('patrol update')} to update''';
void main() {
@@ -31,16 +31,17 @@ void main() {
when(
() => pubUpdater.getLatestVersion(any()),
- ).thenAnswer((_) async => version);
+ ).thenAnswer((_) async => currentVersion);
commandRunner = PatrolCommandRunner(
- platform: FakePlatform(),
+ platform: FakePlatform(environment: {}),
processManager: FakeProcessManager(),
pubUpdater: pubUpdater,
fs: MemoryFileSystem.test(),
analytics: MockAnalytics(),
logger: logger,
isCI: false,
+ version: currentVersion,
);
});
@@ -91,7 +92,7 @@ void main() {
});
test(
- 'prints error message and usage when command option is passed',
+ 'prints error message and usage when invalid command is invoked',
() async {
final result = await commandRunner.run(['foo']);
expect(result, equals(1));
@@ -122,7 +123,7 @@ void main() {
test('prints current version', () async {
final result = await commandRunner.run(['--version']);
expect(result, equals(0));
- verify(() => logger.info('patrol_cli v$version')).called(1);
+ verify(() => logger.info('patrol_cli v$currentVersion')).called(1);
});
});
diff --git a/packages/patrol_gen/.gitignore b/packages/patrol_gen/.gitignore
new file mode 100644
index 000000000..e5208175c
--- /dev/null
+++ b/packages/patrol_gen/.gitignore
@@ -0,0 +1,3 @@
+# Omit committing pubspec.lock for library packages; see
+# https://dart.dev/guides/libraries/private-files#pubspeclock.
+pubspec.lock
diff --git a/packages/patrol_gen/pubspec.lock b/packages/patrol_gen/pubspec.lock
deleted file mode 100644
index 8651b828f..000000000
--- a/packages/patrol_gen/pubspec.lock
+++ /dev/null
@@ -1,349 +0,0 @@
-# Generated by pub
-# See https://dart.dev/tools/pub/glossary#lockfile
-packages:
- _fe_analyzer_shared:
- dependency: transitive
- description:
- name: _fe_analyzer_shared
- sha256: "36a321c3d2cbe01cbcb3540a87b8843846e0206df3e691fa7b23e19e78de6d49"
- url: "https://pub.dev"
- source: hosted
- version: "65.0.0"
- analyzer:
- dependency: "direct main"
- description:
- name: analyzer
- sha256: dfe03b90ec022450e22513b5e5ca1f01c0c01de9c3fba2f7fd233cb57a6b9a07
- url: "https://pub.dev"
- source: hosted
- version: "6.3.0"
- analyzer_plugin:
- dependency: transitive
- description:
- name: analyzer_plugin
- sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
- url: "https://pub.dev"
- source: hosted
- version: "0.11.3"
- args:
- dependency: transitive
- description:
- name: args
- sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
- url: "https://pub.dev"
- source: hosted
- version: "2.4.2"
- async:
- dependency: transitive
- description:
- name: async
- sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
- url: "https://pub.dev"
- source: hosted
- version: "2.11.0"
- boolean_selector:
- dependency: transitive
- description:
- name: boolean_selector
- sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.1"
- checked_yaml:
- dependency: transitive
- description:
- name: checked_yaml
- sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
- url: "https://pub.dev"
- source: hosted
- version: "2.0.3"
- ci:
- dependency: transitive
- description:
- name: ci
- sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
- url: "https://pub.dev"
- source: hosted
- version: "0.1.0"
- cli_util:
- dependency: transitive
- description:
- name: cli_util
- sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
- url: "https://pub.dev"
- source: hosted
- version: "0.4.0"
- collection:
- dependency: transitive
- description:
- name: collection
- sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
- url: "https://pub.dev"
- source: hosted
- version: "1.18.0"
- convert:
- dependency: transitive
- description:
- name: convert
- sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
- url: "https://pub.dev"
- source: hosted
- version: "3.1.1"
- crypto:
- dependency: transitive
- description:
- name: crypto
- sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
- url: "https://pub.dev"
- source: hosted
- version: "3.0.3"
- custom_lint:
- dependency: transitive
- description:
- name: custom_lint
- sha256: f9a828b696930cf8307f9a3617b2b65c9b370e484dc845d69100cadb77506778
- url: "https://pub.dev"
- source: hosted
- version: "0.5.6"
- custom_lint_builder:
- dependency: transitive
- description:
- name: custom_lint_builder
- sha256: c6f656a4d83385fc0656ae60410ed06bb382898c45627bfb8bbaa323aea97883
- url: "https://pub.dev"
- source: hosted
- version: "0.5.6"
- custom_lint_core:
- dependency: transitive
- description:
- name: custom_lint_core
- sha256: e20a67737adcf0cf2465e734dd624af535add11f9edd1f2d444909b5b0749650
- url: "https://pub.dev"
- source: hosted
- version: "0.5.6"
- dart_style:
- dependency: "direct main"
- description:
- name: dart_style
- sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334
- url: "https://pub.dev"
- source: hosted
- version: "2.3.3"
- file:
- dependency: transitive
- description:
- name: file
- sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
- url: "https://pub.dev"
- source: hosted
- version: "7.0.0"
- freezed_annotation:
- dependency: transitive
- description:
- name: freezed_annotation
- sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
- url: "https://pub.dev"
- source: hosted
- version: "2.4.1"
- glob:
- dependency: transitive
- description:
- name: glob
- sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.2"
- hotreloader:
- dependency: transitive
- description:
- name: hotreloader
- sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449"
- url: "https://pub.dev"
- source: hosted
- version: "4.1.0"
- json_annotation:
- dependency: transitive
- description:
- name: json_annotation
- sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
- url: "https://pub.dev"
- source: hosted
- version: "4.8.1"
- leancode_lint:
- dependency: "direct dev"
- description:
- name: leancode_lint
- sha256: "1e99cba16e084a18ce966a7df270d6da6a11ab236caac716aa1fb2359eb277eb"
- url: "https://pub.dev"
- source: hosted
- version: "7.0.0+1"
- logging:
- dependency: transitive
- description:
- name: logging
- sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- matcher:
- dependency: transitive
- description:
- name: matcher
- sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
- url: "https://pub.dev"
- source: hosted
- version: "0.12.16"
- meta:
- dependency: "direct main"
- description:
- name: meta
- sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
- url: "https://pub.dev"
- source: hosted
- version: "1.11.0"
- package_config:
- dependency: transitive
- description:
- name: package_config
- sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.0"
- path:
- dependency: "direct main"
- description:
- name: path
- sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
- url: "https://pub.dev"
- source: hosted
- version: "1.8.3"
- pub_semver:
- dependency: transitive
- description:
- name: pub_semver
- sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.4"
- pubspec_parse:
- dependency: transitive
- description:
- name: pubspec_parse
- sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367
- url: "https://pub.dev"
- source: hosted
- version: "1.2.3"
- rxdart:
- dependency: transitive
- description:
- name: rxdart
- sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
- url: "https://pub.dev"
- source: hosted
- version: "0.27.7"
- source_span:
- dependency: transitive
- description:
- name: source_span
- sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
- url: "https://pub.dev"
- source: hosted
- version: "1.10.0"
- sprintf:
- dependency: transitive
- description:
- name: sprintf
- sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
- url: "https://pub.dev"
- source: hosted
- version: "7.0.0"
- stack_trace:
- dependency: transitive
- description:
- name: stack_trace
- sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
- url: "https://pub.dev"
- source: hosted
- version: "1.11.1"
- stream_channel:
- dependency: transitive
- description:
- name: stream_channel
- sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
- url: "https://pub.dev"
- source: hosted
- version: "2.1.2"
- stream_transform:
- dependency: transitive
- description:
- name: stream_transform
- sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
- url: "https://pub.dev"
- source: hosted
- version: "2.1.0"
- string_scanner:
- dependency: transitive
- description:
- name: string_scanner
- sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
- url: "https://pub.dev"
- source: hosted
- version: "1.2.0"
- term_glyph:
- dependency: transitive
- description:
- name: term_glyph
- sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
- url: "https://pub.dev"
- source: hosted
- version: "1.2.1"
- test_api:
- dependency: transitive
- description:
- name: test_api
- sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
- url: "https://pub.dev"
- source: hosted
- version: "0.6.1"
- typed_data:
- dependency: transitive
- description:
- name: typed_data
- sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
- url: "https://pub.dev"
- source: hosted
- version: "1.3.2"
- uuid:
- dependency: transitive
- description:
- name: uuid
- sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921
- url: "https://pub.dev"
- source: hosted
- version: "4.2.1"
- vm_service:
- dependency: transitive
- description:
- name: vm_service
- sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
- url: "https://pub.dev"
- source: hosted
- version: "13.0.0"
- watcher:
- dependency: transitive
- description:
- name: watcher
- sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
- url: "https://pub.dev"
- source: hosted
- version: "1.1.0"
- yaml:
- dependency: transitive
- description:
- name: yaml
- sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
- url: "https://pub.dev"
- source: hosted
- version: "3.1.2"
-sdks:
- dart: ">=3.2.0 <4.0.0"
diff --git a/schema.dart b/schema.dart
index 0a7efb8b1..4d2003a31 100644
--- a/schema.dart
+++ b/schema.dart
@@ -7,6 +7,9 @@
// - Generic types (IOSServer, IOSClient, AndroidServer, AndroidClient, DartServer, DartClient)
// control where we need clients and servers
+// void doesn't work
+class Empty {}
+
class DartGroupEntry {
late String name;
late GroupEntryType type;
@@ -19,6 +22,11 @@ class ListDartTestsResponse {
late DartGroupEntry group;
}
+class ListDartLifecycleCallbacksResponse {
+ late List setUpAlls;
+ late List tearDownAlls;
+}
+
enum RunDartTestResponseResult {
success,
skipped,
@@ -34,8 +42,14 @@ class RunDartTestResponse {
String? details;
}
+class SetLifecycleCallbacksStateRequest {
+ late Map state;
+}
+
abstract class PatrolAppService {
ListDartTestsResponse listDartTests();
+ ListDartLifecycleCallbacksResponse listDartLifecycleCallbacks();
+ Empty setLifecycleCallbacksState(SetLifecycleCallbacksStateRequest request);
RunDartTestResponse runDartTest(RunDartTestRequest request);
}
@@ -174,6 +188,10 @@ class SetLocationAccuracyRequest {
late SetLocationAccuracyRequestLocationAccuracy locationAccuracy;
}
+class MarkLifecycleCallbackExecutedRequest {
+ late String name;
+}
+
abstract class NativeAutomator {
void initialize();
void configure(ConfigureRequest request);
@@ -223,6 +241,8 @@ abstract class NativeAutomator {
// other
void debug();
-// TODO(bartekpacia): Move this RPC into a new PatrolNativeTestService service because it doesn't fit here
+// TODO: Move these RPCc into a new service (PatrolNativeTestService) because it doesn't fit here
void markPatrolAppServiceReady();
+ void markLifecycleCallbackExecuted(
+ MarkLifecycleCallbackExecutedRequest request);
}