[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:
Chris Bracken 2025-02-25 18:40:07 -08:00 committed by GitHub
parent 9af844c5c9
commit 45702a26ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 159 additions and 14 deletions

View File

@ -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

View File

@ -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'),
);
});
}

View File

@ -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 */,
);

View File

@ -72,6 +72,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">

View File

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

View File

@ -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

View File

@ -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

View File

@ -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()),
],
),
),
);
}

View File

@ -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();
}
}
}

View File

@ -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