diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index 10459ac686541..b6c77bab8781d 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 0A57B3BD2323C4BD00DD9521 /* ScreenBeforeFlutter.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */; }; + 0A57B3BF2323C74200DD9521 /* FlutterEngine+ScenariosTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */; }; + 0A57B3C22323D2D700DD9521 /* AppLifecycleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */; }; 0DB781EF22E931BE00E9B371 /* Flutter.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 246B4E4522E3B61000073EBF /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0DB781F122E933E800E9B371 /* Flutter.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 246B4E4522E3B61000073EBF /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 0DB781FE22EA2C6D00E9B371 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 246B4E4522E3B61000073EBF /* Flutter.framework */; }; @@ -84,6 +87,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0A57B3BB2323C4BD00DD9521 /* ScreenBeforeFlutter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ScreenBeforeFlutter.h; sourceTree = ""; }; + 0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ScreenBeforeFlutter.m; sourceTree = ""; }; + 0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FlutterEngine+ScenariosTest.m"; sourceTree = ""; }; + 0A57B3C02323C74D00DD9521 /* FlutterEngine+ScenariosTest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FlutterEngine+ScenariosTest.h"; sourceTree = ""; }; + 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppLifecycleTests.m; sourceTree = ""; }; 0DB781FC22EA2C0300E9B371 /* FlutterViewControllerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FlutterViewControllerTest.m; sourceTree = ""; }; 244EA6CF230DBE8900B2D26E /* golden_platform_view_D21AP.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = golden_platform_view_D21AP.png; sourceTree = ""; }; 246B4E4122E3B5F700073EBF /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = App.framework; sourceTree = ""; }; @@ -167,6 +175,10 @@ 248D76D322E388380012F0C1 /* Assets.xcassets */, 248D76D822E388380012F0C1 /* Info.plist */, 248D76D922E388380012F0C1 /* main.m */, + 0A57B3BB2323C4BD00DD9521 /* ScreenBeforeFlutter.h */, + 0A57B3BC2323C4BD00DD9521 /* ScreenBeforeFlutter.m */, + 0A57B3BE2323C74200DD9521 /* FlutterEngine+ScenariosTest.m */, + 0A57B3C02323C74D00DD9521 /* FlutterEngine+ScenariosTest.h */, ); path = Scenarios; sourceTree = ""; @@ -177,6 +189,7 @@ 248FDFC322FE7CD0009CC7CD /* FlutterEngineTest.m */, 0DB781FC22EA2C0300E9B371 /* FlutterViewControllerTest.m */, 248D76E522E388380012F0C1 /* Info.plist */, + 0A57B3C12323D2D700DD9521 /* AppLifecycleTests.m */, ); path = ScenariosTests; sourceTree = ""; @@ -273,13 +286,16 @@ TargetAttributes = { 248D76C622E388370012F0C1 = { CreatedOnToolsVersion = 10.2.1; + LastSwiftMigration = 1030; }; 248D76DE22E388380012F0C1 = { CreatedOnToolsVersion = 10.2.1; + LastSwiftMigration = 1030; TestTargetID = 248D76C622E388370012F0C1; }; 248D76E922E388380012F0C1 = { CreatedOnToolsVersion = 10.2.1; + LastSwiftMigration = 1030; TestTargetID = 248D76C622E388370012F0C1; }; }; @@ -340,6 +356,8 @@ 248D76DA22E388380012F0C1 /* main.m in Sources */, 24F1FB89230B4579005ACE7C /* TextPlatformView.m in Sources */, 248D76CC22E388370012F0C1 /* AppDelegate.m in Sources */, + 0A57B3BF2323C74200DD9521 /* FlutterEngine+ScenariosTest.m in Sources */, + 0A57B3BD2323C4BD00DD9521 /* ScreenBeforeFlutter.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -348,6 +366,7 @@ buildActionMask = 2147483647; files = ( 0DB7820222EA493B00E9B371 /* FlutterViewControllerTest.m in Sources */, + 0A57B3C22323D2D700DD9521 /* AppLifecycleTests.m in Sources */, 248FDFC422FE7CD0009CC7CD /* FlutterEngineTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -492,6 +511,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = S8QB4VV633; diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme index a780f4aacf3ca..87799ad5e6434 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme @@ -83,6 +83,16 @@ ReferencedContainer = "container:Scenarios.xcodeproj"> + + + + + + diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.h b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.h index 36e21bbf9cf40..b7984f089a9c5 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.h +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.h @@ -1,3 +1,7 @@ +// Copyright 2019 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 #import diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 38e14bda4f359..6a419e99a7bc3 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -1,4 +1,10 @@ +// Copyright 2019 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. + #include "AppDelegate.h" +#import "FlutterEngine+ScenariosTest.h" +#import "ScreenBeforeFlutter.h" #import "TextPlatformView.h" @interface NoStatusBarFlutterViewController : FlutterViewController @@ -20,24 +26,20 @@ - (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] initWithName:@"PlatformViewTest" project:nil]; + FlutterEngine* engine = [[FlutterEngine alloc] initWithScenario:@"text_platform_view" + withCompletion:nil]; [engine runWithEntrypoint:nil]; FlutterViewController* flutterViewController = [[NoStatusBarFlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; - [engine.binaryMessenger - setMessageHandlerOnChannel:@"scenario_status" - binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) { - [engine.binaryMessenger - sendOnChannel:@"set_scenario" - message:[@"text_platform_view" dataUsingEncoding:NSUTF8StringEncoding]]; - }]; TextPlatformViewFactory* textPlatformViewFactory = [[TextPlatformViewFactory alloc] initWithMessenger:flutterViewController.binaryMessenger]; NSObject* registrar = [flutterViewController.engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; [registrar registerViewFactory:textPlatformViewFactory withId:@"scenarios/textPlatformView"]; self.window.rootViewController = flutterViewController; + } else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) { + self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; } else { self.window.rootViewController = [[UIViewController alloc] init]; } diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.h b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.h new file mode 100644 index 0000000000000..8b04f666f2676 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.h @@ -0,0 +1,12 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +NS_ASSUME_NONNULL_BEGIN +@interface FlutterEngine (ScenariosTest) +- (instancetype)initWithScenario:(NSString*)scenario + withCompletion:(nullable void (^)(void))engineRunCompletion; +@end +NS_ASSUME_NONNULL_END diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m new file mode 100644 index 0000000000000..6291381c0fabe --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/Scenarios/FlutterEngine+ScenariosTest.m @@ -0,0 +1,28 @@ +// Copyright 2019 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 "FlutterEngine+ScenariosTest.h" + +@implementation FlutterEngine (ScenariosTest) + +- (instancetype)initWithScenario:(NSString*)scenario + withCompletion:(nullable void (^)(void))engineRunCompletion { + NSAssert([scenario length] != 0, @"You need to provide a scenario"); + self = [self initWithName:[NSString stringWithFormat:@"Test engine for %@", scenario] + project:nil]; + [self runWithEntrypoint:nil]; + [self.binaryMessenger + setMessageHandlerOnChannel:@"scenario_status" + binaryMessageHandler:^(NSData* message, FlutterBinaryReply reply) { + [self.binaryMessenger + sendOnChannel:@"set_scenario" + message:[scenario dataUsingEncoding:NSUTF8StringEncoding]]; + if (engineRunCompletion != nil) { + engineRunCompletion(); + } + }]; + return self; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/ScreenBeforeFlutter.h b/testing/scenario_app/ios/Scenarios/Scenarios/ScreenBeforeFlutter.h new file mode 100644 index 0000000000000..4eb644f5ccb94 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/Scenarios/ScreenBeforeFlutter.h @@ -0,0 +1,14 @@ +// Copyright 2019 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 + +@interface ScreenBeforeFlutter : UIViewController + +- (id)initWithEngineRunCompletion:(void (^)(void))engineRunCompletion; +- (FlutterViewController*)showFlutter; + +@property(nonatomic, readonly) FlutterEngine* engine; + +@end diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/ScreenBeforeFlutter.m b/testing/scenario_app/ios/Scenarios/Scenarios/ScreenBeforeFlutter.m new file mode 100644 index 0000000000000..f5eef154390c9 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/Scenarios/ScreenBeforeFlutter.m @@ -0,0 +1,54 @@ +// Copyright 2019 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 "ScreenBeforeFlutter.h" +#import "FlutterEngine+ScenariosTest.h" + +@implementation ScreenBeforeFlutter + +FlutterEngine* _engine; + +- (id)initWithEngineRunCompletion:(void (^)(void))engineRunCompletion { + self = [super init]; + _engine = [[FlutterEngine alloc] initWithScenario:@"poppable_screen" + withCompletion:engineRunCompletion]; + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = UIColor.grayColor; + + UIButton* showFlutterButton = [UIButton buttonWithType:UIButtonTypeSystem]; + showFlutterButton.translatesAutoresizingMaskIntoConstraints = NO; + showFlutterButton.backgroundColor = UIColor.blueColor; + [showFlutterButton setTitle:@"Show Flutter" forState:UIControlStateNormal]; + showFlutterButton.tintColor = UIColor.whiteColor; + showFlutterButton.clipsToBounds = YES; + [showFlutterButton addTarget:self + action:@selector(showFlutter) + forControlEvents:UIControlEventTouchUpInside]; + + [self.view addSubview:showFlutterButton]; + [[showFlutterButton.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor] setActive:YES]; + [[showFlutterButton.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor] setActive:YES]; + [[showFlutterButton.heightAnchor constraintEqualToConstant:50] setActive:YES]; + [[showFlutterButton.widthAnchor constraintEqualToConstant:150] setActive:YES]; + + [_engine runWithEntrypoint:nil]; +} + +- (FlutterViewController*)showFlutter { + FlutterViewController* flutterVC = [[FlutterViewController alloc] initWithEngine:_engine + nibName:nil + bundle:nil]; + [self presentViewController:flutterVC animated:NO completion:nil]; + return flutterVC; +} + +- (FlutterEngine*)engine { + return _engine; +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m b/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m new file mode 100644 index 0000000000000..4598c04e711ba --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosTests/AppLifecycleTests.m @@ -0,0 +1,144 @@ +// Copyright 2019 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 +#import +#import "ScreenBeforeFlutter.h" + +@interface AppLifecycleTests : XCTestCase +@end + +@implementation AppLifecycleTests + +- (void)setUp { + [super setUp]; + self.continueAfterFailure = NO; +} + +- (void)testLifecycleChannel { + XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"]; + + // Let the engine finish booting (at the end of which the channels are properly set-up) before + // moving onto the next step of showing the next view controller. + ScreenBeforeFlutter* rootVC = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:^void() { + [engineStartedExpectation fulfill]; + }]; + + [self waitForExpectationsWithTimeout:5 handler:nil]; + + UIApplication* application = UIApplication.sharedApplication; + application.delegate.window.rootViewController = rootVC; + FlutterEngine* engine = rootVC.engine; + + NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10]; + NSMutableArray* lifecycleEvents = [NSMutableArray arrayWithCapacity:10]; + + [lifecycleExpectations addObject:[[XCTestExpectation alloc] + initWithDescription:@"A loading FlutterViewController goes " + @"through AppLifecycleState.inactive"]]; + [lifecycleExpectations + addObject:[[XCTestExpectation alloc] + initWithDescription: + @"A loading FlutterViewController goes through AppLifecycleState.resumed"]]; + + FlutterViewController* flutterVC = [rootVC showFlutter]; + [engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) { + if (lifecycleExpectations.count == 0) { + XCTFail(@"Unexpected lifecycle transition: %@", message); + } + [lifecycleEvents addObject:message]; + [[lifecycleExpectations objectAtIndex:0] fulfill]; + [lifecycleExpectations removeObjectAtIndex:0]; + }]; + + [self waitForExpectations:lifecycleExpectations timeout:5]; + + // Expected sequence from showing the FlutterViewController is inactive and resumed. + NSArray* expectedStates = @[ @"AppLifecycleState.inactive", @"AppLifecycleState.resumed" ]; + XCTAssertEqualObjects(lifecycleEvents, expectedStates, + @"AppLifecycleState transitions while presenting not as expected"); + + // Now dismiss the FlutterViewController again and expect another inactive and paused. + [lifecycleExpectations + addObject:[[XCTestExpectation alloc] + initWithDescription:@"A dismissed FlutterViewController goes through " + @"AppLifecycleState.inactive"]]; + [lifecycleExpectations + addObject:[[XCTestExpectation alloc] + initWithDescription:@"A dismissed FlutterViewController goes through " + @"AppLifecycleState.paused"]]; + [flutterVC dismissViewControllerAnimated:NO completion:nil]; + [self waitForExpectations:lifecycleExpectations timeout:5]; + expectedStates = @[ + @"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive", + @"AppLifecycleState.paused" + ]; + XCTAssertEqualObjects(lifecycleEvents, expectedStates, + @"AppLifecycleState transitions while dismissing not as expected"); + + // Now put the app in the background (while the engine is still running) and bring it back to + // the foreground. Granted, we're not winning any awards for hyper-realism but at least we're + // checking that we aren't observing the UIApplication notifications and double registering + // for AppLifecycleState events. + + // However the production is temporarily wrong. https://github.com/flutter/flutter/issues/37226. + // It will be fixed in a next PR that removes the wrong asserts. + [lifecycleExpectations + addObject: + [[XCTestExpectation alloc] + initWithDescription:@"Current implementation sends another AppLifecycleState event"]]; + [[NSNotificationCenter defaultCenter] + postNotificationName:UIApplicationWillResignActiveNotification + object:nil]; + [lifecycleExpectations + addObject: + [[XCTestExpectation alloc] + initWithDescription:@"Current implementation sends another AppLifecycleState event"]]; + [[NSNotificationCenter defaultCenter] + postNotificationName:UIApplicationDidEnterBackgroundNotification + object:nil]; + [lifecycleExpectations + addObject: + [[XCTestExpectation alloc] + initWithDescription:@"Current implementation sends another AppLifecycleState event"]]; + [[NSNotificationCenter defaultCenter] + postNotificationName:UIApplicationWillEnterForegroundNotification + object:nil]; + [lifecycleExpectations + addObject: + [[XCTestExpectation alloc] + initWithDescription:@"Current implementation sends another AppLifecycleState event"]]; + [[NSNotificationCenter defaultCenter] + postNotificationName:UIApplicationDidBecomeActiveNotification + object:nil]; + + // There's no timing latch for our semi-fake background-foreground cycle so launch the + // FlutterViewController again to check the complete event list again. + [lifecycleExpectations addObject:[[XCTestExpectation alloc] + initWithDescription:@"A second FlutterViewController goes " + @"through AppLifecycleState.inactive"]]; + [lifecycleExpectations + addObject:[[XCTestExpectation alloc] + initWithDescription: + @"A second FlutterViewController goes through AppLifecycleState.resumed"]]; + flutterVC = [rootVC showFlutter]; + [self waitForExpectations:lifecycleExpectations timeout:5]; + expectedStates = @[ + @"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive", + @"AppLifecycleState.paused", + + // The production code currently misbehaves. https://github.com/flutter/flutter/issues/37226. + // It will be fixed in a next PR that removes the wrong asserts. + @"AppLifecycleState.inactive", @"AppLifecycleState.paused", @"AppLifecycleState.inactive", + @"AppLifecycleState.resumed", + + // We only added 2 from re-launching the FlutterViewController + // and none from the background-foreground cycle. + @"AppLifecycleState.inactive", @"AppLifecycleState.resumed" + ]; + XCTAssertEqualObjects( + lifecycleEvents, expectedStates, + @"AppLifecycleState transitions while presenting a second time not as expected"); +} +@end diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 18a64dab08327..11126afe69427 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -11,11 +11,13 @@ import 'dart:ui'; import 'src/animated_color_square.dart'; import 'src/platform_view.dart'; +import 'src/poppable_screen.dart'; import 'src/scenario.dart'; Map _scenarios = { 'animated_color_square': AnimatedColorSquareScenario(window), 'text_platform_view': PlatformViewScenario(window, 'Hello from Scenarios (Platform View)'), + 'poppable_screen': PoppableScreenScenario(window), }; Scenario _currentScenario = _scenarios['animated_color_square']; @@ -26,6 +28,7 @@ void main() { ..onBeginFrame = _onBeginFrame ..onDrawFrame = _onDrawFrame ..onMetricsChanged = _onMetricsChanged + ..onPointerDataPacket = _onPointerDataPacket ..scheduleFrame(); final ByteData data = ByteData(1); data.setUint8(0, 1); @@ -34,8 +37,8 @@ void main() { Future _handlePlatformMessage( String name, ByteData data, PlatformMessageResponseCallback callback) async { - print(name); - print(utf8.decode(data.buffer.asUint8List())); + 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]; @@ -51,6 +54,8 @@ Future _handlePlatformMessage( } else if (name == 'write_timeline') { final String timelineData = await _getTimelineData(); callback(Uint8List.fromList(utf8.encode(timelineData)).buffer.asByteData()); + } else { + _currentScenario?.onPlatformMessage(name, data, callback); } } @@ -95,3 +100,7 @@ void _onDrawFrame() { void _onMetricsChanged() { _currentScenario.onMetricsChanged(); } + +void _onPointerDataPacket(PointerDataPacket packet) { + _currentScenario.onPointerDataPacket(packet); +} diff --git a/testing/scenario_app/lib/src/platform_echo_mixin.dart b/testing/scenario_app/lib/src/platform_echo_mixin.dart new file mode 100644 index 0000000000000..96d8bc8f1a9a3 --- /dev/null +++ b/testing/scenario_app/lib/src/platform_echo_mixin.dart @@ -0,0 +1,19 @@ +// Copyright 2019 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:typed_data'; +import 'dart:ui'; + +import 'scenario.dart'; + +mixin PlatformEchoMixin on Scenario { + @override + void onPlatformMessage( + String name, + ByteData data, + PlatformMessageResponseCallback callback, + ) { + window.sendPlatformMessage(name, data, null); + } +} diff --git a/testing/scenario_app/lib/src/poppable_screen.dart b/testing/scenario_app/lib/src/poppable_screen.dart new file mode 100644 index 0000000000000..e6a7ac62253c0 --- /dev/null +++ b/testing/scenario_app/lib/src/poppable_screen.dart @@ -0,0 +1,92 @@ +// Copyright 2019 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:ui'; + +import 'platform_echo_mixin.dart'; +import 'scenario.dart'; + +/// A blank page with a button that pops the page when tapped. +class PoppableScreenScenario extends Scenario with PlatformEchoMixin { + /// Creates the PoppableScreenScenario. + /// + /// The [window] parameter must not be null. + PoppableScreenScenario(Window window) + : assert(window != null), + super(window); + + // Rect for the pop button. Only defined once onMetricsChanged is called. + Rect _buttonRect; + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + + canvas.drawPaint(Paint()..color = const Color.fromARGB(255, 255, 255, 255)); + + if (_buttonRect != null) { + canvas.drawRect( + _buttonRect, + Paint()..color = const Color.fromARGB(255, 255, 0, 0), + ); + } + final Picture picture = recorder.endRecording(); + + builder.pushOffset(0, 0); + builder.addPicture(Offset.zero, picture); + final Scene scene = builder.build(); + window.render(scene); + scene.dispose(); + } + + @override + void onDrawFrame() { + // Just draw once since the content never changes. + } + + @override + void onMetricsChanged() { + _buttonRect = Rect.fromLTRB( + window.physicalSize.width / 4, + window.physicalSize.height * 2 / 5, + window.physicalSize.width * 3 / 4, + window.physicalSize.height * 3 / 5, + ); + window.scheduleFrame(); + } + + @override + void onPointerDataPacket(PointerDataPacket packet) { + for (PointerData data in packet.data) { + if (data.change == PointerChange.up && + _buttonRect?.contains(Offset(data.physicalX, data.physicalY)) == true + ) { + _pop(); + } + } + } + + void _pop() { + window.sendPlatformMessage( + // 'flutter/platform' is the hardcoded name of the 'platform' + // `SystemChannel` from the `SystemNavigator` API. + // https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/services/system_navigator.dart. + 'flutter/platform', + // This recreates a combination of OptionalMethodChannel, JSONMethodCodec, + // and _DefaultBinaryMessenger in the framework. + utf8.encoder.convert( + const JsonCodec().encode({ + 'method': 'SystemNavigator.pop', + 'args': null, + }) + ).buffer.asByteData(), + // Don't care about the response. If it doesn't go through, the test + // will fail. + null, + ); + } +} diff --git a/testing/scenario_app/lib/src/scenario.dart b/testing/scenario_app/lib/src/scenario.dart index be474016a3162..db7b5890c0d51 100644 --- a/testing/scenario_app/lib/src/scenario.dart +++ b/testing/scenario_app/lib/src/scenario.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:typed_data'; import 'dart:ui'; /// A scenario to run for testing. @@ -27,4 +28,19 @@ abstract class Scenario { /// /// See [Window.onMetricsChanged]. void onMetricsChanged() {} + + /// Called by the program when a pointer event is received. + /// + /// See [Window.onPointerDataPacket]. + void onPointerDataPacket(PointerDataPacket packet) {} + + /// Called by the program when an engine side platform channel message is + /// received. + /// + /// See [Window.onPlatformMessage]. + void onPlatformMessage( + String name, + ByteData data, + PlatformMessageResponseCallback callback, + ) {} }