Unverified Commit 9e3de9a3 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Refactor iOS integration_test API to support Swift, dynamically add native tests (#88013)

parent 2d0f40e8
...@@ -281,14 +281,27 @@ To build `integration_test/foo_test.dart` from the command line, run: ...@@ -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 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: replace the file:
```objective-c ```objective-c
@import XCTest; @import XCTest;
@import integration_test; @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. Run `Product > Test` to run the integration tests on your selected device.
......
...@@ -25,6 +25,24 @@ void main() { ...@@ -25,6 +25,24 @@ void main() {
// Build our app. // Build our app.
app.main(); 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. // On Android, this is required prior to taking the screenshot.
await binding.convertFlutterSurfaceToImage(); await binding.convertFlutterSurfaceToImage();
...@@ -39,15 +57,5 @@ void main() { ...@@ -39,15 +57,5 @@ void main() {
expect(secondPng.isNotEmpty, isTrue); expect(secondPng.isNotEmpty, isTrue);
expect(listEquals(firstPng, secondPng), 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,
);
}); });
} }
...@@ -10,13 +10,14 @@ ...@@ -10,13 +10,14 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
4DB404AC7CF2C89658A01173 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BF64028CE7AE2E6196250D /* libPods-RunnerTests.a */; }; 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 */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; };
97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
C2A5EDF11F4FDBF3ABFD7006 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 625A5A90428602E25C0DE2F6 /* libPods-Runner.a */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
...@@ -52,7 +53,7 @@ ...@@ -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>"; }; 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; }; 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; }; 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>"; }; 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>"; }; 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>"; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
...@@ -68,6 +69,7 @@ ...@@ -68,6 +69,7 @@
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
...@@ -104,7 +106,8 @@ ...@@ -104,7 +106,8 @@
769541C923A0351900E5C350 /* RunnerTests */ = { 769541C923A0351900E5C350 /* RunnerTests */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
769541CA23A0351900E5C350 /* RunnerTests.m */, 769541CA23A0351900E5C350 /* RunnerObjCTests.m */,
F77B951826C3504400F785B3 /* RunnerSwiftTests.swift */,
769541CC23A0351900E5C350 /* Info.plist */, 769541CC23A0351900E5C350 /* Info.plist */,
); );
path = RunnerTests; path = RunnerTests;
...@@ -233,6 +236,7 @@ ...@@ -233,6 +236,7 @@
TargetAttributes = { TargetAttributes = {
769541C723A0351900E5C350 = { 769541C723A0351900E5C350 = {
CreatedOnToolsVersion = 11.0; CreatedOnToolsVersion = 11.0;
LastSwiftMigration = 1300;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
TestTargetID = 97C146ED1CF9000F007C117D; TestTargetID = 97C146ED1CF9000F007C117D;
}; };
...@@ -361,7 +365,8 @@ ...@@ -361,7 +365,8 @@
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
769541CB23A0351900E5C350 /* RunnerTests.m in Sources */, 769541CB23A0351900E5C350 /* RunnerObjCTests.m in Sources */,
F77B951926C3504400F785B3 /* RunnerSwiftTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
...@@ -475,11 +480,14 @@ ...@@ -475,11 +480,14 @@
baseConfigurationReference = 09505407E99803EF7AA92DE7 /* Pods-RunnerTests.debug.xcconfig */; baseConfigurationReference = 09505407E99803EF7AA92DE7 /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = RunnerTests/Info.plist; INFOPLIST_FILE = RunnerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
}; };
name = Debug; name = Debug;
...@@ -489,11 +497,13 @@ ...@@ -489,11 +497,13 @@
baseConfigurationReference = FCE3953801588FC13ED9E898 /* Pods-RunnerTests.release.xcconfig */; baseConfigurationReference = FCE3953801588FC13ED9E898 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = RunnerTests/Info.plist; INFOPLIST_FILE = RunnerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
}; };
name = Release; name = Release;
...@@ -503,11 +513,13 @@ ...@@ -503,11 +513,13 @@
baseConfigurationReference = 750225973AAB5D7832AFA60C /* Pods-RunnerTests.profile.xcconfig */; baseConfigurationReference = 750225973AAB5D7832AFA60C /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
INFOPLIST_FILE = RunnerTests/Info.plist; INFOPLIST_FILE = RunnerTests/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests; PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
}; };
name = Profile; name = Profile;
......
...@@ -20,6 +20,20 @@ ...@@ -20,6 +20,20 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildActionEntry> </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> </BuildActionEntries>
</BuildAction> </BuildAction>
<TestAction <TestAction
......
// 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
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
@import XCTest; import integration_test
@import integration_test; import XCTest
INTEGRATION_TEST_IOS_RUNNER(RunnerTests) class RunnerSwiftTests: FLTIntegrationTestCase {
}
// 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
// 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
// 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
...@@ -2,62 +2,85 @@ ...@@ -2,62 +2,85 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
#import "IntegrationTestIosTest.h" #import "FLTIntegrationTestRunner.h"
#import "IntegrationTestPlugin.h" #import "IntegrationTestPlugin.h"
@interface IntegrationTestIosTest() @import UIKit;
@property (nonatomic) IntegrationTestPlugin *integrationTestPlugin;
@interface FLTIntegrationTestRunner ()
@property IntegrationTestPlugin *integrationTestPlugin;
@end @end
@implementation IntegrationTestIosTest @implementation FLTIntegrationTestRunner
- (instancetype)initWithScreenshotDelegate:(id<FLTIntegrationTestScreenshotDelegate>)delegate { - (instancetype)init {
self = [super init]; self = [super init];
_integrationTestPlugin = [IntegrationTestPlugin instance]; _integrationTestPlugin = [IntegrationTestPlugin instance];
_integrationTestPlugin.screenshotDelegate = delegate;
return self;
}
- (instancetype)init { return self;
return [self initWithScreenshotDelegate:nil];
} }
- (BOOL)testIntegrationTest:(NSString **)testResult { - (void)testIntegrationTestWithResults:(NS_NOESCAPE FLTIntegrationTestResults)testResult {
IntegrationTestPlugin *integrationTestPlugin = self.integrationTestPlugin; IntegrationTestPlugin *integrationTestPlugin = self.integrationTestPlugin;
UIViewController *rootViewController = UIApplication.sharedApplication.delegate.window.rootViewController;
UIViewController *rootViewController =
[[[[UIApplication sharedApplication] delegate] window] rootViewController];
if (![rootViewController isKindOfClass:[FlutterViewController class]]) { if (![rootViewController isKindOfClass:[FlutterViewController class]]) {
NSLog(@"expected FlutterViewController as rootViewController."); testResult(@"setup", NO, @"rootViewController was not expected FlutterViewController");
return NO;
} }
FlutterViewController *flutterViewController = (FlutterViewController *)rootViewController; FlutterViewController *flutterViewController = (FlutterViewController *)rootViewController;
[integrationTestPlugin setupChannels:flutterViewController.engine.binaryMessenger]; [integrationTestPlugin setupChannels:flutterViewController.engine.binaryMessenger];
// Spin the runloop.
while (!integrationTestPlugin.testResults) { while (!integrationTestPlugin.testResults) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.f, NO); [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
} }
NSDictionary<NSString *, NSString *> *testResults = integrationTestPlugin.testResults;
NSMutableArray<NSString *> *passedTests = [NSMutableArray array]; [integrationTestPlugin.testResults enumerateKeysAndObjectsUsingBlock:^(NSString *test, NSString *result, BOOL *stop) {
NSMutableArray<NSString *> *failedTests = [NSMutableArray array];
NSLog(@"==================== Test Results =====================");
for (NSString *test in testResults.allKeys) {
NSString *result = testResults[test];
if ([result isEqualToString:@"success"]) { if ([result isEqualToString:@"success"]) {
NSLog(@"%@ passed.", test); testResult(test, YES, nil);
[passedTests addObject:test];
} else { } else {
NSLog(@"%@ failed: %@", test, result); testResult(test, NO, result);
[failedTests addObject:test];
} }
} }];
}
- (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 ===================="); NSLog(@"================== Test Results End ====================");
BOOL testPass = failedTests.count == 0; BOOL testPass = failedTests.count == 0;
if (!testPass && testResult) { if (!testPass && testResult != NULL) {
*testResult = *testResult =
[NSString stringWithFormat:@"Detected failed integration test(s) %@ among %@", [NSString stringWithFormat:@"Detected failed integration test(s) %@ among %@",
failedTests.description, testResults.allKeys.description]; failedTests.description, testNames.description];
} }
return testPass; return testPass;
} }
@end @end
#pragma clang diagnostic pop
// 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
...@@ -6,13 +6,6 @@ ...@@ -6,13 +6,6 @@
NS_ASSUME_NONNULL_BEGIN 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 /** A Flutter plugin that's responsible for communicating the test results back
* to iOS XCTest. */ * to iOS XCTest. */
@interface IntegrationTestPlugin : NSObject <FlutterPlugin> @interface IntegrationTestPlugin : NSObject <FlutterPlugin>
...@@ -23,6 +16,11 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -23,6 +16,11 @@ NS_ASSUME_NONNULL_BEGIN
*/ */
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *testResults; @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. */ /** Fetches the singleton instance of the plugin. */
+ (IntegrationTestPlugin *)instance; + (IntegrationTestPlugin *)instance;
...@@ -30,8 +28,6 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -30,8 +28,6 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
@property(weak, nonatomic) id<FLTIntegrationTestScreenshotDelegate> screenshotDelegate;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
@import UIKit;
#import "IntegrationTestPlugin.h" #import "IntegrationTestPlugin.h"
@import UIKit;
static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integration_test"; static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integration_test";
static NSString *const kMethodTestFinished = @"allTestsFinished"; static NSString *const kMethodTestFinished = @"allTestsFinished";
static NSString *const kMethodScreenshot = @"captureScreenshot"; static NSString *const kMethodScreenshot = @"captureScreenshot";
...@@ -16,10 +16,13 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; ...@@ -16,10 +16,13 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage";
@property(nonatomic, readwrite) NSDictionary<NSString *, NSString *> *testResults; @property(nonatomic, readwrite) NSDictionary<NSString *, NSString *> *testResults;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
@end @end
@implementation IntegrationTestPlugin { @implementation IntegrationTestPlugin {
NSDictionary<NSString *, NSString *> *_testResults; NSDictionary<NSString *, NSString *> *_testResults;
NSMutableDictionary<NSString *, UIImage *> *_capturedScreenshotsByName;
} }
+ (IntegrationTestPlugin *)instance { + (IntegrationTestPlugin *)instance {
...@@ -32,7 +35,13 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; ...@@ -32,7 +35,13 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage";
} }
- (instancetype)initForRegistration { - (instancetype)initForRegistration {
return [super init]; return [self init];
}
- (instancetype)init {
self = [super init];
_capturedScreenshotsByName = [NSMutableDictionary new];
return self;
} }
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar { + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar> *)registrar {
...@@ -59,7 +68,7 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; ...@@ -59,7 +68,7 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage";
// If running as a native Xcode test, attach to test. // If running as a native Xcode test, attach to test.
UIImage *screenshot = [self capturePngScreenshot]; UIImage *screenshot = [self capturePngScreenshot];
NSString *name = call.arguments[@"name"]; 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. // Also pass back along the channel for the driver to handle.
NSData *pngData = UIImagePNGRepresentation(screenshot); NSData *pngData = UIImagePNGRepresentation(screenshot);
......
...@@ -19,7 +19,14 @@ LICENSE ...@@ -19,7 +19,14 @@ LICENSE
s.public_header_files = 'Classes/**/*.h' s.public_header_files = 'Classes/**/*.h'
s.dependency 'Flutter' s.dependency 'Flutter'
s.ios.framework = 'UIKit' 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.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 end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment