Unverified Commit 0dd0c2ed authored by hellohuanlin's avatar hellohuanlin Committed by GitHub

[platform_view]Send platform message when platform view is focused (#105050)

parent 94e31845
...@@ -3657,6 +3657,17 @@ targets: ...@@ -3657,6 +3657,17 @@ targets:
- bin/** - bin/**
- .ci.yaml - .ci.yaml
- name: Mac native_platform_view_ui_tests_ios
bringup: true
recipe: devicelab/devicelab_drone
presubmit: false
timeout: 60
properties:
tags: >
["devicelab", "hostonly"]
task_name: native_platform_view_ui_tests_ios
scheduler: luci
- name: Mac run_release_test_macos - name: Mac run_release_test_macos
recipe: devicelab/devicelab_drone recipe: devicelab/devicelab_drone
timeout: 60 timeout: 60
......
...@@ -195,7 +195,7 @@ ...@@ -195,7 +195,7 @@
/dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart @zanderso @flutter/tool
/dev/devicelab/bin/tasks/module_host_with_custom_build_test.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/module_host_with_custom_build_test.dart @zanderso @flutter/tool
/dev/devicelab/bin/tasks/module_test.dart @zanderso @flutter/tool /dev/devicelab/bin/tasks/module_test.dart @zanderso @flutter/tool
/dev/devicelab/bin/tasks/native_ui_tests_ios.dart @jmagman @flutter/engine /dev/devicelab/bin/tasks/native_platform_view_ui_tests_ios.dart @hellohuanlin @flutter/ios
/dev/devicelab/bin/tasks/native_ui_tests_macos.dart @cbracken @flutter/desktop /dev/devicelab/bin/tasks/native_ui_tests_macos.dart @cbracken @flutter/desktop
/dev/devicelab/bin/tasks/plugin_test.dart @stuartmorgan @flutter/plugin /dev/devicelab/bin/tasks/plugin_test.dart @stuartmorgan @flutter/plugin
/dev/devicelab/bin/tasks/plugin_test_ios.dart @jmagman @flutter/ios /dev/devicelab/bin/tasks/plugin_test_ios.dart @jmagman @flutter/ios
......
// 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_devicelab/framework/devices.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/framework/ios.dart';
import 'package:flutter_devicelab/framework/task_result.dart';
import 'package:flutter_devicelab/framework/utils.dart';
import 'package:path/path.dart' as path;
Future<void> main() async {
await task(() async {
final String projectDirectory = '${flutterDirectory.path}/dev/integration_tests/ios_platform_view_tests';
await inDirectory(projectDirectory, () async {
section('Build clean');
await flutter('clean');
section('Build platform view app');
await flutter(
'build',
options: <String>[
'ios',
'-v',
'--release',
'--config-only',
],
);
});
section('Run platform view XCUITests');
final Device device = await devices.workingDevice;
if (!await runXcodeTests(
platformDirectory: path.join(projectDirectory, 'ios'),
destination: 'id=${device.deviceId}',
testName: 'native_platform_view_ui_tests_ios',
)) {
return TaskResult.failure('Platform view XCUITests failed');
}
return TaskResult.success(null);
});
}
...@@ -83,6 +83,9 @@ TaskFunction createIOSPlatformViewTests() { ...@@ -83,6 +83,9 @@ TaskFunction createIOSPlatformViewTests() {
return DriverTest( return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/ios_platform_view_tests', '${flutterDirectory.path}/dev/integration_tests/ios_platform_view_tests',
'lib/main.dart', 'lib/main.dart',
extraOptions: <String>[
'--dart-define=ENABLE_DRIVER_EXTENSION=true',
],
); );
} }
......
// 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 XCTest;
@interface XCUIElement(KeyboardFocus)
@property (nonatomic, readonly) BOOL flt_hasKeyboardFocus;
@end
@implementation XCUIElement(KeyboardFocus)
- (BOOL)flt_hasKeyboardFocus {
return [[self valueForKey:@"hasKeyboardFocus"] boolValue];
}
@end
@interface PlatformViewUITests : XCTestCase
@property (strong) XCUIApplication *app;
@end
@implementation PlatformViewUITests
- (void)setUp {
self.continueAfterFailure = NO;
self.app = [[XCUIApplication alloc] init];
[self.app launch];
}
- (void)testPlatformViewFocus {
XCUIElement *entranceButton = self.app.buttons[@"platform view focus test"];
XCTAssertTrue([entranceButton waitForExistenceWithTimeout:1]);
[entranceButton tap];
XCUIElement *platformView = self.app.textFields[@"platform_view[0]"];
XCTAssertTrue([platformView waitForExistenceWithTimeout:1]);
XCUIElement *flutterTextField = self.app.textFields[@"Flutter Text Field"];
XCTAssertTrue([flutterTextField waitForExistenceWithTimeout:1]);
[flutterTextField tap];
XCTAssertTrue([self.app.windows.element waitForExistenceWithTimeout:1]);
XCTAssertFalse(platformView.flt_hasKeyboardFocus);
XCTAssertTrue(flutterTextField.flt_hasKeyboardFocus);
// Tapping on platformView should unfocus the previously focused flutterTextField
[platformView tap];
XCTAssertTrue(platformView.flt_hasKeyboardFocus);
XCTAssertFalse(flutterTextField.flt_hasKeyboardFocus);
}
@end
...@@ -27,8 +27,6 @@ ...@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion> <MacroExpansion>
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
...@@ -38,8 +36,18 @@ ...@@ -38,8 +36,18 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</MacroExpansion> </MacroExpansion>
<AdditionalOptions> <Testables>
</AdditionalOptions> <TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E09898E52853E9F000064317"
BuildableName = "PlatformViewUITests.xctest"
BlueprintName = "PlatformViewUITests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
...@@ -61,8 +69,6 @@ ...@@ -61,8 +69,6 @@
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Profile" buildConfiguration = "Profile"
......
...@@ -4,43 +4,8 @@ ...@@ -4,43 +4,8 @@
#import "AppDelegate.h" #import "AppDelegate.h"
#import "GeneratedPluginRegistrant.h" #import "GeneratedPluginRegistrant.h"
#import "ViewFactory.h"
@interface PlatformView: NSObject<FlutterPlatformView> #import "TextFieldFactory.h"
@property (strong, nonatomic) UIView *platformView;
@end
@implementation PlatformView
- (instancetype)init
{
self = [super init];
if (self) {
self.platformView = [[UIView alloc] init];
self.platformView.backgroundColor = [UIColor blueColor];
}
return self;
}
- (UIView *)view {
return self.platformView;
}
@end
@interface ViewFactory: NSObject<FlutterPlatformViewFactory>
@end
@implementation ViewFactory
- (NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args {
PlatformView *platformView = [[PlatformView alloc] init];
return platformView;
}
@end
@implementation AppDelegate @implementation AppDelegate
...@@ -48,7 +13,9 @@ ...@@ -48,7 +13,9 @@
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self]; [GeneratedPluginRegistrant registerWithRegistry:self];
// Override point for customization after application launch. // Override point for customization after application launch.
[[self registrarForPlugin:@"flutter"] registerViewFactory:[ViewFactory new] withId:@"platform_view"]; id<FlutterPluginRegistrar> registrar = [self registrarForPlugin:@"flutter"];
[registrar registerViewFactory:[[ViewFactory alloc] init] withId:@"platform_view"];
[registrar registerViewFactory:[[TextFieldFactory alloc] init] withId:@"platform_text_field"];
return [super application:application didFinishLaunchingWithOptions:launchOptions]; return [super application:application didFinishLaunchingWithOptions:launchOptions];
} }
......
// 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>
NS_ASSUME_NONNULL_BEGIN
@interface TextFieldFactory : NSObject<FlutterPlatformViewFactory>
@end
NS_ASSUME_NONNULL_END
// 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 "TextFieldFactory.h"
@interface PlatformTextField: NSObject<FlutterPlatformView>
@property (strong, nonatomic) UITextField *textField;
@end
@implementation PlatformTextField
- (instancetype)init
{
self = [super init];
if (self) {
_textField = [[UITextField alloc] init];
_textField.text = @"Platform Text Field";
}
return self;
}
- (UIView *)view {
return self.textField;
}
@end
@implementation TextFieldFactory
- (NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args {
return [[PlatformTextField alloc] init];
}
@end
// 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>
NS_ASSUME_NONNULL_BEGIN
@interface ViewFactory: NSObject<FlutterPlatformViewFactory>
@end
NS_ASSUME_NONNULL_END
// 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 "ViewFactory.h"
@interface PlatformView: NSObject<FlutterPlatformView>
@property (strong, nonatomic) UIView *platformView;
@end
@implementation PlatformView
- (instancetype)init
{
self = [super init];
if (self) {
_platformView = [[UIView alloc] init];
_platformView.backgroundColor = [UIColor blueColor];
}
return self;
}
- (UIView *)view {
return self.platformView;
}
@end
@implementation ViewFactory
- (NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args {
return [[PlatformView alloc] init];
}
@end
...@@ -6,7 +6,12 @@ import 'package:flutter/material.dart'; ...@@ -6,7 +6,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_driver/driver_extension.dart'; import 'package:flutter_driver/driver_extension.dart';
void main() { void main() {
// enableFlutterDriverExtension() will disable keyboard,
// which is required for flutter_driver tests
// But breaks the XCUITests
if (const bool.fromEnvironment('ENABLE_DRIVER_EXTENSION')) {
enableFlutterDriverExtension(); enableFlutterDriverExtension();
}
runApp(const MyApp()); runApp(const MyApp());
} }
...@@ -26,7 +31,7 @@ class MyApp extends StatelessWidget { ...@@ -26,7 +31,7 @@ class MyApp extends StatelessWidget {
} }
} }
/// A page with a button in the center. /// A page with several buttons in the center.
/// ///
/// On press the button, a page with platform view should be pushed into the scene. /// On press the button, a page with platform view should be pushed into the scene.
class MyHomePage extends StatefulWidget { class MyHomePage extends StatefulWidget {
...@@ -51,8 +56,8 @@ class _MyHomePageState extends State<MyHomePage> { ...@@ -51,8 +56,8 @@ class _MyHomePageState extends State<MyHomePage> {
onPressed: () { onPressed: () {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute<PlatformViewPage>( MaterialPageRoute<MergeThreadTestPage>(
builder: (BuildContext context) => const PlatformViewPage()), builder: (BuildContext context) => const MergeThreadTestPage()),
); );
}, },
), ),
...@@ -62,14 +67,25 @@ class _MyHomePageState extends State<MyHomePage> { ...@@ -62,14 +67,25 @@ class _MyHomePageState extends State<MyHomePage> {
child: const Text('Tap to unmerge threads'), child: const Text('Tap to unmerge threads'),
onPressed: () {}, onPressed: () {},
), ),
TextButton(
key: const ValueKey<String>('platform_view_focus_test'),
child: const Text('platform view focus test'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<FocusTestPage>(
builder: (BuildContext context) => const FocusTestPage()),
);
},
),
]), ]),
); );
} }
} }
/// A page contains the platform view to be tested. /// A page to test thread merge for platform view.
class PlatformViewPage extends StatelessWidget { class MergeThreadTestPage extends StatelessWidget {
const PlatformViewPage({super.key}); const MergeThreadTestPage({super.key});
static Key button = const ValueKey<String>('plus_button'); static Key button = const ValueKey<String>('plus_button');
...@@ -77,7 +93,7 @@ class PlatformViewPage extends StatelessWidget { ...@@ -77,7 +93,7 @@ class PlatformViewPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Platform View'), title: const Text('Platform View Thread Merge Tests'),
), ),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
...@@ -97,3 +113,44 @@ class PlatformViewPage extends StatelessWidget { ...@@ -97,3 +113,44 @@ class PlatformViewPage extends StatelessWidget {
); );
} }
} }
/// A page to test platform view focus.
class FocusTestPage extends StatefulWidget {
const FocusTestPage({super.key});
@override
State<FocusTestPage> createState() => _FocusTestPageState();
}
class _FocusTestPageState extends State<FocusTestPage> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_controller.text = 'Flutter Text Field';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Platform View Focus Tests'),
),
body: Column(
children: <Widget>[
const SizedBox(
width: 300,
height: 50,
child: UiKitView(viewType: 'platform_text_field'),
),
TextField(
controller: _controller,
),
],
),
);
}
}
...@@ -569,12 +569,13 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -569,12 +569,13 @@ class _UiKitViewState extends State<UiKitView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_controller == null) { final UiKitViewController? controller = _controller;
if (controller == null) {
return const SizedBox.expand(); return const SizedBox.expand();
} }
return Focus( return Focus(
focusNode: _focusNode, focusNode: _focusNode,
onFocusChange: _onFocusChange, onFocusChange: (bool isFocused) => _onFocusChange(isFocused, controller),
child: _UiKitPlatformView( child: _UiKitPlatformView(
controller: _controller!, controller: _controller!,
hitTestBehavior: widget.hitTestBehavior, hitTestBehavior: widget.hitTestBehavior,
...@@ -659,8 +660,17 @@ class _UiKitViewState extends State<UiKitView> { ...@@ -659,8 +660,17 @@ class _UiKitViewState extends State<UiKitView> {
}); });
} }
void _onFocusChange(bool isFocused) { void _onFocusChange(bool isFocused, UiKitViewController controller) {
// TODO(hellohuanlin): send 'TextInput.setPlatformViewClient' channel message to engine after the engine is updated to handle this message. if (!isFocused) {
// Unlike Android, we do not need to send "clearFocus" channel message
// to the engine, because focusing on another view will automatically
// cancel the focus on the previously focused platform view.
return;
}
SystemChannels.textInput.invokeMethod<void>(
'TextInput.setPlatformViewClient',
<String, dynamic>{'platformViewId': controller.id},
);
} }
} }
......
...@@ -2064,6 +2064,45 @@ void main() { ...@@ -2064,6 +2064,45 @@ void main() {
expect(uiKitViewFocusNode.hasFocus, isTrue); expect(uiKitViewFocusNode.hasFocus, isTrue);
}); });
testWidgets('UiKitView sends TextInput.setPlatformViewClient when focused', (WidgetTester tester) async {
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
final FakeIosPlatformViewsController viewsController = FakeIosPlatformViewsController();
viewsController.registerViewType('webview');
await tester.pumpWidget(
const UiKitView(viewType: 'webview', layoutDirection: TextDirection.ltr)
);
// First frame is before the platform view was created so the render object
// is not yet in the tree.
await tester.pump();
final Focus uiKitViewFocusWidget = tester.widget(
find.descendant(
of: find.byType(UiKitView),
matching: find.byType(Focus),
),
);
final FocusNode uiKitViewFocusNode = uiKitViewFocusWidget.focusNode!;
late Map<String, dynamic> channelArguments;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.textInput, (MethodCall call) {
if (call.method == 'TextInput.setPlatformViewClient') {
channelArguments = call.arguments as Map<String, dynamic>;
}
return null;
});
expect(uiKitViewFocusNode.hasFocus, false);
uiKitViewFocusNode.requestFocus();
await tester.pump();
expect(uiKitViewFocusNode.hasFocus, true);
expect(channelArguments['platformViewId'], currentViewId + 1);
});
testWidgets('UiKitView has correct semantics', (WidgetTester tester) async { testWidgets('UiKitView has correct semantics', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics(); final SemanticsHandle handle = tester.ensureSemantics();
final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment