[iOS] Add platform view to integration_test example (#164144)
Adds a platform view that simply renders a blue square to the integration_test example, and updates the screenshot capture code to capture all windows, ensuring that any native picture-in-picture, split views, etc. are catpured. Adds a screenshot test that captures the platform view alongside the existing test. Improved API for screenshot capture will land in a followup patch. Fixes: https://github.com/flutter/flutter/issues/164129 Issue: https://github.com/flutter/flutter/issues/51890 ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [X] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
9af844c5c9
commit
45702a26ca
3
packages/integration_test/example/.gitignore
vendored
3
packages/integration_test/example/.gitignore
vendored
@ -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
|
||||
|
@ -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<int> screenshot = await IntegrationTestWidgetsFlutterBinding.instance.takeScreenshot(
|
||||
'integration_test_screen_matches_golden_file',
|
||||
);
|
||||
await expectLater(
|
||||
screenshot,
|
||||
matchesGoldenFile('integration_test_screen_matches_golden_file.png'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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 = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
3BA56D992D36D939004F0F1C /* SimplePlatformView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimplePlatformView.h; sourceTree = "<group>"; };
|
||||
3BA56D9A2D36D960004F0F1C /* SimplePlatformView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SimplePlatformView.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 */,
|
||||
);
|
||||
|
@ -72,6 +72,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
enableGPUValidationMode = "1"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
|
@ -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<FlutterPluginRegistrar>* 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];
|
||||
}
|
||||
|
@ -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 <Flutter/Flutter.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface SimplePlatformViewFactory : NSObject<FlutterPlatformViewFactory>
|
||||
|
||||
- (instancetype _Nullable)initWithMessenger:(NSObject<FlutterBinaryMessenger>* _Nonnull)messenger;
|
||||
|
||||
@end
|
||||
|
||||
@interface SimplePlatformView : NSObject<FlutterPlatformView>
|
||||
|
||||
- (instancetype _Nullable)initWithFrame:(CGRect)frame
|
||||
viewIdentifier:(int64_t)viewId
|
||||
arguments:(id _Nullable)args
|
||||
binaryMessenger:(NSObject<FlutterBinaryMessenger>* _Nonnull)messenger;
|
||||
|
||||
- (UIView* _Nonnull)view;
|
||||
|
||||
@end
|
@ -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<FlutterBinaryMessenger>* _messenger;
|
||||
}
|
||||
|
||||
- (instancetype _Nullable)initWithMessenger:(NSObject<FlutterBinaryMessenger>* _Nonnull)messenger {
|
||||
if (self = [super init]) {
|
||||
_messenger = messenger;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nonnull NSObject<FlutterPlatformView>*)createWithFrame:(CGRect)frame
|
||||
viewIdentifier:(int64_t)viewId
|
||||
arguments:(id _Nullable)args {
|
||||
return [[SimplePlatformView alloc] initWithFrame:frame
|
||||
viewIdentifier:viewId
|
||||
arguments:args
|
||||
binaryMessenger:_messenger];
|
||||
}
|
||||
|
||||
- (NSObject<FlutterMessageCodec>*)createArgsCodec {
|
||||
return [FlutterStandardMessageCodec sharedInstance];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation SimplePlatformView {
|
||||
UIView* _view;
|
||||
}
|
||||
|
||||
- (instancetype _Nullable)initWithFrame:(CGRect)frame
|
||||
viewIdentifier:(int64_t)viewId
|
||||
arguments:(id _Nullable)args
|
||||
binaryMessenger:(NSObject<FlutterBinaryMessenger>* _Nonnull)messenger {
|
||||
if (self = [super init]) {
|
||||
_view = [[UIView alloc] initWithFrame:frame];
|
||||
_view.backgroundColor = UIColor.blueColor;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (UIView* _Nonnull)view {
|
||||
return _view;
|
||||
}
|
||||
|
||||
@end
|
@ -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<MyApp> {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('Plugin example app')),
|
||||
body: Center(child: Text('Platform: ${Platform.operatingSystem}\n')),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Text('Platform: ${Platform.operatingSystem}\n'),
|
||||
const Expanded(child: SimplePlatformView()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<UIWindow *> *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
|
||||
|
Loading…
x
Reference in New Issue
Block a user