diff --git a/packages/integration_test/example/.gitignore b/packages/integration_test/example/.gitignore index 4944aa59d9..47d2423d08 100644 --- a/packages/integration_test/example/.gitignore +++ b/packages/integration_test/example/.gitignore @@ -46,4 +46,5 @@ app.*.map.json /android/app/release # Golden images. -integration_test/integration_test_matches_golden_file.png +integration_test/integration_test_widget_matches_golden_file.png +integration_test/integration_test_screen_matches_golden_file.png diff --git a/packages/integration_test/example/integration_test/matches_golden_test.dart b/packages/integration_test/example/integration_test/matches_golden_test.dart index 0172b971bd..42b17ca0a4 100644 --- a/packages/integration_test/example/integration_test/matches_golden_test.dart +++ b/packages/integration_test/example/integration_test/matches_golden_test.dart @@ -30,11 +30,22 @@ void main() { // TODO(matanlurey): Is this necessary? await tester.pumpAndSettle(); + // TODO(cbracken): not only is it necessary, but so is this. + await tester.pumpAndSettle(); - // Take a screenshot. + // Take a widget screenshot. await expectLater( find.byType(MaterialApp), - matchesGoldenFile('integration_test_matches_golden_file.png'), + matchesGoldenFile('integration_test_widget_matches_golden_file.png'), + ); + + // Take a full-screen screenshot. + final List screenshot = await IntegrationTestWidgetsFlutterBinding.instance.takeScreenshot( + 'integration_test_screen_matches_golden_file', + ); + await expectLater( + screenshot, + matchesGoldenFile('integration_test_screen_matches_golden_file.png'), ); }); } diff --git a/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj b/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj index afa962924c..c650ea23f1 100644 --- a/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3BA56D9B2D36D966004F0F1C /* SimplePlatformView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BA56D9A2D36D960004F0F1C /* SimplePlatformView.m */; }; 4DB404AC7CF2C89658A01173 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 81BF64028CE7AE2E6196250D /* libPods-RunnerTests.a */; }; 769541CB23A0351900E5C350 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 769541CA23A0351900E5C350 /* RunnerTests.m */; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; @@ -48,6 +49,8 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3BA56D992D36D939004F0F1C /* SimplePlatformView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimplePlatformView.h; sourceTree = ""; }; + 3BA56D9A2D36D960004F0F1C /* SimplePlatformView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SimplePlatformView.m; sourceTree = ""; }; 625A5A90428602E25C0DE2F6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 750225973AAB5D7832AFA60C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 769541BF23A0337200E5C350 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; @@ -145,6 +148,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 3BA56D992D36D939004F0F1C /* SimplePlatformView.h */, + 3BA56D9A2D36D960004F0F1C /* SimplePlatformView.m */, 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 97C146FA1CF9000F007C117D /* Main.storyboard */, @@ -373,6 +378,7 @@ buildActionMask = 2147483647; files = ( 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 3BA56D9B2D36D966004F0F1C /* SimplePlatformView.m in Sources */, 97C146F31CF9000F007C117D /* main.m in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, ); diff --git a/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 591a137715..1f990a599b 100644 --- a/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -72,6 +72,7 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/packages/integration_test/example/ios/Runner/AppDelegate.m b/packages/integration_test/example/ios/Runner/AppDelegate.m index 50f10d0d53..bc46e9945d 100644 --- a/packages/integration_test/example/ios/Runner/AppDelegate.m +++ b/packages/integration_test/example/ios/Runner/AppDelegate.m @@ -4,12 +4,19 @@ #include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" +#include "SimplePlatformView.h" @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; + + // Register platform view factory. + NSObject* registrar = [self registrarForPlugin:@"spv-plugin"]; + SimplePlatformViewFactory* factory = [[SimplePlatformViewFactory alloc] initWithMessenger:registrar.messenger]; + [registrar registerViewFactory:factory withId:@"simple-platform-view"]; + // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/packages/integration_test/example/ios/Runner/SimplePlatformView.h b/packages/integration_test/example/ios/Runner/SimplePlatformView.h new file mode 100644 index 0000000000..6e7892c989 --- /dev/null +++ b/packages/integration_test/example/ios/Runner/SimplePlatformView.h @@ -0,0 +1,23 @@ +// Copyright 2014 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 + +@interface SimplePlatformViewFactory : NSObject + +- (instancetype _Nullable)initWithMessenger:(NSObject* _Nonnull)messenger; + +@end + +@interface SimplePlatformView : NSObject + +- (instancetype _Nullable)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject* _Nonnull)messenger; + +- (UIView* _Nonnull)view; + +@end diff --git a/packages/integration_test/example/ios/Runner/SimplePlatformView.m b/packages/integration_test/example/ios/Runner/SimplePlatformView.m new file mode 100644 index 0000000000..17c579cee7 --- /dev/null +++ b/packages/integration_test/example/ios/Runner/SimplePlatformView.m @@ -0,0 +1,52 @@ +// Copyright 2014 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. + +#include "SimplePlatformView.h" + +@implementation SimplePlatformViewFactory { + NSObject* _messenger; +} + +- (instancetype _Nullable)initWithMessenger:(NSObject* _Nonnull)messenger { + if (self = [super init]) { + _messenger = messenger; + } + return self; +} + +- (nonnull NSObject*)createWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args { + return [[SimplePlatformView alloc] initWithFrame:frame + viewIdentifier:viewId + arguments:args + binaryMessenger:_messenger]; +} + +- (NSObject*)createArgsCodec { + return [FlutterStandardMessageCodec sharedInstance]; +} + +@end + +@implementation SimplePlatformView { + UIView* _view; +} + +- (instancetype _Nullable)initWithFrame:(CGRect)frame + viewIdentifier:(int64_t)viewId + arguments:(id _Nullable)args + binaryMessenger:(NSObject* _Nonnull)messenger { + if (self = [super init]) { + _view = [[UIView alloc] initWithFrame:frame]; + _view.backgroundColor = UIColor.blueColor; + } + return self; +} + +- (UIView* _Nonnull)view { + return _view; +} + +@end diff --git a/packages/integration_test/example/lib/my_app.dart b/packages/integration_test/example/lib/my_app.dart index 2f88e0fb28..8334979618 100644 --- a/packages/integration_test/example/lib/my_app.dart +++ b/packages/integration_test/example/lib/my_app.dart @@ -2,8 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:io' show Platform; +import 'dart:io'; import 'package:flutter/material.dart'; +import 'simple_platform_view.dart'; // ignore_for_file: public_member_api_docs @@ -22,7 +23,12 @@ class _MyAppState extends State { return MaterialApp( home: Scaffold( appBar: AppBar(title: const Text('Plugin example app')), - body: Center(child: Text('Platform: ${Platform.operatingSystem}\n')), + body: Column( + children: [ + Text('Platform: ${Platform.operatingSystem}\n'), + const Expanded(child: SimplePlatformView()), + ], + ), ), ); } diff --git a/packages/integration_test/example/lib/simple_platform_view.dart b/packages/integration_test/example/lib/simple_platform_view.dart new file mode 100644 index 0000000000..81be292dac --- /dev/null +++ b/packages/integration_test/example/lib/simple_platform_view.dart @@ -0,0 +1,28 @@ +// Copyright 2014 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 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +/// A platform view that displays a blue fill. +class SimplePlatformView extends StatelessWidget { + /// Creates a platform view that displays a blue fill. + const SimplePlatformView({super.key}); + + @override + Widget build(BuildContext context) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + // TODO(cbracken): Implement. https://github.com/flutter/flutter/issues/164130 + return Container(); + case TargetPlatform.iOS: + return const UiKitView(viewType: 'simple-platform-view'); + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + throw UnimplementedError(); + } + } +} diff --git a/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m b/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m index b3ac58138f..8ce71b580d 100644 --- a/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m +++ b/packages/integration_test/ios/integration_test/Sources/integration_test/IntegrationTestPlugin.m @@ -50,6 +50,11 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; [registrar addMethodCallDelegate:[self instance] channel:channel]; } +/// Handle method calls from Dart code: +/// - allTestsFinished: Populate NSString* testResults property with a string summary of test run. +/// - captureScreenshot: Capture a screenshot. Populate capturedScreenshotsByName["name"] with image. +/// - convertSurfaceToImage: Android-only. Not implemented on iOS. +/// - revertFlutterImage: Android-only. Not implemented on iOS. - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { if ([call.method isEqualToString:kMethodTestFinished]) { self.testResults = call.arguments[@"results"]; @@ -73,18 +78,23 @@ static NSString *const kMethodRevertImage = @"revertFlutterImage"; } - (UIImage *)capturePngScreenshot { - UIWindow *window = [UIApplication.sharedApplication.windows - filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"keyWindow = YES"]].firstObject; - CGRect screenshotBounds = window.bounds; - UIImage *image; + // Get all windows in the app + NSArray *windows = [UIApplication sharedApplication].windows; - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithBounds:screenshotBounds]; + // Find the overall bounding rect for all windows + CGRect screenBounds = [UIScreen mainScreen].bounds; - image = [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) { - [window drawViewHierarchyInRect:screenshotBounds afterScreenUpdates:YES]; - }]; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithBounds:screenBounds]; + UIImage *screenshot = + [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { + for (UIWindow *window in windows) { + if (!window.hidden) { // Render only visible windows + [window drawViewHierarchyInRect:window.frame afterScreenUpdates:YES]; + } + } + }]; - return image; + return screenshot; } @end