Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

iOS platform view mutation XCUITests #11652

Merged
merged 11 commits into from
Sep 15, 2019

Conversation

cyanglaz
Copy link
Contributor

@cyanglaz cyanglaz commented Aug 28, 2019

  • Added scenarios for all the available mutations on iOS platform view. Including: clipRect, clipRRect, clipPath, transform, opacity
  • Added screenshot tests for each scenarios that added.
  • for each test(including the existing test), added code to wait until the platform view is visible on the screen.
  • Added golden for simulator iphone se
  • ClippingView was previously size (0, 0). The XCUITest ignore the ClippingView because of this. Updated the ClippingView to the same size as flutterView so it can be found by the XCUITest.
  • Factored the platform_view.dart so it is easier to add a new platform related scenario
  • Factored some util methods out of PlatformViewUITests.m so it is easier to add new platform view related UITests

@dnfield let me know if you prefer smaller patches.

All the platform view mutation updates will be unblocked after landing this.

@cyanglaz cyanglaz requested a review from dnfield August 28, 2019 21:26
#import "PlatformViewUITestUtil.h"
#include <sys/sysctl.h>

const NSInteger kTimeToWaitForPlatformView = 5;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make this higher. Particularly on a simulator on a bogged down CI machine this might be slow. There's no harm in giving it 30 seconds before we fail the whole test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also have a comment here explaining that this is in seconds.

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.",
@(kTimeToWaitForPlatformView));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can this just be moved to setUp after launching?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, all of these similar ones to either setUp or some utility/super method to avoid the repetition

Copy link
Member

@jmagman jmagman Aug 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asserting in -setUp can have weird test failures, the details of which are slipping my mine at the moment (it says some test failed but doesn't say which?). However refactoring out into a method that's used at the top of each test would work (assuming the bigger subclass refactor I suggest doesn't get done).

XCTAssertTrue([PlatformViewUITestUtil compareImage:golden toOther:screenshot.image]);
}

@end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For pretty much all of these - maybe just one method that wrapps all the common behavior and takes in the expected golden file name?


final PictureRecorder recorder = PictureRecorder();
final Canvas canvas = Canvas(recorder);
canvas.drawCircle(const Offset(50, 50), 50, Paint()..color = const Color(0xFFABCDEF));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do something like this in the base case? It seemed valuable to me to make sure that Flutter drawn UI would still show up with or over a platform view.

final SceneBuilder builder = SceneBuilder();

builder.pushOffset(0, 0);
builder.pushClipRRect(RRect.fromLTRBAndCorners(50, 50, 300, 300, topLeft:Radius.circular(15), topRight:Radius.circular(50), bottomLeft:Radius.circular(50)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - add some trailing commas and split up this long line


builder.pushOffset(0, 0);
Path path = Path();
path.moveTo(200, 0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit - add a comment explaining what shape you're expecting to build here.

matrix4[15] = 1.0;


// rotate for 1 degree radians
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this should just be rotate 1 radian.

It also might be worht just using Matrix4 here and taking a dependency on vector_math, but probably not a big deal either way.

}
}
}
Copy link
Contributor

@dnfield dnfield Aug 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

new line at EOF

Copy link
Contributor

@dnfield dnfield left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Over all looks good, mostly just nits.

@dnfield dnfield requested a review from jmagman August 28, 2019 22:36
@dnfield
Copy link
Contributor

dnfield commented Aug 28, 2019

@jmagman might have some good tips about structuring the Unit tests and generally reviewing the Objective C :)

// found in the LICENSE file.

#import <XCTest/XCTest.h>
#import "../Scenarios/TextPlatformView.h"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect to not need a relative path:
#import "TextPlatformView.h"

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.",
@(kTimeToWaitForPlatformView));
Copy link
Member

@jmagman jmagman Aug 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Asserting in -setUp can have weird test failures, the details of which are slipping my mine at the moment (it says some test failed but doesn't say which?). However refactoring out into a method that's used at the top of each test would work (assuming the bigger subclass refactor I suggest doesn't get done).

NSBundle* bundle = [NSBundle bundleForClass:[self class]];
NSString* goldenName = [NSString
stringWithFormat:@"golden_platform_view_cliprect_%@", [PlatformViewUITestUtil platformName]];
NSString* path = [bundle pathForResource:goldenName ofType:@"png"];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer NSURL APIs over NSString path ones. I realized you copied this from @dnfield's code, but I missed it in that code review! Unfortunately there isn't a +[UIImage imageWithData:] API so it adds a local.

NSURL* goldenURL = [bundle URLForResource:goldenName withExtension:@"png"];
NSData* goldenData = [NSData dataWithContentsOfURL:goldenURL]
UIImage* golden = [[UIImage alloc] initWithData:goldenData];

// Clip Rect Tests
@interface PlatformViewMutationClipRectTests : XCTestCase

@property(nonatomic, strong) XCUIApplication* application;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this file be DRY'd out? It looks like a lot of boilerplate.
Maybe something like this? I wrote this up but didn't try to compile, so check it first.

@interface GoldenPlatformViewTests : XCTestCase
- (instancetype)initWithLaunchArguments:(NSString*)platformViewLaunchArguments goldenName:(NSString*)goldenName NS_DESIGNATED_INITIALIZER;
- (instancetype)init NS_UNAVAILABLE;
@end

@interface GoldenPlatformViewTests ()
@property (copy) NSString* platformViewLaunchArguments;
@property (copy) NSString* goldenName;
@end

@implementation GoldenPlatformViewTests

- (instancetype)initWithLaunchArguments:(NSString*)platformViewLaunchArguments goldenName:(NSString*)goldenName {
   self = [super init];
   _platformViewLaunchArguments = platformViewLaunchArguments;
   _goldenName = goldenName;
   return self;
}

- (void)setUp {
  [super setUp];
  self.continueAfterFailure = NO;

   self.application = [[XCUIApplication alloc] init];
  self.application.launchArguments = @[ self.platformViewLaunchArguments ];
  [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:kTimeToWaitForPlatformView];
  if (!exists) {
...
}
}
@end
@interface PlatformViewMutationClipRRectTests : GoldenPlatformViewTests
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithLaunchArguments:(NSString*)platformViewLaunchArguments goldenName:(NSString*)goldenName NS_UNAVAILABLE;
@end
@implementation PlatformViewMutationClipRectTests
- (instancetype)init {
   NSString* goldenName = [NSString
      stringWithFormat:@"golden_platform_view_cliprect_%@", [PlatformViewUITestUtil platformName]];
   return [super initWithLaunchArguments:@"--platform-view-cliprect" goldenName:goldenName];
}
@end
- (void)testPlatformView {
  [self checkGolden];
}


@interface PlatformViewUITestUtil : NSObject

+ (NSString*)platformName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a class property.

@property (class, copy, readonly) NSString* platformName;


extern const NSInteger kTimeToWaitForPlatformView;

@interface PlatformViewUITestUtil : NSObject
Copy link
Member

@jmagman jmagman Aug 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of this utility class, can you make a GoldenImage class with a UIImage property, then implement -isEqual: and -hash?
Then you can initialize it with a name prefix, and it can have a goldenName property that uses the hardware logic, and even figure out the launch args.
If you do that, then my above suggested GoldenPlatformViewTests initializer could become:

- (instancetype)initWithGoldenImage:(GoldenImage*)goldenImage;

@cyanglaz
Copy link
Contributor Author

Updated the PR!

@dnfield
I ended up mostly followed with @jmagman's suggestion on the objc side.
On the dart side, I updated with your suggestion and also added a little drawing to show what would the clip path look like :). Also added Matrix4 deps for transform. And refactored more code in onBeginFrame into the parent class including adding a picture on top of the platform view

@jmagman
I improvised a bit and created a PlatformViewGoldenTestManager along with the GoldenImage class.
GoldenImage handles things related to golden, such as getting a UIImage from golden name, construct the golden name, and compare the golden image to a UIImage(screenshot).
PlatformViewGoldenTestManager contains a map mapping an id with the run arguments. So it chose what run args to use when initialized with an id. It also creates a GoldenImage based on the id., which the id is used as the prefix of the goldenImageName.

// The left side of the rectangle becomes a symmetric curve towards the left.
// The right side of the rectangle becomes a double curve. From top of bottom of the curve, it goes left then right.
// _______
// | |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe just a reference to the golden image for this, or a link to it on github instead of the ASCII art.


builder.pushOffset(0, 0);
final Matrix4 matrix4 = Matrix4.identity();
matrix4.rotateZ(1.0);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

final Matrix4 matrix4 = Matrix4.identity()
  ..rotateZ(1.0)
  ..scale(0.5, 0.5, 1.0)
  ..translate(1000.0, 100.0, 0.0);

matrix4.scale(0.5, 0.5, 1.0);
matrix4.translate(1000.0, 100.0, 0.0);

final Float64List matrix4_64 = Float64List.fromList(matrix4.storage);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need the copy here - just do builder.pushTransform(matrix4.storage)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matrix4.storage is a Float32List, builder.pushTransform(matrix4.storage) throws a compile error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh - make sure you import vector_math_64.dart :)

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.
'create'
.length, // this is safe as long as these are all single byte characters.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move this back up to line 186 - it's ok if the comment goes over 80. Maybe just ditch the comment, or rephrase as // this won't work if we use multibyte characters

@@ -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: any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a specific version for vector_math, e.g. ^2.0.8

@dnfield
Copy link
Contributor

dnfield commented Aug 29, 2019

Looking better! A few comments on the Dart side, I'll defer to @jmagman to finish out the ObjC review but it looks better overall.

@@ -6,6 +6,7 @@ import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';
import 'package:vector_math/vector_math.dart';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you want import 'package:vector_math/vector_math_64.dart';


@interface GoldenImage : NSObject

@property(readonly, strong, nonatomic) NSString* goldenName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy

// Compare this GoldenImage to `image`.
//
// Return YES if the `image` of this GoldenImage have the same pixels of provided `image`.
- (BOOL)compareGoldenToImage:(nonnull UIImage*)image;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove nonnull, you are in a NS_ASSUME_NONNULL_BEGIN. You only need to mark nullables.


@interface GoldenImage ()

@property(readwrite, strong, nonatomic) NSString* goldenName;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove all these read-write properties, you are setting their backing instance variable in the init so the setters aren't called anywhere.


@property(readwrite, strong, nonatomic) NSString* goldenName;
@property(readwrite, strong, nonatomic) UIImage* image;
@property(strong, nonatomic) NSString* platformName;
Copy link
Member

@jmagman jmagman Aug 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this property, you don't need the backing instance variable since you only use it one time in init to create _goldenName.
You can make it a NS_INLINE function:

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;
}

return self;
}

- (BOOL)compareGoldenToImage:(UIImage*)image {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Override -isEqual: and -hash instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Override -isEqual: and -hash instead.

Oh actually never mind, that would require you to make a GoldenImage out of the screen shot. This is fine.


static NSDictionary* _launchArgsMap;

+ (void)initialize {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overriding +initialize is very frowned upon. You can make the static dictionary a local variable in -initWithIdentifier. Or create the tested golden out of the launch args. Or make it an extern somewhere to be used by AppDelegate as @dnfield suggests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or make it an extern somewhere to be used by AppDelegate as @dnfield suggests.

@cyanglaz correctly points out this isn't possible in a XCUITest.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh right.


builder.pushOffset(0, 0);
final Matrix4 matrix4 = Matrix4.identity()
..rotateZ(1.0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit agian: indent these :)

Copy link
Contributor

@dnfield dnfield left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dart code and overall approach LGTM. Will defer to @jmagman for rest of ObjC review

@cyanglaz
Copy link
Contributor Author

@jmagman

Updated the objc code. I think it is ready for another review.

I ended up adding duplicate maps in AppDelegate and PlatformViewGoldenTestManager.

Copy link
Member

@jmagman jmagman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with nits about the static dictionary, fast enumeration, and auditing for generics.
Thank you for making all these changes, it looks good!

};
});
BOOL hasGoldenLaunchArg = NO;
for (NSString* key in launchArgsMap.allKeys) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use fast-enumeration for iterating dictionaries instead of allocating a new array and then doing a value lookup later.

__block BOOL hasGoldenLaunchArg = NO;
[launchArgsMap enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL *stop) {
   if ([[[NSProcessInfo processInfo] arguments] containsObject:argument]) {
     [self readyContextForPlatformViewTests:testName];
     hasGoldenLaunchArg = YES;
     *stop = YES;
  }
}];

self.window.rootViewController = flutterViewController;
} else {
// The launchArgsMap should match the one in the `PlatformVieGoldenTestManager`.
static NSDictionary* launchArgsMap;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

application:didFinishLaunchingWithOptions: will only be called once, so you don't need the overhead of the static and dispatch_once.
Also, always use generics.

NSDictionary<NSString*, NSString*>* launchArgsMap;

@cyanglaz cyanglaz merged commit b73cfda into flutter:master Sep 15, 2019
@cyanglaz cyanglaz deleted the ios_platform_view_mutation_uitest branch September 15, 2019 23:56
iskakaushik pushed a commit to iskakaushik/engine that referenced this pull request Sep 16, 2019
engine-flutter-autoroll added a commit to flutter/flutter that referenced this pull request Sep 16, 2019
git@github.com:flutter/engine.git/compare/834cd15010b2...31e5149

git log 834cd15..31e5149 --no-merges --oneline
2019-09-16 iska.kaushik@gmail.com Revert "Revert "Roll src/third_party/dart a554c8be6b..d0052c1b31 (22 commits)" (#12290)" (flutter/engine#12291)
2019-09-16 iska.kaushik@gmail.com Revert "Add iOS platform view mutation XCUITests to the scenario app (#11652)" (flutter/engine#12292)
2019-09-15 ychris@google.com Add iOS platform view mutation XCUITests to the scenario app (flutter/engine#11652)


If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-engine-flutter-autoroll
Please CC jimgraham@google.com on the revert to ensure that a human
is aware of the problem.

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+/master/autoroll/README.md
@cyanglaz cyanglaz restored the ios_platform_view_mutation_uitest branch September 23, 2019 02:00
Inconnu08 pushed a commit to Inconnu08/flutter that referenced this pull request Sep 30, 2019
git@github.com:flutter/engine.git/compare/834cd15010b2...31e5149

git log 834cd15..31e5149 --no-merges --oneline
2019-09-16 iska.kaushik@gmail.com Revert "Revert "Roll src/third_party/dart a554c8be6b..d0052c1b31 (22 commits)" (flutter#12290)" (flutter/engine#12291)
2019-09-16 iska.kaushik@gmail.com Revert "Add iOS platform view mutation XCUITests to the scenario app (flutter#11652)" (flutter/engine#12292)
2019-09-15 ychris@google.com Add iOS platform view mutation XCUITests to the scenario app (flutter/engine#11652)


If this roll has caused a breakage, revert this CL and stop the roller
using the controls here:
https://autoroll.skia.org/r/flutter-engine-flutter-autoroll
Please CC jimgraham@google.com on the revert to ensure that a human
is aware of the problem.

To report a problem with the AutoRoller itself, please file a bug:
https://bugs.chromium.org/p/skia/issues/entry?template=Autoroller+Bug

Documentation for the AutoRoller is here:
https://skia.googlesource.com/buildbot/+/master/autoroll/README.md
cyanglaz pushed a commit to cyanglaz/engine that referenced this pull request Sep 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants