Refactor iOS integration_test API to support Swift, dynamically add native tests (#88013)
diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md
index 0837112..f9d01ef 100644
--- a/packages/integration_test/README.md
+++ b/packages/integration_test/README.md
@@ -281,14 +281,27 @@
flutter build ios --config-only integration_test/foo_test.dart
```
-In Xcode, add a test file called `RunnerTests.m` (or any name of your choice) to the new target and
+In Xcode, add a test file called `RunnerTests.m` or `RunnerTests.swift` (or any name of your choice) to the new target and
replace the file:
```objective-c
@import XCTest;
@import integration_test;
-INTEGRATION_TEST_IOS_RUNNER(RunnerTests)
+@interface RunnerTests : FLTIntegrationTestCase
+@end
+
+@implementation RunnerTests
+@end
+```
+or in Swift:
+````swift
+import integration_test
+import XCTest
+
+class RunnerSwiftTests: FLTIntegrationTestCase {
+}
+
```
Run `Product > Test` to run the integration tests on your selected device.
diff --git a/packages/integration_test/example/integration_test/_extended_test_io.dart b/packages/integration_test/example/integration_test/_extended_test_io.dart
index 377aa42..8c2456a 100644
--- a/packages/integration_test/example/integration_test/_extended_test_io.dart
+++ b/packages/integration_test/example/integration_test/_extended_test_io.dart
@@ -25,6 +25,24 @@
// Build our app.
app.main();
+ // Pump a frame.
+ await tester.pumpAndSettle();
+
+ // Verify that platform version is retrieved.
+ expect(
+ find.byWidgetPredicate(
+ (Widget widget) =>
+ widget is Text &&
+ widget.data!.startsWith('Platform: ${Platform.operatingSystem}'),
+ ),
+ findsOneWidget,
+ );
+ });
+
+ testWidgets('verify screenshot', (WidgetTester tester) async {
+ // Build our app.
+ app.main();
+
// On Android, this is required prior to taking the screenshot.
await binding.convertFlutterSurfaceToImage();
@@ -39,15 +57,5 @@
expect(secondPng.isNotEmpty, isTrue);
expect(listEquals(firstPng, secondPng), isTrue);
-
- // Verify that platform version is retrieved.
- expect(
- find.byWidgetPredicate(
- (Widget widget) =>
- widget is Text &&
- widget.data!.startsWith('Platform: ${Platform.operatingSystem}'),
- ),
- findsOneWidget,
- );
});
}
diff --git a/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj b/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj
index a519162..d3b7c24 100644
--- a/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj
@@ -10,13 +10,14 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
4DB404AC7CF2C89658A01173 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BF64028CE7AE2E6196250D /* libPods-RunnerTests.a */; };
- 769541CB23A0351900E5C350 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 769541CA23A0351900E5C350 /* RunnerTests.m */; };
+ 769541CB23A0351900E5C350 /* RunnerObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 769541CA23A0351900E5C350 /* RunnerObjCTests.m */; };
978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C2A5EDF11F4FDBF3ABFD7006 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 625A5A90428602E25C0DE2F6 /* libPods-Runner.a */; };
+ F77B951926C3504400F785B3 /* RunnerSwiftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F77B951826C3504400F785B3 /* RunnerSwiftTests.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -52,7 +53,7 @@
750225973AAB5D7832AFA60C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
769541BF23A0337200E5C350 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
769541C823A0351900E5C350 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 769541CA23A0351900E5C350 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = "<group>"; };
+ 769541CA23A0351900E5C350 /* RunnerObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerObjCTests.m; sourceTree = "<group>"; };
769541CC23A0351900E5C350 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
@@ -68,6 +69,7 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D69CCAD5F82E76E2E22BFA96 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
E23EF4D45DAE46B9DDB9B445 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
+ F77B951826C3504400F785B3 /* RunnerSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerSwiftTests.swift; sourceTree = "<group>"; };
FCE3953801588FC13ED9E898 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -104,7 +106,8 @@
769541C923A0351900E5C350 /* RunnerTests */ = {
isa = PBXGroup;
children = (
- 769541CA23A0351900E5C350 /* RunnerTests.m */,
+ 769541CA23A0351900E5C350 /* RunnerObjCTests.m */,
+ F77B951826C3504400F785B3 /* RunnerSwiftTests.swift */,
769541CC23A0351900E5C350 /* Info.plist */,
);
path = RunnerTests;
@@ -233,6 +236,7 @@
TargetAttributes = {
769541C723A0351900E5C350 = {
CreatedOnToolsVersion = 11.0;
+ LastSwiftMigration = 1300;
ProvisioningStyle = Automatic;
TestTargetID = 97C146ED1CF9000F007C117D;
};
@@ -361,7 +365,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 769541CB23A0351900E5C350 /* RunnerTests.m in Sources */,
+ 769541CB23A0351900E5C350 /* RunnerObjCTests.m in Sources */,
+ F77B951926C3504400F785B3 /* RunnerSwiftTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -475,11 +480,14 @@
baseConfigurationReference = 09505407E99803EF7AA92DE7 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = RunnerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
};
name = Debug;
@@ -489,11 +497,13 @@
baseConfigurationReference = FCE3953801588FC13ED9E898 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = RunnerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
};
name = Release;
@@ -503,11 +513,13 @@
baseConfigurationReference = 750225973AAB5D7832AFA60C /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = RunnerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
};
name = Profile;
diff --git a/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index 72fa146..ea00904 100644
--- a/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -20,6 +20,20 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
+ <BuildActionEntry
+ buildForTesting = "YES"
+ buildForRunning = "NO"
+ buildForProfiling = "NO"
+ buildForArchiving = "NO"
+ buildForAnalyzing = "YES">
+ <BuildableReference
+ BuildableIdentifier = "primary"
+ BlueprintIdentifier = "769541C723A0351900E5C350"
+ BuildableName = "RunnerTests.xctest"
+ BlueprintName = "RunnerTests"
+ ReferencedContainer = "container:Runner.xcodeproj">
+ </BuildableReference>
+ </BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
diff --git a/packages/integration_test/example/ios/RunnerTests/RunnerObjCTests.m b/packages/integration_test/example/ios/RunnerTests/RunnerObjCTests.m
new file mode 100644
index 0000000..39dce98
--- /dev/null
+++ b/packages/integration_test/example/ios/RunnerTests/RunnerObjCTests.m
@@ -0,0 +1,49 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@import integration_test;
+@import XCTest;
+
+// Test without macro.
+@interface RunnerObjCTests : FLTIntegrationTestCase
+@end
+
+@implementation RunnerObjCTests
+
++ (NSArray<NSInvocation *> *)testInvocations {
+ // Add a test to verify the Flutter dart tests have been dynamically added to this test case.
+ SEL selector = @selector(testDynamicTestMethods);
+ NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+ invocation.selector = selector;
+
+ return [super.testInvocations arrayByAddingObject:invocation];
+}
+
+- (void)testDynamicTestMethods {
+ XCTAssertTrue([self respondsToSelector:NSSelectorFromString(@"testVerifyScreenshot")]);
+ XCTAssertTrue([self respondsToSelector:NSSelectorFromString(@"testVerifyText")]);
+ XCTAssertTrue([self respondsToSelector:NSSelectorFromString(@"screenshotPlaceholder")]);
+}
+
+@end
+
+// Test deprecated macro. Do not use.
+INTEGRATION_TEST_IOS_RUNNER(RunnerObjCMacroTests)
+
+@interface DeprecatedIntegrationTestIosTests : XCTestCase
+@end
+
+@implementation DeprecatedIntegrationTestIosTests
+
+- (void)testIntegrationTest {
+ NSString *testResult;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+ BOOL testPass = [[IntegrationTestIosTest new] testIntegrationTest:&testResult];
+#pragma clang diagnostic pop
+ XCTAssertTrue(testPass, @"%@", testResult);
+}
+
+@end
diff --git a/packages/integration_test/example/ios/RunnerTests/RunnerTests.m b/packages/integration_test/example/ios/RunnerTests/RunnerSwiftTests.swift
similarity index 64%
rename from packages/integration_test/example/ios/RunnerTests/RunnerTests.m
rename to packages/integration_test/example/ios/RunnerTests/RunnerSwiftTests.swift
index edd7f10..433f828 100644
--- a/packages/integration_test/example/ios/RunnerTests/RunnerTests.m
+++ b/packages/integration_test/example/ios/RunnerTests/RunnerSwiftTests.swift
@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-@import XCTest;
-@import integration_test;
+import integration_test
+import XCTest
-INTEGRATION_TEST_IOS_RUNNER(RunnerTests)
+class RunnerSwiftTests: FLTIntegrationTestCase {
+}
diff --git a/packages/integration_test/ios/Classes/FLTIntegrationTestCase.h b/packages/integration_test/ios/Classes/FLTIntegrationTestCase.h
new file mode 100644
index 0000000..64a6ac6
--- /dev/null
+++ b/packages/integration_test/ios/Classes/FLTIntegrationTestCase.h
@@ -0,0 +1,27 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// XCTest is weakly linked.
+#if __has_include(<XCTest/XCTest.h>)
+
+@import XCTest;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FLTIntegrationTestCase : XCTestCase
+@end
+
+/*!
+ Deprecated. Prefer directly inheriting from @c FLTIntegrationTestCase
+ */
+#define INTEGRATION_TEST_IOS_RUNNER(__test_class) \
+ @interface __test_class : FLTIntegrationTestCase \
+ @end \
+ \
+ @implementation __test_class \
+ @end
+
+NS_ASSUME_NONNULL_END
+
+#endif
diff --git a/packages/integration_test/ios/Classes/FLTIntegrationTestCase.m b/packages/integration_test/ios/Classes/FLTIntegrationTestCase.m
new file mode 100644
index 0000000..e75bcf8
--- /dev/null
+++ b/packages/integration_test/ios/Classes/FLTIntegrationTestCase.m
@@ -0,0 +1,75 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// XCTest is weakly linked.
+#if __has_include(<XCTest/XCTest.h>)
+
+#import "FLTIntegrationTestCase.h"
+
+#import "FLTIntegrationTestRunner.h"
+#import "IntegrationTestPlugin.h"
+
+@import ObjectiveC.runtime;
+@import XCTest;
+
+@implementation FLTIntegrationTestCase
+
++ (NSArray<NSInvocation *> *)testInvocations {
+ if (self == [FLTIntegrationTestCase class]) {
+ // Do not add any tests for this base class.
+ return @[];
+ }
+ FLTIntegrationTestRunner *integrationTestRunner = [FLTIntegrationTestRunner new];
+ NSMutableArray<NSInvocation *> *testInvocations = [NSMutableArray new];
+ [integrationTestRunner testIntegrationTestWithResults:^(NSString *testName, BOOL success, NSString *failureMessage) {
+ // For every Flutter dart test, dynamically generate an Objective-C method mirroring the test results
+ // so it is reported as a native XCTest run result.
+ IMP assertImplementation = imp_implementationWithBlock(^(id _self) {
+ XCTAssertTrue(success, @"%@", failureMessage);
+ });
+
+ // Create an appropriate XCTest method name based on the dart test name.
+ // Example: dart test "verify widget" becomes "testVerifyWidget"
+ NSString *upperCamelTestName = [testName.localizedCapitalizedString stringByReplacingOccurrencesOfString:@" " withString:@""];
+ NSString *testSelectorName = [NSString stringWithFormat:@"test%@", upperCamelTestName];
+ SEL testSelector = NSSelectorFromString(testSelectorName);
+ class_addMethod(self, testSelector, assertImplementation, "v@:");
+
+ // Add the new class method as a test invocation to the XCTestCase.
+ NSMethodSignature *signature = [self instanceMethodSignatureForSelector:testSelector];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
+ invocation.selector = testSelector;
+
+ [testInvocations addObject:invocation];
+ }];
+
+ NSDictionary<NSString *, UIImage *> *capturedScreenshotsByName = integrationTestRunner.capturedScreenshotsByName;
+ if (capturedScreenshotsByName.count > 0) {
+ // If the Flutter dart tests have captured screenshots, add them to the XCTest bundle.
+ IMP screenshotImplementation = imp_implementationWithBlock(^(id _self) {
+ [capturedScreenshotsByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, UIImage *screenshot, BOOL *stop) {
+ XCTAttachment *attachment = [XCTAttachment attachmentWithImage:screenshot];
+ attachment.lifetime = XCTAttachmentLifetimeKeepAlways;
+ if (name != nil) {
+ attachment.name = name;
+ }
+ [_self addAttachment:attachment];
+ }];
+ });
+
+ SEL attachmentSelector = NSSelectorFromString(@"screenshotPlaceholder");
+ class_addMethod(self, attachmentSelector, screenshotImplementation, "v@:");
+
+ NSMethodSignature *attachmentSignature = [self instanceMethodSignatureForSelector:attachmentSelector];
+ NSInvocation *attachmentInvocation = [NSInvocation invocationWithMethodSignature:attachmentSignature];
+ attachmentInvocation.selector = attachmentSelector;
+
+ [testInvocations addObject:attachmentInvocation];
+ }
+ return testInvocations;
+}
+
+@end
+
+#endif
diff --git a/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.h b/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.h
new file mode 100644
index 0000000..3b0407b
--- /dev/null
+++ b/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.h
@@ -0,0 +1,43 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+@import Foundation;
+
+@class UIImage;
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef void (^FLTIntegrationTestResults)(NSString *testName, BOOL success, NSString *_Nullable failureMessage);
+
+
+@interface FLTIntegrationTestRunner : NSObject
+
+/**
+ * Any screenshots captured by the plugin.
+ */
+@property (copy, readonly) NSDictionary<NSString *, UIImage *> *capturedScreenshotsByName;
+
+/*!
+ Start dart tests and wait for results.
+
+ @param testResult Will be called once per every completed dart test.
+ */
+- (void)testIntegrationTestWithResults:(NS_NOESCAPE FLTIntegrationTestResults)testResult;
+
+@end
+
+DEPRECATED_MSG_ATTRIBUTE("Use FLTIntegrationTestRunner instead.")
+@interface IntegrationTestIosTest : NSObject
+
+/*!
+ Initate dart tests and wait for results.
+
+ @param testResult Will be set to a string describing the results.
+ @returns @c YES if all tests succeeded.
+ */
+- (BOOL)testIntegrationTest:(NSString *_Nullable *_Nullable)testResult;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.m b/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.m
new file mode 100644
index 0000000..766102f
--- /dev/null
+++ b/packages/integration_test/ios/Classes/FLTIntegrationTestRunner.m
@@ -0,0 +1,86 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "FLTIntegrationTestRunner.h"
+
+#import "IntegrationTestPlugin.h"
+
+@import UIKit;
+
+@interface FLTIntegrationTestRunner ()
+
+@property IntegrationTestPlugin *integrationTestPlugin;
+
+@end
+
+@implementation FLTIntegrationTestRunner
+
+- (instancetype)init {
+ self = [super init];
+ _integrationTestPlugin = [IntegrationTestPlugin instance];
+
+ return self;
+}
+
+- (void)testIntegrationTestWithResults:(NS_NOESCAPE FLTIntegrationTestResults)testResult {
+ IntegrationTestPlugin *integrationTestPlugin = self.integrationTestPlugin;
+ UIViewController *rootViewController = UIApplication.sharedApplication.delegate.window.rootViewController;
+ if (![rootViewController isKindOfClass:[FlutterViewController class]]) {
+ testResult(@"setup", NO, @"rootViewController was not expected FlutterViewController");
+ }
+ FlutterViewController *flutterViewController = (FlutterViewController *)rootViewController;
+ [integrationTestPlugin setupChannels:flutterViewController.engine.binaryMessenger];
+
+ // Spin the runloop.
+ while (!integrationTestPlugin.testResults) {
+ [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
+ }
+
+ [integrationTestPlugin.testResults enumerateKeysAndObjectsUsingBlock:^(NSString *test, NSString *result, BOOL *stop) {
+ if ([result isEqualToString:@"success"]) {
+ testResult(test, YES, nil);
+ } else {
+ testResult(test, NO, result);
+ }
+ }];
+}
+
+- (NSDictionary<NSString *,UIImage *> *)capturedScreenshotsByName {
+ return self.integrationTestPlugin.capturedScreenshotsByName;
+}
+
+@end
+
+#pragma mark - Deprecated
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-implementations"
+
+@implementation IntegrationTestIosTest
+
+- (BOOL)testIntegrationTest:(NSString **)testResult {
+ NSLog(@"==================== Test Results =====================");
+ NSMutableArray<NSString *> *failedTests = [NSMutableArray array];
+ NSMutableArray<NSString *> *testNames = [NSMutableArray array];
+ [[FLTIntegrationTestRunner new] testIntegrationTestWithResults:^(NSString *testName, BOOL success, NSString *message) {
+ [testNames addObject:testName];
+ if (success) {
+ NSLog(@"%@ passed.", testName);
+ } else {
+ NSLog(@"%@ failed: %@", testName, message);
+ [failedTests addObject:testName];
+ }
+ }];
+ NSLog(@"================== Test Results End ====================");
+ BOOL testPass = failedTests.count == 0;
+ if (!testPass && testResult != NULL) {
+ *testResult =
+ [NSString stringWithFormat:@"Detected failed integration test(s) %@ among %@",
+ failedTests.description, testNames.description];
+ }
+ return testPass;
+}
+
+@end
+#pragma clang diagnostic pop
diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.h b/packages/integration_test/ios/Classes/IntegrationTestIosTest.h
deleted file mode 100644
index 333b0ec..0000000
--- a/packages/integration_test/ios/Classes/IntegrationTestIosTest.h
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2014 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import <Foundation/Foundation.h>
-
-NS_ASSUME_NONNULL_BEGIN
-
-@protocol FLTIntegrationTestScreenshotDelegate;
-
-@interface IntegrationTestIosTest : NSObject
-
-- (instancetype)initWithScreenshotDelegate:(nullable id<FLTIntegrationTestScreenshotDelegate>)delegate NS_DESIGNATED_INITIALIZER;
-
-/**
- * Initate dart tests and wait for results. @c testResult will be set to a string describing the results.
- *
- * @return @c YES if all tests succeeded.
- */
-- (BOOL)testIntegrationTest:(NSString *_Nullable *_Nullable)testResult;
-
-@end
-
-#define INTEGRATION_TEST_IOS_RUNNER(__test_class) \
- @interface __test_class : XCTestCase<FLTIntegrationTestScreenshotDelegate> \
- @end \
- \
- @implementation __test_class \
- \
- - (void)testIntegrationTest { \
- NSString *testResult; \
- IntegrationTestIosTest *integrationTestIosTest = integrationTestIosTest = [[IntegrationTestIosTest alloc] initWithScreenshotDelegate:self]; \
- BOOL testPass = [integrationTestIosTest testIntegrationTest:&testResult]; \
- XCTAssertTrue(testPass, @"%@", testResult); \
- } \
- \
- - (void)didTakeScreenshot:(UIImage *)screenshot attachmentName:(NSString *)name { \
- XCTAttachment *attachment = [XCTAttachment attachmentWithImage:screenshot]; \
- attachment.lifetime = XCTAttachmentLifetimeKeepAlways; \
- if (name != nil) { \
- attachment.name = name; \
- } \
- [self addAttachment:attachment]; \
- } \
- \
- @end
-
-NS_ASSUME_NONNULL_END
diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m b/packages/integration_test/ios/Classes/IntegrationTestIosTest.m
deleted file mode 100644
index 6a54ed2..0000000
--- a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright 2014 The Flutter Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#import "IntegrationTestIosTest.h"
-#import "IntegrationTestPlugin.h"
-
-@interface IntegrationTestIosTest()
-@property (nonatomic) IntegrationTestPlugin *integrationTestPlugin;
-@end
-
-@implementation IntegrationTestIosTest
-
-- (instancetype)initWithScreenshotDelegate:(id<FLTIntegrationTestScreenshotDelegate>)delegate {
- self = [super init];
- _integrationTestPlugin = [IntegrationTestPlugin instance];
- _integrationTestPlugin.screenshotDelegate = delegate;
- return self;
-}
-
-- (instancetype)init {
- return [self initWithScreenshotDelegate:nil];
-}
-
-- (BOOL)testIntegrationTest:(NSString **)testResult {
- IntegrationTestPlugin *integrationTestPlugin = self.integrationTestPlugin;
-
- UIViewController *rootViewController =
- [[[[UIApplication sharedApplication] delegate] window] rootViewController];
- if (![rootViewController isKindOfClass:[FlutterViewController class]]) {
- NSLog(@"expected FlutterViewController as rootViewController.");
- return NO;
- }
- FlutterViewController *flutterViewController = (FlutterViewController *)rootViewController;
- [integrationTestPlugin setupChannels:flutterViewController.engine.binaryMessenger];
- while (!integrationTestPlugin.testResults) {
- CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.f, NO);
- }
- NSDictionary<NSString *, NSString *> *testResults = integrationTestPlugin.testResults;
- NSMutableArray<NSString *> *passedTests = [NSMutableArray array];
- NSMutableArray<NSString *> *failedTests = [NSMutableArray array];
- NSLog(@"==================== Test Results =====================");
- for (NSString *test in testResults.allKeys) {
- NSString *result = testResults[test];
- if ([result isEqualToString:@"success"]) {
- NSLog(@"%@ passed.", test);
- [passedTests addObject:test];
- } else {
- NSLog(@"%@ failed: %@", test, result);
- [failedTests addObject:test];
- }
- }
- NSLog(@"================== Test Results End ====================");
- BOOL testPass = failedTests.count == 0;
- if (!testPass && testResult) {
- *testResult =
- [NSString stringWithFormat:@"Detected failed integration test(s) %@ among %@",
- failedTests.description, testResults.allKeys.description];
- }
- return testPass;
-}
-
-@end
diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.h b/packages/integration_test/ios/Classes/IntegrationTestPlugin.h
index 9684835..4836339 100644
--- a/packages/integration_test/ios/Classes/IntegrationTestPlugin.h
+++ b/packages/integration_test/ios/Classes/IntegrationTestPlugin.h
@@ -6,13 +6,6 @@
NS_ASSUME_NONNULL_BEGIN
-@protocol FLTIntegrationTestScreenshotDelegate
-
-/** This will be called when a dart integration test triggers a window screenshot with @c takeScreenshot. */
-- (void)didTakeScreenshot:(UIImage *)screenshot attachmentName:(nullable NSString *)name;
-
-@end
-
/** A Flutter plugin that's responsible for communicating the test results back
* to iOS XCTest. */
@interface IntegrationTestPlugin : NSObject <FlutterPlugin>
@@ -23,6 +16,11 @@
*/
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *testResults;
+/**
+ * Mapping of screenshot images by suggested names, captured by the dart tests.
+ */
+@property (copy, readonly) NSDictionary<NSString *, UIImage *> *capturedScreenshotsByName;
+
/** Fetches the singleton instance of the plugin. */
+ (IntegrationTestPlugin *)instance;
@@ -30,8 +28,6 @@
- (instancetype)init NS_UNAVAILABLE;
-@property(weak, nonatomic) id<FLTIntegrationTestScreenshotDelegate> screenshotDelegate;
-
@end
NS_ASSUME_NONNULL_END
diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m b/packages/integration_test/ios/Classes/IntegrationTestPlugin.m
index 82d2635..a8a80b6 100644
--- a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m
+++ b/packages/integration_test/ios/Classes/IntegrationTestPlugin.m
@@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-@import UIKit;
-
#import "IntegrationTestPlugin.h"
+@import UIKit;
+
static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integration_test";
static NSString *const kMethodTestFinished = @"allTestsFinished";
static NSString *const kMethodScreenshot = @"captureScreenshot";
@@ -16,10 +16,13 @@
@property(nonatomic, readwrite) NSDictionary<NSString *, NSString *> *testResults;
+- (instancetype)init NS_DESIGNATED_INITIALIZER;
+
@end
@implementation IntegrationTestPlugin {
NSDictionary<NSString *, NSString *> *_testResults;
+ NSMutableDictionary<NSString *, UIImage *> *_capturedScreenshotsByName;
}
+ (IntegrationTestPlugin *)instance {
@@ -32,7 +35,13 @@
}
- (instancetype)initForRegistration {
- return [super init];
+ return [self init];
+}
+
+- (instancetype)init {
+ self = [super init];
+ _capturedScreenshotsByName = [NSMutableDictionary new];
+ return self;
}
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
@@ -59,7 +68,7 @@
// If running as a native Xcode test, attach to test.
UIImage *screenshot = [self capturePngScreenshot];
NSString *name = call.arguments[@"name"];
- [self.screenshotDelegate didTakeScreenshot:screenshot attachmentName:name];
+ _capturedScreenshotsByName[name] = screenshot;
// Also pass back along the channel for the driver to handle.
NSData *pngData = UIImagePNGRepresentation(screenshot);
diff --git a/packages/integration_test/ios/integration_test.podspec b/packages/integration_test/ios/integration_test.podspec
index fb24cb0..5255073 100644
--- a/packages/integration_test/ios/integration_test.podspec
+++ b/packages/integration_test/ios/integration_test.podspec
@@ -19,7 +19,14 @@
s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter'
s.ios.framework = 'UIKit'
+ # Weakly link for parts of API that need to be run in XCTest targets.
+ s.ios.weak_framework = 'XCTest'
s.platform = :ios, '8.0'
- s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
+ # Find XCTest framework.
+ 'FRAMEWORK_SEARCH_PATHS' => '$(PLATFORM_DIR)/Developer/Library/Frameworks',
+ }
end