diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 00a7c9ecda5bd..be8574eb30249 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -252,7 +252,8 @@ } // If there were not enough existing clip views, add more. while (clipIndex < number_of_clips) { - ChildClippingView* clippingView = [ChildClippingView new]; + ChildClippingView* clippingView = + [[ChildClippingView alloc] initWithFrame:flutter_view_.get().bounds]; [clippingView addSubview:head]; head = clippingView; clipIndex++; diff --git a/testing/scenario_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/testing/scenario_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000000000..d007606a44d83 --- /dev/null +++ b/testing/scenario_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,23 @@ +package io.flutter.plugins; + +import io.flutter.plugin.common.PluginRegistry; + +/** + * Generated file. Do not edit. + */ +public final class GeneratedPluginRegistrant { + public static void registerWith(PluginRegistry registry) { + if (alreadyRegisteredWith(registry)) { + return; + } + } + + private static boolean alreadyRegisteredWith(PluginRegistry registry) { + final String key = GeneratedPluginRegistrant.class.getCanonicalName(); + if (registry.hasPlugin(key)) { + return true; + } + registry.registrarFor(key); + return false; + } +} diff --git a/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.h b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000000000..3b700eb481958 --- /dev/null +++ b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +#endif /* GeneratedPluginRegistrant_h */ diff --git a/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.m b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000000000..60dfa42b328db --- /dev/null +++ b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +#import "GeneratedPluginRegistrant.h" + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index b6c77bab8781d..a95587c1a7fd0 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -30,6 +30,14 @@ 24D47D1B230C79840069DD5E /* golden_platform_view_D211AP.png in Resources */ = {isa = PBXBuildFile; fileRef = 24D47D1A230C79840069DD5E /* golden_platform_view_D211AP.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 */; }; + 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */; }; + 6816DBA12317573300A51400 /* GoldenImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA02317573300A51400 /* GoldenImage.m */; }; + 6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6816DBA32318358200A51400 /* PlatformViewGoldenTestManager.m */; }; + 6816DBAA2318696600A51400 /* golden_platform_view_clippath_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA52318696600A51400 /* golden_platform_view_clippath_iPhone SE_simulator.png */; }; + 6816DBAB2318696600A51400 /* golden_platform_view_transform_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA62318696600A51400 /* golden_platform_view_transform_iPhone SE_simulator.png */; }; + 6816DBAC2318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA72318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png */; }; + 6816DBAD2318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */; }; + 6816DBAE2318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -113,6 +121,17 @@ 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 = ""; }; + 6816DB9C231750ED00A51400 /* GoldenPlatformViewTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenPlatformViewTests.h; sourceTree = ""; }; + 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoldenPlatformViewTests.m; sourceTree = ""; }; + 6816DB9F2317573300A51400 /* GoldenImage.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GoldenImage.h; sourceTree = ""; }; + 6816DBA02317573300A51400 /* GoldenImage.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoldenImage.m; sourceTree = ""; }; + 6816DBA22318358200A51400 /* PlatformViewGoldenTestManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PlatformViewGoldenTestManager.h; sourceTree = ""; }; + 6816DBA32318358200A51400 /* PlatformViewGoldenTestManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewGoldenTestManager.m; sourceTree = ""; }; + 6816DBA52318696600A51400 /* golden_platform_view_clippath_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_clippath_iPhone SE_simulator.png"; sourceTree = ""; }; + 6816DBA62318696600A51400 /* golden_platform_view_transform_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_transform_iPhone SE_simulator.png"; sourceTree = ""; }; + 6816DBA72318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_opacity_iPhone SE_simulator.png"; sourceTree = ""; }; + 6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_iPhone SE_simulator.png"; sourceTree = ""; }; + 6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_iPhone SE_simulator.png"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -200,9 +219,20 @@ 244EA6CF230DBE8900B2D26E /* golden_platform_view_D21AP.png */, 24D47D1C230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png */, 24D47D1A230C79840069DD5E /* golden_platform_view_D211AP.png */, + 6816DBA52318696600A51400 /* golden_platform_view_clippath_iPhone SE_simulator.png */, + 6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */, + 6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */, + 6816DBA72318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png */, + 6816DBA62318696600A51400 /* golden_platform_view_transform_iPhone SE_simulator.png */, 248D76EE22E388380012F0C1 /* PlatformViewUITests.m */, 248D76F022E388380012F0C1 /* Info.plist */, 24D47D1E230CA4480069DD5E /* README.md */, + 6816DB9C231750ED00A51400 /* GoldenPlatformViewTests.h */, + 6816DB9D231750ED00A51400 /* GoldenPlatformViewTests.m */, + 6816DB9F2317573300A51400 /* GoldenImage.h */, + 6816DBA02317573300A51400 /* GoldenImage.m */, + 6816DBA22318358200A51400 /* PlatformViewGoldenTestManager.h */, + 6816DBA32318358200A51400 /* PlatformViewGoldenTestManager.m */, ); path = ScenariosUITests; sourceTree = ""; @@ -340,9 +370,14 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6816DBAE2318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png in Resources */, + 6816DBAB2318696600A51400 /* golden_platform_view_transform_iPhone SE_simulator.png in Resources */, + 6816DBAA2318696600A51400 /* golden_platform_view_clippath_iPhone SE_simulator.png in Resources */, + 6816DBAD2318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png in Resources */, 24D47D1B230C79840069DD5E /* golden_platform_view_D211AP.png in Resources */, 24D47D1D230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png in Resources */, 244EA6D0230DBE8900B2D26E /* golden_platform_view_D21AP.png in Resources */, + 6816DBAC2318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -375,6 +410,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 6816DBA12317573300A51400 /* GoldenImage.m in Sources */, + 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */, + 6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.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 6a419e99a7bc3..205ce79384c9c 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -25,27 +25,55 @@ - (BOOL)application:(UIApplication*)application // 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] initWithScenario:@"text_platform_view" - withCompletion:nil]; - [engine runWithEntrypoint:nil]; - - FlutterViewController* flutterViewController = - [[NoStatusBarFlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - TextPlatformViewFactory* textPlatformViewFactory = - [[TextPlatformViewFactory alloc] initWithMessenger:flutterViewController.binaryMessenger]; - NSObject* registrar = - [flutterViewController.engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; - [registrar registerViewFactory:textPlatformViewFactory withId:@"scenarios/textPlatformView"]; - self.window.rootViewController = flutterViewController; + // The launchArgsMap should match the one in the `PlatformVieGoldenTestManager`. + NSDictionary* launchArgsMap = @{ + @"--platform-view" : @"platform_view", + @"--platform-view-cliprect" : @"platform_view_cliprect", + @"--platform-view-cliprrect" : @"platform_view_cliprrect", + @"--platform-view-clippath" : @"platform_view_clippath", + @"--platform-view-transform" : @"platform_view_transform", + @"--platform-view-opacity" : @"platform_view_opacity", + }; + __block NSString* goldenTestName = nil; + [launchArgsMap + enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL* stop) { + if ([[[NSProcessInfo processInfo] arguments] containsObject:argument]) { + goldenTestName = testName; + *stop = YES; + } + }]; + + if (goldenTestName) { + [self readyContextForPlatformViewTests:goldenTestName]; } else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) { self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; } else { self.window.rootViewController = [[UIViewController alloc] init]; } - [self.window makeKeyAndVisible]; + [self.window makeKeyAndVisible]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; } +- (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier { + 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:[scenarioIdentifier 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; +} + @end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.h b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.h new file mode 100644 index 0000000000000..dead0c530e126 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.h @@ -0,0 +1,26 @@ +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@interface GoldenImage : NSObject + +@property(readonly, copy, nonatomic) NSString* goldenName; +@property(readonly, strong, nonatomic) UIImage* image; + +// Initilize with the golden file's prefix. +// +// Create an image from a golden file named prefix+devicemodel. +- (instancetype)initWithGoldenNamePrefix:(NSString*)prefix; + +// Compare this GoldenImage to `image`. +// +// Return YES if the `image` of this GoldenImage have the same pixels of provided `image`. +- (BOOL)compareGoldenToImage:(UIImage*)image; + +@end + +NS_ASSUME_NONNULL_END diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m new file mode 100644 index 0000000000000..6dbcd8e73e72a --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenImage.m @@ -0,0 +1,94 @@ +// 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 "GoldenImage.h" +#import +#include + +@interface GoldenImage () + +@end + +@implementation GoldenImage + +- (instancetype)initWithGoldenNamePrefix:(NSString*)prefix { + self = [super init]; + if (self) { + _goldenName = [prefix stringByAppendingString:_platformName()]; + NSBundle* bundle = [NSBundle bundleForClass:[self class]]; + NSURL* goldenURL = [bundle URLForResource:_goldenName withExtension:@"png"]; + NSData* data = [NSData dataWithContentsOfURL:goldenURL]; + _image = [[UIImage alloc] initWithData:data]; + } + return self; +} + +- (BOOL)compareGoldenToImage:(UIImage*)image { + if (!self.image || !image) { + return NO; + } + CGImageRef imageRefA = [self.image CGImage]; + CGImageRef imageRefB = [image 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; +} + +NS_INLINE 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; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.h b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.h new file mode 100644 index 0000000000000..de2b175368261 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.h @@ -0,0 +1,30 @@ +// 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 "PlatformViewGoldenTestManager.h" + +NS_ASSUME_NONNULL_BEGIN + +// The base class of all the PlatformView golden tests. +// +// A new PlatformView golden tests can subclass this and override the `-initiWithInvocation:` +// method, which then retun the `-initWithManager:invocation:` +// +// Then in any test method, call `checkGolden` to perform the golden test. +// +// This base class doesn't run any test case on its own. +@interface GoldenPlatformViewTests : XCTestCase + +// Initialize with a `PlatformViewGoldenTestManager`. +- (instancetype)initWithManager:(PlatformViewGoldenTestManager*)manager + invocation:(NSInvocation*)invocation; + +// Take a sceenshot of the test app and check it has the same pixels with goldenImage inside the +// `PlatformViewGoldenTestManager`. +- (void)checkGolden; + +@end + +NS_ASSUME_NONNULL_END diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m new file mode 100644 index 0000000000000..80e2af25e3bff --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/GoldenPlatformViewTests.m @@ -0,0 +1,68 @@ +// 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 "GoldenPlatformViewTests.h" +#include +#import "PlatformViewGoldenTestManager.h" + +static const NSInteger kSecondsToWaitForPlatformView = 30; + +@interface GoldenPlatformViewTests () + +@property(nonatomic, copy) NSString* goldenName; +@property(nonatomic, strong) XCUIApplication* application; + +@property(nonatomic, strong) PlatformViewGoldenTestManager* manager; + +@end + +@implementation GoldenPlatformViewTests + +- (instancetype)initWithManager:(PlatformViewGoldenTestManager*)manager + invocation:(NSInvocation*)invocation { + self = [super initWithInvocation:invocation]; + _manager = manager; + return self; +} + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; + + self.application = [[XCUIApplication alloc] init]; + self.application.launchArguments = @[ self.manager.launchArg ]; + [self.application launch]; +} + +// Note: don't prefix with "test" or GoldenPlatformViewTests will run instead of the subclasses. +- (void)checkGolden { + XCUIElement* element = self.application.textViews.firstMatch; + BOOL exists = [element waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]; + if (!exists) { + XCTFail(@"It took longer than %@ second to find the platform view." + @"There might be issues with the platform view's construction," + @"or with how the scenario is built.", + @(kSecondsToWaitForPlatformView)); + } + + GoldenImage* golden = self.manager.goldenImage; + + XCUIScreenshot* screenshot = [[XCUIScreen mainScreen] screenshot]; + XCTAttachment* attachment = [XCTAttachment attachmentWithScreenshot:screenshot]; + attachment.lifetime = XCTAttachmentLifetimeKeepAlways; + [self addAttachment:attachment]; + + if (golden.image) { + XCTAttachment* goldenAttachment = [XCTAttachment attachmentWithImage:golden.image]; + goldenAttachment.lifetime = XCTAttachmentLifetimeKeepAlways; + [self addAttachment:goldenAttachment]; + } else { + XCTFail(@"This test will fail - no golden named %@ found. Follow the steps in the " + @"README to add a new golden.", + golden.goldenName); + } + + XCTAssertTrue([golden compareGoldenToImage:screenshot.image]); +} +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGoldenTestManager.h b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGoldenTestManager.h new file mode 100644 index 0000000000000..f138aea88da3f --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGoldenTestManager.h @@ -0,0 +1,29 @@ +// 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 "GoldenImage.h" + +NS_ASSUME_NONNULL_BEGIN + +extern NSDictionary* launchArgsMap; + +// Manages a `GoldenPlatformViewTests`. +// +// It creates the correct `identifer` based on the `launchArg`. +// It also generates the correct GoldenImage based on the `identifier`. +@interface PlatformViewGoldenTestManager : NSObject + +@property(readonly, strong, nonatomic) GoldenImage* goldenImage; +@property(readonly, copy, nonatomic) NSString* identifier; +@property(readonly, copy, nonatomic) NSString* launchArg; + +// Initilize with launchArg. +// +// Crahes if the launchArg is not mapped in `Appdelegate.launchArgsMap`. +- (instancetype)initWithLaunchArg:(NSString*)launchArg; + +@end + +NS_ASSUME_NONNULL_END diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGoldenTestManager.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGoldenTestManager.m new file mode 100644 index 0000000000000..9e512d5234889 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGoldenTestManager.m @@ -0,0 +1,41 @@ +// 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 "PlatformViewGoldenTestManager.h" + +@interface PlatformViewGoldenTestManager () + +@property(readwrite, strong, nonatomic) GoldenImage* goldenImage; + +@end + +@implementation PlatformViewGoldenTestManager + +NSDictionary* launchArgsMap; + +- (instancetype)initWithLaunchArg:(NSString*)launchArg { + self = [super init]; + if (self) { + // The launchArgsMap should match the one in the `PlatformVieGoldenTestManager`. + static NSDictionary* launchArgsMap; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + launchArgsMap = @{ + @"--platform-view" : @"platform_view", + @"--platform-view-cliprect" : @"platform_view_cliprect", + @"--platform-view-cliprrect" : @"platform_view_cliprrect", + @"--platform-view-clippath" : @"platform_view_clippath", + @"--platform-view-transform" : @"platform_view_transform", + @"--platform-view-opacity" : @"platform_view_opacity", + }; + }); + _identifier = launchArgsMap[launchArg]; + NSString* prefix = [NSString stringWithFormat:@"golden_%@_", _identifier]; + _goldenImage = [[GoldenImage alloc] initWithGoldenNamePrefix:prefix]; + _launchArg = launchArg; + } + return self; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m index e0a929572b81e..00708ecc282f4 100644 --- a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewUITests.m @@ -2,114 +2,113 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -#import -#import -#include +#import "GoldenPlatformViewTests.h" -#import "../Scenarios/TextPlatformView.h" +@interface PlatformViewUITests : GoldenPlatformViewTests -@interface PlatformViewUITests : XCTestCase -@property(nonatomic, strong) XCUIApplication* application; @end @implementation PlatformViewUITests -- (void)setUp { - [super setUp]; - self.continueAfterFailure = NO; +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + PlatformViewGoldenTestManager* manager = + [[PlatformViewGoldenTestManager alloc] initWithLaunchArg:@"--platform-view"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkGolden]; +} + +@end + +// Clip Rect Tests +@interface PlatformViewMutationClipRectTests : GoldenPlatformViewTests + +@end + +@implementation PlatformViewMutationClipRectTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + PlatformViewGoldenTestManager* manager = + [[PlatformViewGoldenTestManager alloc] initWithLaunchArg:@"--platform-view-cliprect"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkGolden]; +} + +@end + +@interface PlatformViewMutationClipRRectTests : GoldenPlatformViewTests + +@end - self.application = [[XCUIApplication alloc] init]; - self.application.launchArguments = @[ @"--platform-view" ]; - [self.application launch]; +@implementation PlatformViewMutationClipRRectTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + PlatformViewGoldenTestManager* manager = + [[PlatformViewGoldenTestManager alloc] initWithLaunchArg:@"--platform-view-cliprrect"]; + return [super initWithManager:manager invocation:invocation]; } - (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]; - XCTAttachment* attachment = [XCTAttachment attachmentWithScreenshot:screenshot]; - attachment.lifetime = XCTAttachmentLifetimeKeepAlways; - [self addAttachment:attachment]; - - if (golden) { - XCTAttachment* goldenAttachment = [XCTAttachment attachmentWithImage:golden]; - goldenAttachment.lifetime = XCTAttachmentLifetimeKeepAlways; - [self addAttachment:goldenAttachment]; - } else { - XCTFail(@"This test will fail - no golden named %@ found. Follow the steps in the " - @"README to add a new golden.", - goldenName); - } - - XCTAssertTrue([self compareImage:golden toOther:screenshot.image]); + [self checkGolden]; +} + +@end + +@interface PlatformViewMutationClipPathTests : GoldenPlatformViewTests + +@end + +@implementation PlatformViewMutationClipPathTests + +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + PlatformViewGoldenTestManager* manager = + [[PlatformViewGoldenTestManager alloc] initWithLaunchArg:@"--platform-view-clippath"]; + return [super initWithManager:manager invocation:invocation]; } -- (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; +- (void)testPlatformView { + [self checkGolden]; } -- (BOOL)compareImage:(UIImage*)a toOther:(UIImage*)b { - CGImageRef imageRefA = [a CGImage]; - CGImageRef imageRefB = [b CGImage]; +@end - NSUInteger widthA = CGImageGetWidth(imageRefA); - NSUInteger heightA = CGImageGetHeight(imageRefA); - NSUInteger widthB = CGImageGetWidth(imageRefB); - NSUInteger heightB = CGImageGetHeight(imageRefB); +@interface PlatformViewMutationTransformTests : GoldenPlatformViewTests - 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]; +@end - if (!rawA || !rawB) { - return NO; - } +@implementation PlatformViewMutationTransformTests - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + PlatformViewGoldenTestManager* manager = + [[PlatformViewGoldenTestManager alloc] initWithLaunchArg:@"--platform-view-transform"]; + return [super initWithManager:manager invocation:invocation]; +} + +- (void)testPlatformView { + [self checkGolden]; +} - NSUInteger bytesPerRow = bytesPerPixel * widthA; - NSUInteger bitsPerComponent = 8; - CGContextRef contextA = - CGBitmapContextCreate(rawA.mutableBytes, widthA, heightA, bitsPerComponent, bytesPerRow, - colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); +@end - CGContextDrawImage(contextA, CGRectMake(0, 0, widthA, heightA), imageRefA); - CGContextRelease(contextA); +@interface PlatformViewMutationOpacityTests : GoldenPlatformViewTests - CGContextRef contextB = - CGBitmapContextCreate(rawB.mutableBytes, widthA, heightA, bitsPerComponent, bytesPerRow, - colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); - CGColorSpaceRelease(colorSpace); +@end - CGContextDrawImage(contextB, CGRectMake(0, 0, widthA, heightA), imageRefB); - CGContextRelease(contextB); +@implementation PlatformViewMutationOpacityTests - if (memcmp(rawA.mutableBytes, rawB.mutableBytes, size)) { - return NO; - } +- (instancetype)initWithInvocation:(NSInvocation*)invocation { + PlatformViewGoldenTestManager* manager = + [[PlatformViewGoldenTestManager alloc] initWithLaunchArg:@"--platform-view-opacity"]; + return [super initWithManager:manager invocation:invocation]; +} - return YES; +- (void)testPlatformView { + [self checkGolden]; } @end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE_simulator.png new file mode 100644 index 0000000000000..8e776f220e849 Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_iPhone SE_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_iPhone SE_simulator.png new file mode 100644 index 0000000000000..9049412903215 Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprect_iPhone SE_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone SE_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone SE_simulator.png new file mode 100644 index 0000000000000..94667b9a8254d Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_cliprrect_iPhone SE_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_iPhone SE_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_iPhone SE_simulator.png new file mode 100644 index 0000000000000..32e0a179a5c6c Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_opacity_iPhone SE_simulator.png differ diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone SE_simulator.png b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone SE_simulator.png new file mode 100644 index 0000000000000..678c7fd6bc7bb Binary files /dev/null and b/testing/scenario_app/ios/Scenarios/ScenariosUITests/golden_platform_view_transform_iPhone SE_simulator.png differ diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 11126afe69427..bbc16454470aa 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -16,7 +16,12 @@ import 'src/scenario.dart'; Map _scenarios = { 'animated_color_square': AnimatedColorSquareScenario(window), - 'text_platform_view': PlatformViewScenario(window, 'Hello from Scenarios (Platform View)'), + 'platform_view': PlatformViewScenario(window, 'Hello from Scenarios (Platform View)', id: 0), + 'platform_view_cliprect': PlatformViewClipRectScenario(window, 'PlatformViewClipRect', id: 1), + 'platform_view_cliprrect': PlatformViewClipRRectScenario(window, 'PlatformViewClipRRect', id: 2), + 'platform_view_clippath': PlatformViewClipPathScenario(window, 'PlatformViewClipPath', id: 3), + 'platform_view_transform': PlatformViewTransformScenario(window, 'PlatformViewTransform', id: 4), + 'platform_view_opacity': PlatformViewOpacityScenario(window, 'PlatformViewOpacity', id: 5), 'poppable_screen': PoppableScreenScenario(window), }; diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 4286469ce2937..354a2dcabf14d 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -4,8 +4,10 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data'; import 'dart:ui'; +import 'package:vector_math/vector_math_64.dart'; import 'scenario.dart'; @@ -26,13 +28,147 @@ List _to64(num value) { } /// A simple platform view. -class PlatformViewScenario extends Scenario { +class PlatformViewScenario extends Scenario + with _BasePlatformViewScenarioMixin { /// Creates the PlatformView scenario. /// /// The [window] parameter must not be null. PlatformViewScenario(Window window, String text, {int id = 0}) : assert(window != null), super(window) { + constructScenario(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + finishBuilderByAddingPlatformViewAndPicture(builder, 0); + } +} + +/// Platform view with clip rect. +class PlatformViewClipRectScenario extends Scenario + with _BasePlatformViewScenarioMixin { + /// Constructs a platform view with clip rect scenario. + PlatformViewClipRectScenario(Window window, String text, {int id = 0}) + : assert(window != null), + super(window) { + constructScenario(window, text, id); + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + builder.pushOffset(0, 0); + builder.pushClipRect(const Rect.fromLTRB(100, 100, 400, 400)); + finishBuilderByAddingPlatformViewAndPicture(builder, 1); + } +} + +/// Platform view with clip rrect. +class PlatformViewClipRRectScenario extends PlatformViewScenario { + /// Constructs a platform view with clip rrect scenario. + PlatformViewClipRRectScenario(Window window, String text, {int id = 0}) + : super(window, text, id: id); + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + builder.pushClipRRect( + RRect.fromLTRBAndCorners( + 100, + 100, + 400, + 400, + topLeft: const Radius.circular(15), + topRight: const Radius.circular(50), + bottomLeft: const Radius.circular(50), + ), + ); + finishBuilderByAddingPlatformViewAndPicture(builder, 2); + } +} + +/// Platform view with clip path. +class PlatformViewClipPathScenario extends PlatformViewScenario { + /// Constructs a platform view with clip rrect scenario. + PlatformViewClipPathScenario(Window window, String text, {int id = 0}) + : super(window, text, id: id); + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + + // Create a path of rectangle with width of 200 and height of 300, starting from (100, 100). + // + // Refer to "../../ios/Scenarios/Scenarios/ScenariosUITests/golden_platform_view_clippath_iPhone SE_simulator.png" for the exact path after clipping. + Path path = Path(); + path.moveTo(100, 100); + path.quadraticBezierTo(50, 250, 100, 400); + path.lineTo(350, 400); + path.cubicTo(400, 300, 300, 200, 350, 100); + path.close(); + builder.pushClipPath(path); + + finishBuilderByAddingPlatformViewAndPicture(builder, 3); + } +} + +/// Platform view with transform. +class PlatformViewTransformScenario extends PlatformViewScenario { + /// Constructs a platform view with transform scenario. + PlatformViewTransformScenario(Window window, String text, {int id = 0}) + : super(window, text, id: id); + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + final Matrix4 matrix4 = Matrix4.identity() + ..rotateZ(1) + ..scale(0.5, 0.5, 1.0) + ..translate(1000.0, 100.0, 0.0); + + builder.pushTransform(matrix4.storage); + + finishBuilderByAddingPlatformViewAndPicture(builder, 4); + } +} + +/// Platform view with opacity. +class PlatformViewOpacityScenario extends PlatformViewScenario { + /// Constructs a platform view with transform scenario. + PlatformViewOpacityScenario(Window window, String text, {int id = 0}) + : super(window, text, id: id); + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + builder.pushOpacity(150); + + finishBuilderByAddingPlatformViewAndPicture(builder, 5); + } +} + +mixin _BasePlatformViewScenarioMixin on Scenario { + int _textureId; + + /// Construct the platform view related scenario + /// + /// It prepare a TextPlatformView so it can be added to the SceneBuilder in `onBeginFrame`. + /// Call this method in the constructor of the platform view related scenarios + /// to perform necessary set up. + void constructScenario(Window window, String text, int id) { const int _valueInt32 = 3; const int _valueFloat64 = 6; const int _valueString = 7; @@ -40,7 +176,7 @@ class PlatformViewScenario extends Scenario { const int _valueMap = 13; final Uint8List message = Uint8List.fromList([ _valueString, - 'create'.length, // this is safe as long as these are all single byte characters. + 'create'.length, // this won't work if we use multi-byte characters. ...utf8.encode('create'), _valueMap, if (Platform.isIOS) @@ -94,29 +230,27 @@ class PlatformViewScenario extends Scenario { ); } - int _textureId; - - @override - void onBeginFrame(Duration duration) { - final SceneBuilder builder = SceneBuilder(); - - builder.pushOffset(0, 0); - + // Add a platform view and a picture to the scene, then finish the `sceneBuilder`. + void finishBuilderByAddingPlatformViewAndPicture(SceneBuilder sceneBuilder, int viewId) { if (Platform.isIOS) { - builder.addPlatformView(0, width: 500, height: 500); + sceneBuilder.addPlatformView(viewId, width: 500, height: 500); } else if (Platform.isAndroid && _textureId != null) { - builder.addTexture(_textureId, offset: const Offset(150, 300), width: 500, height: 500); + sceneBuilder.addTexture(_textureId, + offset: const Offset(150, 300), width: 500, height: 500); } else { - throw UnsupportedError('Platform ${Platform.operatingSystem} is not supported'); + 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)); + 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(); + sceneBuilder.addPicture(const Offset(300, 300), picture); + final Scene scene = sceneBuilder.build(); window.render(scene); scene.dispose(); } diff --git a/testing/scenario_app/pubspec.yaml b/testing/scenario_app/pubspec.yaml index 8748b51653ac9..f17ddb080a7dc 100644 --- a/testing/scenario_app/pubspec.yaml +++ b/testing/scenario_app/pubspec.yaml @@ -8,3 +8,4 @@ dependencies: path: ../../../out/host_debug_unopt/gen/dart-pkg/sky_engine sky_services: path: ../../../out/host_debug_unopt/gen/dart-pkg/sky_services + vector_math: ^2.0.8