Add native iOS screenshots to integration_test (#84611)
This commit is contained in:
parent
a694778531
commit
28dfb44559
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -100,9 +100,9 @@ flutter drive \
|
||||
You can use `integration_test` to take screenshots of the UI rendered on the mobile device or
|
||||
Web browser at a specific time during the test.
|
||||
|
||||
This feature is currently supported on Android, and Web.
|
||||
This feature is currently supported on Android, iOS, and Web.
|
||||
|
||||
#### Android
|
||||
#### Android and iOS
|
||||
|
||||
**integration_test/screenshot_test.dart**
|
||||
|
||||
@ -115,7 +115,7 @@ void main() {
|
||||
// Build the app.
|
||||
app.main();
|
||||
|
||||
// This is required prior to taking the screenshot.
|
||||
// This is required prior to taking the screenshot (Android only).
|
||||
await binding.convertFlutterSurfaceToImage();
|
||||
|
||||
// Trigger a frame.
|
||||
@ -126,7 +126,8 @@ void main() {
|
||||
```
|
||||
|
||||
You can use a driver script to pull in the screenshot from the device.
|
||||
This way, you can store the images locally on your computer.
|
||||
This way, you can store the images locally on your computer. On iOS, the
|
||||
screenshot will also be available in Xcode test results.
|
||||
|
||||
**test_driver/integration_test.dart**
|
||||
|
||||
|
@ -475,21 +475,11 @@
|
||||
baseConfigurationReference = 09505407E99803EF7AA92DE7 /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = RunnerTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
|
||||
};
|
||||
name = Debug;
|
||||
@ -499,20 +489,11 @@
|
||||
baseConfigurationReference = FCE3953801588FC13ED9E898 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = RunnerTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
|
||||
};
|
||||
name = Release;
|
||||
@ -522,20 +503,11 @@
|
||||
baseConfigurationReference = 750225973AAB5D7832AFA60C /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = RunnerTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
|
||||
};
|
||||
name = Profile;
|
||||
|
@ -4,23 +4,45 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol FLTIntegrationTestScreenshotDelegate;
|
||||
|
||||
@interface IntegrationTestIosTest : NSObject
|
||||
|
||||
- (BOOL)testIntegrationTest:(NSString **)testResult;
|
||||
- (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 \
|
||||
@interface __test_class : XCTestCase<FLTIntegrationTestScreenshotDelegate> \
|
||||
@end \
|
||||
\
|
||||
@implementation __test_class \
|
||||
\
|
||||
-(void)testIntegrationTest { \
|
||||
- (void)testIntegrationTest { \
|
||||
NSString *testResult; \
|
||||
IntegrationTestIosTest *integrationTestIosTest = [[IntegrationTestIosTest alloc] init]; \
|
||||
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
|
||||
|
@ -5,10 +5,26 @@
|
||||
#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 = [IntegrationTestPlugin instance];
|
||||
IntegrationTestPlugin *integrationTestPlugin = self.integrationTestPlugin;
|
||||
|
||||
UIViewController *rootViewController =
|
||||
[[[[UIApplication sharedApplication] delegate] window] rootViewController];
|
||||
if (![rootViewController isKindOfClass:[FlutterViewController class]]) {
|
||||
|
@ -6,14 +6,20 @@
|
||||
|
||||
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>
|
||||
|
||||
/**
|
||||
* Test results that are sent from Dart when integration test completes. Before the
|
||||
* completion, it is
|
||||
* @c nil.
|
||||
* completion, it is @c nil.
|
||||
*/
|
||||
@property(nonatomic, readonly, nullable) NSDictionary<NSString *, NSString *> *testResults;
|
||||
|
||||
@ -24,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@property(weak, nonatomic) id<FLTIntegrationTestScreenshotDelegate> screenshotDelegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
@ -2,10 +2,15 @@
|
||||
// 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"
|
||||
|
||||
static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integration_test";
|
||||
static NSString *const kMethodTestFinished = @"allTestsFinished";
|
||||
static NSString *const kMethodScreenshot = @"captureScreenshot";
|
||||
static NSString *const kMethodConvertSurfaceToImage = @"convertFlutterSurfaceToImage";
|
||||
static NSString *const kMethodRevertImage = @"revertFlutterImage";
|
||||
|
||||
@interface IntegrationTestPlugin ()
|
||||
|
||||
@ -39,20 +44,55 @@ static NSString *const kMethodTestFinished = @"allTestsFinished";
|
||||
|
||||
- (void)setupChannels:(id<FlutterBinaryMessenger>)binaryMessenger {
|
||||
FlutterMethodChannel *channel =
|
||||
[FlutterMethodChannel methodChannelWithName:kIntegrationTestPluginChannel
|
||||
binaryMessenger:binaryMessenger];
|
||||
[FlutterMethodChannel methodChannelWithName:kIntegrationTestPluginChannel
|
||||
binaryMessenger:binaryMessenger];
|
||||
[channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) {
|
||||
[self handleMethodCall:call result:result];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
|
||||
if ([kMethodTestFinished isEqual:call.method]) {
|
||||
if ([call.method isEqualToString:kMethodTestFinished]) {
|
||||
self.testResults = call.arguments[@"results"];
|
||||
result(nil);
|
||||
} else if ([call.method isEqualToString:kMethodScreenshot]) {
|
||||
// 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];
|
||||
|
||||
// Also pass back along the channel for the driver to handle.
|
||||
NSData *pngData = UIImagePNGRepresentation(screenshot);
|
||||
result([FlutterStandardTypedData typedDataWithBytes:pngData]);
|
||||
} else if ([call.method isEqualToString:kMethodConvertSurfaceToImage]
|
||||
|| [call.method isEqualToString:kMethodRevertImage]) {
|
||||
// Android only, no-op on iOS.
|
||||
result(nil);
|
||||
} else {
|
||||
result(FlutterMethodNotImplemented);
|
||||
}
|
||||
}
|
||||
|
||||
- (UIImage *)capturePngScreenshot {
|
||||
UIWindow *window = [UIApplication.sharedApplication.windows
|
||||
filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"keyWindow = YES"]].firstObject;
|
||||
CGRect screenshotBounds = window.bounds;
|
||||
UIImage *image;
|
||||
|
||||
if (@available(iOS 10, *)) {
|
||||
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithBounds:screenshotBounds];
|
||||
|
||||
image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
|
||||
[window drawViewHierarchyInRect:screenshotBounds afterScreenUpdates:YES];
|
||||
}];
|
||||
} else {
|
||||
UIGraphicsBeginImageContextWithOptions(screenshotBounds.size, NO, UIScreen.mainScreen.scale);
|
||||
[window drawViewHierarchyInRect:screenshotBounds afterScreenUpdates:YES];
|
||||
image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -18,6 +18,8 @@ LICENSE
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.dependency 'Flutter'
|
||||
s.ios.framework = 'UIKit'
|
||||
|
||||
s.platform = :ios, '8.0'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||
end
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
@ -60,37 +61,41 @@ class IOCallbackManager implements CallbackManager {
|
||||
// comes up in the future. For example: `WebCallbackManager.cleanup`.
|
||||
}
|
||||
|
||||
// Whether the Flutter surface uses an Image.
|
||||
bool _usesFlutterImage = false;
|
||||
// [convertFlutterSurfaceToImage] has been called and [takeScreenshot] is ready to capture the surface (Android only).
|
||||
bool _isSurfaceRendered = false;
|
||||
|
||||
@override
|
||||
Future<void> convertFlutterSurfaceToImage() async {
|
||||
assert(!_usesFlutterImage, 'Surface already converted to an image');
|
||||
if (!Platform.isAndroid) {
|
||||
// No-op on other platforms.
|
||||
return;
|
||||
}
|
||||
assert(!_isSurfaceRendered, 'Surface already converted to an image');
|
||||
await integrationTestChannel.invokeMethod<void>(
|
||||
'convertFlutterSurfaceToImage',
|
||||
null,
|
||||
);
|
||||
_usesFlutterImage = true;
|
||||
_isSurfaceRendered = true;
|
||||
|
||||
addTearDown(() async {
|
||||
assert(_usesFlutterImage, 'Surface is not an image');
|
||||
assert(_isSurfaceRendered, 'Surface is not an image');
|
||||
await integrationTestChannel.invokeMethod<void>(
|
||||
'revertFlutterImage',
|
||||
null,
|
||||
);
|
||||
_usesFlutterImage = false;
|
||||
_isSurfaceRendered = false;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> takeScreenshot(String screenshot) async {
|
||||
if (!_usesFlutterImage) {
|
||||
if (Platform.isAndroid && !_isSurfaceRendered) {
|
||||
throw StateError('Call convertFlutterSurfaceToImage() before taking a screenshot');
|
||||
}
|
||||
integrationTestChannel.setMethodCallHandler(_onMethodChannelCall);
|
||||
final List<int>? rawBytes = await integrationTestChannel.invokeMethod<List<int>>(
|
||||
'captureScreenshot',
|
||||
null,
|
||||
<String, dynamic>{'name': screenshot},
|
||||
);
|
||||
if (rawBytes == null) {
|
||||
throw StateError('Expected a list of bytes, but instead captureScreenshot returned null');
|
||||
|
Loading…
x
Reference in New Issue
Block a user