diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index 08371120c6..f9d01ef1c1 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -281,14 +281,27 @@ To build `integration_test/foo_test.dart` from the command line, run: 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 377aa42995..8c2456ac41 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 @@ void main() { // 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 @@ void main() { 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 a519162de3..d3b7c244bb 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 = ""; }; 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 = ""; }; + 769541CA23A0351900E5C350 /* RunnerObjCTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerObjCTests.m; sourceTree = ""; }; 769541CC23A0351900E5C350 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; @@ -68,6 +69,7 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; + F77B951826C3504400F785B3 /* RunnerSwiftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerSwiftTests.swift; sourceTree = ""; }; 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 = ""; }; /* 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 72fa1469f5..ea00904e8d 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"> + + + + *)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 65% rename from packages/integration_test/example/ios/RunnerTests/RunnerTests.m rename to packages/integration_test/example/ios/RunnerTests/RunnerSwiftTests.swift index edd7f102c7..433f828856 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 0000000000..64a6ac6fba --- /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() + +@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 0000000000..e75bcf8715 --- /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() + +#import "FLTIntegrationTestCase.h" + +#import "FLTIntegrationTestRunner.h" +#import "IntegrationTestPlugin.h" + +@import ObjectiveC.runtime; +@import XCTest; + +@implementation FLTIntegrationTestCase + ++ (NSArray *)testInvocations { + if (self == [FLTIntegrationTestCase class]) { + // Do not add any tests for this base class. + return @[]; + } + FLTIntegrationTestRunner *integrationTestRunner = [FLTIntegrationTestRunner new]; + NSMutableArray *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 *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 0000000000..3b0407b6d0 --- /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 *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 0000000000..766102f17f --- /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 *)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 *failedTests = [NSMutableArray array]; + NSMutableArray *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 333b0ece93..0000000000 --- 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 - -NS_ASSUME_NONNULL_BEGIN - -@protocol FLTIntegrationTestScreenshotDelegate; - -@interface IntegrationTestIosTest : NSObject - -- (instancetype)initWithScreenshotDelegate:(nullable id)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 \ - @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 6a54ed2c77..0000000000 --- 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)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 *testResults = integrationTestPlugin.testResults; - NSMutableArray *passedTests = [NSMutableArray array]; - NSMutableArray *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 9684835acf..4836339a5f 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 @@ -23,6 +16,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property(nonatomic, readonly, nullable) NSDictionary *testResults; +/** + * Mapping of screenshot images by suggested names, captured by the dart tests. + */ +@property (copy, readonly) NSDictionary *capturedScreenshotsByName; + /** Fetches the singleton instance of the plugin. */ + (IntegrationTestPlugin *)instance; @@ -30,8 +28,6 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; -@property(weak, nonatomic) id 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 82d263595e..a8a80b6785 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 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; @property(nonatomic, readwrite) NSDictionary *testResults; +- (instancetype)init NS_DESIGNATED_INITIALIZER; + @end @implementation IntegrationTestPlugin { NSDictionary *_testResults; + NSMutableDictionary *_capturedScreenshotsByName; } + (IntegrationTestPlugin *)instance { @@ -32,7 +35,13 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; } - (instancetype)initForRegistration { - return [super init]; + return [self init]; +} + +- (instancetype)init { + self = [super init]; + _capturedScreenshotsByName = [NSMutableDictionary new]; + return self; } + (void)registerWithRegistrar:(NSObject *)registrar { @@ -59,7 +68,7 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; // 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 fb24cb0b8f..525507384b 100644 --- a/packages/integration_test/ios/integration_test.podspec +++ b/packages/integration_test/ios/integration_test.podspec @@ -19,7 +19,14 @@ LICENSE 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