mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
493 lines
20 KiB
Dart
493 lines
20 KiB
Dart
// 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 'dart:async';
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/scheduler.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
import 'package:flutter_driver/driver_extension.dart';
|
|
import 'package:flutter_driver/src/extension/wait_conditions.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'diagnostics_tree.dart';
|
|
import 'error.dart';
|
|
import 'find.dart';
|
|
import 'frame_sync.dart';
|
|
import 'geometry.dart';
|
|
import 'gesture.dart';
|
|
import 'health.dart';
|
|
import 'layer_tree.dart';
|
|
import 'message.dart';
|
|
import 'render_tree.dart';
|
|
import 'request_data.dart';
|
|
import 'semantics.dart';
|
|
import 'text.dart';
|
|
import 'wait.dart';
|
|
|
|
/// A factory which creates [Finder]s from [SerializableFinder]s.
|
|
mixin CreateFinderFactory {
|
|
/// Creates the flutter widget finder from [SerializableFinder].
|
|
Finder createFinder(SerializableFinder finder) {
|
|
final String finderType = finder.finderType;
|
|
switch (finderType) {
|
|
case 'ByText':
|
|
return _createByTextFinder(finder as ByText);
|
|
case 'ByTooltipMessage':
|
|
return _createByTooltipMessageFinder(finder as ByTooltipMessage);
|
|
case 'BySemanticsLabel':
|
|
return _createBySemanticsLabelFinder(finder as BySemanticsLabel);
|
|
case 'ByValueKey':
|
|
return _createByValueKeyFinder(finder as ByValueKey);
|
|
case 'ByType':
|
|
return _createByTypeFinder(finder as ByType);
|
|
case 'PageBack':
|
|
return _createPageBackFinder();
|
|
case 'Ancestor':
|
|
return _createAncestorFinder(finder as Ancestor);
|
|
case 'Descendant':
|
|
return _createDescendantFinder(finder as Descendant);
|
|
default:
|
|
throw DriverError('Unsupported search specification type $finderType');
|
|
}
|
|
}
|
|
|
|
Finder _createByTextFinder(ByText arguments) {
|
|
return find.text(arguments.text);
|
|
}
|
|
|
|
Finder _createByTooltipMessageFinder(ByTooltipMessage arguments) {
|
|
return find.byElementPredicate((Element element) {
|
|
final Widget widget = element.widget;
|
|
if (widget is Tooltip) {
|
|
return widget.message == arguments.text;
|
|
}
|
|
return false;
|
|
}, description: 'widget with text tooltip "${arguments.text}"');
|
|
}
|
|
|
|
Finder _createBySemanticsLabelFinder(BySemanticsLabel arguments) {
|
|
return find.byElementPredicate((Element element) {
|
|
if (element is! RenderObjectElement) {
|
|
return false;
|
|
}
|
|
final String? semanticsLabel = element.renderObject.debugSemantics?.label;
|
|
if (semanticsLabel == null) {
|
|
return false;
|
|
}
|
|
final Pattern label = arguments.label;
|
|
return label is RegExp
|
|
? label.hasMatch(semanticsLabel)
|
|
: label == semanticsLabel;
|
|
}, description: 'widget with semantic label "${arguments.label}"');
|
|
}
|
|
|
|
Finder _createByValueKeyFinder(ByValueKey arguments) {
|
|
switch (arguments.keyValueType) {
|
|
case 'int':
|
|
return find.byKey(ValueKey<int>(arguments.keyValue as int));
|
|
case 'String':
|
|
return find.byKey(ValueKey<String>(arguments.keyValue as String));
|
|
default:
|
|
throw 'Unsupported ByValueKey type: ${arguments.keyValueType}';
|
|
}
|
|
}
|
|
|
|
Finder _createByTypeFinder(ByType arguments) {
|
|
return find.byElementPredicate((Element element) {
|
|
return element.widget.runtimeType.toString() == arguments.type;
|
|
}, description: 'widget with runtimeType "${arguments.type}"');
|
|
}
|
|
|
|
Finder _createPageBackFinder() {
|
|
return find.byElementPredicate((Element element) {
|
|
final Widget widget = element.widget;
|
|
if (widget is Tooltip) {
|
|
return widget.message == 'Back';
|
|
}
|
|
if (widget is CupertinoNavigationBarBackButton) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}, description: 'Material or Cupertino back button');
|
|
}
|
|
|
|
Finder _createAncestorFinder(Ancestor arguments) {
|
|
final Finder finder = find.ancestor(
|
|
of: createFinder(arguments.of),
|
|
matching: createFinder(arguments.matching),
|
|
matchRoot: arguments.matchRoot,
|
|
);
|
|
return arguments.firstMatchOnly ? finder.first : finder;
|
|
}
|
|
|
|
Finder _createDescendantFinder(Descendant arguments) {
|
|
final Finder finder = find.descendant(
|
|
of: createFinder(arguments.of),
|
|
matching: createFinder(arguments.matching),
|
|
matchRoot: arguments.matchRoot,
|
|
);
|
|
return arguments.firstMatchOnly ? finder.first : finder;
|
|
}
|
|
}
|
|
|
|
/// A factory for [Command] handlers.
|
|
mixin CommandHandlerFactory {
|
|
/// With [_frameSync] enabled, Flutter Driver will wait to perform an action
|
|
/// until there are no pending frames in the app under test.
|
|
bool _frameSync = true;
|
|
|
|
/// Gets [DataHandler] for result delivery.
|
|
@protected
|
|
DataHandler? getDataHandler() => null;
|
|
|
|
/// Registers text input emulation.
|
|
@protected
|
|
void registerTextInput() {
|
|
_testTextInput.register();
|
|
}
|
|
|
|
final TestTextInput _testTextInput = TestTextInput();
|
|
|
|
/// Deserializes the finder from JSON generated by [Command.serialize] or [CommandWithTarget.serialize].
|
|
Future<Result?> handleCommand(Command command, WidgetController prober, CreateFinderFactory finderFactory) {
|
|
switch(command.kind) {
|
|
case 'get_health': return _getHealth(command);
|
|
case 'get_layer_tree': return _getLayerTree(command);
|
|
case 'get_render_tree': return _getRenderTree(command);
|
|
case 'enter_text': return _enterText(command);
|
|
case 'get_text': return _getText(command, finderFactory);
|
|
case 'request_data': return _requestData(command);
|
|
case 'scroll': return _scroll(command, prober, finderFactory);
|
|
case 'scrollIntoView': return _scrollIntoView(command, finderFactory);
|
|
case 'set_frame_sync': return _setFrameSync(command);
|
|
case 'set_semantics': return _setSemantics(command);
|
|
case 'set_text_entry_emulation': return _setTextEntryEmulation(command);
|
|
case 'tap': return _tap(command, prober, finderFactory);
|
|
case 'waitFor': return _waitFor(command, finderFactory);
|
|
case 'waitForAbsent': return _waitForAbsent(command, finderFactory);
|
|
case 'waitForCondition': return _waitForCondition(command);
|
|
case 'waitUntilNoTransientCallbacks': return _waitUntilNoTransientCallbacks(command);
|
|
case 'waitUntilNoPendingFrame': return _waitUntilNoPendingFrame(command);
|
|
case 'waitUntilFirstFrameRasterized': return _waitUntilFirstFrameRasterized(command);
|
|
case 'get_semantics_id': return _getSemanticsId(command, finderFactory);
|
|
case 'get_offset': return _getOffset(command, finderFactory);
|
|
case 'get_diagnostics_tree': return _getDiagnosticsTree(command, finderFactory);
|
|
}
|
|
|
|
throw DriverError('Unsupported command kind ${command.kind}');
|
|
}
|
|
|
|
Future<Health> _getHealth(Command command) async => const Health(HealthStatus.ok);
|
|
|
|
Future<LayerTree> _getLayerTree(Command command) async {
|
|
return LayerTree(RendererBinding.instance?.renderView.debugLayer?.toStringDeep());
|
|
}
|
|
|
|
Future<RenderTree> _getRenderTree(Command command) async {
|
|
return RenderTree(RendererBinding.instance?.renderView.toStringDeep());
|
|
}
|
|
|
|
Future<EnterTextResult> _enterText(Command command) async {
|
|
if (!_testTextInput.isRegistered) {
|
|
throw 'Unable to fulfill `FlutterDriver.enterText`. Text emulation is '
|
|
'disabled. You can enable it using `FlutterDriver.setTextEntryEmulation`.';
|
|
}
|
|
final EnterText enterTextCommand = command as EnterText;
|
|
_testTextInput.enterText(enterTextCommand.text);
|
|
return const EnterTextResult();
|
|
}
|
|
|
|
Future<RequestDataResult> _requestData(Command command) async {
|
|
final RequestData requestDataCommand = command as RequestData;
|
|
final DataHandler? dataHandler = getDataHandler();
|
|
return RequestDataResult(dataHandler == null
|
|
? 'No requestData Extension registered'
|
|
: await dataHandler(requestDataCommand.message));
|
|
}
|
|
|
|
Future<SetFrameSyncResult> _setFrameSync(Command command) async {
|
|
final SetFrameSync setFrameSyncCommand = command as SetFrameSync;
|
|
_frameSync = setFrameSyncCommand.enabled;
|
|
return const SetFrameSyncResult();
|
|
}
|
|
|
|
Future<TapResult> _tap(Command command, WidgetController prober, CreateFinderFactory finderFactory) async {
|
|
final Tap tapCommand = command as Tap;
|
|
final Finder computedFinder = await waitForElement(
|
|
finderFactory.createFinder(tapCommand.finder).hitTestable(),
|
|
);
|
|
await prober.tap(computedFinder);
|
|
return const TapResult();
|
|
}
|
|
|
|
Future<WaitForResult> _waitFor(Command command, CreateFinderFactory finderFactory) async {
|
|
final WaitFor waitForCommand = command as WaitFor;
|
|
await waitForElement(finderFactory.createFinder(waitForCommand.finder));
|
|
return const WaitForResult();
|
|
}
|
|
|
|
Future<WaitForAbsentResult> _waitForAbsent(Command command, CreateFinderFactory finderFactory) async {
|
|
final WaitForAbsent waitForAbsentCommand = command as WaitForAbsent;
|
|
await waitForAbsentElement(finderFactory.createFinder(waitForAbsentCommand.finder));
|
|
return const WaitForAbsentResult();
|
|
}
|
|
|
|
Future<Result?> _waitForCondition(Command command) async {
|
|
assert(command != null);
|
|
final WaitForCondition waitForConditionCommand = command as WaitForCondition;
|
|
final WaitCondition condition = deserializeCondition(waitForConditionCommand.condition);
|
|
await condition.wait();
|
|
return null;
|
|
}
|
|
|
|
@Deprecated(
|
|
'This method has been deprecated in favor of _waitForCondition. '
|
|
'This feature was deprecated after v1.9.3.'
|
|
)
|
|
Future<Result?> _waitUntilNoTransientCallbacks(Command command) async {
|
|
if (SchedulerBinding.instance!.transientCallbackCount != 0)
|
|
await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);
|
|
return null;
|
|
}
|
|
|
|
/// Returns a future that waits until no pending frame is scheduled (frame is synced).
|
|
///
|
|
/// Specifically, it checks:
|
|
/// * Whether the count of transient callbacks is zero.
|
|
/// * Whether there's no pending request for scheduling a new frame.
|
|
///
|
|
/// We consider the frame is synced when both conditions are met.
|
|
///
|
|
/// This method relies on a Flutter Driver mechanism called "frame sync",
|
|
/// which waits for transient animations to finish. Persistent animations will
|
|
/// cause this to wait forever.
|
|
///
|
|
/// If a test needs to interact with the app while animations are running, it
|
|
/// should avoid this method and instead disable the frame sync using
|
|
/// `set_frame_sync` method. See [FlutterDriver.runUnsynchronized] for more
|
|
/// details on how to do this. Note, disabling frame sync will require the
|
|
/// test author to use some other method to avoid flakiness.
|
|
///
|
|
/// This method has been deprecated in favor of [_waitForCondition].
|
|
@Deprecated(
|
|
'This method has been deprecated in favor of _waitForCondition. '
|
|
'This feature was deprecated after v1.9.3.'
|
|
)
|
|
Future<Result?> _waitUntilNoPendingFrame(Command command) async {
|
|
await _waitUntilFrame(() {
|
|
return SchedulerBinding.instance!.transientCallbackCount == 0
|
|
&& !SchedulerBinding.instance!.hasScheduledFrame;
|
|
});
|
|
return null;
|
|
}
|
|
|
|
Future<GetSemanticsIdResult> _getSemanticsId(Command command, CreateFinderFactory finderFactory) async {
|
|
final GetSemanticsId semanticsCommand = command as GetSemanticsId;
|
|
final Finder target = await waitForElement(finderFactory.createFinder(semanticsCommand.finder));
|
|
final Iterable<Element> elements = target.evaluate();
|
|
if (elements.length > 1) {
|
|
throw StateError('Found more than one element with the same ID: $elements');
|
|
}
|
|
final Element element = elements.single;
|
|
RenderObject? renderObject = element.renderObject;
|
|
SemanticsNode? node;
|
|
while (renderObject != null && node == null) {
|
|
node = renderObject.debugSemantics;
|
|
renderObject = renderObject.parent as RenderObject?;
|
|
}
|
|
if (node == null)
|
|
throw StateError('No semantics data found');
|
|
return GetSemanticsIdResult(node.id);
|
|
}
|
|
|
|
Future<GetOffsetResult> _getOffset(Command command, CreateFinderFactory finderFactory) async {
|
|
final GetOffset getOffsetCommand = command as GetOffset;
|
|
final Finder finder = await waitForElement(finderFactory.createFinder(getOffsetCommand.finder));
|
|
final Element element = finder.evaluate().single;
|
|
final RenderBox box = (element.renderObject as RenderBox?)!;
|
|
Offset localPoint;
|
|
switch (getOffsetCommand.offsetType) {
|
|
case OffsetType.topLeft:
|
|
localPoint = Offset.zero;
|
|
break;
|
|
case OffsetType.topRight:
|
|
localPoint = box.size.topRight(Offset.zero);
|
|
break;
|
|
case OffsetType.bottomLeft:
|
|
localPoint = box.size.bottomLeft(Offset.zero);
|
|
break;
|
|
case OffsetType.bottomRight:
|
|
localPoint = box.size.bottomRight(Offset.zero);
|
|
break;
|
|
case OffsetType.center:
|
|
localPoint = box.size.center(Offset.zero);
|
|
break;
|
|
}
|
|
final Offset globalPoint = box.localToGlobal(localPoint);
|
|
return GetOffsetResult(dx: globalPoint.dx, dy: globalPoint.dy);
|
|
}
|
|
|
|
Future<DiagnosticsTreeResult> _getDiagnosticsTree(Command command, CreateFinderFactory finderFactory) async {
|
|
final GetDiagnosticsTree diagnosticsCommand = command as GetDiagnosticsTree;
|
|
final Finder finder = await waitForElement(finderFactory.createFinder(diagnosticsCommand.finder));
|
|
final Element element = finder.evaluate().single;
|
|
DiagnosticsNode diagnosticsNode;
|
|
switch (diagnosticsCommand.diagnosticsType) {
|
|
case DiagnosticsType.renderObject:
|
|
diagnosticsNode = element.renderObject!.toDiagnosticsNode();
|
|
break;
|
|
case DiagnosticsType.widget:
|
|
diagnosticsNode = element.toDiagnosticsNode();
|
|
break;
|
|
}
|
|
return DiagnosticsTreeResult(diagnosticsNode.toJsonMap(DiagnosticsSerializationDelegate(
|
|
subtreeDepth: diagnosticsCommand.subtreeDepth,
|
|
includeProperties: diagnosticsCommand.includeProperties,
|
|
)));
|
|
}
|
|
|
|
Future<ScrollResult> _scroll(Command command, WidgetController _prober, CreateFinderFactory finderFactory) async {
|
|
final Scroll scrollCommand = command as Scroll;
|
|
final Finder target = await waitForElement(finderFactory.createFinder(scrollCommand.finder));
|
|
final int totalMoves = scrollCommand.duration.inMicroseconds * scrollCommand.frequency ~/ Duration.microsecondsPerSecond;
|
|
final Offset delta = Offset(scrollCommand.dx, scrollCommand.dy) / totalMoves.toDouble();
|
|
final Duration pause = scrollCommand.duration ~/ totalMoves;
|
|
final Offset startLocation = _prober.getCenter(target);
|
|
Offset currentLocation = startLocation;
|
|
final TestPointer pointer = TestPointer(1);
|
|
_prober.binding.handlePointerEvent(pointer.down(startLocation));
|
|
await Future<void>.value(); // so that down and move don't happen in the same microtask
|
|
for (int moves = 0; moves < totalMoves; moves += 1) {
|
|
currentLocation = currentLocation + delta;
|
|
_prober.binding.handlePointerEvent(pointer.move(currentLocation));
|
|
await Future<void>.delayed(pause);
|
|
}
|
|
_prober.binding.handlePointerEvent(pointer.up());
|
|
|
|
return const ScrollResult();
|
|
}
|
|
|
|
Future<ScrollResult> _scrollIntoView(Command command, CreateFinderFactory finderFactory) async {
|
|
final ScrollIntoView scrollIntoViewCommand = command as ScrollIntoView;
|
|
final Finder target = await waitForElement(finderFactory.createFinder(scrollIntoViewCommand.finder));
|
|
await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: scrollIntoViewCommand.alignment);
|
|
return const ScrollResult();
|
|
}
|
|
|
|
Future<GetTextResult> _getText(Command command, CreateFinderFactory finderFactory) async {
|
|
final GetText getTextCommand = command as GetText;
|
|
final Finder target = await waitForElement(finderFactory.createFinder(getTextCommand.finder));
|
|
|
|
final Widget widget = target.evaluate().single.widget;
|
|
String? text;
|
|
|
|
if (widget.runtimeType == Text) {
|
|
text = (widget as Text).data;
|
|
} else if (widget.runtimeType == RichText) {
|
|
final RichText richText = widget as RichText;
|
|
if (richText.text.runtimeType == TextSpan) {
|
|
text = (richText.text as TextSpan).text;
|
|
}
|
|
} else if (widget.runtimeType == TextField) {
|
|
text = (widget as TextField).controller?.text;
|
|
} else if (widget.runtimeType == TextFormField) {
|
|
text = (widget as TextFormField).controller?.text;
|
|
} else if (widget.runtimeType == EditableText) {
|
|
text = (widget as EditableText).controller.text;
|
|
}
|
|
|
|
if (text == null) {
|
|
throw UnsupportedError('Type ${widget.runtimeType.toString()} is currently not supported by getText');
|
|
}
|
|
|
|
return GetTextResult(text);
|
|
}
|
|
|
|
Future<SetTextEntryEmulationResult> _setTextEntryEmulation(Command command) async {
|
|
final SetTextEntryEmulation setTextEntryEmulationCommand = command as SetTextEntryEmulation;
|
|
if (setTextEntryEmulationCommand.enabled) {
|
|
_testTextInput.register();
|
|
} else {
|
|
_testTextInput.unregister();
|
|
}
|
|
return const SetTextEntryEmulationResult();
|
|
}
|
|
|
|
SemanticsHandle? _semantics;
|
|
bool get _semanticsIsEnabled => RendererBinding.instance!.pipelineOwner.semanticsOwner != null;
|
|
|
|
Future<SetSemanticsResult> _setSemantics(Command command) async {
|
|
final SetSemantics setSemanticsCommand = command as SetSemantics;
|
|
final bool semanticsWasEnabled = _semanticsIsEnabled;
|
|
if (setSemanticsCommand.enabled && _semantics == null) {
|
|
_semantics = RendererBinding.instance!.pipelineOwner.ensureSemantics();
|
|
if (!semanticsWasEnabled) {
|
|
// wait for the first frame where semantics is enabled.
|
|
final Completer<void> completer = Completer<void>();
|
|
SchedulerBinding.instance!.addPostFrameCallback((Duration d) {
|
|
completer.complete();
|
|
});
|
|
await completer.future;
|
|
}
|
|
} else if (!setSemanticsCommand.enabled && _semantics != null) {
|
|
_semantics!.dispose();
|
|
_semantics = null;
|
|
}
|
|
return SetSemanticsResult(semanticsWasEnabled != _semanticsIsEnabled);
|
|
}
|
|
|
|
// This can be used to wait for the first frame being rasterized during app launch.
|
|
@Deprecated(
|
|
'This method has been deprecated in favor of _waitForCondition. '
|
|
'This feature was deprecated after v1.9.3.'
|
|
)
|
|
Future<Result?> _waitUntilFirstFrameRasterized(Command command) async {
|
|
await WidgetsBinding.instance!.waitUntilFirstFrameRasterized;
|
|
return null;
|
|
}
|
|
|
|
/// Runs `finder` repeatedly until it finds one or more [Element]s.
|
|
Future<Finder> waitForElement(Finder finder) async {
|
|
if (_frameSync)
|
|
await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);
|
|
|
|
await _waitUntilFrame(() => finder.evaluate().isNotEmpty);
|
|
|
|
if (_frameSync)
|
|
await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);
|
|
|
|
return finder;
|
|
}
|
|
|
|
/// Runs `finder` repeatedly until it finds zero [Element]s.
|
|
Future<Finder> waitForAbsentElement(Finder finder) async {
|
|
if (_frameSync)
|
|
await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);
|
|
|
|
await _waitUntilFrame(() => finder.evaluate().isEmpty);
|
|
|
|
if (_frameSync)
|
|
await _waitUntilFrame(() => SchedulerBinding.instance!.transientCallbackCount == 0);
|
|
|
|
return finder;
|
|
}
|
|
|
|
// Waits until at the end of a frame the provided [condition] is [true].
|
|
Future<void> _waitUntilFrame(bool condition(), [ Completer<void>? completer ]) {
|
|
completer ??= Completer<void>();
|
|
if (!condition()) {
|
|
SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
|
|
_waitUntilFrame(condition, completer);
|
|
});
|
|
} else {
|
|
completer.complete();
|
|
}
|
|
return completer.future;
|
|
}
|
|
}
|