Unverified Commit d053a4d0 authored by Ming Lyu (CareF)'s avatar Ming Lyu (CareF) Committed by GitHub

Move key event and semantics related method from WidgetTester to WidgetController (#62362)

parent 0a69e810
...@@ -7,9 +7,11 @@ import 'dart:async'; ...@@ -7,9 +7,11 @@ import 'dart:async';
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'all_elements.dart'; import 'all_elements.dart';
import 'event_simulation.dart';
import 'finders.dart'; import 'finders.dart';
import 'test_async_utils.dart'; import 'test_async_utils.dart';
import 'test_pointer.dart'; import 'test_pointer.dart';
...@@ -686,10 +688,121 @@ abstract class WidgetController { ...@@ -686,10 +688,121 @@ abstract class WidgetController {
return box.size; 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<void> 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<void> 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<void> 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 /// Returns the rect of the given widget. This is only valid once
/// the widget's render object has been laid out at least once. /// the widget's render object has been laid out at least once.
Rect getRect(Finder finder) => getTopLeft(finder) & getSize(finder); 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<Element> 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 /// 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. /// its ancestry tree, this scrolls `S` so as to make `W` visible.
/// ///
......
...@@ -20,7 +20,6 @@ import 'package:test_api/test_api.dart' as test_package; ...@@ -20,7 +20,6 @@ import 'package:test_api/test_api.dart' as test_package;
import 'all_elements.dart'; import 'all_elements.dart';
import 'binding.dart'; import 'binding.dart';
import 'controller.dart'; import 'controller.dart';
import 'event_simulation.dart';
import 'finders.dart'; import 'finders.dart';
import 'matchers.dart'; import 'matchers.dart';
import 'restoration.dart'; import 'restoration.dart';
...@@ -560,6 +559,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -560,6 +559,10 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
/// frames) for `duration` amount of time, and then received a "Vsync" signal /// frames) for `duration` amount of time, and then received a "Vsync" signal
/// to paint the application. /// 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 /// This is a convenience function that just calls
/// [TestWidgetsFlutterBinding.pump]. /// [TestWidgetsFlutterBinding.pump].
/// ///
...@@ -1055,74 +1058,6 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -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<void> 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<void> 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<void> 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 /// Makes an effort to dismiss the current page with a Material [Scaffold] or
/// a [CupertinoPageScaffold]. /// a [CupertinoPageScaffold].
/// ///
...@@ -1139,49 +1074,6 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker ...@@ -1139,49 +1074,6 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
await tap(backButton); 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<Element> 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); typedef _TickerDisposeCallback = void Function(_TestTicker ticker);
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/semantics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -22,6 +23,140 @@ class TestDragData { ...@@ -22,6 +23,140 @@ class TestDragData {
} }
void main() { 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 <Widget>[
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( testWidgets(
'WidgetTester.drag must break the offset into multiple parallel components if ' 'WidgetTester.drag must break the offset into multiple parallel components if '
'the drag goes outside the touch slop values', 'the drag goes outside the touch slop values',
......
...@@ -24,140 +24,6 @@ const List<Widget> fooBarTexts = <Text>[ ...@@ -24,140 +24,6 @@ const List<Widget> fooBarTexts = <Text>[
]; ];
void main() { 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 <Widget>[
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', () { group('expectLater', () {
testWidgets('completes when matcher completes', (WidgetTester tester) async { testWidgets('completes when matcher completes', (WidgetTester tester) async {
final Completer<void> completer = Completer<void>(); final Completer<void> completer = Completer<void>();
......
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