Add tests for platform views (#11319)

This commit is contained in:
Dan Field
2019-08-20 20:48:40 -07:00
committed by GitHub
parent 51bdf83420
commit c8f02c95ea
16 changed files with 536 additions and 103 deletions
@@ -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<ByteBuffer> 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<ByteBuffer> 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();
});
}
}
@@ -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() {}
}
@@ -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<Object>() {
@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);
}
}
@@ -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 = "<group>"; };
248D76D922E388380012F0C1 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
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 = "<group>"; };
248D76E522E388380012F0C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
248D76EE22E388380012F0C1 /* PlatformViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewUITests.m; sourceTree = "<group>"; };
248D76F022E388380012F0C1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FlutterEngineTest.m; sourceTree = "<group>"; };
24D47D1A230C79840069DD5E /* golden_platform_view_D21AAP.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = golden_platform_view_D21AAP.png; sourceTree = "<group>"; };
24D47D1C230CA2700069DD5E /* golden_platform_view_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_iPhone SE_simulator.png"; sourceTree = "<group>"; };
24D47D1E230CA4480069DD5E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
24F1FB87230B4579005ACE7C /* TextPlatformView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextPlatformView.m; sourceTree = "<group>"; };
24F1FB88230B4579005ACE7C /* TextPlatformView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextPlatformView.h; sourceTree = "<group>"; };
/* 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 = "<group>";
@@ -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;
};
@@ -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<FlutterPluginRegistrar>* 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];
}
@@ -30,6 +30,8 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>io.flutter.embedded_views_preview</key>
<true/>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -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 <Flutter/Flutter.h>
NS_ASSUME_NONNULL_BEGIN
@interface TextPlatformView : NSObject <FlutterPlatformView>
- (instancetype)initWithFrame:(CGRect)frame
viewIdentifier:(int64_t)viewId
arguments:(id _Nullable)args
binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
- (UIView*)view;
@end
@interface TextPlatformViewFactory : NSObject <FlutterPlatformViewFactory>
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
@end
NS_ASSUME_NONNULL_END
@@ -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<FlutterBinaryMessenger>* _messenger;
}
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messenger {
self = [super init];
if (self) {
_messenger = messenger;
}
return self;
}
- (NSObject<FlutterPlatformView>*)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<FlutterMessageCodec>*)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<FlutterBinaryMessenger>*)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
@@ -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 <Flutter/flutter.h>
#import <XCTest/XCTest.h>
#include <sys/sysctl.h>
#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
@@ -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.
@@ -1,42 +0,0 @@
//
// ScenariosUITests.m
// ScenariosUITests
//
// Created by Dan Field on 7/20/19.
// Copyright © 2019 flutter. All rights reserved.
//
#import <XCTest/XCTest.h>
@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 its 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+8 -2
View File
@@ -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<String, Scenario> _scenarios = <String, Scenario>{
'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<void> _handlePlatformMessage(String name, ByteData data, PlatformMessageResponseCallback callback) async {
Future<void> _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<String> _getTimelineData() async {
final Map<String, dynamic> cpuTimelineJson = await _getJson(cpuProfileTimelineUri);
final Map<String, dynamic> vmServiceTimelineJson = await _getJson(vmServiceTimelineUri);
final Map<String, dynamic> cpuResult = cpuTimelineJson['result'].cast<String, dynamic>();
final Map<String, dynamic> vmServiceResult = vmServiceTimelineJson['result'].cast<String, dynamic>();
final Map<String, dynamic> vmServiceResult =
vmServiceTimelineJson['result'].cast<String, dynamic>();
return json.encode(<String, dynamic>{
'stackFrames': cpuResult['stackFrames'],
@@ -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<int> _to32(int value) {
final Uint8List temp = Uint8List(4);
temp.buffer.asByteData().setInt32(0, value, Endian.little);
return temp;
}
List<int> _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(<int>[
_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) ...<int>[
_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();
}
}
+2 -2
View File
@@ -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() {}
}