diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java index d1632efa79f..16c964f2276 100644 --- a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/MainActivity.java @@ -17,66 +17,78 @@ import io.flutter.Log; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.android.FlutterFragment; import io.flutter.embedding.android.FlutterView; +import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterShellArgs; import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; import io.flutter.plugin.common.BasicMessageChannel; import io.flutter.plugin.common.BinaryCodec; public class MainActivity extends FlutterActivity implements OnFirstFrameRenderedListener { - final static String TAG = "Scenarios"; + final static String TAG = "Scenarios"; - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - final Intent launchIntent = getIntent(); - if ("com.google.intent.action.TEST_LOOP".equals(launchIntent.getAction())) { - if(Build.VERSION.SDK_INT > 22){ - requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); - } - // Run for one minute, get the timeline data, write it, and finish. - final Uri logFileUri = launchIntent.getData(); - new Handler().postDelayed(() -> writeTimelineData(logFileUri), 20000); - } + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + final Intent launchIntent = getIntent(); + if ("com.google.intent.action.TEST_LOOP".equals(launchIntent.getAction())) { + if (Build.VERSION.SDK_INT > 22) { + requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); + } + // Run for one minute, get the timeline data, write it, and finish. + final Uri logFileUri = launchIntent.getData(); + new Handler().postDelayed(() -> writeTimelineData(logFileUri), 20000); } + } - public void onFirstFrameRendered() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - reportFullyDrawn(); - } + public void onFirstFrameRendered() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + reportFullyDrawn(); } + } - @Override - @NonNull - public FlutterShellArgs getFlutterShellArgs() { - FlutterShellArgs args = FlutterShellArgs.fromIntent(getIntent()); - args.add(FlutterShellArgs.ARG_TRACE_STARTUP); - args.add(FlutterShellArgs.ARG_ENABLE_DART_PROFILING); - args.add(FlutterShellArgs.ARG_VERBOSE_LOGGING); + @Override + @NonNull + public FlutterShellArgs getFlutterShellArgs() { + FlutterShellArgs args = FlutterShellArgs.fromIntent(getIntent()); + args.add(FlutterShellArgs.ARG_TRACE_STARTUP); + args.add(FlutterShellArgs.ARG_ENABLE_DART_PROFILING); + args.add(FlutterShellArgs.ARG_VERBOSE_LOGGING); - return args; + return args; + } + + @Override + public void configureFlutterEngine(FlutterEngine flutterEngine) { + flutterEngine.getPlatformViewsController() + .getRegistry() + .registerViewFactory( + "scenarios/textPlatformView", + new TextPlatformViewFactory()); + } + + + private void writeTimelineData(Uri logFile) { + if (logFile == null) { + throw new IllegalArgumentException(); } - - private void writeTimelineData(Uri logFile) { - if (logFile == null) { - throw new IllegalArgumentException(); - } - if (getFlutterEngine() == null) { - Log.e(TAG, "Could not write timeline data - no engine."); - return; - } - final BasicMessageChannel channel = new BasicMessageChannel<>( - getFlutterEngine().getDartExecutor(), "write_timeline", BinaryCodec.INSTANCE); - channel.send(null, (ByteBuffer reply) -> { - try { - final FileDescriptor fd = getContentResolver() - .openAssetFileDescriptor(logFile, "w").getFileDescriptor(); - final FileOutputStream outputStream = new FileOutputStream(fd); - outputStream.write(reply.array()); - outputStream.close(); - } catch (IOException ex) { - Log.e(TAG, "Could not write timeline file: " + ex.toString()); - } - finish(); - }); + if (getFlutterEngine() == null) { + Log.e(TAG, "Could not write timeline data - no engine."); + return; } + final BasicMessageChannel channel = new BasicMessageChannel<>( + getFlutterEngine().getDartExecutor(), "write_timeline", BinaryCodec.INSTANCE); + channel.send(null, (ByteBuffer reply) -> { + try { + final FileDescriptor fd = getContentResolver() + .openAssetFileDescriptor(logFile, "w") + .getFileDescriptor(); + final FileOutputStream outputStream = new FileOutputStream(fd); + outputStream.write(reply.array()); + outputStream.close(); + } catch (IOException ex) { + Log.e(TAG, "Could not write timeline file: " + ex.toString()); + } + finish(); + }); + } } diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformView.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformView.java new file mode 100644 index 00000000000..b54d0c22c5f --- /dev/null +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformView.java @@ -0,0 +1,32 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.scenarios; + +import android.content.Context; +import android.graphics.Color; +import android.view.View; +import android.widget.TextView; + +import io.flutter.plugin.platform.PlatformView; + +public class TextPlatformView implements PlatformView { + private final TextView textView; + + @SuppressWarnings("unchecked") + TextPlatformView(final Context context, int id, String params) { + textView = new TextView(context); + textView.setTextSize(72); + textView.setBackgroundColor(Color.rgb(255, 255, 255)); + textView.setText(params); + } + + @Override + public View getView() { + return textView; + } + + @Override + public void dispose() {} +} diff --git a/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java new file mode 100644 index 00000000000..79653ba8994 --- /dev/null +++ b/testing/scenario_app/android/app/src/main/java/dev/flutter/scenarios/TextPlatformViewFactory.java @@ -0,0 +1,43 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package dev.flutter.scenarios; + +import android.content.Context; +import android.support.annotation.Nullable; + +import java.nio.ByteBuffer; + +import io.flutter.plugin.common.MessageCodec; +import io.flutter.plugin.common.StringCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; + +public final class TextPlatformViewFactory extends PlatformViewFactory { + TextPlatformViewFactory() { + super(new MessageCodec() { + @Nullable + @Override + public ByteBuffer encodeMessage(@Nullable Object o) { + if (o instanceof String) { + return StringCodec.INSTANCE.encodeMessage((String)o); + } + return null; + } + + @Nullable + @Override + public Object decodeMessage(@Nullable ByteBuffer byteBuffer) { + return StringCodec.INSTANCE.decodeMessage(byteBuffer); + } + }); + } + + @SuppressWarnings("unchecked") + @Override + public PlatformView create(Context context, int id, Object args) { + String params = (String) args; + return new TextPlatformView(context, id, params); + } +} diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 5e76e5c4334..17ef6c9a421 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -21,9 +21,11 @@ 248D76CC22E388370012F0C1 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76CB22E388370012F0C1 /* AppDelegate.m */; }; 248D76D422E388380012F0C1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 248D76D322E388380012F0C1 /* Assets.xcassets */; }; 248D76DA22E388380012F0C1 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76D922E388380012F0C1 /* main.m */; }; - 248D76E422E388380012F0C1 /* ScenariosTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76E322E388380012F0C1 /* ScenariosTests.m */; }; - 248D76EF22E388380012F0C1 /* ScenariosUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76EE22E388380012F0C1 /* ScenariosUITests.m */; }; + 248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 248D76EE22E388380012F0C1 /* PlatformViewUITests.m */; }; 248FDFC422FE7CD0009CC7CD /* FlutterEngineTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */; }; + 24D47D1B230C79840069DD5E /* golden_platform_view_D21AAP.png in Resources */ = {isa = PBXBuildFile; fileRef = 24D47D1A230C79840069DD5E /* golden_platform_view_D21AAP.png */; }; + 24D47D1D230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 24D47D1C230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png */; }; + 24F1FB89230B4579005ACE7C /* TextPlatformView.m in Sources */ = {isa = PBXBuildFile; fileRef = 24F1FB87230B4579005ACE7C /* TextPlatformView.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -91,12 +93,16 @@ 248D76D822E388380012F0C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 248D76D922E388380012F0C1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 248D76DF22E388380012F0C1 /* ScenariosTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ScenariosTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 248D76E322E388380012F0C1 /* ScenariosTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScenariosTests.m; sourceTree = ""; }; 248D76E522E388380012F0C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 248D76EA22E388380012F0C1 /* ScenariosUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ScenariosUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 248D76EE22E388380012F0C1 /* ScenariosUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScenariosUITests.m; sourceTree = ""; }; + 248D76EE22E388380012F0C1 /* PlatformViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewUITests.m; sourceTree = ""; }; 248D76F022E388380012F0C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FlutterEngineTest.m; sourceTree = ""; }; + 24D47D1A230C79840069DD5E /* golden_platform_view_D21AAP.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = golden_platform_view_D21AAP.png; sourceTree = ""; }; + 24D47D1C230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_iPhone SE_simulator.png"; sourceTree = ""; }; + 24D47D1E230CA4480069DD5E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 24F1FB87230B4579005ACE7C /* TextPlatformView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextPlatformView.m; sourceTree = ""; }; + 24F1FB88230B4579005ACE7C /* TextPlatformView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextPlatformView.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -152,6 +158,8 @@ 248D76C922E388370012F0C1 /* Scenarios */ = { isa = PBXGroup; children = ( + 24F1FB88230B4579005ACE7C /* TextPlatformView.h */, + 24F1FB87230B4579005ACE7C /* TextPlatformView.m */, 248D76CA22E388370012F0C1 /* AppDelegate.h */, 248D76CB22E388370012F0C1 /* AppDelegate.m */, 248D76D322E388380012F0C1 /* Assets.xcassets */, @@ -166,7 +174,6 @@ children = ( 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */, 0DB781FC22EA2C0300E9B371 /* FlutterViewControllerTest.m */, - 248D76E322E388380012F0C1 /* ScenariosTests.m */, 248D76E522E388380012F0C1 /* Info.plist */, ); path = ScenariosTests; @@ -175,8 +182,11 @@ 248D76ED22E388380012F0C1 /* ScenariosUITests */ = { isa = PBXGroup; children = ( - 248D76EE22E388380012F0C1 /* ScenariosUITests.m */, + 24D47D1C230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png */, + 24D47D1A230C79840069DD5E /* golden_platform_view_D21AAP.png */, + 248D76EE22E388380012F0C1 /* PlatformViewUITests.m */, 248D76F022E388380012F0C1 /* Info.plist */, + 24D47D1E230CA4480069DD5E /* README.md */, ); path = ScenariosUITests; sourceTree = ""; @@ -311,6 +321,8 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 24D47D1B230C79840069DD5E /* golden_platform_view_D21AAP.png in Resources */, + 24D47D1D230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -322,6 +334,7 @@ buildActionMask = 2147483647; files = ( 248D76DA22E388380012F0C1 /* main.m in Sources */, + 24F1FB89230B4579005ACE7C /* TextPlatformView.m in Sources */, 248D76CC22E388370012F0C1 /* AppDelegate.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -330,7 +343,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 248D76E422E388380012F0C1 /* ScenariosTests.m in Sources */, 0DB7820222EA493B00E9B371 /* FlutterViewControllerTest.m in Sources */, 248FDFC422FE7CD0009CC7CD /* FlutterEngineTest.m in Sources */, ); @@ -340,7 +352,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 248D76EF22E388380012F0C1 /* ScenariosUITests.m in Sources */, + 248D76EF22E388380012F0C1 /* PlatformViewUITests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 97af94c5320..38e14bda4f3 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -1,12 +1,48 @@ #include "AppDelegate.h" +#import "TextPlatformView.h" + +@interface NoStatusBarFlutterViewController : FlutterViewController + +@end + +@implementation NoStatusBarFlutterViewController +- (BOOL)prefersStatusBarHidden { + return YES; +} +@end @implementation AppDelegate - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; - self.window.rootViewController = [[UIViewController alloc] init]; + + // This argument is used by the XCUITest for Platform Views so that the app + // under test will create platform views. + if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--platform-view"]) { + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"PlatformViewTest" project:nil]; + [engine runWithEntrypoint:nil]; + + FlutterViewController* flutterViewController = + [[NoStatusBarFlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; + [engine.binaryMessenger + setMessageHandlerOnChannel:@"scenario_status" + binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { + [engine.binaryMessenger + sendOnChannel:@"set_scenario" + message:[@"text_platform_view" dataUsingEncoding:NSUTF8StringEncoding]]; + }]; + TextPlatformViewFactory* textPlatformViewFactory = + [[TextPlatformViewFactory alloc] initWithMessenger:flutterViewController.binaryMessenger]; + NSObject* registrar = + [flutterViewController.engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; + [registrar registerViewFactory:textPlatformViewFactory withId:@"scenarios/textPlatformView"]; + self.window.rootViewController = flutterViewController; + } else { + self.window.rootViewController = [[UIViewController alloc] init]; + } [self.window makeKeyAndVisible]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist b/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist index ce0bb08859a..3c41170863c 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist +++ b/testing/scenario_app/ios/Scenarios/Scenarios/Info.plist @@ -30,6 +30,8 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + io.flutter.embedded_views_preview + UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.h b/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.h new file mode 100644 index 00000000000..e88fbe9ee2f --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.h @@ -0,0 +1,23 @@ +// Copyright 2013 The Chromium 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 + +@interface TextPlatformView : NSObject + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger; + +- (UIView*)view; +@end + +@interface TextPlatformViewFactory : NSObject +- (instancetype)initWithMessenger:(NSObject*)messenger; +@end + +NS_ASSUME_NONNULL_END diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.m b/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.m new file mode 100644 index 00000000000..c75b5c815d0 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.m @@ -0,0 +1,63 @@ +// Copyright 2018 The Chromium 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 "TextPlatformView.h" + +@implementation TextPlatformViewFactory { + NSObject* _messenger; +} + +- (instancetype)initWithMessenger:(NSObject*)messenger { + self = [super init]; + if (self) { + _messenger = messenger; + } + return self; +} + +- (NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + TextPlatformView* textPlatformView = [[TextPlatformView alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + binaryMessenger:_messenger]; + return textPlatformView; +} + +- (NSObject*)createArgsCodec { + return [FlutterStringCodec sharedInstance]; +} + +@end + +@implementation TextPlatformView { + int64_t _viewId; + UITextView* _textView; + FlutterMethodChannel* _channel; +} + +- (instancetype)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject*)messenger { + if ([super init]) { + _viewId = viewId; + _textView = [[UITextView alloc] initWithFrame:CGRectMake(50.0, 50.0, 250.0, 100.0)]; + _textView.textColor = UIColor.blueColor; + _textView.backgroundColor = UIColor.lightGrayColor; + [_textView setFont:[UIFont systemFontOfSize:52]]; + _textView.text = args; + } + return self; +} + +- (UIView*)view { + return _textView; +} + +// - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { +// } + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m new file mode 100644 index 00000000000..0252f185ba7 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m @@ -0,0 +1,110 @@ +// Copyright 2013 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 +#import +#include + +#import "../Scenarios/TextPlatformView.h" + +@interface PlatformViewUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* application; +@end + +@implementation PlatformViewUITests + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; + + self.application = [[XCUIApplication alloc] init]; + self.application.launchArguments = @[ @"--platform-view" ]; + [self.application launch]; +} + +- (void)testPlatformView { + NSBundle* bundle = [NSBundle bundleForClass:[self class]]; + NSString* goldenName = + [NSString stringWithFormat:@"golden_platform_view_%@", [self platformName]]; + NSString* path = [bundle pathForResource:goldenName ofType:@"png"]; + UIImage* golden = [[UIImage alloc] initWithContentsOfFile:path]; + + XCUIScreenshot* screenshot = [[XCUIScreen mainScreen] screenshot]; + if (golden) { + XCTAttachment* goldenAttachment = [XCTAttachment attachmentWithImage:golden]; + goldenAttachment.lifetime = XCTAttachmentLifetimeKeepAlways; + [self addAttachment:goldenAttachment]; + } + + XCTAttachment* attachment = [XCTAttachment attachmentWithScreenshot:screenshot]; + attachment.lifetime = XCTAttachmentLifetimeKeepAlways; + [self addAttachment:attachment]; + XCTAssertTrue([self compareImage:golden toOther:screenshot.image]); +} + +- (NSString*)platformName { + NSString* simulatorName = + [[NSProcessInfo processInfo].environment objectForKey:@"SIMULATOR_DEVICE_NAME"]; + if (simulatorName) { + return [NSString stringWithFormat:@"%@_simulator", simulatorName]; + } + + size_t size; + sysctlbyname("hw.model", NULL, &size, NULL, 0); + char* answer = malloc(size); + sysctlbyname("hw.model", answer, &size, NULL, 0); + + NSString* results = [NSString stringWithCString:answer encoding:NSUTF8StringEncoding]; + free(answer); + return results; +} + +- (BOOL)compareImage:(UIImage*)a toOther:(UIImage*)b { + CGImageRef imageRefA = [a CGImage]; + CGImageRef imageRefB = [b CGImage]; + + NSUInteger widthA = CGImageGetWidth(imageRefA); + NSUInteger heightA = CGImageGetHeight(imageRefA); + NSUInteger widthB = CGImageGetWidth(imageRefB); + NSUInteger heightB = CGImageGetHeight(imageRefB); + + if (widthA != widthB || heightA != heightB) { + return NO; + } + NSUInteger bytesPerPixel = 4; + NSUInteger size = widthA * heightA * bytesPerPixel; + NSMutableData* rawA = [NSMutableData dataWithLength:size]; + NSMutableData* rawB = [NSMutableData dataWithLength:size]; + + if (!rawA || !rawB) { + return NO; + } + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + + NSUInteger bytesPerRow = bytesPerPixel * widthA; + NSUInteger bitsPerComponent = 8; + CGContextRef contextA = + CGBitmapContextCreate(rawA.mutableBytes, widthA, heightA, bitsPerComponent, bytesPerRow, + colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + + CGContextDrawImage(contextA, CGRectMake(0, 0, widthA, heightA), imageRefA); + CGContextRelease(contextA); + + CGContextRef contextB = + CGBitmapContextCreate(rawB.mutableBytes, widthA, heightA, bitsPerComponent, bytesPerRow, + colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + CGColorSpaceRelease(colorSpace); + + CGContextDrawImage(contextB, CGRectMake(0, 0, widthA, heightA), imageRefB); + CGContextRelease(contextB); + + if (memcmp(rawA.mutableBytes, rawB.mutableBytes, size)) { + return NO; + } + + return YES; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/README.md b/testing/scenario_app/ios/Scenarios/ScenariosUITests/README.md new file mode 100644 index 00000000000..d5d3bc62860 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/README.md @@ -0,0 +1,13 @@ +# PlatformView UI Tests + +This folder contains a test for platform views. It renders a platform view +and does a screen shot comparison against a known good configuration. + +The screen shots are named `golden_platform_view_MODEL`, with `_simulator` +appended for simulators. The model numbers for physical devices correspond +to the `hw.model` sys call, and will represent the model numbers. Simulator +names are taken from the environment. + +New devices require running the test on the device, gathering the attachment +and verifying it manually, and then adding an appropriately named file to +this folder. diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/ScenariosUITests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/ScenariosUITests.m deleted file mode 100644 index fc47337b583..00000000000 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/ScenariosUITests.m +++ /dev/null @@ -1,42 +0,0 @@ -// -// ScenariosUITests.m -// ScenariosUITests -// -// Created by Dan Field on 7/20/19. -// Copyright © 2019 flutter. All rights reserved. -// - -#import - -@interface ScenariosUITests : XCTestCase - -@end - -@implementation ScenariosUITests - -- (void)setUp { - // Put setup code here. This method is called before the invocation of each test method in the - // class. - - // In UI tests it is usually best to stop immediately when a failure occurs. - self.continueAfterFailure = NO; - - // UI tests must launch the application that they test. Doing this in setup will make sure it - // happens for each test method. - [[[XCUIApplication alloc] init] launch]; - - // In UI tests it’s important to set the initial state - such as interface orientation - required - // for your tests before they run. The setUp method is a good place to do this. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the - // class. -} - -- (void)testExample { - // Use recording to get started writing UI tests. - // Use XCTAssert and related functions to verify your tests produce the correct results. -} - -@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_D21AAP.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_D21AAP.png new file mode 100644 index 00000000000..bd59044cc20 Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_D21AAP.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_iPhone SE_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_iPhone SE_simulator.png new file mode 100644 index 00000000000..1f9ee8937d9 Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_iPhone SE_simulator.png differ diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index db707955465..18a64dab083 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -10,10 +10,12 @@ import 'dart:typed_data'; import 'dart:ui'; import 'src/animated_color_square.dart'; +import 'src/platform_view.dart'; import 'src/scenario.dart'; Map _scenarios = { 'animated_color_square': AnimatedColorSquareScenario(window), + 'text_platform_view': PlatformViewScenario(window, 'Hello from Scenarios (Platform View)'), }; Scenario _currentScenario = _scenarios['animated_color_square']; @@ -30,7 +32,10 @@ void main() { window.sendPlatformMessage('scenario_status', data, null); } -Future _handlePlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) async { +Future _handlePlatformMessage( + String name, ByteData data, PlatformMessageResponseCallback callback) async { + print(name); + print(utf8.decode(data.buffer.asUint8List())); if (name == 'set_scenario' && data != null) { final String scenarioName = utf8.decode(data.buffer.asUint8List()); final Scenario candidateScenario = _scenarios[scenarioName]; @@ -59,7 +64,8 @@ Future _getTimelineData() async { final Map cpuTimelineJson = await _getJson(cpuProfileTimelineUri); final Map vmServiceTimelineJson = await _getJson(vmServiceTimelineUri); final Map cpuResult = cpuTimelineJson['result'].cast(); - final Map vmServiceResult = vmServiceTimelineJson['result'].cast(); + final Map vmServiceResult = + vmServiceTimelineJson['result'].cast(); return json.encode({ 'stackFrames': cpuResult['stackFrames'], diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart new file mode 100644 index 00000000000..4286469ce29 --- /dev/null +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -0,0 +1,123 @@ +// Copyright 2013 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 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'scenario.dart'; + +List _to32(int value) { + final Uint8List temp = Uint8List(4); + temp.buffer.asByteData().setInt32(0, value, Endian.little); + return temp; +} + +List _to64(num value) { + final Uint8List temp = Uint8List(15); + if (value is double) { + temp.buffer.asByteData().setFloat64(7, value, Endian.little); + } else if (value is int) { + temp.buffer.asByteData().setInt64(7, value, Endian.little); + } + return temp; +} + +/// A simple platform view. +class PlatformViewScenario extends Scenario { + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + const int _valueInt32 = 3; + const int _valueFloat64 = 6; + const int _valueString = 7; + const int _valueUint8List = 8; + const int _valueMap = 13; + final Uint8List message = Uint8List.fromList([ + _valueString, + 'create'.length, // this is safe as long as these are all single byte characters. + ...utf8.encode('create'), + _valueMap, + if (Platform.isIOS) + 3, // 3 entries in map for iOS. + if (Platform.isAndroid) + 6, // 6 entries in map for Android. + _valueString, + 'id'.length, + ...utf8.encode('id'), + _valueInt32, + ..._to32(id), + _valueString, + 'viewType'.length, + ...utf8.encode('viewType'), + _valueString, + 'scenarios/textPlatformView'.length, + ...utf8.encode('scenarios/textPlatformView'), + if (Platform.isAndroid) ...[ + _valueString, + 'width'.length, + ...utf8.encode('width'), + _valueFloat64, + ..._to64(500.0), + _valueString, + 'height'.length, + ...utf8.encode('height'), + _valueFloat64, + ..._to64(500.0), + _valueString, + 'direction'.length, + ...utf8.encode('direction'), + _valueInt32, + ..._to32(0), // LTR + ], + _valueString, + 'params'.length, + ...utf8.encode('params'), + _valueUint8List, + text.length, + ...utf8.encode(text), + ]); + + window.sendPlatformMessage( + 'flutter/platform_views', + message.buffer.asByteData(), + (ByteData response) { + if (Platform.isAndroid) { + _textureId = response.getInt64(2); + } + }, + ); + } + + int _textureId; + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + if (Platform.isIOS) { + builder.addPlatformView(0, width: 500, height: 500); + } else if (Platform.isAndroid && _textureId != null) { + builder.addTexture(_textureId, offset: const Offset(150, 300), width: 500, height: 500); + } else { + throw UnsupportedError('Platform ${Platform.operatingSystem} is not supported'); + } + + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + canvas.drawCircle(const Offset(50, 50), 50, Paint()..color = const Color(0xFFABCDEF)); + final Picture picture = recorder.endRecording(); + builder.addPicture(const Offset(300, 300), picture); + + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } +} diff --git a/testing/scenario_app/lib/src/scenario.dart b/testing/scenario_app/lib/src/scenario.dart index 348e204d6e1..be474016a31 100644 --- a/testing/scenario_app/lib/src/scenario.dart +++ b/testing/scenario_app/lib/src/scenario.dart @@ -21,10 +21,10 @@ abstract class Scenario { /// flushed. /// /// See [Window.onDrawFrame] for more details. - void onDrawFrame(); + void onDrawFrame() {} /// Called by the program when the window metrics have changed. /// /// See [Window.onMetricsChanged]. - void onMetricsChanged(); + void onMetricsChanged() {} }