From d053a4d00a3eed34cba0795883d66cd9d36ff4f0 Mon Sep 17 00:00:00 2001 From: "Ming Lyu (CareF)" Date: Wed, 29 Jul 2020 11:11:06 -0400 Subject: [PATCH] Move key event and semantics related method from WidgetTester to WidgetController (#62362) --- packages/flutter_test/lib/src/controller.dart | 113 +++++++++++++++ .../flutter_test/lib/src/widget_tester.dart | 116 +-------------- .../flutter_test/test/controller_test.dart | 135 ++++++++++++++++++ .../flutter_test/test/widget_tester_test.dart | 134 ----------------- 4 files changed, 252 insertions(+), 246 deletions(-) diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index f2081f16ae2..4cea9867afd 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -7,9 +7,11 @@ import 'dart:async'; import 'package:clock/clock.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'all_elements.dart'; +import 'event_simulation.dart'; import 'finders.dart'; import 'test_async_utils.dart'; import 'test_pointer.dart'; @@ -686,10 +688,121 @@ abstract class WidgetController { return box.size; } + /// Simulates sending physical key down and up events through the system channel. + /// + /// This only simulates key events coming from a physical keyboard, not from a + /// soft keyboard. + /// + /// Specify `platform` as one of the platforms allowed in + /// [Platform.operatingSystem] to make the event appear to be from that type + /// of system. Defaults to "android". Must not be null. Some platforms (e.g. + /// Windows, iOS) are not yet supported. + /// + /// Keys that are down when the test completes are cleared after each test. + /// + /// This method sends both the key down and the key up events, to simulate a + /// key press. To simulate individual down and/or up events, see + /// [sendKeyDownEvent] and [sendKeyUpEvent]. + /// + /// See also: + /// + /// - [sendKeyDownEvent] to simulate only a key down event. + /// - [sendKeyUpEvent] to simulate only a key up event. + Future sendKeyEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { + assert(platform != null); + await simulateKeyDownEvent(key, platform: platform); + // Internally wrapped in async guard. + return simulateKeyUpEvent(key, platform: platform); + } + + /// Simulates sending a physical key down event through the system channel. + /// + /// This only simulates key down events coming from a physical keyboard, not + /// from a soft keyboard. + /// + /// Specify `platform` as one of the platforms allowed in + /// [Platform.operatingSystem] to make the event appear to be from that type + /// of system. Defaults to "android". Must not be null. Some platforms (e.g. + /// Windows, iOS) are not yet supported. + /// + /// Keys that are down when the test completes are cleared after each test. + /// + /// See also: + /// + /// - [sendKeyUpEvent] to simulate the corresponding key up event. + /// - [sendKeyEvent] to simulate both the key up and key down in the same call. + Future sendKeyDownEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { + assert(platform != null); + // Internally wrapped in async guard. + return simulateKeyDownEvent(key, platform: platform); + } + + /// Simulates sending a physical key up event through the system channel. + /// + /// This only simulates key up events coming from a physical keyboard, + /// not from a soft keyboard. + /// + /// Specify `platform` as one of the platforms allowed in + /// [Platform.operatingSystem] to make the event appear to be from that type + /// of system. Defaults to "android". May not be null. + /// + /// See also: + /// + /// - [sendKeyDownEvent] to simulate the corresponding key down event. + /// - [sendKeyEvent] to simulate both the key up and key down in the same call. + Future sendKeyUpEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { + assert(platform != null); + // Internally wrapped in async guard. + return simulateKeyUpEvent(key, platform: platform); + } + /// Returns the rect of the given widget. This is only valid once /// the widget's render object has been laid out at least once. Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder); + /// Attempts to find the [SemanticsNode] of first result from `finder`. + /// + /// If the object identified by the finder doesn't own it's semantic node, + /// this will return the semantics data of the first ancestor with semantics. + /// The ancestor's semantic data will include the child's as well as + /// other nodes that have been merged together. + /// + /// If the [SemanticsNode] of the object identified by the finder is + /// force-merged into an ancestor (e.g. via the [MergeSemantics] widget) + /// the node into which it is merged is returned. That node will include + /// all the semantics information of the nodes merged into it. + /// + /// Will throw a [StateError] if the finder returns more than one element or + /// if no semantics are found or are not enabled. + SemanticsNode getSemantics(Finder finder) { + if (binding.pipelineOwner.semanticsOwner == null) + throw StateError('Semantics are not enabled.'); + final Iterable candidates = finder.evaluate(); + if (candidates.isEmpty) { + throw StateError('Finder returned no matching elements.'); + } + if (candidates.length > 1) { + throw StateError('Finder returned more than one element.'); + } + final Element element = candidates.single; + RenderObject renderObject = element.findRenderObject(); + SemanticsNode result = renderObject.debugSemantics; + while (renderObject != null && (result == null || result.isMergedIntoParent)) { + renderObject = renderObject?.parent as RenderObject; + result = renderObject?.debugSemantics; + } + if (result == null) + throw StateError('No Semantics data found.'); + return result; + } + + /// Enable semantics in a test by creating a [SemanticsHandle]. + /// + /// The handle must be disposed at the end of the test. + SemanticsHandle ensureSemantics() { + return binding.pipelineOwner.ensureSemantics(); + } + /// Given a widget `W` specified by [finder] and a [Scrollable] widget `S` in /// its ancestry tree, this scrolls `S` so as to make `W` visible. /// diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index 64b663e441e..59dca042537 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -20,7 +20,6 @@ import 'package:test_api/test_api.dart' as test_package; import 'all_elements.dart'; import 'binding.dart'; import 'controller.dart'; -import 'event_simulation.dart'; import 'finders.dart'; import 'matchers.dart'; import 'restoration.dart'; @@ -560,6 +559,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker /// frames) for `duration` amount of time, and then received a "Vsync" signal /// to paint the application. /// + /// For a [FakeAsync] environment (typically in `flutter test`), this advances + /// time and timeout counting; for a live environment this delays `duration` + /// time. + /// /// This is a convenience function that just calls /// [TestWidgetsFlutterBinding.pump]. /// @@ -1055,74 +1058,6 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker }); } - /// Simulates sending physical key down and up events through the system channel. - /// - /// This only simulates key events coming from a physical keyboard, not from a - /// soft keyboard. - /// - /// Specify `platform` as one of the platforms allowed in - /// [Platform.operatingSystem] to make the event appear to be from that type - /// of system. Defaults to "android". Must not be null. Some platforms (e.g. - /// Windows, iOS) are not yet supported. - /// - /// Keys that are down when the test completes are cleared after each test. - /// - /// This method sends both the key down and the key up events, to simulate a - /// key press. To simulate individual down and/or up events, see - /// [sendKeyDownEvent] and [sendKeyUpEvent]. - /// - /// See also: - /// - /// - [sendKeyDownEvent] to simulate only a key down event. - /// - [sendKeyUpEvent] to simulate only a key up event. - Future sendKeyEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { - assert(platform != null); - await simulateKeyDownEvent(key, platform: platform); - // Internally wrapped in async guard. - return simulateKeyUpEvent(key, platform: platform); - } - - /// Simulates sending a physical key down event through the system channel. - /// - /// This only simulates key down events coming from a physical keyboard, not - /// from a soft keyboard. - /// - /// Specify `platform` as one of the platforms allowed in - /// [Platform.operatingSystem] to make the event appear to be from that type - /// of system. Defaults to "android". Must not be null. Some platforms (e.g. - /// Windows, iOS) are not yet supported. - /// - /// Keys that are down when the test completes are cleared after each test. - /// - /// See also: - /// - /// - [sendKeyUpEvent] to simulate the corresponding key up event. - /// - [sendKeyEvent] to simulate both the key up and key down in the same call. - Future sendKeyDownEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { - assert(platform != null); - // Internally wrapped in async guard. - return simulateKeyDownEvent(key, platform: platform); - } - - /// Simulates sending a physical key up event through the system channel. - /// - /// This only simulates key up events coming from a physical keyboard, - /// not from a soft keyboard. - /// - /// Specify `platform` as one of the platforms allowed in - /// [Platform.operatingSystem] to make the event appear to be from that type - /// of system. Defaults to "android". May not be null. - /// - /// See also: - /// - /// - [sendKeyDownEvent] to simulate the corresponding key down event. - /// - [sendKeyEvent] to simulate both the key up and key down in the same call. - Future sendKeyUpEvent(LogicalKeyboardKey key, { String platform = 'android' }) async { - assert(platform != null); - // Internally wrapped in async guard. - return simulateKeyUpEvent(key, platform: platform); - } - /// Makes an effort to dismiss the current page with a Material [Scaffold] or /// a [CupertinoPageScaffold]. /// @@ -1139,49 +1074,6 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker await tap(backButton); }); } - - /// Attempts to find the [SemanticsNode] of first result from `finder`. - /// - /// If the object identified by the finder doesn't own it's semantic node, - /// this will return the semantics data of the first ancestor with semantics. - /// The ancestor's semantic data will include the child's as well as - /// other nodes that have been merged together. - /// - /// If the [SemanticsNode] of the object identified by the finder is - /// force-merged into an ancestor (e.g. via the [MergeSemantics] widget) - /// the node into which it is merged is returned. That node will include - /// all the semantics information of the nodes merged into it. - /// - /// Will throw a [StateError] if the finder returns more than one element or - /// if no semantics are found or are not enabled. - SemanticsNode getSemantics(Finder finder) { - if (binding.pipelineOwner.semanticsOwner == null) - throw StateError('Semantics are not enabled.'); - final Iterable candidates = finder.evaluate(); - if (candidates.isEmpty) { - throw StateError('Finder returned no matching elements.'); - } - if (candidates.length > 1) { - throw StateError('Finder returned more than one element.'); - } - final Element element = candidates.single; - RenderObject renderObject = element.findRenderObject(); - SemanticsNode result = renderObject.debugSemantics; - while (renderObject != null && (result == null || result.isMergedIntoParent)) { - renderObject = renderObject?.parent as RenderObject; - result = renderObject?.debugSemantics; - } - if (result == null) - throw StateError('No Semantics data found.'); - return result; - } - - /// Enable semantics in a test by creating a [SemanticsHandle]. - /// - /// The handle must be disposed at the end of the test. - SemanticsHandle ensureSemantics() { - return binding.pipelineOwner.ensureSemantics(); - } } typedef _TickerDisposeCallback = void Function(_TestTicker ticker); diff --git a/packages/flutter_test/test/controller_test.dart b/packages/flutter_test/test/controller_test.dart index 2e90101940e..1a6ff337368 100644 --- a/packages/flutter_test/test/controller_test.dart +++ b/packages/flutter_test/test/controller_test.dart @@ -4,6 +4,7 @@ import 'dart:ui'; +import 'package:flutter/semantics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -22,6 +23,140 @@ class TestDragData { } void main() { + group('getSemanticsData', () { + testWidgets('throws when there are no semantics', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: Text('hello'), + ), + ), + ); + + expect(() => tester.getSemantics(find.text('hello')), throwsStateError); + }, semanticsEnabled: false); + + testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Row( + children: const [ + Text('hello'), + Text('hello'), + ], + ), + ), + ), + ); + + expect(() => tester.getSemantics(find.text('hello')), throwsStateError); + semanticsHandle.dispose(); + }); + + testWidgets('Returns the correct SemanticsData', (WidgetTester tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Container( + child: OutlineButton( + onPressed: () { }, + child: const Text('hello'), + ), + ), + ), + ), + ); + + final SemanticsNode node = tester.getSemantics(find.text('hello')); + final SemanticsData semantics = node.getSemanticsData(); + expect(semantics.label, 'hello'); + expect(semantics.hasAction(SemanticsAction.tap), true); + expect(semantics.hasFlag(SemanticsFlag.isButton), true); + semanticsHandle.dispose(); + }); + + testWidgets('Can enable semantics for tests via semanticsEnabled', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Container( + child: OutlineButton( + onPressed: () { }, + child: const Text('hello'), + ), + ), + ), + ), + ); + + final SemanticsNode node = tester.getSemantics(find.text('hello')); + final SemanticsData semantics = node.getSemanticsData(); + expect(semantics.label, 'hello'); + expect(semantics.hasAction(SemanticsAction.tap), true); + expect(semantics.hasFlag(SemanticsFlag.isButton), true); + }, semanticsEnabled: true); + + testWidgets('Returns merged SemanticsData', (WidgetTester tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + const Key key = Key('test'); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Semantics( + label: 'A', + child: Semantics( + label: 'B', + child: Semantics( + key: key, + label: 'C', + child: Container(), + ), + ), + ), + ), + ), + ); + + final SemanticsNode node = tester.getSemantics(find.byKey(key)); + final SemanticsData semantics = node.getSemanticsData(); + expect(semantics.label, 'A\nB\nC'); + semanticsHandle.dispose(); + }); + + testWidgets('Does not return partial semantics', (WidgetTester tester) async { + final SemanticsHandle semanticsHandle = tester.ensureSemantics(); + final Key key = UniqueKey(); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: MergeSemantics( + child: Semantics( + container: true, + label: 'A', + child: Semantics( + container: true, + key: key, + label: 'B', + child: Container(), + ), + ), + ), + ), + ), + ); + + final SemanticsNode node = tester.getSemantics(find.byKey(key)); + final SemanticsData semantics = node.getSemanticsData(); + expect(semantics.label, 'A\nB'); + semanticsHandle.dispose(); + }); + }); + testWidgets( 'WidgetTester.drag must break the offset into multiple parallel components if ' 'the drag goes outside the touch slop values', diff --git a/packages/flutter_test/test/widget_tester_test.dart b/packages/flutter_test/test/widget_tester_test.dart index 180d08ecc8a..bd91a48d6bf 100644 --- a/packages/flutter_test/test/widget_tester_test.dart +++ b/packages/flutter_test/test/widget_tester_test.dart @@ -24,140 +24,6 @@ const List fooBarTexts = [ ]; void main() { - group('getSemanticsData', () { - testWidgets('throws when there are no semantics', (WidgetTester tester) async { - await tester.pumpWidget( - const MaterialApp( - home: Scaffold( - body: Text('hello'), - ), - ), - ); - - expect(() => tester.getSemantics(find.text('hello')), throwsStateError); - }, semanticsEnabled: false); - - testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async { - final SemanticsHandle semanticsHandle = tester.ensureSemantics(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Row( - children: const [ - Text('hello'), - Text('hello'), - ], - ), - ), - ), - ); - - expect(() => tester.getSemantics(find.text('hello')), throwsStateError); - semanticsHandle.dispose(); - }); - - testWidgets('Returns the correct SemanticsData', (WidgetTester tester) async { - final SemanticsHandle semanticsHandle = tester.ensureSemantics(); - - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Container( - child: OutlineButton( - onPressed: () { }, - child: const Text('hello'), - ), - ), - ), - ), - ); - - final SemanticsNode node = tester.getSemantics(find.text('hello')); - final SemanticsData semantics = node.getSemanticsData(); - expect(semantics.label, 'hello'); - expect(semantics.hasAction(SemanticsAction.tap), true); - expect(semantics.hasFlag(SemanticsFlag.isButton), true); - semanticsHandle.dispose(); - }); - - testWidgets('Can enable semantics for tests via semanticsEnabled', (WidgetTester tester) async { - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Container( - child: OutlineButton( - onPressed: () { }, - child: const Text('hello'), - ), - ), - ), - ), - ); - - final SemanticsNode node = tester.getSemantics(find.text('hello')); - final SemanticsData semantics = node.getSemanticsData(); - expect(semantics.label, 'hello'); - expect(semantics.hasAction(SemanticsAction.tap), true); - expect(semantics.hasFlag(SemanticsFlag.isButton), true); - }, semanticsEnabled: true); - - testWidgets('Returns merged SemanticsData', (WidgetTester tester) async { - final SemanticsHandle semanticsHandle = tester.ensureSemantics(); - const Key key = Key('test'); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: Semantics( - label: 'A', - child: Semantics( - label: 'B', - child: Semantics( - key: key, - label: 'C', - child: Container(), - ), - ), - ), - ), - ), - ); - - final SemanticsNode node = tester.getSemantics(find.byKey(key)); - final SemanticsData semantics = node.getSemanticsData(); - expect(semantics.label, 'A\nB\nC'); - semanticsHandle.dispose(); - }); - - testWidgets('Does not return partial semantics', (WidgetTester tester) async { - final SemanticsHandle semanticsHandle = tester.ensureSemantics(); - final Key key = UniqueKey(); - await tester.pumpWidget( - MaterialApp( - home: Scaffold( - body: MergeSemantics( - child: Semantics( - container: true, - label: 'A', - child: Semantics( - container: true, - key: key, - label: 'B', - child: Container(), - ), - ), - ), - ), - ), - ); - - final SemanticsNode node = tester.getSemantics(find.byKey(key)); - final SemanticsData semantics = node.getSemanticsData(); - expect(semantics.label, 'A\nB'); - semanticsHandle.dispose(); - }); - }); - group('expectLater', () { testWidgets('completes when matcher completes', (WidgetTester tester) async { final Completer completer = Completer();