mirror of
https://github.com/alibaba/flutter-go.git
synced 2025-07-15 03:04:25 +08:00
Add:创建 flutter go web 版
This commit is contained in:
388
packages/flutter_web_test/lib/flutter_web_test.dart
Normal file
388
packages/flutter_web_test/lib/flutter_web_test.dart
Normal file
@ -0,0 +1,388 @@
|
||||
// Synced 2019-05-30T14:20:57.788350.
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:flutter_web/rendering.dart';
|
||||
import 'package:flutter_web_ui/ui.dart' as ui;
|
||||
import 'package:flutter_web_ui/src/engine.dart' as ui;
|
||||
import 'package:html/parser.dart' as html_package;
|
||||
import 'package:html/dom.dart' as html_package;
|
||||
import 'package:test/test.dart' as test_package;
|
||||
|
||||
export 'dart:async' show Future;
|
||||
|
||||
export 'src/accessibility.dart';
|
||||
export 'src/binding.dart';
|
||||
export 'src/controller.dart';
|
||||
export 'src/finders.dart';
|
||||
export 'src/matchers.dart';
|
||||
export 'src/nonconst.dart';
|
||||
export 'src/test_async_utils.dart';
|
||||
export 'src/test_pointer.dart';
|
||||
export 'src/widget_tester.dart';
|
||||
export 'src/window.dart';
|
||||
|
||||
/// Wrapper around Dart's [test_package.test] to ensure that Ahem font is
|
||||
/// properly loaded before running tests.
|
||||
void test(
|
||||
dynamic description,
|
||||
Function body, {
|
||||
String testOn,
|
||||
test_package.Timeout timeout,
|
||||
dynamic skip,
|
||||
dynamic tags,
|
||||
Map<String, dynamic> onPlatform,
|
||||
int retry,
|
||||
}) {
|
||||
test_package.test(
|
||||
description,
|
||||
// TODO(flutter_web): remove this by wrapping tests in a test harness that
|
||||
// performs this initialization automatically.
|
||||
() => ui.ensureTestPlatformInitializedThenRunTest(body),
|
||||
testOn: testOn,
|
||||
timeout: timeout,
|
||||
skip: skip,
|
||||
tags: tags,
|
||||
onPlatform: onPlatform,
|
||||
retry: retry,
|
||||
);
|
||||
}
|
||||
|
||||
/// Controls how test HTML is canonicalized by [canonicalizeHtml] function.
|
||||
///
|
||||
/// In all cases whitespace between elements is stripped.
|
||||
enum HtmlComparisonMode {
|
||||
/// Retains all attributes.
|
||||
///
|
||||
/// Useful when very precise HTML comparison is needed that includes both
|
||||
/// layout and non-layout style attributes. This mode is rarely needed. Most
|
||||
/// tests should use [layoutOnly] or [nonLayoutOnly].
|
||||
everything,
|
||||
|
||||
/// Retains only layout style attributes, such as "width".
|
||||
///
|
||||
/// Useful when testing layout because it filters out all the noise that does
|
||||
/// not affect layout.
|
||||
layoutOnly,
|
||||
|
||||
/// Retains only non-layout style attributes, such as "color".
|
||||
///
|
||||
/// Useful when testing styling because it filters out all the noise from the
|
||||
/// layout attributes.
|
||||
nonLayoutOnly,
|
||||
}
|
||||
|
||||
/// Rewrites [html] by removing irrelevant style attributes.
|
||||
///
|
||||
/// If [throwOnUnusedAttributes] is `true`, throws instead of rewriting. Set
|
||||
/// [throwOnUnusedAttributes] to `true` to check that expected HTML strings do
|
||||
/// not contain irrelevant attributes. It is ok for actual HTML to contain all
|
||||
/// kinds of attributes. They only need to be filtered out before testing.
|
||||
String canonicalizeHtml(String html,
|
||||
{HtmlComparisonMode mode = HtmlComparisonMode.nonLayoutOnly,
|
||||
bool throwOnUnusedAttributes = false}) {
|
||||
if (html == null || html.trim().isEmpty) {
|
||||
return '';
|
||||
}
|
||||
|
||||
String _unusedAttribute(String name) {
|
||||
if (throwOnUnusedAttributes) {
|
||||
test_package.fail('Provided HTML contains style attribute "$name" which '
|
||||
'is not used for comparison in the test. The HTML was:\n\n$html');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
html_package.Element _cleanup(html_package.Element original) {
|
||||
String replacementTag = original.localName;
|
||||
switch (replacementTag) {
|
||||
case 'flt-scene':
|
||||
replacementTag = 's';
|
||||
break;
|
||||
case 'flt-transform':
|
||||
replacementTag = 't';
|
||||
break;
|
||||
case 'flt-opacity':
|
||||
replacementTag = 'o';
|
||||
break;
|
||||
case 'flt-clip':
|
||||
final String clipType = original.attributes['clip-type'];
|
||||
switch (clipType) {
|
||||
case 'rect':
|
||||
replacementTag = 'clip';
|
||||
break;
|
||||
case 'rrect':
|
||||
replacementTag = 'rclip';
|
||||
break;
|
||||
case 'physical-shape':
|
||||
replacementTag = 'pshape';
|
||||
break;
|
||||
default:
|
||||
throw Exception('Unknown clip type: ${clipType}');
|
||||
}
|
||||
break;
|
||||
case 'flt-clip-interior':
|
||||
replacementTag = 'clip-i';
|
||||
break;
|
||||
case 'flt-picture':
|
||||
replacementTag = 'pic';
|
||||
break;
|
||||
case 'flt-canvas':
|
||||
replacementTag = 'c';
|
||||
break;
|
||||
case 'flt-dom-canvas':
|
||||
replacementTag = 'd';
|
||||
break;
|
||||
case 'flt-semantics':
|
||||
replacementTag = 'sem';
|
||||
break;
|
||||
case 'flt-semantics-container':
|
||||
replacementTag = 'sem-c';
|
||||
break;
|
||||
case 'flt-semantics-value':
|
||||
replacementTag = 'sem-v';
|
||||
break;
|
||||
case 'flt-semantics-img':
|
||||
replacementTag = 'sem-img';
|
||||
break;
|
||||
case 'flt-semantics-text-field':
|
||||
replacementTag = 'sem-tf';
|
||||
break;
|
||||
}
|
||||
|
||||
html_package.Element replacement = html_package.Element.tag(replacementTag);
|
||||
|
||||
original.attributes.forEach((dynamic name, String value) {
|
||||
if (name == 'style') {
|
||||
return;
|
||||
}
|
||||
if (name.startsWith('aria-')) {
|
||||
replacement.attributes[name] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (original.attributes.containsKey('style')) {
|
||||
final styleValue = original.attributes['style'];
|
||||
|
||||
int attrCount = 0;
|
||||
String processedAttributes = styleValue
|
||||
.split(';')
|
||||
.map((attr) {
|
||||
attr = attr.trim();
|
||||
if (attr.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mode != HtmlComparisonMode.everything) {
|
||||
final forLayout = mode == HtmlComparisonMode.layoutOnly;
|
||||
List<String> parts = attr.split(':');
|
||||
if (parts.length == 2) {
|
||||
String name = parts.first;
|
||||
// Whether the attribute is one that's set to the same value and
|
||||
// never changes. Such attributes are usually not interesting to
|
||||
// test.
|
||||
bool isStaticAttribute = const <String>[
|
||||
'top',
|
||||
'left',
|
||||
'position',
|
||||
].contains(name);
|
||||
|
||||
if (isStaticAttribute) {
|
||||
return _unusedAttribute(name);
|
||||
}
|
||||
|
||||
// Whether the attribute is set by the layout system.
|
||||
bool isLayoutAttribute = const <String>[
|
||||
'top',
|
||||
'left',
|
||||
'bottom',
|
||||
'right',
|
||||
'position',
|
||||
'width',
|
||||
'height',
|
||||
'font-size',
|
||||
'transform',
|
||||
'transform-origin',
|
||||
'white-space',
|
||||
].contains(name);
|
||||
|
||||
if (forLayout && !isLayoutAttribute ||
|
||||
!forLayout && isLayoutAttribute) {
|
||||
return _unusedAttribute(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attrCount++;
|
||||
return attr.trim();
|
||||
})
|
||||
.where((attr) => attr != null && attr.isNotEmpty)
|
||||
.join('; ');
|
||||
|
||||
if (attrCount > 0) {
|
||||
replacement.attributes['style'] = processedAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
for (html_package.Node child in original.nodes) {
|
||||
if (child is html_package.Text && child.text.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (child is html_package.Element) {
|
||||
replacement.append(_cleanup(child));
|
||||
} else {
|
||||
replacement.append(child.clone(true));
|
||||
}
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
|
||||
html_package.DocumentFragment originalDom = html_package.parseFragment(html);
|
||||
|
||||
html_package.DocumentFragment cleanDom = html_package.DocumentFragment();
|
||||
for (var child in originalDom.children) {
|
||||
cleanDom.append(_cleanup(child));
|
||||
}
|
||||
|
||||
void unexpectedSceneStructure() {
|
||||
test_package.fail(
|
||||
'The root scene element <s> (or <flt-scene>) must have a single '
|
||||
'child transform element <t> (or <flt-transform>), but found '
|
||||
'${cleanDom.children.length} children '
|
||||
'${originalDom.children.map((e) => '<${e.localName}>').join(', ')}',
|
||||
);
|
||||
}
|
||||
|
||||
if (cleanDom.children.length == 1 &&
|
||||
cleanDom.children.first.localName == 's') {
|
||||
final scene = cleanDom.children.single;
|
||||
|
||||
if (scene.children.length != 1) {
|
||||
unexpectedSceneStructure();
|
||||
}
|
||||
|
||||
final rootTransform = scene.children.single;
|
||||
|
||||
if (rootTransform.localName != 't') {
|
||||
unexpectedSceneStructure();
|
||||
}
|
||||
|
||||
return rootTransform.children.map((e) => e.outerHtml).join('').trim();
|
||||
} else {
|
||||
return cleanDom.outerHtml;
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests that [currentHtml] matches [expectedHtml].
|
||||
///
|
||||
/// The comparison does not consider every minutia of the DOM. By default it
|
||||
/// tests the element tree structure and non-layout style attributes, and
|
||||
/// ignores everything else. If you are testing layout specifically, pass the
|
||||
/// [HtmlComparisonMode.layoutOnly] as the [mode] argument.
|
||||
///
|
||||
/// To keep test HTML strings manageable, you may use short HTML tag names
|
||||
/// instead of the full names:
|
||||
///
|
||||
/// * <flt-scene> is interchangeable with <s>
|
||||
/// * <flt-transform> is interchangeable with <t>
|
||||
/// * <flt-opacity> is interchangeable with <o>
|
||||
/// * <flt-clip clip-type="rect"> is interchangeable with <clip>
|
||||
/// * <flt-clip clip-type="rrect"> is interchangeable with <rclip>
|
||||
/// * <flt-clip clip-type="physical-shape"> is interchangeable with <pshape>
|
||||
/// * <flt-picture> is interchangeable with <pic>
|
||||
/// * <flt-canvas> is interchangeable with <c>
|
||||
///
|
||||
/// To simplify test HTML strings further the elements corresponding to the
|
||||
/// root view [RenderView], such as <flt-scene> (i.e. <s>), are also stripped
|
||||
/// out before comparison.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// If you call [WidgetTester.pumpWidget] that results in HTML
|
||||
/// `<s><t><pic><c><p>Hello</p></c></pic></t></s>`, you don't have to specify
|
||||
/// `<s><t>` tags and simply expect `<pic><c><p>Hello</p></c></pic>`.
|
||||
void expectCurrentHtml(String expectedHtml,
|
||||
{HtmlComparisonMode mode = HtmlComparisonMode.nonLayoutOnly}) {
|
||||
expectedHtml = canonicalizeHtml(expectedHtml, mode: mode);
|
||||
String actualHtml = canonicalizeHtml(currentHtml, mode: mode);
|
||||
test_package.expect(actualHtml, expectedHtml);
|
||||
}
|
||||
|
||||
/// Expects that we render a single picture layer.
|
||||
///
|
||||
/// The [expectedHtml] should only contain the _contents_ of the picture. It
|
||||
/// should not contain the wrapper elements, such as `<flt-picture>` or
|
||||
/// `<flt-canvas>`. Use this method to reduce the boilerplate in your test HTML
|
||||
/// strings.
|
||||
void expectPictureHtml(String expectedHtml,
|
||||
{HtmlComparisonMode mode = HtmlComparisonMode.nonLayoutOnly}) {
|
||||
if (expectedHtml.trim().isEmpty) {
|
||||
// Pictures are rendered lazily. If there's no content in the canvas, we
|
||||
// do not create any elements.
|
||||
expectCurrentHtml('', mode: mode);
|
||||
} else {
|
||||
expectCurrentHtml('<pic><c>${expectedHtml}</c></pic>', mode: mode);
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests the [currentHtml] against [expectedHtml] considering only the
|
||||
/// properties pertaining to the layout, such as CSS transforms.
|
||||
void expectCurrentLayout(String expectedHtml) {
|
||||
expectCurrentHtml(expectedHtml, mode: HtmlComparisonMode.layoutOnly);
|
||||
}
|
||||
|
||||
/// Currently rendered HTML DOM as an HTML string.
|
||||
String get currentHtml {
|
||||
return ui.domRenderer.sceneElement?.outerHtml ?? '';
|
||||
}
|
||||
|
||||
/// A widget that creates an element and sizes itself by constraining its
|
||||
/// [width] and [height] to the incoming parent constraints.
|
||||
///
|
||||
/// Useful for testing various layout widgets because the output can be verified
|
||||
/// in the resulting HTML ([SizedBox] can't be used in this manner because it
|
||||
/// does not create HTML elements).
|
||||
class TestSizedBoxWithElement extends LeafRenderObjectWidget {
|
||||
TestSizedBoxWithElement.expand() : this.isExpand = true;
|
||||
TestSizedBoxWithElement.shrink() : this.isExpand = false;
|
||||
|
||||
final bool isExpand;
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) =>
|
||||
_RenderTestSizedBoxWithElement(this);
|
||||
}
|
||||
|
||||
class _RenderTestSizedBoxWithElement extends RenderBox
|
||||
with RenderObjectWithChildMixin {
|
||||
_RenderTestSizedBoxWithElement(this.widget);
|
||||
|
||||
final TestSizedBoxWithElement widget;
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
if (widget.isExpand) {
|
||||
size = constraints.biggest;
|
||||
} else {
|
||||
size = constraints.smallest;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
context.pushClipRect(true, offset, Offset.zero & size, (_, __) {});
|
||||
}
|
||||
}
|
||||
|
||||
/// A [TickerProvider] that creates a standalone ticker.
|
||||
///
|
||||
/// Useful in tests that create an [AnimationController] outside of the widget
|
||||
/// tree.
|
||||
class TestVSync implements TickerProvider {
|
||||
/// Creates a ticker provider that creates standalone tickers.
|
||||
const TestVSync();
|
||||
|
||||
@override
|
||||
Ticker createTicker(TickerCallback onTick) => Ticker(onTick);
|
||||
}
|
472
packages/flutter_web_test/lib/src/accessibility.dart
Normal file
472
packages/flutter_web_test/lib/src/accessibility.dart
Normal file
@ -0,0 +1,472 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter_web_ui/ui.dart' as ui;
|
||||
|
||||
import 'package:flutter_web/rendering.dart';
|
||||
import 'package:flutter_web/semantics.dart';
|
||||
import 'package:flutter_web/widgets.dart';
|
||||
|
||||
import 'finders.dart';
|
||||
import 'widget_tester.dart';
|
||||
|
||||
/// The result of evaluating a semantics node by a [AccessibilityGuideline].
|
||||
class Evaluation {
|
||||
/// Create a passing evaluation.
|
||||
const Evaluation.pass()
|
||||
: passed = true,
|
||||
reason = null;
|
||||
|
||||
/// Create a failing evaluation, with an optional [reason] explaining the
|
||||
/// result.
|
||||
const Evaluation.fail([this.reason]) : passed = false;
|
||||
|
||||
// private constructor for adding cases together.
|
||||
const Evaluation._(this.passed, this.reason);
|
||||
|
||||
/// Whether the given tree or node passed the policy evaluation.
|
||||
final bool passed;
|
||||
|
||||
/// If [passed] is false, contains the reason for failure.
|
||||
final String reason;
|
||||
|
||||
/// Combines two evaluation results.
|
||||
///
|
||||
/// The [reason] will be concatenated with a newline, and [passed] will be
|
||||
/// combined with an `&&` operator.
|
||||
Evaluation operator +(Evaluation other) {
|
||||
if (other == null) return this;
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
if (reason != null) {
|
||||
buffer.write(reason);
|
||||
buffer.write(' ');
|
||||
}
|
||||
if (other.reason != null) buffer.write(other.reason);
|
||||
return Evaluation._(
|
||||
passed && other.passed, buffer.isEmpty ? null : buffer.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// An accessibility guideline describes a recommendation an application should
|
||||
/// meet to be considered accessible.
|
||||
abstract class AccessibilityGuideline {
|
||||
/// A const constructor allows subclasses to be const.
|
||||
const AccessibilityGuideline();
|
||||
|
||||
/// Evaluate whether the current state of the `tester` conforms to the rule.
|
||||
FutureOr<Evaluation> evaluate(WidgetTester tester);
|
||||
|
||||
/// A description of the policy restrictions and criteria.
|
||||
String get description;
|
||||
}
|
||||
|
||||
/// A guideline which enforces that all tapable semantics nodes have a minimum
|
||||
/// size.
|
||||
///
|
||||
/// Each platform defines its own guidelines for minimum tap areas.
|
||||
@visibleForTesting
|
||||
class MinimumTapTargetGuideline extends AccessibilityGuideline {
|
||||
const MinimumTapTargetGuideline._(this.size, this.link);
|
||||
|
||||
/// The minimum allowed size of a tapable node.
|
||||
final Size size;
|
||||
|
||||
/// A link describing the tap target guidelines for a platform.
|
||||
final String link;
|
||||
|
||||
@override
|
||||
FutureOr<Evaluation> evaluate(WidgetTester tester) {
|
||||
final SemanticsNode root =
|
||||
tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
|
||||
Evaluation traverse(SemanticsNode node) {
|
||||
Evaluation result = const Evaluation.pass();
|
||||
node.visitChildren((SemanticsNode child) {
|
||||
result += traverse(child);
|
||||
return true;
|
||||
});
|
||||
if (node.isMergedIntoParent) return result;
|
||||
final SemanticsData data = node.getSemanticsData();
|
||||
// Skip node if it has no actions, or is marked as hidden.
|
||||
if ((!data.hasAction(ui.SemanticsAction.longPress) &&
|
||||
!data.hasAction(ui.SemanticsAction.tap)) ||
|
||||
data.hasFlag(ui.SemanticsFlag.isHidden)) return result;
|
||||
Rect paintBounds = node.rect;
|
||||
SemanticsNode current = node;
|
||||
while (current != null) {
|
||||
if (current.transform != null) {
|
||||
paintBounds =
|
||||
MatrixUtils.transformRect(current.transform, paintBounds);
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
// skip node if it is touching the edge of the screen, since it might
|
||||
// be partially scrolled offscreen.
|
||||
const double delta = 0.001;
|
||||
if (paintBounds.left <= delta ||
|
||||
paintBounds.top <= delta ||
|
||||
(paintBounds.bottom - ui.window.physicalSize.height).abs() <= delta ||
|
||||
(paintBounds.right - ui.window.physicalSize.width).abs() <= delta) {
|
||||
return result;
|
||||
}
|
||||
// shrink by device pixel ratio.
|
||||
final Size candidateSize = paintBounds.size / ui.window.devicePixelRatio;
|
||||
if (candidateSize.width < size.width ||
|
||||
candidateSize.height < size.height) {
|
||||
result += Evaluation.fail(
|
||||
'$node: expected tap target size of at least $size, but found $candidateSize\n'
|
||||
'See also: $link');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return traverse(root);
|
||||
}
|
||||
|
||||
@override
|
||||
String get description => 'Tappable objects should be at least $size';
|
||||
}
|
||||
|
||||
/// A guideline which enforces that all nodes with a tap or long press action
|
||||
/// also have a label.
|
||||
@visibleForTesting
|
||||
class LabeledTapTargetGuideline extends AccessibilityGuideline {
|
||||
const LabeledTapTargetGuideline._();
|
||||
|
||||
@override
|
||||
String get description => 'Tappable widgets should have a semantic label';
|
||||
|
||||
@override
|
||||
FutureOr<Evaluation> evaluate(WidgetTester tester) {
|
||||
final SemanticsNode root =
|
||||
tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
|
||||
Evaluation traverse(SemanticsNode node) {
|
||||
Evaluation result = const Evaluation.pass();
|
||||
node.visitChildren((SemanticsNode child) {
|
||||
result += traverse(child);
|
||||
return true;
|
||||
});
|
||||
if (node.isMergedIntoParent ||
|
||||
node.isInvisible ||
|
||||
node.hasFlag(ui.SemanticsFlag.isHidden)) return result;
|
||||
final SemanticsData data = node.getSemanticsData();
|
||||
// Skip node if it has no actions, or is marked as hidden.
|
||||
if (!data.hasAction(ui.SemanticsAction.longPress) &&
|
||||
!data.hasAction(ui.SemanticsAction.tap)) return result;
|
||||
if (data.label == null || data.label.isEmpty) {
|
||||
result += Evaluation.fail(
|
||||
'$node: expected tappable node to have semantic label, but none was found\n',
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
return traverse(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// A guideline which verifies that all nodes that contribute semantics via text
|
||||
/// meet minimum contrast levels.
|
||||
///
|
||||
/// The guidelines are defined by the Web Content Accessibility Guidelines,
|
||||
/// http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html.
|
||||
@visibleForTesting
|
||||
class MinimumTextContrastGuideline extends AccessibilityGuideline {
|
||||
const MinimumTextContrastGuideline._();
|
||||
|
||||
/// The minimum text size considered large for contrast checking.
|
||||
///
|
||||
/// Defined by http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
|
||||
static const int kLargeTextMinimumSize = 18;
|
||||
|
||||
/// The minimum text size for bold text to be considered large for contrast
|
||||
/// checking.
|
||||
///
|
||||
/// Defined by http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
|
||||
static const int kBoldTextMinimumSize = 14;
|
||||
|
||||
/// The minimum contrast ratio for normal text.
|
||||
///
|
||||
/// Defined by http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
|
||||
static const double kMinimumRatioNormalText = 4.5;
|
||||
|
||||
/// The minimum contrast ratio for large text.
|
||||
///
|
||||
/// Defined by http://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
|
||||
static const double kMinimumRatioLargeText = 3.0;
|
||||
|
||||
@override
|
||||
Future<Evaluation> evaluate(WidgetTester tester) async {
|
||||
final SemanticsNode root =
|
||||
tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
|
||||
final RenderView renderView = tester.binding.renderView;
|
||||
final OffsetLayer layer = renderView.layer;
|
||||
ui.Image image;
|
||||
final ByteData byteData = await tester.binding.runAsync<ByteData>(() async {
|
||||
// Needs to be the same pixel ratio otherwise our dimensions won't match the
|
||||
// last transform layer.
|
||||
image = await layer.toImage(renderView.paintBounds, pixelRatio: 1.0);
|
||||
return image.toByteData();
|
||||
});
|
||||
|
||||
Future<Evaluation> evaluateNode(SemanticsNode node) async {
|
||||
Evaluation result = const Evaluation.pass();
|
||||
if (node.isInvisible ||
|
||||
node.isMergedIntoParent ||
|
||||
node.hasFlag(ui.SemanticsFlag.isHidden)) return result;
|
||||
final SemanticsData data = node.getSemanticsData();
|
||||
final List<SemanticsNode> children = <SemanticsNode>[];
|
||||
node.visitChildren((SemanticsNode child) {
|
||||
children.add(child);
|
||||
return true;
|
||||
});
|
||||
for (SemanticsNode child in children) {
|
||||
result += await evaluateNode(child);
|
||||
}
|
||||
if (_shouldSkipNode(data)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// We need to look up the inherited text properties to determine the
|
||||
// contrast ratio based on text size/weight.
|
||||
double fontSize;
|
||||
bool isBold;
|
||||
final String text =
|
||||
(data.label?.isEmpty == true) ? data.value : data.label;
|
||||
final List<Element> elements =
|
||||
find.text(text).hitTestable().evaluate().toList();
|
||||
if (elements.length == 1) {
|
||||
final Element element = elements.single;
|
||||
final Widget widget = element.widget;
|
||||
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(element);
|
||||
if (widget is Text) {
|
||||
TextStyle effectiveTextStyle = widget.style;
|
||||
if (widget.style == null || widget.style.inherit) {
|
||||
effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
|
||||
}
|
||||
fontSize = effectiveTextStyle.fontSize;
|
||||
isBold = effectiveTextStyle.fontWeight == FontWeight.bold;
|
||||
} else if (widget is EditableText) {
|
||||
isBold = widget.style.fontWeight == FontWeight.bold;
|
||||
fontSize = widget.style.fontSize;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
} else if (elements.length > 1) {
|
||||
return Evaluation.fail(
|
||||
'Multiple nodes with the same label: ${data.label}\n');
|
||||
} else {
|
||||
// If we can't find the text node then assume the label does not
|
||||
// correspond to actual text.
|
||||
return result;
|
||||
}
|
||||
|
||||
// Transform local coordinate to screen coordinates.
|
||||
Rect paintBounds = node.rect;
|
||||
SemanticsNode current = node;
|
||||
while (current != null && current.parent != null) {
|
||||
if (current.transform != null) {
|
||||
paintBounds =
|
||||
MatrixUtils.transformRect(current.transform, paintBounds);
|
||||
}
|
||||
paintBounds =
|
||||
paintBounds.shift(current.parent?.rect?.topLeft ?? Offset.zero);
|
||||
current = current.parent;
|
||||
}
|
||||
if (_isNodeOffScreen(paintBounds)) return result;
|
||||
final List<int> subset =
|
||||
_subsetToRect(byteData, paintBounds, image.width, image.height);
|
||||
// Node was too far off screen.
|
||||
if (subset.isEmpty) return result;
|
||||
final _ContrastReport report = _ContrastReport(subset);
|
||||
final double contrastRatio = report.contrastRatio();
|
||||
const double delta = -0.01;
|
||||
double targetContrastRatio;
|
||||
if ((isBold && fontSize > kBoldTextMinimumSize) ||
|
||||
(fontSize ?? 12.0) > kLargeTextMinimumSize) {
|
||||
targetContrastRatio = kMinimumRatioLargeText;
|
||||
} else {
|
||||
targetContrastRatio = kMinimumRatioNormalText;
|
||||
}
|
||||
if (contrastRatio - targetContrastRatio >= delta) {
|
||||
return result + const Evaluation.pass();
|
||||
}
|
||||
return result +
|
||||
Evaluation.fail('$node:\nExpected contrast ratio of at least '
|
||||
'$targetContrastRatio but found ${contrastRatio.toStringAsFixed(2)} for a font size of $fontSize. '
|
||||
'The computed foreground color was: ${report.lightColor}, '
|
||||
'The computed background color was: ${report.darkColor}\n'
|
||||
'See also: https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html');
|
||||
}
|
||||
|
||||
return evaluateNode(root);
|
||||
}
|
||||
|
||||
// Skip routes which might have labels, and nodes without any text.
|
||||
bool _shouldSkipNode(SemanticsData data) {
|
||||
if (data.hasFlag(ui.SemanticsFlag.scopesRoute)) return true;
|
||||
if (data.label?.trim()?.isEmpty == true &&
|
||||
data.value?.trim()?.isEmpty == true) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns a rect that is entirely on screen, or null if it is too far off.
|
||||
//
|
||||
// Given an 1800 * 2400 pixel buffer, can we actually get all the data from
|
||||
// this node? allow a small delta overlap before culling the node.
|
||||
bool _isNodeOffScreen(Rect paintBounds) {
|
||||
return paintBounds.top < -50.0 ||
|
||||
paintBounds.left < -50.0 ||
|
||||
paintBounds.bottom > 2400.0 + 50.0 ||
|
||||
paintBounds.right > 1800.0 + 50.0;
|
||||
}
|
||||
|
||||
List<int> _subsetToRect(
|
||||
ByteData data, Rect paintBounds, int width, int height) {
|
||||
final int newWidth = paintBounds.size.width.ceil();
|
||||
final int newHeight = paintBounds.size.height.ceil();
|
||||
final int leftX = paintBounds.topLeft.dx.ceil();
|
||||
final int rightX = leftX + newWidth;
|
||||
final int topY = paintBounds.topLeft.dy.ceil();
|
||||
final int bottomY = topY + newHeight;
|
||||
final List<int> buffer = <int>[];
|
||||
|
||||
// Data is stored in row major order.
|
||||
for (int i = 0; i < data.lengthInBytes; i += 4) {
|
||||
final int index = i ~/ 4;
|
||||
final int dx = index % width;
|
||||
final int dy = index ~/ width;
|
||||
if (dx >= leftX && dx <= rightX && dy >= topY && dy <= bottomY) {
|
||||
final int r = data.getUint8(i);
|
||||
final int g = data.getUint8(i + 1);
|
||||
final int b = data.getUint8(i + 2);
|
||||
final int a = data.getUint8(i + 3);
|
||||
final int color = (((a & 0xff) << 24) |
|
||||
((r & 0xff) << 16) |
|
||||
((g & 0xff) << 8) |
|
||||
((b & 0xff) << 0)) &
|
||||
0xFFFFFFFF;
|
||||
buffer.add(color);
|
||||
}
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@override
|
||||
String get description => 'Text contrast should follow WCAG guidelines';
|
||||
}
|
||||
|
||||
class _ContrastReport {
|
||||
factory _ContrastReport(List<int> colors) {
|
||||
final Map<int, int> colorHistogram = <int, int>{};
|
||||
for (int color in colors) {
|
||||
colorHistogram[color] = (colorHistogram[color] ?? 0) + 1;
|
||||
}
|
||||
if (colorHistogram.length == 1) {
|
||||
final Color hslColor = Color(colorHistogram.keys.first);
|
||||
return _ContrastReport._(hslColor, hslColor);
|
||||
}
|
||||
// to determine the lighter and darker color, partition the colors
|
||||
// by lightness and then choose the mode from each group.
|
||||
double averageLightness = 0.0;
|
||||
for (int color in colorHistogram.keys) {
|
||||
final HSLColor hslColor = HSLColor.fromColor(Color(color));
|
||||
averageLightness += hslColor.lightness * colorHistogram[color];
|
||||
}
|
||||
averageLightness /= colors.length;
|
||||
assert(averageLightness != double.nan);
|
||||
int lightColor = 0;
|
||||
int darkColor = 0;
|
||||
int lightCount = 0;
|
||||
int darkCount = 0;
|
||||
// Find the most frequently occurring light and dark color.
|
||||
for (MapEntry<int, int> entry in colorHistogram.entries) {
|
||||
final HSLColor color = HSLColor.fromColor(Color(entry.key));
|
||||
final int count = entry.value;
|
||||
if (color.lightness <= averageLightness && count > darkCount) {
|
||||
darkColor = entry.key;
|
||||
darkCount = count;
|
||||
} else if (color.lightness > averageLightness && count > lightCount) {
|
||||
lightColor = entry.key;
|
||||
lightCount = count;
|
||||
}
|
||||
}
|
||||
assert(lightColor != 0 && darkColor != 0);
|
||||
return _ContrastReport._(Color(lightColor), Color(darkColor));
|
||||
}
|
||||
|
||||
const _ContrastReport._(this.lightColor, this.darkColor);
|
||||
|
||||
final Color lightColor;
|
||||
final Color darkColor;
|
||||
|
||||
/// Computes the contrast ratio as defined by the WCAG.
|
||||
///
|
||||
/// source: https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html
|
||||
double contrastRatio() {
|
||||
return (_luminance(lightColor) + 0.05) / (_luminance(darkColor) + 0.05);
|
||||
}
|
||||
|
||||
/// Relative luminance calculation.
|
||||
///
|
||||
/// Based on https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
|
||||
static double _luminance(Color color) {
|
||||
double r = color.red / 255.0;
|
||||
double g = color.green / 255.0;
|
||||
double b = color.blue / 255.0;
|
||||
if (r <= 0.03928) {
|
||||
r /= 12.92;
|
||||
} else {
|
||||
r = math.pow((r + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
if (g <= 0.03928) {
|
||||
g /= 12.92;
|
||||
} else {
|
||||
g = math.pow((g + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
if (b <= 0.03928) {
|
||||
b /= 12.92;
|
||||
} else {
|
||||
b = math.pow((b + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
}
|
||||
|
||||
/// A guideline which requires tapable semantic nodes a minimum size of 48 by 48.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Android tap target guidelines](https://support.google.com/accessibility/android/answer/7101858?hl=en).
|
||||
const AccessibilityGuideline androidTapTargetGuideline =
|
||||
MinimumTapTargetGuideline._(
|
||||
Size(48.0, 48.0),
|
||||
'https://support.google.com/accessibility/android/answer/7101858?hl=en',
|
||||
);
|
||||
|
||||
/// A guideline which requires tapable semantic nodes a minimum size of 44 by 44.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [iOS human interface guidelines](https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/).
|
||||
const AccessibilityGuideline iOSTapTargetGuideline =
|
||||
MinimumTapTargetGuideline._(
|
||||
Size(44.0, 44.0),
|
||||
'https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/',
|
||||
);
|
||||
|
||||
/// A guideline which requires text contrast to meet minimum values.
|
||||
///
|
||||
/// This guideline traverses the semantics tree looking for nodes with values or
|
||||
/// labels that corresponds to a Text or Editable text widget. Given the
|
||||
/// background pixels for the area around this widget, it performs a very naive
|
||||
/// partitioning of the colors into "light" and "dark" and then chooses the most
|
||||
/// frequently occurring color in each partition as a representative of the
|
||||
/// foreground and background colors. The contrast ratio is calculated from
|
||||
/// these colors according to the [WCAG](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html#contrast-ratiodef)
|
||||
const AccessibilityGuideline textContrastGuideline =
|
||||
MinimumTextContrastGuideline._();
|
||||
|
||||
/// A guideline which enforces that all nodes with a tap or long press action
|
||||
/// also have a label.
|
||||
const AccessibilityGuideline labeledTapTargetGuideline =
|
||||
LabeledTapTargetGuideline._();
|
57
packages/flutter_web_test/lib/src/all_elements.dart
Normal file
57
packages/flutter_web_test/lib/src/all_elements.dart
Normal file
@ -0,0 +1,57 @@
|
||||
import 'package:flutter_web/material.dart';
|
||||
|
||||
/// Provides an iterable that efficiently returns all the elements
|
||||
/// rooted at the given element. See [CachingIterable] for details.
|
||||
///
|
||||
/// This method must be called again if the tree changes. You cannot
|
||||
/// call this function once, then reuse the iterable after having
|
||||
/// changed the state of the tree, because the iterable returned by
|
||||
/// this function caches the results and only walks the tree once.
|
||||
///
|
||||
/// The same applies to any iterable obtained indirectly through this
|
||||
/// one, for example the results of calling `where` on this iterable
|
||||
/// are also cached.
|
||||
Iterable<Element> collectAllElementsFrom(
|
||||
Element rootElement, {
|
||||
@required bool skipOffstage,
|
||||
}) {
|
||||
return new CachingIterable<Element>(
|
||||
new _DepthFirstChildIterator(rootElement, skipOffstage));
|
||||
}
|
||||
|
||||
class _DepthFirstChildIterator implements Iterator<Element> {
|
||||
_DepthFirstChildIterator(Element rootElement, this.skipOffstage)
|
||||
: _stack = _reverseChildrenOf(rootElement, skipOffstage).toList();
|
||||
|
||||
final bool skipOffstage;
|
||||
|
||||
Element _current;
|
||||
|
||||
final List<Element> _stack;
|
||||
|
||||
@override
|
||||
Element get current => _current;
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
if (_stack.isEmpty) return false;
|
||||
|
||||
_current = _stack.removeLast();
|
||||
// Stack children in reverse order to traverse first branch first
|
||||
_stack.addAll(_reverseChildrenOf(_current, skipOffstage));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static Iterable<Element> _reverseChildrenOf(
|
||||
Element element, bool skipOffstage) {
|
||||
assert(element != null);
|
||||
final List<Element> children = <Element>[];
|
||||
if (skipOffstage) {
|
||||
element.debugVisitOnstageChildren(children.add);
|
||||
} else {
|
||||
element.visitChildren(children.add);
|
||||
}
|
||||
return children.reversed;
|
||||
}
|
||||
}
|
1816
packages/flutter_web_test/lib/src/binding.dart
Normal file
1816
packages/flutter_web_test/lib/src/binding.dart
Normal file
File diff suppressed because it is too large
Load Diff
690
packages/flutter_web_test/lib/src/controller.dart
Normal file
690
packages/flutter_web_test/lib/src/controller.dart
Normal file
@ -0,0 +1,690 @@
|
||||
// Copyright 2016 The Chromium 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_web/gestures.dart';
|
||||
import 'package:flutter_web/rendering.dart';
|
||||
import 'package:flutter_web/widgets.dart';
|
||||
|
||||
import 'all_elements.dart';
|
||||
import 'finders.dart';
|
||||
import 'test_async_utils.dart';
|
||||
import 'test_pointer.dart';
|
||||
|
||||
/// The default drag touch slop used to break up a large drag into multiple
|
||||
/// smaller moves.
|
||||
///
|
||||
/// This value must be greater than [kTouchSlop].
|
||||
const double kDragSlopDefault = 20.0;
|
||||
|
||||
/// Class that programmatically interacts with widgets.
|
||||
///
|
||||
/// For a variant of this class suited specifically for unit tests, see
|
||||
/// [WidgetTester]. For one suitable for live tests on a device, consider
|
||||
/// [LiveWidgetController].
|
||||
///
|
||||
/// Concrete subclasses must implement the [pump] method.
|
||||
abstract class WidgetController {
|
||||
/// Creates a widget controller that uses the given binding.
|
||||
WidgetController(this.binding);
|
||||
|
||||
/// A reference to the current instance of the binding.
|
||||
final WidgetsBinding binding;
|
||||
|
||||
// FINDER API
|
||||
|
||||
// TODO(ianh): verify that the return values are of type T and throw
|
||||
// a good message otherwise, in all the generic methods below
|
||||
|
||||
/// Checks if `finder` exists in the tree.
|
||||
bool any(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().isNotEmpty;
|
||||
}
|
||||
|
||||
/// All widgets currently in the widget tree (lazy pre-order traversal).
|
||||
///
|
||||
/// Can contain duplicates, since widgets can be used in multiple
|
||||
/// places in the widget tree.
|
||||
Iterable<Widget> get allWidgets {
|
||||
TestAsyncUtils.guardSync();
|
||||
return allElements.map<Widget>((Element element) => element.widget);
|
||||
}
|
||||
|
||||
/// The matching widget in the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or matches more than
|
||||
/// one widget.
|
||||
///
|
||||
/// * Use [firstWidget] if you expect to match several widgets but only want the first.
|
||||
/// * Use [widgetList] if you expect to match several widgets and want all of them.
|
||||
T widget<T extends Widget>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().single.widget;
|
||||
}
|
||||
|
||||
/// The first matching widget according to a depth-first pre-order
|
||||
/// traversal of the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty.
|
||||
///
|
||||
/// * Use [widget] if you only expect to match one widget.
|
||||
T firstWidget<T extends Widget>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().first.widget;
|
||||
}
|
||||
|
||||
/// The matching widgets in the widget tree.
|
||||
///
|
||||
/// * Use [widget] if you only expect to match one widget.
|
||||
/// * Use [firstWidget] if you expect to match several but only want the first.
|
||||
Iterable<T> widgetList<T extends Widget>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().map<T>((Element element) {
|
||||
final T result = element.widget;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// All elements currently in the widget tree (lazy pre-order traversal).
|
||||
///
|
||||
/// The returned iterable is lazy. It does not walk the entire widget tree
|
||||
/// immediately, but rather a chunk at a time as the iteration progresses
|
||||
/// using [Iterator.moveNext].
|
||||
Iterable<Element> get allElements {
|
||||
TestAsyncUtils.guardSync();
|
||||
return collectAllElementsFrom(binding.renderViewElement,
|
||||
skipOffstage: false);
|
||||
}
|
||||
|
||||
/// The matching element in the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or matches more than
|
||||
/// one element.
|
||||
///
|
||||
/// * Use [firstElement] if you expect to match several elements but only want the first.
|
||||
/// * Use [elementList] if you expect to match several elements and want all of them.
|
||||
T element<T extends Element>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().single;
|
||||
}
|
||||
|
||||
/// The first matching element according to a depth-first pre-order
|
||||
/// traversal of the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty.
|
||||
///
|
||||
/// * Use [element] if you only expect to match one element.
|
||||
T firstElement<T extends Element>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().first;
|
||||
}
|
||||
|
||||
/// The matching elements in the widget tree.
|
||||
///
|
||||
/// * Use [element] if you only expect to match one element.
|
||||
/// * Use [firstElement] if you expect to match several but only want the first.
|
||||
Iterable<T> elementList<T extends Element>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate();
|
||||
}
|
||||
|
||||
/// All states currently in the widget tree (lazy pre-order traversal).
|
||||
///
|
||||
/// The returned iterable is lazy. It does not walk the entire widget tree
|
||||
/// immediately, but rather a chunk at a time as the iteration progresses
|
||||
/// using [Iterator.moveNext].
|
||||
Iterable<State> get allStates {
|
||||
TestAsyncUtils.guardSync();
|
||||
return allElements
|
||||
.whereType<StatefulElement>()
|
||||
.map<State>((StatefulElement element) => element.state);
|
||||
}
|
||||
|
||||
/// The matching state in the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty, matches more than
|
||||
/// one state, or matches a widget that has no state.
|
||||
///
|
||||
/// * Use [firstState] if you expect to match several states but only want the first.
|
||||
/// * Use [stateList] if you expect to match several states and want all of them.
|
||||
T state<T extends State>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return _stateOf<T>(finder.evaluate().single, finder);
|
||||
}
|
||||
|
||||
/// The first matching state according to a depth-first pre-order
|
||||
/// traversal of the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or if the first
|
||||
/// matching widget has no state.
|
||||
///
|
||||
/// * Use [state] if you only expect to match one state.
|
||||
T firstState<T extends State>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return _stateOf<T>(finder.evaluate().first, finder);
|
||||
}
|
||||
|
||||
/// The matching states in the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if any of the elements in `finder` match a widget
|
||||
/// that has no state.
|
||||
///
|
||||
/// * Use [state] if you only expect to match one state.
|
||||
/// * Use [firstState] if you expect to match several but only want the first.
|
||||
Iterable<T> stateList<T extends State>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder
|
||||
.evaluate()
|
||||
.map<T>((Element element) => _stateOf<T>(element, finder));
|
||||
}
|
||||
|
||||
T _stateOf<T extends State>(Element element, Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
if (element is StatefulElement) return element.state;
|
||||
throw StateError(
|
||||
'Widget of type ${element.widget.runtimeType}, with ${finder.description}, is not a StatefulWidget.');
|
||||
}
|
||||
|
||||
/// Render objects of all the widgets currently in the widget tree
|
||||
/// (lazy pre-order traversal).
|
||||
///
|
||||
/// This will almost certainly include many duplicates since the
|
||||
/// render object of a [StatelessWidget] or [StatefulWidget] is the
|
||||
/// render object of its child; only [RenderObjectWidget]s have
|
||||
/// their own render object.
|
||||
Iterable<RenderObject> get allRenderObjects {
|
||||
TestAsyncUtils.guardSync();
|
||||
return allElements
|
||||
.map<RenderObject>((Element element) => element.renderObject);
|
||||
}
|
||||
|
||||
/// The render object of the matching widget in the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty or matches more than
|
||||
/// one widget (even if they all have the same render object).
|
||||
///
|
||||
/// * Use [firstRenderObject] if you expect to match several render objects but only want the first.
|
||||
/// * Use [renderObjectList] if you expect to match several render objects and want all of them.
|
||||
T renderObject<T extends RenderObject>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().single.renderObject;
|
||||
}
|
||||
|
||||
/// The render object of the first matching widget according to a
|
||||
/// depth-first pre-order traversal of the widget tree.
|
||||
///
|
||||
/// Throws a [StateError] if `finder` is empty.
|
||||
///
|
||||
/// * Use [renderObject] if you only expect to match one render object.
|
||||
T firstRenderObject<T extends RenderObject>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().first.renderObject;
|
||||
}
|
||||
|
||||
/// The render objects of the matching widgets in the widget tree.
|
||||
///
|
||||
/// * Use [renderObject] if you only expect to match one render object.
|
||||
/// * Use [firstRenderObject] if you expect to match several but only want the first.
|
||||
Iterable<T> renderObjectList<T extends RenderObject>(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
return finder.evaluate().map<T>((Element element) {
|
||||
final T result = element.renderObject;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns a list of all the [Layer] objects in the rendering.
|
||||
List<Layer> get layers => _walkLayers(binding.renderView.layer).toList();
|
||||
Iterable<Layer> _walkLayers(Layer layer) sync* {
|
||||
TestAsyncUtils.guardSync();
|
||||
yield layer;
|
||||
if (layer is ContainerLayer) {
|
||||
final ContainerLayer root = layer;
|
||||
Layer child = root.firstChild;
|
||||
while (child != null) {
|
||||
yield* _walkLayers(child);
|
||||
child = child.nextSibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// INTERACTION
|
||||
|
||||
/// Dispatch a pointer down / pointer up sequence at the center of
|
||||
/// the given widget, assuming it is exposed.
|
||||
///
|
||||
/// If the center of the widget is not exposed, this might send events to
|
||||
/// another object.
|
||||
Future<void> tap(Finder finder, {int pointer}) {
|
||||
return tapAt(getCenter(finder), pointer: pointer);
|
||||
}
|
||||
|
||||
/// Dispatch a pointer down / pointer up sequence at the given location.
|
||||
Future<void> tapAt(Offset location, {int pointer}) {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
final TestGesture gesture =
|
||||
await startGesture(location, pointer: pointer);
|
||||
await gesture.up();
|
||||
});
|
||||
}
|
||||
|
||||
/// Dispatch a pointer down at the center of the given widget, assuming it is
|
||||
/// exposed.
|
||||
///
|
||||
/// If the center of the widget is not exposed, this might send events to
|
||||
/// another object.
|
||||
Future<TestGesture> press(Finder finder, {int pointer}) {
|
||||
return TestAsyncUtils.guard<TestGesture>(() {
|
||||
return startGesture(getCenter(finder), pointer: pointer);
|
||||
});
|
||||
}
|
||||
|
||||
/// Dispatch a pointer down / pointer up sequence (with a delay of
|
||||
/// [kLongPressTimeout] + [kPressTimeout] between the two events) at the
|
||||
/// center of the given widget, assuming it is exposed.
|
||||
///
|
||||
/// If the center of the widget is not exposed, this might send events to
|
||||
/// another object.
|
||||
Future<void> longPress(Finder finder, {int pointer}) {
|
||||
return longPressAt(getCenter(finder), pointer: pointer);
|
||||
}
|
||||
|
||||
/// Dispatch a pointer down / pointer up sequence at the given location with
|
||||
/// a delay of [kLongPressTimeout] + [kPressTimeout] between the two events.
|
||||
Future<void> longPressAt(Offset location, {int pointer}) {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
final TestGesture gesture =
|
||||
await startGesture(location, pointer: pointer);
|
||||
await pump(kLongPressTimeout + kPressTimeout);
|
||||
await gesture.up();
|
||||
});
|
||||
}
|
||||
|
||||
/// Attempts a fling gesture starting from the center of the given
|
||||
/// widget, moving the given distance, reaching the given speed.
|
||||
///
|
||||
/// If the middle of the widget is not exposed, this might send
|
||||
/// events to another object.
|
||||
///
|
||||
/// This can pump frames. See [flingFrom] for a discussion of how the
|
||||
/// `offset`, `velocity` and `frameInterval` arguments affect this.
|
||||
///
|
||||
/// The `speed` is in pixels per second in the direction given by `offset`.
|
||||
///
|
||||
/// A fling is essentially a drag that ends at a particular speed. If you
|
||||
/// just want to drag and end without a fling, use [drag].
|
||||
///
|
||||
/// The `initialOffset` argument, if non-zero, causes the pointer to first
|
||||
/// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
|
||||
/// used to simulate a drag followed by a fling, including dragging in the
|
||||
/// opposite direction of the fling (e.g. dragging 200 pixels to the right,
|
||||
/// then fling to the left over 200 pixels, ending at the exact point that the
|
||||
/// drag started).
|
||||
Future<void> fling(
|
||||
Finder finder,
|
||||
Offset offset,
|
||||
double speed, {
|
||||
int pointer,
|
||||
Duration frameInterval = const Duration(milliseconds: 16),
|
||||
Offset initialOffset = Offset.zero,
|
||||
Duration initialOffsetDelay = const Duration(seconds: 1),
|
||||
}) {
|
||||
return flingFrom(
|
||||
getCenter(finder),
|
||||
offset,
|
||||
speed,
|
||||
pointer: pointer,
|
||||
frameInterval: frameInterval,
|
||||
initialOffset: initialOffset,
|
||||
initialOffsetDelay: initialOffsetDelay,
|
||||
);
|
||||
}
|
||||
|
||||
/// Attempts a fling gesture starting from the given location, moving the
|
||||
/// given distance, reaching the given speed.
|
||||
///
|
||||
/// Exactly 50 pointer events are synthesized.
|
||||
///
|
||||
/// The offset and speed control the interval between each pointer event. For
|
||||
/// example, if the offset is 200 pixels down, and the speed is 800 pixels per
|
||||
/// second, the pointer events will be sent for each increment of 4 pixels
|
||||
/// (200/50), over 250ms (200/800), meaning events will be sent every 1.25ms
|
||||
/// (250/200).
|
||||
///
|
||||
/// To make tests more realistic, frames may be pumped during this time (using
|
||||
/// calls to [pump]). If the total duration is longer than `frameInterval`,
|
||||
/// then one frame is pumped each time that amount of time elapses while
|
||||
/// sending events, or each time an event is synthesized, whichever is rarer.
|
||||
///
|
||||
/// A fling is essentially a drag that ends at a particular speed. If you
|
||||
/// just want to drag and end without a fling, use [dragFrom].
|
||||
///
|
||||
/// The `initialOffset` argument, if non-zero, causes the pointer to first
|
||||
/// apply that offset, then pump a delay of `initialOffsetDelay`. This can be
|
||||
/// used to simulate a drag followed by a fling, including dragging in the
|
||||
/// opposite direction of the fling (e.g. dragging 200 pixels to the right,
|
||||
/// then fling to the left over 200 pixels, ending at the exact point that the
|
||||
/// drag started).
|
||||
Future<void> flingFrom(
|
||||
Offset startLocation,
|
||||
Offset offset,
|
||||
double speed, {
|
||||
int pointer,
|
||||
Duration frameInterval = const Duration(milliseconds: 16),
|
||||
Offset initialOffset = Offset.zero,
|
||||
Duration initialOffsetDelay = const Duration(seconds: 1),
|
||||
}) {
|
||||
assert(offset.distance > 0.0);
|
||||
assert(speed > 0.0); // speed is pixels/second
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
final TestPointer testPointer = TestPointer(pointer ?? _getNextPointer());
|
||||
final HitTestResult result = hitTestOnBinding(startLocation);
|
||||
const int kMoveCount =
|
||||
50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
|
||||
final double timeStampDelta =
|
||||
1000.0 * offset.distance / (kMoveCount * speed);
|
||||
double timeStamp = 0.0;
|
||||
double lastTimeStamp = timeStamp;
|
||||
await sendEventToBinding(
|
||||
testPointer.down(startLocation,
|
||||
timeStamp: Duration(milliseconds: timeStamp.round())),
|
||||
result);
|
||||
if (initialOffset.distance > 0.0) {
|
||||
await sendEventToBinding(
|
||||
testPointer.move(startLocation + initialOffset,
|
||||
timeStamp: Duration(milliseconds: timeStamp.round())),
|
||||
result);
|
||||
timeStamp += initialOffsetDelay.inMilliseconds;
|
||||
await pump(initialOffsetDelay);
|
||||
}
|
||||
for (int i = 0; i <= kMoveCount; i += 1) {
|
||||
final Offset location = startLocation +
|
||||
initialOffset +
|
||||
Offset.lerp(Offset.zero, offset, i / kMoveCount);
|
||||
await sendEventToBinding(
|
||||
testPointer.move(location,
|
||||
timeStamp: Duration(milliseconds: timeStamp.round())),
|
||||
result);
|
||||
timeStamp += timeStampDelta;
|
||||
if (timeStamp - lastTimeStamp > frameInterval.inMilliseconds) {
|
||||
await pump(
|
||||
Duration(milliseconds: (timeStamp - lastTimeStamp).truncate()));
|
||||
lastTimeStamp = timeStamp;
|
||||
}
|
||||
}
|
||||
await sendEventToBinding(
|
||||
testPointer.up(timeStamp: Duration(milliseconds: timeStamp.round())),
|
||||
result);
|
||||
});
|
||||
}
|
||||
|
||||
/// Called to indicate that time should advance.
|
||||
///
|
||||
/// This is invoked by [flingFrom], for instance, so that the sequence of
|
||||
/// pointer events occurs over time.
|
||||
///
|
||||
/// The [WidgetTester] subclass implements this by deferring to the [binding].
|
||||
///
|
||||
/// See also [SchedulerBinding.endOfFrame], which returns a future that could
|
||||
/// be appropriate to return in the implementation of this method.
|
||||
Future<void> pump(Duration duration);
|
||||
|
||||
/// Attempts to drag the given widget by the given offset, by
|
||||
/// starting a drag in the middle of the widget.
|
||||
///
|
||||
/// If the middle of the widget is not exposed, this might send
|
||||
/// events to another object.
|
||||
///
|
||||
/// If you want the drag to end with a speed so that the gesture recognition
|
||||
/// system identifies the gesture as a fling, consider using [fling] instead.
|
||||
///
|
||||
/// {@template flutter.flutter_test.drag}
|
||||
/// By default, if the x or y component of offset is greater than [kTouchSlop], the
|
||||
/// gesture is broken up into two separate moves calls. Changing 'touchSlopX' or
|
||||
/// `touchSlopY` will change the minimum amount of movement in the respective axis
|
||||
/// before the drag will be broken into multiple calls. To always send the
|
||||
/// drag with just a single call to [TestGesture.moveBy], `touchSlopX` and `touchSlopY`
|
||||
/// should be set to 0.
|
||||
///
|
||||
/// Breaking the drag into multiple moves is necessary for accurate execution
|
||||
/// of drag update calls with a [DragStartBehavior] variable set to
|
||||
/// [DragStartBehavior.start]. Without such a change, the dragUpdate callback
|
||||
/// from a drag recognizer will never be invoked.
|
||||
///
|
||||
/// To force this function to a send a single move event, the 'touchSlopX' and
|
||||
/// 'touchSlopY' variables should be set to 0. However, generally, these values
|
||||
/// should be left to their default values.
|
||||
/// {@end template}
|
||||
Future<void> drag(Finder finder, Offset offset,
|
||||
{int pointer,
|
||||
double touchSlopX = kDragSlopDefault,
|
||||
double touchSlopY = kDragSlopDefault}) {
|
||||
assert(kDragSlopDefault > kTouchSlop);
|
||||
return dragFrom(getCenter(finder), offset,
|
||||
pointer: pointer, touchSlopX: touchSlopX, touchSlopY: touchSlopY);
|
||||
}
|
||||
|
||||
/// Attempts a drag gesture consisting of a pointer down, a move by
|
||||
/// the given offset, and a pointer up.
|
||||
///
|
||||
/// If you want the drag to end with a speed so that the gesture recognition
|
||||
/// system identifies the gesture as a fling, consider using [flingFrom]
|
||||
/// instead.
|
||||
///
|
||||
/// {@macro flutter.flutter_test.drag}
|
||||
Future<void> dragFrom(Offset startLocation, Offset offset,
|
||||
{int pointer,
|
||||
double touchSlopX = kDragSlopDefault,
|
||||
double touchSlopY = kDragSlopDefault}) {
|
||||
assert(kDragSlopDefault > kTouchSlop);
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
final TestGesture gesture =
|
||||
await startGesture(startLocation, pointer: pointer);
|
||||
assert(gesture != null);
|
||||
|
||||
final double xSign = offset.dx.sign;
|
||||
final double ySign = offset.dy.sign;
|
||||
|
||||
final double offsetX = offset.dx;
|
||||
final double offsetY = offset.dy;
|
||||
|
||||
final bool separateX = offset.dx.abs() > touchSlopX && touchSlopX > 0;
|
||||
final bool separateY = offset.dy.abs() > touchSlopY && touchSlopY > 0;
|
||||
|
||||
if (separateY || separateX) {
|
||||
final double offsetSlope = offsetY / offsetX;
|
||||
final double inverseOffsetSlope = offsetX / offsetY;
|
||||
final double slopSlope = touchSlopY / touchSlopX;
|
||||
final double absoluteOffsetSlope = offsetSlope.abs();
|
||||
final double signedSlopX = touchSlopX * xSign;
|
||||
final double signedSlopY = touchSlopY * ySign;
|
||||
if (absoluteOffsetSlope != slopSlope) {
|
||||
// The drag goes through one or both of the extents of the edges of the box.
|
||||
if (absoluteOffsetSlope < slopSlope) {
|
||||
assert(offsetX.abs() > touchSlopX);
|
||||
// The drag goes through the vertical edge of the box.
|
||||
// It is guaranteed that the |offsetX| > touchSlopX.
|
||||
final double diffY = offsetSlope.abs() * touchSlopX * ySign;
|
||||
|
||||
// The vector from the origin to the vertical edge.
|
||||
await gesture.moveBy(Offset(signedSlopX, diffY));
|
||||
if (offsetY.abs() <= touchSlopY) {
|
||||
// The drag ends on or before getting to the horizontal extension of the horizontal edge.
|
||||
await gesture
|
||||
.moveBy(Offset(offsetX - signedSlopX, offsetY - diffY));
|
||||
} else {
|
||||
final double diffY2 = signedSlopY - diffY;
|
||||
final double diffX2 = inverseOffsetSlope * diffY2;
|
||||
|
||||
// The vector from the edge of the box to the horizontal extension of the horizontal edge.
|
||||
await gesture.moveBy(Offset(diffX2, diffY2));
|
||||
await gesture.moveBy(Offset(
|
||||
offsetX - diffX2 - signedSlopX, offsetY - signedSlopY));
|
||||
}
|
||||
} else {
|
||||
assert(offsetY.abs() > touchSlopY);
|
||||
// The drag goes through the horizontal edge of the box.
|
||||
// It is guaranteed that the |offsetY| > touchSlopY.
|
||||
final double diffX = inverseOffsetSlope.abs() * touchSlopY * xSign;
|
||||
|
||||
// The vector from the origin to the vertical edge.
|
||||
await gesture.moveBy(Offset(diffX, signedSlopY));
|
||||
if (offsetX.abs() <= touchSlopX) {
|
||||
// The drag ends on or before getting to the vertical extension of the vertical edge.
|
||||
await gesture
|
||||
.moveBy(Offset(offsetX - diffX, offsetY - signedSlopY));
|
||||
} else {
|
||||
final double diffX2 = signedSlopX - diffX;
|
||||
final double diffY2 = offsetSlope * diffX2;
|
||||
|
||||
// The vector from the edge of the box to the vertical extension of the vertical edge.
|
||||
await gesture.moveBy(Offset(diffX2, diffY2));
|
||||
await gesture.moveBy(Offset(
|
||||
offsetX - signedSlopX, offsetY - diffY2 - signedSlopY));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// The drag goes through the corner of the box.
|
||||
await gesture.moveBy(Offset(signedSlopX, signedSlopY));
|
||||
await gesture
|
||||
.moveBy(Offset(offsetX - signedSlopX, offsetY - signedSlopY));
|
||||
}
|
||||
} else {
|
||||
// The drag ends inside the box.
|
||||
await gesture.moveBy(offset);
|
||||
}
|
||||
await gesture.up();
|
||||
});
|
||||
}
|
||||
|
||||
/// The next available pointer identifier.
|
||||
///
|
||||
/// This is the default pointer identifier that will be used the next time the
|
||||
/// [startGesture] method is called without an explicit pointer identifier.
|
||||
int nextPointer = 1;
|
||||
|
||||
int _getNextPointer() {
|
||||
final int result = nextPointer;
|
||||
nextPointer += 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Creates gesture and returns the [TestGesture] object which you can use
|
||||
/// to continue the gesture using calls on the [TestGesture] object.
|
||||
///
|
||||
/// You can use [startGesture] instead if your gesture begins with a down
|
||||
/// event.
|
||||
Future<TestGesture> createGesture(
|
||||
{int pointer, PointerDeviceKind kind = PointerDeviceKind.touch}) async {
|
||||
return TestGesture(
|
||||
hitTester: hitTestOnBinding,
|
||||
dispatcher: sendEventToBinding,
|
||||
kind: kind,
|
||||
pointer: pointer ?? _getNextPointer(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a gesture with an initial down gesture at a particular point, and
|
||||
/// returns the [TestGesture] object which you can use to continue the
|
||||
/// gesture.
|
||||
///
|
||||
/// You can use [createGesture] if your gesture doesn't begin with an initial
|
||||
/// down gesture.
|
||||
Future<TestGesture> startGesture(
|
||||
Offset downLocation, {
|
||||
int pointer,
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
}) async {
|
||||
final TestGesture result =
|
||||
await createGesture(pointer: pointer, kind: kind);
|
||||
await result.down(downLocation);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Forwards the given location to the binding's hitTest logic.
|
||||
HitTestResult hitTestOnBinding(Offset location) {
|
||||
final HitTestResult result = HitTestResult();
|
||||
binding.hitTest(result, location);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Forwards the given pointer event to the binding.
|
||||
Future<void> sendEventToBinding(PointerEvent event, HitTestResult result) {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
binding.dispatchEvent(event, result);
|
||||
});
|
||||
}
|
||||
|
||||
// GEOMETRY
|
||||
|
||||
/// Returns the point at the center of the given widget.
|
||||
Offset getCenter(Finder finder) {
|
||||
return _getElementPoint(finder, (Size size) => size.center(Offset.zero));
|
||||
}
|
||||
|
||||
/// Returns the point at the top left of the given widget.
|
||||
Offset getTopLeft(Finder finder) {
|
||||
return _getElementPoint(finder, (Size size) => Offset.zero);
|
||||
}
|
||||
|
||||
/// Returns the point at the top right of the given widget. This
|
||||
/// point is not inside the object's hit test area.
|
||||
Offset getTopRight(Finder finder) {
|
||||
return _getElementPoint(finder, (Size size) => size.topRight(Offset.zero));
|
||||
}
|
||||
|
||||
/// Returns the point at the bottom left of the given widget. This
|
||||
/// point is not inside the object's hit test area.
|
||||
Offset getBottomLeft(Finder finder) {
|
||||
return _getElementPoint(
|
||||
finder, (Size size) => size.bottomLeft(Offset.zero));
|
||||
}
|
||||
|
||||
/// Returns the point at the bottom right of the given widget. This
|
||||
/// point is not inside the object's hit test area.
|
||||
Offset getBottomRight(Finder finder) {
|
||||
return _getElementPoint(
|
||||
finder, (Size size) => size.bottomRight(Offset.zero));
|
||||
}
|
||||
|
||||
Offset _getElementPoint(Finder finder, Offset sizeToPoint(Size size)) {
|
||||
TestAsyncUtils.guardSync();
|
||||
final Element element = finder.evaluate().single;
|
||||
final RenderBox box = element.renderObject;
|
||||
assert(box != null);
|
||||
return box.localToGlobal(sizeToPoint(box.size));
|
||||
}
|
||||
|
||||
/// Returns the size of the given widget. This is only valid once
|
||||
/// the widget's render object has been laid out at least once.
|
||||
Size getSize(Finder finder) {
|
||||
TestAsyncUtils.guardSync();
|
||||
final Element element = finder.evaluate().single;
|
||||
final RenderBox box = element.renderObject;
|
||||
assert(box != null);
|
||||
return box.size;
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// Variant of [WidgetController] that can be used in tests running
|
||||
/// on a device.
|
||||
///
|
||||
/// This is used, for instance, by [FlutterDriver].
|
||||
class LiveWidgetController extends WidgetController {
|
||||
/// Creates a widget controller that uses the given binding.
|
||||
LiveWidgetController(WidgetsBinding binding) : super(binding);
|
||||
|
||||
@override
|
||||
Future<void> pump(Duration duration) async {
|
||||
if (duration != null) await Future<void>.delayed(duration);
|
||||
binding.scheduleFrame();
|
||||
await binding.endOfFrame;
|
||||
}
|
||||
}
|
708
packages/flutter_web_test/lib/src/finders.dart
Normal file
708
packages/flutter_web_test/lib/src/finders.dart
Normal file
@ -0,0 +1,708 @@
|
||||
import 'package:flutter_web/gestures.dart';
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'all_elements.dart';
|
||||
|
||||
/// Signature for [CommonFinders.byWidgetPredicate].
|
||||
typedef WidgetPredicate = bool Function(Widget widget);
|
||||
|
||||
/// Signature for [CommonFinders.byElementPredicate].
|
||||
typedef ElementPredicate = bool Function(Element element);
|
||||
|
||||
/// Some frequently used widget [Finder]s.
|
||||
const CommonFinders find = CommonFinders._();
|
||||
|
||||
/// Provides lightweight syntax for getting frequently used widget [Finder]s.
|
||||
///
|
||||
/// This class is instantiated once, as [find].
|
||||
class CommonFinders {
|
||||
const CommonFinders._();
|
||||
|
||||
/// Finds [Text] and [EditableText] widgets containing string equal to the
|
||||
/// `text` argument.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.text('Back'), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder text(String text, {bool skipOffstage = true}) =>
|
||||
_TextFinder(text, skipOffstage: skipOffstage);
|
||||
|
||||
/// Looks for widgets that contain a [Text] descendant with `text`
|
||||
/// in it.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// // Suppose you have a button with text 'Update' in it:
|
||||
/// new Button(
|
||||
/// child: new Text('Update')
|
||||
/// )
|
||||
///
|
||||
/// // You can find and tap on it like this:
|
||||
/// tester.tap(find.widgetWithText(Button, 'Update'));
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder widgetWithText(Type widgetType, String text,
|
||||
{bool skipOffstage = true}) {
|
||||
return find.ancestor(
|
||||
of: find.text(text, skipOffstage: skipOffstage),
|
||||
matching: find.byType(widgetType, skipOffstage: skipOffstage),
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds widgets by searching for one with a particular [Key].
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.byKey(backKey), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byKey(Key key, {bool skipOffstage = true}) =>
|
||||
_KeyFinder(key, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds widgets by searching for widgets with a particular type.
|
||||
///
|
||||
/// This does not do subclass tests, so for example
|
||||
/// `byType(StatefulWidget)` will never find anything since that's
|
||||
/// an abstract class.
|
||||
///
|
||||
/// The `type` argument must be a subclass of [Widget].
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.byType(IconButton), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byType(Type type, {bool skipOffstage = true}) =>
|
||||
_WidgetTypeFinder(type, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds [Icon] widgets containing icon data equal to the `icon`
|
||||
/// argument.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.byIcon(Icons.inbox), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byIcon(IconData icon, {bool skipOffstage = true}) =>
|
||||
_WidgetIconFinder(icon, skipOffstage: skipOffstage);
|
||||
|
||||
/// Looks for widgets that contain an [Icon] descendant displaying [IconData]
|
||||
/// `icon` in it.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// // Suppose you have a button with icon 'arrow_forward' in it:
|
||||
/// new Button(
|
||||
/// child: new Icon(Icons.arrow_forward)
|
||||
/// )
|
||||
///
|
||||
/// // You can find and tap on it like this:
|
||||
/// tester.tap(find.widgetWithIcon(Button, Icons.arrow_forward));
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder widgetWithIcon(Type widgetType, IconData icon,
|
||||
{bool skipOffstage = true}) {
|
||||
return find.ancestor(
|
||||
of: find.byIcon(icon),
|
||||
matching: find.byType(widgetType),
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds widgets by searching for elements with a particular type.
|
||||
///
|
||||
/// This does not do subclass tests, so for example
|
||||
/// `byElementType(VirtualViewportElement)` will never find anything
|
||||
/// since that's an abstract class.
|
||||
///
|
||||
/// The `type` argument must be a subclass of [Element].
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.byElementType(SingleChildRenderObjectElement), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byElementType(Type type, {bool skipOffstage = true}) =>
|
||||
_ElementTypeFinder(type, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds widgets whose current widget is the instance given by the
|
||||
/// argument.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// // Suppose you have a button created like this:
|
||||
/// Widget myButton = new Button(
|
||||
/// child: new Text('Update')
|
||||
/// );
|
||||
///
|
||||
/// // You can find and tap on it like this:
|
||||
/// tester.tap(find.byWidget(myButton));
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byWidget(Widget widget, {bool skipOffstage = true}) =>
|
||||
_WidgetFinder(widget, skipOffstage: skipOffstage);
|
||||
|
||||
/// Finds widgets using a widget [predicate].
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.byWidgetPredicate(
|
||||
/// (Widget widget) => widget is Tooltip && widget.message == 'Back',
|
||||
/// description: 'widget with tooltip "Back"',
|
||||
/// ), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If [description] is provided, then this uses it as the description of the
|
||||
/// [Finder] and appears, for example, in the error message when the finder
|
||||
/// fails to locate the desired widget. Otherwise, the description prints the
|
||||
/// signature of the predicate function.
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byWidgetPredicate(WidgetPredicate predicate,
|
||||
{String description, bool skipOffstage = true}) {
|
||||
return _WidgetPredicateFinder(predicate,
|
||||
description: description, skipOffstage: skipOffstage);
|
||||
}
|
||||
|
||||
/// Finds Tooltip widgets with the given message.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.byTooltip('Back'), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byTooltip(String message, {bool skipOffstage = true}) {
|
||||
return byWidgetPredicate(
|
||||
(Widget widget) => widget is Tooltip && widget.message == message,
|
||||
skipOffstage: skipOffstage,
|
||||
);
|
||||
}
|
||||
|
||||
/// Finds widgets using an element [predicate].
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.byElementPredicate(
|
||||
/// // finds elements of type SingleChildRenderObjectElement, including
|
||||
/// // those that are actually subclasses of that type.
|
||||
/// // (contrast with byElementType, which only returns exact matches)
|
||||
/// (Element element) => element is SingleChildRenderObjectElement,
|
||||
/// description: '$SingleChildRenderObjectElement element',
|
||||
/// ), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If [description] is provided, then this uses it as the description of the
|
||||
/// [Finder] and appears, for example, in the error message when the finder
|
||||
/// fails to locate the desired widget. Otherwise, the description prints the
|
||||
/// signature of the predicate function.
|
||||
///
|
||||
/// If the `skipOffstage` argument is true (the default), then this skips
|
||||
/// nodes that are [Offstage] or that are from inactive [Route]s.
|
||||
Finder byElementPredicate(ElementPredicate predicate,
|
||||
{String description, bool skipOffstage = true}) {
|
||||
return _ElementPredicateFinder(predicate,
|
||||
description: description, skipOffstage: skipOffstage);
|
||||
}
|
||||
|
||||
/// Finds widgets that are descendants of the [of] parameter and that match
|
||||
/// the [matching] parameter.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// expect(find.descendant(
|
||||
/// of: find.widgetWithText(Row, 'label_1'), matching: find.text('value_1')
|
||||
/// ), findsOneWidget);
|
||||
/// ```
|
||||
///
|
||||
/// If the [matchRoot] argument is true then the widget(s) specified by [of]
|
||||
/// will be matched along with the descendants.
|
||||
///
|
||||
/// If the [skipOffstage] argument is true (the default), then nodes that are
|
||||
/// [Offstage] or that are from inactive [Route]s are skipped.
|
||||
Finder descendant(
|
||||
{Finder of,
|
||||
Finder matching,
|
||||
bool matchRoot = false,
|
||||
bool skipOffstage = true}) {
|
||||
return _DescendantFinder(of, matching,
|
||||
matchRoot: matchRoot, skipOffstage: skipOffstage);
|
||||
}
|
||||
|
||||
/// Finds widgets that are ancestors of the [of] parameter and that match
|
||||
/// the [matching] parameter.
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// // Test if a Text widget that contains 'faded' is the
|
||||
/// // descendant of an Opacity widget with opacity 0.5:
|
||||
/// expect(
|
||||
/// tester.widget<Opacity>(
|
||||
/// find.ancestor(
|
||||
/// of: find.text('faded'),
|
||||
/// matching: find.byType('Opacity'),
|
||||
/// )
|
||||
/// ).opacity,
|
||||
/// 0.5
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// If the [matchRoot] argument is true then the widget(s) specified by [of]
|
||||
/// will be matched along with the ancestors.
|
||||
Finder ancestor({Finder of, Finder matching, bool matchRoot = false}) {
|
||||
return _AncestorFinder(of, matching, matchRoot: matchRoot);
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches a widget tree and returns nodes that match a particular
|
||||
/// pattern.
|
||||
abstract class Finder {
|
||||
/// Initializes a Finder. Used by subclasses to initialize the [skipOffstage]
|
||||
/// property.
|
||||
Finder({this.skipOffstage = true});
|
||||
|
||||
/// Describes what the finder is looking for. The description should be
|
||||
/// a brief English noun phrase describing the finder's pattern.
|
||||
String get description;
|
||||
|
||||
/// Returns all the elements in the given list that match this
|
||||
/// finder's pattern.
|
||||
///
|
||||
/// When implementing your own Finders that inherit directly from
|
||||
/// [Finder], this is the main method to override. If your finder
|
||||
/// can efficiently be described just in terms of a predicate
|
||||
/// function, consider extending [MatchFinder] instead.
|
||||
Iterable<Element> apply(Iterable<Element> candidates);
|
||||
|
||||
/// Whether this finder skips nodes that are offstage.
|
||||
///
|
||||
/// If this is true, then the elements are walked using
|
||||
/// [Element.debugVisitOnstageChildren]. This skips offstage children of
|
||||
/// [Offstage] widgets, as well as children of inactive [Route]s.
|
||||
final bool skipOffstage;
|
||||
|
||||
/// Returns all the [Element]s that will be considered by this finder.
|
||||
///
|
||||
/// See [collectAllElementsFrom].
|
||||
@protected
|
||||
Iterable<Element> get allCandidates {
|
||||
return collectAllElementsFrom(WidgetsBinding.instance.renderViewElement,
|
||||
skipOffstage: skipOffstage);
|
||||
}
|
||||
|
||||
Iterable<Element> _cachedResult;
|
||||
|
||||
/// Returns the current result. If [precache] was called and returned true, this will
|
||||
/// cheaply return the result that was computed then. Otherwise, it creates a new
|
||||
/// iterable to compute the answer.
|
||||
///
|
||||
/// Calling this clears the cache from [precache].
|
||||
Iterable<Element> evaluate() {
|
||||
final Iterable<Element> result = _cachedResult ?? apply(allCandidates);
|
||||
_cachedResult = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Attempts to evaluate the finder. Returns whether any elements in the tree
|
||||
/// matched the finder. If any did, then the result is cached and can be obtained
|
||||
/// from [evaluate].
|
||||
///
|
||||
/// If this returns true, you must call [evaluate] before you call [precache] again.
|
||||
bool precache() {
|
||||
assert(_cachedResult == null);
|
||||
final Iterable<Element> result = apply(allCandidates);
|
||||
if (result.isNotEmpty) {
|
||||
_cachedResult = result;
|
||||
return true;
|
||||
}
|
||||
_cachedResult = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Returns a variant of this finder that only matches the first element
|
||||
/// matched by this finder.
|
||||
Finder get first => _FirstFinder(this);
|
||||
|
||||
/// Returns a variant of this finder that only matches the last element
|
||||
/// matched by this finder.
|
||||
Finder get last => _LastFinder(this);
|
||||
|
||||
/// Returns a variant of this finder that only matches the element at the
|
||||
/// given index matched by this finder.
|
||||
Finder at(int index) => _IndexFinder(this, index);
|
||||
|
||||
/// Returns a variant of this finder that only matches elements reachable by
|
||||
/// a hit test.
|
||||
///
|
||||
/// The [at] parameter specifies the location relative to the size of the
|
||||
/// target element where the hit test is performed.
|
||||
Finder hitTestable({Alignment at = Alignment.center}) =>
|
||||
_HitTestableFinder(this, at);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final String additional =
|
||||
skipOffstage ? ' (ignoring offstage widgets)' : '';
|
||||
final List<Element> widgets = evaluate().toList();
|
||||
final int count = widgets.length;
|
||||
if (count == 0) return 'zero widgets with $description$additional';
|
||||
if (count == 1)
|
||||
return 'exactly one widget with $description$additional: ${widgets.single}';
|
||||
if (count < 4)
|
||||
return '$count widgets with $description$additional: $widgets';
|
||||
return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
|
||||
}
|
||||
}
|
||||
|
||||
/// Applies additional filtering against a [parent] [Finder].
|
||||
abstract class ChainedFinder extends Finder {
|
||||
/// Create a Finder chained against the candidates of another [Finder].
|
||||
ChainedFinder(this.parent) : assert(parent != null);
|
||||
|
||||
/// Another [Finder] that will run first.
|
||||
final Finder parent;
|
||||
|
||||
/// Return another [Iterable] when given an [Iterable] of candidates from a
|
||||
/// parent [Finder].
|
||||
///
|
||||
/// This is the method to implement when subclassing [ChainedFinder].
|
||||
Iterable<Element> filter(Iterable<Element> parentCandidates);
|
||||
|
||||
@override
|
||||
Iterable<Element> apply(Iterable<Element> candidates) {
|
||||
return filter(parent.apply(candidates));
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<Element> get allCandidates => parent.allCandidates;
|
||||
}
|
||||
|
||||
class _FirstFinder extends ChainedFinder {
|
||||
_FirstFinder(Finder parent) : super(parent);
|
||||
|
||||
@override
|
||||
String get description => '${parent.description} (ignoring all but first)';
|
||||
|
||||
@override
|
||||
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
|
||||
yield parentCandidates.first;
|
||||
}
|
||||
}
|
||||
|
||||
class _LastFinder extends ChainedFinder {
|
||||
_LastFinder(Finder parent) : super(parent);
|
||||
|
||||
@override
|
||||
String get description => '${parent.description} (ignoring all but last)';
|
||||
|
||||
@override
|
||||
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
|
||||
yield parentCandidates.last;
|
||||
}
|
||||
}
|
||||
|
||||
class _IndexFinder extends ChainedFinder {
|
||||
_IndexFinder(Finder parent, this.index) : super(parent);
|
||||
|
||||
final int index;
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
'${parent.description} (ignoring all but index $index)';
|
||||
|
||||
@override
|
||||
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
|
||||
yield parentCandidates.elementAt(index);
|
||||
}
|
||||
}
|
||||
|
||||
class _HitTestableFinder extends ChainedFinder {
|
||||
_HitTestableFinder(Finder parent, this.alignment) : super(parent);
|
||||
|
||||
final Alignment alignment;
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
'${parent.description} (considering only hit-testable ones)';
|
||||
|
||||
@override
|
||||
Iterable<Element> filter(Iterable<Element> parentCandidates) sync* {
|
||||
for (final Element candidate in parentCandidates) {
|
||||
final RenderBox box = candidate.renderObject;
|
||||
assert(box != null);
|
||||
final Offset absoluteOffset =
|
||||
box.localToGlobal(alignment.alongSize(box.size));
|
||||
final HitTestResult hitResult = HitTestResult();
|
||||
WidgetsBinding.instance.hitTest(hitResult, absoluteOffset);
|
||||
for (final HitTestEntry entry in hitResult.path) {
|
||||
if (entry.target == candidate.renderObject) {
|
||||
yield candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches a widget tree and returns nodes that match a particular
|
||||
/// pattern.
|
||||
abstract class MatchFinder extends Finder {
|
||||
/// Initializes a predicate-based Finder. Used by subclasses to initialize the
|
||||
/// [skipOffstage] property.
|
||||
MatchFinder({bool skipOffstage = true}) : super(skipOffstage: skipOffstage);
|
||||
|
||||
/// Returns true if the given element matches the pattern.
|
||||
///
|
||||
/// When implementing your own MatchFinder, this is the main method to override.
|
||||
bool matches(Element candidate);
|
||||
|
||||
@override
|
||||
Iterable<Element> apply(Iterable<Element> candidates) {
|
||||
return candidates.where(matches);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextFinder extends MatchFinder {
|
||||
_TextFinder(this.text, {bool skipOffstage = true})
|
||||
: super(skipOffstage: skipOffstage);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
String get description => 'text "$text"';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
if (candidate.widget is Text) {
|
||||
final Text textWidget = candidate.widget;
|
||||
if (textWidget.data != null) return textWidget.data == text;
|
||||
return textWidget.textSpan.toPlainText() == text;
|
||||
} else if (candidate.widget is EditableText) {
|
||||
final EditableText editable = candidate.widget;
|
||||
return editable.controller.text == text;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class _KeyFinder extends MatchFinder {
|
||||
_KeyFinder(this.key, {bool skipOffstage = true})
|
||||
: super(skipOffstage: skipOffstage);
|
||||
|
||||
final Key key;
|
||||
|
||||
@override
|
||||
String get description => 'key $key';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
return candidate.widget.key == key;
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetTypeFinder extends MatchFinder {
|
||||
_WidgetTypeFinder(this.widgetType, {bool skipOffstage = true})
|
||||
: super(skipOffstage: skipOffstage);
|
||||
|
||||
final Type widgetType;
|
||||
|
||||
@override
|
||||
String get description => 'type "$widgetType"';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
return candidate.widget.runtimeType == widgetType;
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetIconFinder extends MatchFinder {
|
||||
_WidgetIconFinder(this.icon, {bool skipOffstage = true})
|
||||
: super(skipOffstage: skipOffstage);
|
||||
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
String get description => 'icon "$icon"';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
final Widget widget = candidate.widget;
|
||||
return widget is Icon && widget.icon == icon;
|
||||
}
|
||||
}
|
||||
|
||||
class _ElementTypeFinder extends MatchFinder {
|
||||
_ElementTypeFinder(this.elementType, {bool skipOffstage = true})
|
||||
: super(skipOffstage: skipOffstage);
|
||||
|
||||
final Type elementType;
|
||||
|
||||
@override
|
||||
String get description => 'type "$elementType"';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
return candidate.runtimeType == elementType;
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetFinder extends MatchFinder {
|
||||
_WidgetFinder(this.widget, {bool skipOffstage = true})
|
||||
: super(skipOffstage: skipOffstage);
|
||||
|
||||
final Widget widget;
|
||||
|
||||
@override
|
||||
String get description => 'the given widget ($widget)';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
return candidate.widget == widget;
|
||||
}
|
||||
}
|
||||
|
||||
class _WidgetPredicateFinder extends MatchFinder {
|
||||
_WidgetPredicateFinder(this.predicate,
|
||||
{String description, bool skipOffstage = true})
|
||||
: _description = description,
|
||||
super(skipOffstage: skipOffstage);
|
||||
|
||||
final WidgetPredicate predicate;
|
||||
final String _description;
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
_description ?? 'widget matching predicate ($predicate)';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
return predicate(candidate.widget);
|
||||
}
|
||||
}
|
||||
|
||||
class _ElementPredicateFinder extends MatchFinder {
|
||||
_ElementPredicateFinder(this.predicate,
|
||||
{String description, bool skipOffstage = true})
|
||||
: _description = description,
|
||||
super(skipOffstage: skipOffstage);
|
||||
|
||||
final ElementPredicate predicate;
|
||||
final String _description;
|
||||
|
||||
@override
|
||||
String get description =>
|
||||
_description ?? 'element matching predicate ($predicate)';
|
||||
|
||||
@override
|
||||
bool matches(Element candidate) {
|
||||
return predicate(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
class _DescendantFinder extends Finder {
|
||||
_DescendantFinder(
|
||||
this.ancestor,
|
||||
this.descendant, {
|
||||
this.matchRoot = false,
|
||||
bool skipOffstage = true,
|
||||
}) : super(skipOffstage: skipOffstage);
|
||||
|
||||
final Finder ancestor;
|
||||
final Finder descendant;
|
||||
final bool matchRoot;
|
||||
|
||||
@override
|
||||
String get description {
|
||||
if (matchRoot)
|
||||
return '${descendant.description} in the subtree(s) beginning with ${ancestor.description}';
|
||||
return '${descendant.description} that has ancestor(s) with ${ancestor.description}';
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<Element> apply(Iterable<Element> candidates) {
|
||||
return candidates
|
||||
.where((Element element) => descendant.evaluate().contains(element));
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<Element> get allCandidates {
|
||||
final Iterable<Element> ancestorElements = ancestor.evaluate();
|
||||
final List<Element> candidates = ancestorElements
|
||||
.expand<Element>((Element element) =>
|
||||
collectAllElementsFrom(element, skipOffstage: skipOffstage))
|
||||
.toSet()
|
||||
.toList();
|
||||
if (matchRoot) candidates.insertAll(0, ancestorElements);
|
||||
return candidates;
|
||||
}
|
||||
}
|
||||
|
||||
class _AncestorFinder extends Finder {
|
||||
_AncestorFinder(this.descendant, this.ancestor, {this.matchRoot = false})
|
||||
: super(skipOffstage: false);
|
||||
|
||||
final Finder ancestor;
|
||||
final Finder descendant;
|
||||
final bool matchRoot;
|
||||
|
||||
@override
|
||||
String get description {
|
||||
if (matchRoot)
|
||||
return 'ancestor ${ancestor.description} beginning with ${descendant.description}';
|
||||
return '${ancestor.description} which is an ancestor of ${descendant.description}';
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<Element> apply(Iterable<Element> candidates) {
|
||||
return candidates
|
||||
.where((Element element) => ancestor.evaluate().contains(element));
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<Element> get allCandidates {
|
||||
final List<Element> candidates = <Element>[];
|
||||
for (Element root in descendant.evaluate()) {
|
||||
final List<Element> ancestors = <Element>[];
|
||||
if (matchRoot) ancestors.add(root);
|
||||
root.visitAncestorElements((Element element) {
|
||||
ancestors.add(element);
|
||||
return true;
|
||||
});
|
||||
candidates.addAll(ancestors);
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
}
|
1909
packages/flutter_web_test/lib/src/matchers.dart
Normal file
1909
packages/flutter_web_test/lib/src/matchers.dart
Normal file
File diff suppressed because it is too large
Load Diff
19
packages/flutter_web_test/lib/src/nonconst.dart
Normal file
19
packages/flutter_web_test/lib/src/nonconst.dart
Normal file
@ -0,0 +1,19 @@
|
||||
/// This function can be used to call a const constructor in such a way as to
|
||||
/// create a new instance rather than creating the common const instance.
|
||||
///
|
||||
/// ```dart
|
||||
/// class A {
|
||||
/// const A(this.i);
|
||||
/// int i;
|
||||
/// }
|
||||
///
|
||||
/// main () {
|
||||
/// // prevent prefer_const_constructors lint
|
||||
/// new A(nonconst(null));
|
||||
///
|
||||
/// // prevent prefer_const_declarations lint
|
||||
/// final int $null = nonconst(null);
|
||||
/// final A a = nonconst(const A(null));
|
||||
/// }
|
||||
/// ```
|
||||
T nonconst<T>(T t) => t;
|
36
packages/flutter_web_test/lib/src/stack_manipulation.dart
Normal file
36
packages/flutter_web_test/lib/src/stack_manipulation.dart
Normal file
@ -0,0 +1,36 @@
|
||||
// Synced 2019-05-30T14:20:57.809367.
|
||||
import 'package:flutter_web/foundation.dart';
|
||||
|
||||
// See also test_async_utils.dart which has some stack manipulation code.
|
||||
|
||||
/// Report call site for `expect()` call. Returns the number of frames that
|
||||
/// should be elided if a stack were to be modified to hide the expect call, or
|
||||
/// zero if no such call was found.
|
||||
///
|
||||
/// If the head of the stack trace consists of a failure as a result of calling
|
||||
/// the test_widgets [expect] function, this will fill the given StringBuffer
|
||||
/// with the precise file and line number that called that function.
|
||||
// TODO(yjbanov): this probably always returns 0 in DDC.
|
||||
int reportExpectCall(StackTrace stack, List<DiagnosticsNode> information) {
|
||||
final RegExp line0 = new RegExp(r'^#0 +fail \(.+\)$');
|
||||
final RegExp line1 = new RegExp(r'^#1 +_expect \(.+\)$');
|
||||
final RegExp line2 = new RegExp(r'^#2 +expect \(.+\)$');
|
||||
final RegExp line3 = new RegExp(r'^#3 +expect \(.+\)$');
|
||||
final RegExp line4 =
|
||||
new RegExp(r'^#4 +[^(]+ \((.+?):([0-9]+)(?::[0-9]+)?\)$');
|
||||
final List<String> stackLines = stack.toString().split('\n');
|
||||
if (line0.firstMatch(stackLines[0]) != null &&
|
||||
line1.firstMatch(stackLines[1]) != null &&
|
||||
line2.firstMatch(stackLines[2]) != null &&
|
||||
line3.firstMatch(stackLines[3]) != null) {
|
||||
final Match expectMatch = line4.firstMatch(stackLines[4]);
|
||||
assert(expectMatch != null);
|
||||
assert(expectMatch.groupCount == 2);
|
||||
information.add(DiagnosticsStackTrace.singleFrame(
|
||||
'This was caught by the test expectation on the following line',
|
||||
frame: '${expectMatch.group(1)} line ${expectMatch.group(2)}',
|
||||
));
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
362
packages/flutter_web_test/lib/src/test_async_utils.dart
Normal file
362
packages/flutter_web_test/lib/src/test_async_utils.dart
Normal file
@ -0,0 +1,362 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web/foundation.dart';
|
||||
|
||||
class _AsyncScope {
|
||||
_AsyncScope(this.creationStack, this.zone);
|
||||
final StackTrace creationStack;
|
||||
final Zone zone;
|
||||
}
|
||||
|
||||
/// Utility class for all the async APIs in the `flutter_test` library.
|
||||
///
|
||||
/// This class provides checking for asynchronous APIs, allowing the library to
|
||||
/// verify that all the asynchronous APIs are properly `await`ed before calling
|
||||
/// another.
|
||||
///
|
||||
/// For example, it prevents this kind of code:
|
||||
///
|
||||
/// ```dart
|
||||
/// tester.pump(); // forgot to call "await"!
|
||||
/// tester.pump();
|
||||
/// ```
|
||||
///
|
||||
/// ...by detecting, in the second call to `pump`, that it should actually be:
|
||||
///
|
||||
/// ```dart
|
||||
/// await tester.pump();
|
||||
/// await tester.pump();
|
||||
/// ```
|
||||
///
|
||||
/// It does this while still allowing nested calls, e.g. so that you can
|
||||
/// call [expect] from inside callbacks.
|
||||
///
|
||||
/// You can use this in your own test functions, if you have some asynchronous
|
||||
/// functions that must be used with "await". Wrap the contents of the function
|
||||
/// in a call to TestAsyncUtils.guard(), as follows:
|
||||
///
|
||||
/// ```dart
|
||||
/// Future<void> myTestFunction() => TestAsyncUtils.guard(() async {
|
||||
/// // ...
|
||||
/// });
|
||||
/// ```
|
||||
class TestAsyncUtils {
|
||||
TestAsyncUtils._();
|
||||
static const String _className = 'TestAsyncUtils';
|
||||
|
||||
static final List<_AsyncScope> _scopeStack = <_AsyncScope>[];
|
||||
|
||||
/// Calls the given callback in a new async scope. The callback argument is
|
||||
/// the asynchronous body of the calling method. The calling method is said to
|
||||
/// be "guarded". Nested calls to guarded methods from within the body of this
|
||||
/// one are fine, but calls to other guarded methods from outside the body of
|
||||
/// this one before this one has finished will throw an exception.
|
||||
///
|
||||
/// This method first calls [guardSync].
|
||||
static Future<T> guard<T>(Future<T> body()) {
|
||||
guardSync();
|
||||
final Zone zone = Zone.current.fork(zoneValues: <dynamic, dynamic>{
|
||||
_scopeStack: true // so we can recognize this as our own zone
|
||||
});
|
||||
final _AsyncScope scope = _AsyncScope(StackTrace.current, zone);
|
||||
_scopeStack.add(scope);
|
||||
final Future<T> result = scope.zone.run<Future<T>>(body);
|
||||
T resultValue; // This is set when the body of work completes with a result value.
|
||||
Future<T> completionHandler(dynamic error, StackTrace stack) {
|
||||
assert(_scopeStack.isNotEmpty);
|
||||
assert(_scopeStack.contains(scope));
|
||||
bool leaked = false;
|
||||
_AsyncScope closedScope;
|
||||
final StringBuffer message = StringBuffer();
|
||||
while (_scopeStack.isNotEmpty) {
|
||||
closedScope = _scopeStack.removeLast();
|
||||
if (closedScope == scope) break;
|
||||
if (!leaked) {
|
||||
message.writeln(
|
||||
'Asynchronous call to guarded function leaked. You must use "await" with all Future-returning test APIs.');
|
||||
leaked = true;
|
||||
}
|
||||
final _StackEntry originalGuarder =
|
||||
_findResponsibleMethod(closedScope.creationStack, 'guard', message);
|
||||
if (originalGuarder != null) {
|
||||
message.writeln('The test API method "${originalGuarder.methodName}" '
|
||||
'from class ${originalGuarder.className} '
|
||||
'was called from ${originalGuarder.callerFile} '
|
||||
'on line ${originalGuarder.callerLine}, '
|
||||
'but never completed before its parent scope closed.');
|
||||
}
|
||||
}
|
||||
if (leaked) {
|
||||
if (error != null) {
|
||||
message.writeln(
|
||||
'An uncaught exception may have caused the guarded function leak. The exception was:');
|
||||
message.writeln('$error');
|
||||
message
|
||||
.writeln('The stack trace associated with this exception was:');
|
||||
FlutterError.defaultStackFilter(
|
||||
stack.toString().trimRight().split('\n'))
|
||||
.forEach(message.writeln);
|
||||
}
|
||||
throw FlutterError(message.toString().trimRight());
|
||||
}
|
||||
if (error != null) return Future<T>.error(error, stack);
|
||||
return Future<T>.value(resultValue);
|
||||
}
|
||||
|
||||
return result.then<T>((T value) {
|
||||
resultValue = value;
|
||||
return completionHandler(null, null);
|
||||
}, onError: completionHandler);
|
||||
}
|
||||
|
||||
static Zone get _currentScopeZone {
|
||||
Zone zone = Zone.current;
|
||||
while (zone != null) {
|
||||
if (zone[_scopeStack] == true) return zone;
|
||||
zone = zone.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Verifies that there are no guarded methods currently pending (see [guard]).
|
||||
///
|
||||
/// If a guarded method is currently pending, and this is not a call nested
|
||||
/// from inside that method's body (directly or indirectly), then this method
|
||||
/// will throw a detailed exception.
|
||||
static void guardSync() {
|
||||
if (_scopeStack.isEmpty) {
|
||||
// No scopes open, so we must be fine.
|
||||
return;
|
||||
}
|
||||
// Find the current TestAsyncUtils scope zone so we can see if it's the one we expect.
|
||||
final Zone zone = _currentScopeZone;
|
||||
if (zone == _scopeStack.last.zone) {
|
||||
// We're still in the current scope zone. All good.
|
||||
return;
|
||||
}
|
||||
// If we get here, we know we've got a conflict on our hands.
|
||||
// We got an async barrier, but the current zone isn't the last scope that
|
||||
// we pushed on the stack.
|
||||
// Find which scope the conflict happened in, so that we know
|
||||
// which stack trace to report the conflict as starting from.
|
||||
//
|
||||
// For example, if we called an async method A, which ran its body in a
|
||||
// guarded block, and in its body it ran an async method B, which ran its
|
||||
// body in a guarded block, but we didn't await B, then in A's block we ran
|
||||
// an async method C, which ran its body in a guarded block, then we should
|
||||
// complain about the call to B then the call to C. BUT. If we called an async
|
||||
// method A, which ran its body in a guarded block, and in its body it ran
|
||||
// an async method B, which ran its body in a guarded block, but we didn't
|
||||
// await A, and then at the top level we called a method D, then we should
|
||||
// complain about the call to A then the call to D.
|
||||
//
|
||||
// In both examples, the scope stack would have two scopes. In the first
|
||||
// example, the current zone would be the zone of the _scopeStack[0] scope,
|
||||
// and we would want to show _scopeStack[1]'s creationStack. In the second
|
||||
// example, the current zone would not be in the _scopeStack, and we would
|
||||
// want to show _scopeStack[0]'s creationStack.
|
||||
int skipCount = 0;
|
||||
_AsyncScope candidateScope = _scopeStack.last;
|
||||
_AsyncScope scope;
|
||||
do {
|
||||
skipCount += 1;
|
||||
scope = candidateScope;
|
||||
if (skipCount >= _scopeStack.length) {
|
||||
if (zone == null) break;
|
||||
// Some people have reported reaching this point, but it's not clear
|
||||
// why. For now, just silently return.
|
||||
// TODO(ianh): If we ever get a test case that shows how we reach
|
||||
// this point, reduce it and report the error if there is one.
|
||||
return;
|
||||
}
|
||||
candidateScope = _scopeStack[_scopeStack.length - skipCount - 1];
|
||||
assert(candidateScope != null);
|
||||
assert(candidateScope.zone != null);
|
||||
} while (candidateScope.zone != zone);
|
||||
assert(scope != null);
|
||||
final StringBuffer message = StringBuffer();
|
||||
message.writeln(
|
||||
'Guarded function conflict. You must use "await" with all Future-returning test APIs.');
|
||||
final _StackEntry originalGuarder =
|
||||
_findResponsibleMethod(scope.creationStack, 'guard', message);
|
||||
final _StackEntry collidingGuarder =
|
||||
_findResponsibleMethod(StackTrace.current, 'guardSync', message);
|
||||
if (originalGuarder != null && collidingGuarder != null) {
|
||||
String originalName;
|
||||
if (originalGuarder.className == null) {
|
||||
originalName = '(${originalGuarder.methodName}) ';
|
||||
message.writeln('The guarded "${originalGuarder.methodName}" function '
|
||||
'was called from ${originalGuarder.callerFile} '
|
||||
'on line ${originalGuarder.callerLine}.');
|
||||
} else {
|
||||
originalName =
|
||||
'(${originalGuarder.className}.${originalGuarder.methodName}) ';
|
||||
message.writeln('The guarded method "${originalGuarder.methodName}" '
|
||||
'from class ${originalGuarder.className} '
|
||||
'was called from ${originalGuarder.callerFile} '
|
||||
'on line ${originalGuarder.callerLine}.');
|
||||
}
|
||||
final String again =
|
||||
(originalGuarder.callerFile == collidingGuarder.callerFile) &&
|
||||
(originalGuarder.callerLine == collidingGuarder.callerLine)
|
||||
? 'again '
|
||||
: '';
|
||||
String collidingName;
|
||||
if ((originalGuarder.className == collidingGuarder.className) &&
|
||||
(originalGuarder.methodName == collidingGuarder.methodName)) {
|
||||
originalName = '';
|
||||
collidingName = '';
|
||||
message.writeln('Then, it '
|
||||
'was called ${again}from ${collidingGuarder.callerFile} '
|
||||
'on line ${collidingGuarder.callerLine}.');
|
||||
} else if (collidingGuarder.className == null) {
|
||||
collidingName = '(${collidingGuarder.methodName}) ';
|
||||
message.writeln('Then, the "${collidingGuarder.methodName}" function '
|
||||
'was called ${again}from ${collidingGuarder.callerFile} '
|
||||
'on line ${collidingGuarder.callerLine}.');
|
||||
} else {
|
||||
collidingName =
|
||||
'(${collidingGuarder.className}.${collidingGuarder.methodName}) ';
|
||||
message.writeln('Then, the "${collidingGuarder.methodName}" method '
|
||||
'${originalGuarder.className == collidingGuarder.className ? "(also from class ${collidingGuarder.className})" : "from class ${collidingGuarder.className}"} '
|
||||
'was called ${again}from ${collidingGuarder.callerFile} '
|
||||
'on line ${collidingGuarder.callerLine}.');
|
||||
}
|
||||
message.writeln(
|
||||
'The first ${originalGuarder.className == null ? "function" : "method"} $originalName'
|
||||
'had not yet finished executing at the time that '
|
||||
'the second ${collidingGuarder.className == null ? "function" : "method"} $collidingName'
|
||||
'was called. Since both are guarded, and the second was not a nested call inside the first, the '
|
||||
'first must complete its execution before the second can be called. Typically, this is achieved by '
|
||||
'putting an "await" statement in front of the call to the first.');
|
||||
if (collidingGuarder.className == null &&
|
||||
collidingGuarder.methodName == 'expect') {
|
||||
message.writeln(
|
||||
'If you are confident that all test APIs are being called using "await", and '
|
||||
'this expect() call is not being called at the top level but is itself being '
|
||||
'called from some sort of callback registered before the ${originalGuarder.methodName} '
|
||||
'method was called, then consider using expectSync() instead.');
|
||||
}
|
||||
message.writeln('\n'
|
||||
'When the first ${originalGuarder.className == null ? "function" : "method"} '
|
||||
'$originalName'
|
||||
'was called, this was the stack:');
|
||||
message.writeln(FlutterError.defaultStackFilter(
|
||||
scope.creationStack.toString().trimRight().split('\n'))
|
||||
.join('\n'));
|
||||
}
|
||||
throw FlutterError(message.toString().trimRight());
|
||||
}
|
||||
|
||||
/// Verifies that there are no guarded methods currently pending (see [guard]).
|
||||
///
|
||||
/// This is used at the end of tests to ensure that nothing leaks out of the test.
|
||||
static void verifyAllScopesClosed() {
|
||||
if (_scopeStack.isNotEmpty) {
|
||||
final StringBuffer message = StringBuffer();
|
||||
message.writeln(
|
||||
'Asynchronous call to guarded function leaked. You must use "await" with all Future-returning test APIs.');
|
||||
for (_AsyncScope scope in _scopeStack) {
|
||||
final _StackEntry guarder =
|
||||
_findResponsibleMethod(scope.creationStack, 'guard', message);
|
||||
if (guarder != null) {
|
||||
message.writeln('The guarded method "${guarder.methodName}" '
|
||||
'${guarder.className != null ? "from class ${guarder.className} " : ""}'
|
||||
'was called from ${guarder.callerFile} '
|
||||
'on line ${guarder.callerLine}, '
|
||||
'but never completed before its parent scope closed.');
|
||||
}
|
||||
}
|
||||
throw FlutterError(message.toString().trimRight());
|
||||
}
|
||||
}
|
||||
|
||||
static bool _stripAsynchronousSuspensions(String line) {
|
||||
return line != '<asynchronous suspension>';
|
||||
}
|
||||
|
||||
static _StackEntry _findResponsibleMethod(
|
||||
StackTrace rawStack, String method, StringBuffer errors) {
|
||||
assert(method == 'guard' || method == 'guardSync');
|
||||
final List<String> stack = rawStack
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where(_stripAsynchronousSuspensions)
|
||||
.toList();
|
||||
assert(stack.last == '');
|
||||
stack.removeLast();
|
||||
final RegExp getClassPattern = RegExp(r'^#[0-9]+ +([^. ]+)');
|
||||
Match lineMatch;
|
||||
int index = -1;
|
||||
do {
|
||||
// skip past frames that are from this class
|
||||
index += 1;
|
||||
assert(index < stack.length);
|
||||
lineMatch = getClassPattern.matchAsPrefix(stack[index]);
|
||||
assert(lineMatch != null);
|
||||
assert(lineMatch.groupCount == 1);
|
||||
} while (lineMatch.group(1) == _className);
|
||||
// try to parse the stack to find the interesting frame
|
||||
if (index < stack.length) {
|
||||
final RegExp guardPattern = RegExp(r'^#[0-9]+ +(?:([^. ]+)\.)?([^. ]+)');
|
||||
final Match guardMatch = guardPattern
|
||||
.matchAsPrefix(stack[index]); // find the class that called us
|
||||
if (guardMatch != null) {
|
||||
assert(guardMatch.groupCount == 2);
|
||||
final String guardClass = guardMatch.group(1); // might be null
|
||||
final String guardMethod = guardMatch.group(2);
|
||||
while (index < stack.length) {
|
||||
// find the last stack frame that called the class that called us
|
||||
lineMatch = getClassPattern.matchAsPrefix(stack[index]);
|
||||
if (lineMatch != null) {
|
||||
assert(lineMatch.groupCount == 1);
|
||||
if (lineMatch.group(1) == (guardClass ?? guardMethod)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (index < stack.length) {
|
||||
final RegExp callerPattern =
|
||||
RegExp(r'^#[0-9]+ .* \((.+?):([0-9]+)(?::[0-9]+)?\)$');
|
||||
final Match callerMatch = callerPattern
|
||||
.matchAsPrefix(stack[index]); // extract the caller's info
|
||||
if (callerMatch != null) {
|
||||
assert(callerMatch.groupCount == 2);
|
||||
final String callerFile = callerMatch.group(1);
|
||||
final String callerLine = callerMatch.group(2);
|
||||
return _StackEntry(guardClass, guardMethod, callerFile, callerLine);
|
||||
} else {
|
||||
// One reason you might get here is if the guarding method was called directly from
|
||||
// a 'dart:' API, like from the Future/microtask mechanism, because dart: URLs in the
|
||||
// stack trace don't have a column number and so don't match the regexp above.
|
||||
errors.writeln(
|
||||
'(Unable to parse the stack frame of the method that called the method that called $_className.$method(). The stack may be incomplete or bogus.)');
|
||||
errors.writeln('${stack[index]}');
|
||||
}
|
||||
} else {
|
||||
errors.writeln(
|
||||
'(Unable to find the stack frame of the method that called the method that called $_className.$method(). The stack may be incomplete or bogus.)');
|
||||
}
|
||||
} else {
|
||||
errors.writeln(
|
||||
'(Unable to parse the stack frame of the method that called $_className.$method(). The stack may be incomplete or bogus.)');
|
||||
errors.writeln('${stack[index]}');
|
||||
}
|
||||
} else {
|
||||
errors.writeln(
|
||||
'(Unable to find the method that called $_className.$method(). The stack may be incomplete or bogus.)');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class _StackEntry {
|
||||
const _StackEntry(
|
||||
this.className, this.methodName, this.callerFile, this.callerLine);
|
||||
final String className;
|
||||
final String methodName;
|
||||
final String callerFile;
|
||||
final String callerLine;
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
import 'package:flutter_web/foundation.dart';
|
||||
import 'package:stack_trace/stack_trace.dart' as stack_trace;
|
||||
import 'package:test/test.dart' as test_package;
|
||||
|
||||
/// Signature for the [reportTestException] callback.
|
||||
typedef void TestExceptionReporter(
|
||||
FlutterErrorDetails details, String testDescription);
|
||||
|
||||
/// A function that is called by the test framework when an unexpected error
|
||||
/// occurred during a test.
|
||||
///
|
||||
/// This function is responsible for reporting the error to the user such that
|
||||
/// the user can easily diagnose what failed when inspecting the test results.
|
||||
/// It is also responsible for reporting the error to the test framework itself
|
||||
/// in order to cause the test to fail.
|
||||
///
|
||||
/// This function is pluggable to handle the cases where tests are run in
|
||||
/// contexts _other_ than via `flutter test`.
|
||||
TestExceptionReporter get reportTestException => _reportTestException;
|
||||
TestExceptionReporter _reportTestException = _defaultTestExceptionReporter;
|
||||
set reportTestException(TestExceptionReporter handler) {
|
||||
assert(handler != null);
|
||||
_reportTestException = handler;
|
||||
}
|
||||
|
||||
void _defaultTestExceptionReporter(
|
||||
FlutterErrorDetails errorDetails, String testDescription) {
|
||||
FlutterError.dumpErrorToConsole(errorDetails, forceReport: true);
|
||||
// test_package.registerException actually just calls the current zone's error
|
||||
// handler (that is to say, _parentZone's handleUncaughtError function).
|
||||
// FakeAsync doesn't add one of those, but the test package does, that's how
|
||||
// the test package tracks errors. So really we could get the same effect here
|
||||
// by calling that error handler directly or indeed just throwing. However, we
|
||||
// call registerException because that's the semantically correct thing...
|
||||
String additional = '';
|
||||
if (testDescription.isNotEmpty)
|
||||
additional = '\nThe test description was: $testDescription';
|
||||
test_package.registerException(
|
||||
'Test failed. See exception logs above.$additional', _emptyStackTrace);
|
||||
}
|
||||
|
||||
final StackTrace _emptyStackTrace =
|
||||
new stack_trace.Chain(const <stack_trace.Trace>[]);
|
444
packages/flutter_web_test/lib/src/test_pointer.dart
Normal file
444
packages/flutter_web_test/lib/src/test_pointer.dart
Normal file
@ -0,0 +1,444 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
// Synced 2019-05-30T14:20:57.816491.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web/foundation.dart';
|
||||
import 'package:flutter_web/gestures.dart';
|
||||
|
||||
import 'test_async_utils.dart';
|
||||
|
||||
export 'package:flutter_web_ui/ui.dart' show Offset;
|
||||
|
||||
/// A class for generating coherent artificial pointer events.
|
||||
///
|
||||
/// You can use this to manually simulate individual events, but the simplest
|
||||
/// way to generate coherent gestures is to use [TestGesture].
|
||||
class TestPointer {
|
||||
/// Creates a [TestPointer]. By default, the pointer identifier used is 1,
|
||||
/// however this can be overridden by providing an argument to the
|
||||
/// constructor.
|
||||
///
|
||||
/// Multiple [TestPointer]s created with the same pointer identifier will
|
||||
/// interfere with each other if they are used in parallel.
|
||||
TestPointer([
|
||||
this.pointer = 1,
|
||||
this.kind = PointerDeviceKind.touch,
|
||||
this._device,
|
||||
int buttons = kPrimaryButton,
|
||||
]) : assert(kind != null),
|
||||
assert(pointer != null),
|
||||
assert(buttons != null),
|
||||
_buttons = buttons {
|
||||
switch (kind) {
|
||||
case PointerDeviceKind.mouse:
|
||||
_device ??= 1;
|
||||
break;
|
||||
case PointerDeviceKind.stylus:
|
||||
case PointerDeviceKind.invertedStylus:
|
||||
case PointerDeviceKind.touch:
|
||||
case PointerDeviceKind.unknown:
|
||||
_device ??= 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// The device identifier used for events generated by this object.
|
||||
///
|
||||
/// Set when the object is constructed. Defaults to 1 if the [kind] is
|
||||
/// [PointerDeviceKind.mouse], and 0 otherwise.
|
||||
int get device => _device;
|
||||
int _device;
|
||||
|
||||
/// The pointer identifier used for events generated by this object.
|
||||
///
|
||||
/// Set when the object is constructed. Defaults to 1.
|
||||
final int pointer;
|
||||
|
||||
/// The kind of pointer device to simulate. Defaults to
|
||||
/// [PointerDeviceKind.touch].
|
||||
final PointerDeviceKind kind;
|
||||
|
||||
/// The kind of buttons to simulate on Down and Move events. Defaults to
|
||||
/// [kPrimaryButton].
|
||||
int get buttons => _buttons;
|
||||
int _buttons;
|
||||
|
||||
/// Whether the pointer simulated by this object is currently down.
|
||||
///
|
||||
/// A pointer is released (goes up) by calling [up] or [cancel].
|
||||
///
|
||||
/// Once a pointer is released, it can no longer generate events.
|
||||
bool get isDown => _isDown;
|
||||
bool _isDown = false;
|
||||
|
||||
/// The position of the last event sent by this object.
|
||||
///
|
||||
/// If no event has ever been sent by this object, returns null.
|
||||
Offset get location => _location;
|
||||
Offset _location;
|
||||
|
||||
/// If a custom event is created outside of this class, this function is used
|
||||
/// to set the [isDown].
|
||||
bool setDownInfo(
|
||||
PointerEvent event,
|
||||
Offset newLocation, {
|
||||
int buttons,
|
||||
}) {
|
||||
_location = newLocation;
|
||||
if (buttons != null) _buttons = buttons;
|
||||
switch (event.runtimeType) {
|
||||
case PointerDownEvent:
|
||||
assert(!isDown);
|
||||
_isDown = true;
|
||||
break;
|
||||
case PointerUpEvent:
|
||||
case PointerCancelEvent:
|
||||
assert(isDown);
|
||||
_isDown = false;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return isDown;
|
||||
}
|
||||
|
||||
/// Create a [PointerDownEvent] at the given location.
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
///
|
||||
/// By default, the set of buttons in the last down or move event is used.
|
||||
/// You can give a specific set of buttons by passing the `buttons` argument.
|
||||
PointerDownEvent down(
|
||||
Offset newLocation, {
|
||||
Duration timeStamp = Duration.zero,
|
||||
int buttons,
|
||||
}) {
|
||||
assert(!isDown);
|
||||
_isDown = true;
|
||||
_location = newLocation;
|
||||
if (buttons != null) _buttons = buttons;
|
||||
return PointerDownEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
pointer: pointer,
|
||||
position: location,
|
||||
buttons: _buttons,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerMoveEvent] to the given location.
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
///
|
||||
/// [isDown] must be true when this is called, since move events can only
|
||||
/// be generated when the pointer is down.
|
||||
///
|
||||
/// By default, the set of buttons in the last down or move event is used.
|
||||
/// You can give a specific set of buttons by passing the `buttons` argument.
|
||||
PointerMoveEvent move(
|
||||
Offset newLocation, {
|
||||
Duration timeStamp = Duration.zero,
|
||||
int buttons,
|
||||
}) {
|
||||
assert(
|
||||
isDown,
|
||||
'Move events can only be generated when the pointer is down. To '
|
||||
'create a movement event simulating a pointer move when the pointer is '
|
||||
'up, use hover() instead.');
|
||||
final Offset delta = newLocation - location;
|
||||
_location = newLocation;
|
||||
if (buttons != null) _buttons = buttons;
|
||||
return PointerMoveEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
pointer: pointer,
|
||||
position: newLocation,
|
||||
delta: delta,
|
||||
buttons: _buttons,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerUpEvent].
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
///
|
||||
/// The object is no longer usable after this method has been called.
|
||||
PointerUpEvent up({Duration timeStamp = Duration.zero}) {
|
||||
assert(isDown);
|
||||
_isDown = false;
|
||||
return PointerUpEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
pointer: pointer,
|
||||
position: location,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerCancelEvent].
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
///
|
||||
/// The object is no longer usable after this method has been called.
|
||||
PointerCancelEvent cancel({Duration timeStamp = Duration.zero}) {
|
||||
assert(isDown);
|
||||
_isDown = false;
|
||||
return PointerCancelEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
pointer: pointer,
|
||||
position: location,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerAddedEvent] with the [PointerDeviceKind] the pointer was
|
||||
/// created with.
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
///
|
||||
/// [isDown] must be false, since hover events can't be sent when the pointer
|
||||
/// is up.
|
||||
PointerAddedEvent addPointer({
|
||||
Duration timeStamp = Duration.zero,
|
||||
}) {
|
||||
assert(timeStamp != null);
|
||||
return PointerAddedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerRemovedEvent] with the kind the pointer was created with.
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
///
|
||||
/// [isDown] must be false, since hover events can't be sent when the pointer
|
||||
/// is up.
|
||||
PointerRemovedEvent removePointer({
|
||||
Duration timeStamp = Duration.zero,
|
||||
}) {
|
||||
assert(timeStamp != null);
|
||||
return PointerRemovedEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerHoverEvent] to the given location.
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
///
|
||||
/// [isDown] must be false, since hover events can't be sent when the pointer
|
||||
/// is up.
|
||||
PointerHoverEvent hover(
|
||||
Offset newLocation, {
|
||||
Duration timeStamp = Duration.zero,
|
||||
}) {
|
||||
assert(newLocation != null);
|
||||
assert(timeStamp != null);
|
||||
assert(
|
||||
!isDown,
|
||||
'Hover events can only be generated when the pointer is up. To '
|
||||
'simulate movement when the pointer is down, use move() instead.');
|
||||
assert(kind != PointerDeviceKind.touch,
|
||||
"Touch pointers can't generate hover events");
|
||||
final Offset delta =
|
||||
location != null ? newLocation - location : Offset.zero;
|
||||
_location = newLocation;
|
||||
return PointerHoverEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
position: newLocation,
|
||||
delta: delta,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a [PointerScrollEvent] (e.g., scroll wheel scroll; not finger-drag
|
||||
/// scroll) with the given delta.
|
||||
///
|
||||
/// By default, the time stamp on the event is [Duration.zero]. You can give a
|
||||
/// specific time stamp by passing the `timeStamp` argument.
|
||||
PointerScrollEvent scroll(
|
||||
Offset scrollDelta, {
|
||||
Duration timeStamp = Duration.zero,
|
||||
}) {
|
||||
assert(scrollDelta != null);
|
||||
assert(timeStamp != null);
|
||||
assert(kind != PointerDeviceKind.touch,
|
||||
"Touch pointers can't generate pointer signal events");
|
||||
return PointerScrollEvent(
|
||||
timeStamp: timeStamp,
|
||||
kind: kind,
|
||||
device: _device,
|
||||
position: location,
|
||||
scrollDelta: scrollDelta,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Signature for a callback that can dispatch events and returns a future that
|
||||
/// completes when the event dispatch is complete.
|
||||
typedef EventDispatcher = Future<void> Function(
|
||||
PointerEvent event, HitTestResult result);
|
||||
|
||||
/// Signature for callbacks that perform hit-testing at a given location.
|
||||
typedef HitTester = HitTestResult Function(Offset location);
|
||||
|
||||
/// A class for performing gestures in tests.
|
||||
///
|
||||
/// The simplest way to create a [TestGesture] is to call
|
||||
/// [WidgetTester.startGesture].
|
||||
class TestGesture {
|
||||
/// Create a [TestGesture] without dispatching any events from it.
|
||||
/// The [TestGesture] can then be manipulated to perform future actions.
|
||||
///
|
||||
/// By default, the pointer identifier used is 1. This can be overridden by
|
||||
/// providing the `pointer` argument.
|
||||
///
|
||||
/// A function to use for hit testing must be provided via the `hitTester`
|
||||
/// argument, and a function to use for dispatching events must be provided
|
||||
/// via the `dispatcher` argument.
|
||||
///
|
||||
/// The device `kind` defaults to [PointerDeviceKind.touch], but move events
|
||||
/// when the pointer is "up" require a kind other than
|
||||
/// [PointerDeviceKind.touch], like [PointerDeviceKind.mouse], for example,
|
||||
/// because touch devices can't produce movement events when they are "up".
|
||||
///
|
||||
/// None of the arguments may be null. The `dispatcher` and `hitTester`
|
||||
/// arguments are required.
|
||||
TestGesture({
|
||||
@required EventDispatcher dispatcher,
|
||||
@required HitTester hitTester,
|
||||
int pointer = 1,
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
int device,
|
||||
int buttons = kPrimaryButton,
|
||||
}) : assert(dispatcher != null),
|
||||
assert(hitTester != null),
|
||||
assert(pointer != null),
|
||||
assert(kind != null),
|
||||
assert(buttons != null),
|
||||
_dispatcher = dispatcher,
|
||||
_hitTester = hitTester,
|
||||
_pointer = TestPointer(pointer, kind, device, buttons),
|
||||
_result = null;
|
||||
|
||||
/// Dispatch a pointer down event at the given `downLocation`, caching the
|
||||
/// hit test result.
|
||||
Future<void> down(Offset downLocation) async {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
_result = _hitTester(downLocation);
|
||||
return _dispatcher(_pointer.down(downLocation), _result);
|
||||
});
|
||||
}
|
||||
|
||||
/// Dispatch a pointer down event at the given `downLocation`, caching the
|
||||
/// hit test result with a custom down event.
|
||||
Future<void> downWithCustomEvent(
|
||||
Offset downLocation, PointerDownEvent event) async {
|
||||
_pointer.setDownInfo(event, downLocation);
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
_result = _hitTester(downLocation);
|
||||
return _dispatcher(event, _result);
|
||||
});
|
||||
}
|
||||
|
||||
final EventDispatcher _dispatcher;
|
||||
final HitTester _hitTester;
|
||||
final TestPointer _pointer;
|
||||
HitTestResult _result;
|
||||
|
||||
/// In a test, send a move event that moves the pointer by the given offset.
|
||||
@visibleForTesting
|
||||
Future<void> updateWithCustomEvent(PointerEvent event,
|
||||
{Duration timeStamp = Duration.zero}) {
|
||||
_pointer.setDownInfo(event, event.position);
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
return _dispatcher(event, _result);
|
||||
});
|
||||
}
|
||||
|
||||
/// In a test, send a pointer add event for this pointer.
|
||||
Future<void> addPointer({Duration timeStamp = Duration.zero}) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
return _dispatcher(_pointer.addPointer(timeStamp: timeStamp), null);
|
||||
});
|
||||
}
|
||||
|
||||
/// In a test, send a pointer remove event for this pointer.
|
||||
Future<void> removePointer({Duration timeStamp = Duration.zero}) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
return _dispatcher(_pointer.removePointer(timeStamp: timeStamp), null);
|
||||
});
|
||||
}
|
||||
|
||||
/// Send a move event moving the pointer by the given offset.
|
||||
///
|
||||
/// If the pointer is down, then a move event is dispatched. If the pointer is
|
||||
/// up, then a hover event is dispatched. Touch devices are not able to send
|
||||
/// hover events.
|
||||
Future<void> moveBy(Offset offset, {Duration timeStamp = Duration.zero}) {
|
||||
return moveTo(_pointer.location + offset, timeStamp: timeStamp);
|
||||
}
|
||||
|
||||
/// Send a move event moving the pointer to the given location.
|
||||
///
|
||||
/// If the pointer is down, then a move event is dispatched. If the pointer is
|
||||
/// up, then a hover event is dispatched. Touch devices are not able to send
|
||||
/// hover events.
|
||||
Future<void> moveTo(Offset location, {Duration timeStamp = Duration.zero}) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
if (_pointer._isDown) {
|
||||
assert(
|
||||
_result != null,
|
||||
'Move events with the pointer down must be preceeded by a down '
|
||||
'event that captures a hit test result.');
|
||||
return _dispatcher(
|
||||
_pointer.move(location, timeStamp: timeStamp), _result);
|
||||
} else {
|
||||
assert(_pointer.kind != PointerDeviceKind.touch,
|
||||
'Touch device move events can only be sent if the pointer is down.');
|
||||
return _dispatcher(
|
||||
_pointer.hover(location, timeStamp: timeStamp), null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// End the gesture by releasing the pointer.
|
||||
Future<void> up() {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
assert(_pointer._isDown);
|
||||
await _dispatcher(_pointer.up(), _result);
|
||||
assert(!_pointer._isDown);
|
||||
_result = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// End the gesture by canceling the pointer (as would happen if the
|
||||
/// system showed a modal dialog on top of the Flutter application,
|
||||
/// for instance).
|
||||
Future<void> cancel() {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
assert(_pointer._isDown);
|
||||
await _dispatcher(_pointer.cancel(), _result);
|
||||
assert(!_pointer._isDown);
|
||||
_result = null;
|
||||
});
|
||||
}
|
||||
}
|
174
packages/flutter_web_test/lib/src/test_text_input.dart
Normal file
174
packages/flutter_web_test/lib/src/test_text_input.dart
Normal file
@ -0,0 +1,174 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:flutter_web/services.dart';
|
||||
|
||||
import 'test_async_utils.dart';
|
||||
import 'widget_tester.dart';
|
||||
|
||||
export 'package:flutter_web/services.dart'
|
||||
show TextEditingValue, TextInputAction;
|
||||
|
||||
/// A testing stub for the system's onscreen keyboard.
|
||||
///
|
||||
/// Typical app tests will not need to use this class directly.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WidgetTester.enterText], which uses this class to simulate keyboard
|
||||
/// input.
|
||||
/// * [WidgetTester.showKeyboard], which uses this class to simulate showing the
|
||||
/// popup keyboard and initializing its text.
|
||||
class TestTextInput {
|
||||
/// Create a fake keyboard backend.
|
||||
///
|
||||
/// The [onCleared] argument may be set to be notified of when the keyboard
|
||||
/// is dismissed.
|
||||
TestTextInput({this.onCleared});
|
||||
|
||||
/// Called when the keyboard goes away.
|
||||
///
|
||||
/// To use the methods on this API that send fake keyboard messages (such as
|
||||
/// [updateEditingValue], [enterText], or [receiveAction]), the keyboard must
|
||||
/// first be requested, e.g. using [WidgetTester.showKeyboard].
|
||||
final VoidCallback onCleared;
|
||||
|
||||
int _client = 0;
|
||||
|
||||
/// Arguments supplied to the TextInput.setClient method call.
|
||||
Map<String, dynamic> setClientArgs;
|
||||
|
||||
/// The last set of arguments that [TextInputConnection.setEditingState] sent
|
||||
/// to the embedder.
|
||||
///
|
||||
/// This is a map representation of a [TextEditingValue] object. For example,
|
||||
/// it will have a `text` entry whose value matches the most recent
|
||||
/// [TextEditingValue.text] that was sent to the embedder.
|
||||
Map<String, dynamic> editingState;
|
||||
|
||||
Future<dynamic> _handleTextInputCall(MethodCall methodCall) async {
|
||||
switch (methodCall.method) {
|
||||
case 'TextInput.setClient':
|
||||
_client = methodCall.arguments[0];
|
||||
setClientArgs = methodCall.arguments[1];
|
||||
break;
|
||||
case 'TextInput.clearClient':
|
||||
_client = 0;
|
||||
_isVisible = false;
|
||||
if (onCleared != null) onCleared();
|
||||
break;
|
||||
case 'TextInput.setEditingState':
|
||||
editingState = methodCall.arguments;
|
||||
break;
|
||||
case 'TextInput.show':
|
||||
_isVisible = true;
|
||||
break;
|
||||
case 'TextInput.hide':
|
||||
_isVisible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the onscreen keyboard is visible to the user.
|
||||
bool get isVisible => _isVisible;
|
||||
bool _isVisible = false;
|
||||
|
||||
/// Simulates the user changing the [TextEditingValue] to the given value.
|
||||
void updateEditingValue(TextEditingValue value) {
|
||||
// Not using the `expect` function because in the case of a FlutterDriver
|
||||
// test this code does not run in a package:test test zone.
|
||||
if (_client == 0)
|
||||
throw TestFailure(
|
||||
'Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
|
||||
BinaryMessages.handlePlatformMessage(
|
||||
SystemChannels.textInput.name,
|
||||
SystemChannels.textInput.codec.encodeMethodCall(
|
||||
MethodCall(
|
||||
'TextInputClient.updateEditingState',
|
||||
<dynamic>[_client, value.toJSON()],
|
||||
),
|
||||
),
|
||||
(ByteData data) {/* response from framework is discarded */},
|
||||
);
|
||||
}
|
||||
|
||||
/// Simulates the user typing the given text.
|
||||
void enterText(String text) {
|
||||
updateEditingValue(TextEditingValue(
|
||||
text: text,
|
||||
));
|
||||
}
|
||||
|
||||
/// Simulates the user pressing one of the [TextInputAction] buttons.
|
||||
/// Does not check that the [TextInputAction] performed is an acceptable one
|
||||
/// based on the `inputAction` [setClientArgs].
|
||||
Future<void> receiveAction(TextInputAction action) async {
|
||||
return TestAsyncUtils.guard(() {
|
||||
// Not using the `expect` function because in the case of a FlutterDriver
|
||||
// test this code does not run in a package:test test zone.
|
||||
if (_client == 0) {
|
||||
throw TestFailure(
|
||||
'Tried to use TestTextInput with no keyboard attached. You must use WidgetTester.showKeyboard() first.');
|
||||
}
|
||||
|
||||
final Completer<Null> completer = Completer<Null>();
|
||||
|
||||
BinaryMessages.handlePlatformMessage(
|
||||
SystemChannels.textInput.name,
|
||||
SystemChannels.textInput.codec.encodeMethodCall(
|
||||
MethodCall(
|
||||
'TextInputClient.performAction',
|
||||
<dynamic>[_client, action.toString()],
|
||||
),
|
||||
),
|
||||
(ByteData data) {
|
||||
try {
|
||||
// Decoding throws a PlatformException if the data represents an
|
||||
// error, and that's all we care about here.
|
||||
SystemChannels.textInput.codec.decodeEnvelope(data);
|
||||
|
||||
// No error was found. Complete without issue.
|
||||
completer.complete();
|
||||
} catch (error) {
|
||||
// An exception occurred as a result of receiveAction()'ing. Report
|
||||
// that error.
|
||||
completer.completeError(error);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
});
|
||||
}
|
||||
|
||||
/// Installs this object as a mock handler for [SystemChannels.textInput].
|
||||
void register() {
|
||||
SystemChannels.textInput.setMockMethodCallHandler(_handleTextInputCall);
|
||||
_isRegistered = true;
|
||||
}
|
||||
|
||||
/// Removes this object as a mock handler for [SystemChannels.textInput].
|
||||
///
|
||||
/// After calling this method, the channel will exchange messages with the
|
||||
/// Flutter engine. Use this with [FlutterDriver] tests that need to display
|
||||
/// on-screen keyboard provided by the operating system.
|
||||
void unregister() {
|
||||
// TODO(yjbanov): implement text editing via channels
|
||||
//SystemChannels.textInput.setMockMethodCallHandler(null);
|
||||
_isRegistered = false;
|
||||
}
|
||||
|
||||
/// Whether this [TestTextInput] is registered with
|
||||
/// [SystemChannels.textInput].
|
||||
///
|
||||
/// Use [register] and [unregister] methods to control this value.
|
||||
bool get isRegistered => _isRegistered;
|
||||
bool _isRegistered = false;
|
||||
|
||||
/// Simulates the user hiding the onscreen keyboard.
|
||||
void hide() {
|
||||
_isVisible = false;
|
||||
}
|
||||
}
|
737
packages/flutter_web_test/lib/src/widget_tester.dart
Normal file
737
packages/flutter_web_test/lib/src/widget_tester.dart
Normal file
@ -0,0 +1,737 @@
|
||||
// Synced 2019-05-30T14:20:57.821452.
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_web/gestures.dart';
|
||||
import 'package:flutter_web/material.dart';
|
||||
import 'package:flutter_web/rendering.dart';
|
||||
import 'package:flutter_web/scheduler.dart';
|
||||
import 'package:flutter_web_ui/ui.dart';
|
||||
import 'package:flutter_web/widgets.dart';
|
||||
import 'package:flutter_web/src/util.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:test/test.dart' as test_package;
|
||||
|
||||
import 'all_elements.dart';
|
||||
import 'binding.dart';
|
||||
import 'controller.dart';
|
||||
import 'finders.dart';
|
||||
import 'matchers.dart';
|
||||
import 'test_async_utils.dart';
|
||||
import 'test_text_input.dart';
|
||||
|
||||
/// Keep users from needing multiple imports to test semantics.
|
||||
export 'package:flutter_web/rendering.dart' show SemanticsHandle;
|
||||
|
||||
export 'package:test/test.dart'
|
||||
hide
|
||||
expect, // we have our own wrapper below
|
||||
TypeMatcher, // matcher's TypeMatcher conflicts with the one in the Flutter framework
|
||||
isInstanceOf; // we have our own wrapper in matchers.dart
|
||||
|
||||
/// Signature for callback to [testWidgets] and [benchmarkWidgets].
|
||||
typedef WidgetTesterCallback = Future<void> Function(WidgetTester widgetTester);
|
||||
|
||||
/// Runs the [callback] inside the Flutter test environment.
|
||||
///
|
||||
/// Use this function for testing custom [StatelessWidget]s and
|
||||
/// [StatefulWidget]s.
|
||||
///
|
||||
/// The callback can be asynchronous (using `async`/`await` or
|
||||
/// using explicit [Future]s).
|
||||
///
|
||||
/// This function uses the [test] function in the test package to
|
||||
/// register the given callback as a test. The callback, when run,
|
||||
/// will be given a new instance of [WidgetTester]. The [find] object
|
||||
/// provides convenient widget [Finder]s for use with the
|
||||
/// [WidgetTester].
|
||||
///
|
||||
/// ## Sample code
|
||||
///
|
||||
/// ```dart
|
||||
/// testWidgets('MyWidget', (WidgetTester tester) async {
|
||||
/// await tester.pumpWidget(new MyWidget());
|
||||
/// await tester.tap(find.text('Save'));
|
||||
/// expect(find.text('Success'), findsOneWidget);
|
||||
/// });
|
||||
/// ```
|
||||
@isTest
|
||||
void testWidgets(
|
||||
String description,
|
||||
WidgetTesterCallback callback, {
|
||||
bool skip = false,
|
||||
test_package.Timeout timeout,
|
||||
bool semanticsEnabled = false,
|
||||
}) {
|
||||
debugIsInTest = true;
|
||||
|
||||
final Future<void> webEngineInitialization =
|
||||
webOnlyInitializeTestDomRenderer();
|
||||
|
||||
final TestWidgetsFlutterBinding binding =
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
final WidgetTester tester = WidgetTester._(binding);
|
||||
timeout ??= binding.defaultTestTimeout;
|
||||
test_package.test(description, () async {
|
||||
await webEngineInitialization;
|
||||
SemanticsHandle semanticsHandle;
|
||||
if (semanticsEnabled == true) {
|
||||
semanticsHandle = tester.ensureSemantics();
|
||||
}
|
||||
tester._recordNumberOfSemanticsHandles();
|
||||
test_package.addTearDown(binding.postTest);
|
||||
return binding.runTest(
|
||||
() async {
|
||||
await callback(tester);
|
||||
semanticsHandle?.dispose();
|
||||
},
|
||||
tester._endOfTestVerifications,
|
||||
description: description ?? '',
|
||||
);
|
||||
}, skip: skip, timeout: timeout);
|
||||
}
|
||||
|
||||
/// Runs the [callback] inside the Flutter benchmark environment.
|
||||
///
|
||||
/// Use this function for benchmarking custom [StatelessWidget]s and
|
||||
/// [StatefulWidget]s when you want to be able to use features from
|
||||
/// [TestWidgetsFlutterBinding]. The callback, when run, will be given
|
||||
/// a new instance of [WidgetTester]. The [find] object provides
|
||||
/// convenient widget [Finder]s for use with the [WidgetTester].
|
||||
///
|
||||
/// The callback can be asynchronous (using `async`/`await` or using
|
||||
/// explicit [Future]s). If it is, then [benchmarkWidgets] will return
|
||||
/// a [Future] that completes when the callback's does. Otherwise, it
|
||||
/// will return a Future that is always complete.
|
||||
///
|
||||
/// If the callback is asynchronous, make sure you `await` the call
|
||||
/// to [benchmarkWidgets], otherwise it won't run!
|
||||
///
|
||||
/// Benchmarks must not be run in checked mode. To avoid this, this
|
||||
/// function will print a big message if it is run in checked mode.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// main() async {
|
||||
/// assert(false); // fail in checked mode
|
||||
/// await benchmarkWidgets((WidgetTester tester) async {
|
||||
/// await tester.pumpWidget(new MyWidget());
|
||||
/// final Stopwatch timer = new Stopwatch()..start();
|
||||
/// for (int index = 0; index < 10000; index += 1) {
|
||||
/// await tester.tap(find.text('Tap me'));
|
||||
/// await tester.pump();
|
||||
/// }
|
||||
/// timer.stop();
|
||||
/// debugPrint('Time taken: ${timer.elapsedMilliseconds}ms');
|
||||
/// });
|
||||
/// exit(0);
|
||||
/// }
|
||||
Future<void> benchmarkWidgets(WidgetTesterCallback callback) {
|
||||
assert(() {
|
||||
print('┏╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┓');
|
||||
print('┇ ⚠ THIS BENCHMARK IS BEING RUN WITH ASSERTS ENABLED ⚠ ┇');
|
||||
print('┡╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍╍┦');
|
||||
print('│ │');
|
||||
print('│ Numbers obtained from a benchmark while asserts are │');
|
||||
print('│ enabled will not accurately reflect the performance │');
|
||||
print('│ that will be experienced by end users using release ╎');
|
||||
print('│ builds. Benchmarks should be run using this command ┆');
|
||||
print('│ line: flutter run --release benchmark.dart ┊');
|
||||
print('│ ');
|
||||
print('└─────────────────────────────────────────────────╌┄┈ 🐢');
|
||||
return true;
|
||||
}());
|
||||
final TestWidgetsFlutterBinding binding =
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
assert(binding is! AutomatedTestWidgetsFlutterBinding);
|
||||
final WidgetTester tester = WidgetTester._(binding);
|
||||
tester._recordNumberOfSemanticsHandles();
|
||||
return binding.runTest(
|
||||
() => callback(tester),
|
||||
tester._endOfTestVerifications,
|
||||
) ??
|
||||
Future<void>.value();
|
||||
}
|
||||
|
||||
/// Assert that `actual` matches `matcher`.
|
||||
///
|
||||
/// See [test_package.expect] for details. This is a variant of that function
|
||||
/// that additionally verifies that there are no asynchronous APIs
|
||||
/// that have not yet resolved.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [expectLater] for use with asynchronous matchers.
|
||||
void expect(
|
||||
dynamic actual,
|
||||
dynamic matcher, {
|
||||
String reason,
|
||||
dynamic skip, // true or a String
|
||||
}) {
|
||||
TestAsyncUtils.guardSync();
|
||||
test_package.expect(actual, matcher, reason: reason, skip: skip);
|
||||
}
|
||||
|
||||
/// Assert that `actual` matches `matcher`.
|
||||
///
|
||||
/// See [test_package.expect] for details. This variant will _not_ check that
|
||||
/// there are no outstanding asynchronous API requests. As such, it can be
|
||||
/// called from, e.g., callbacks that are run during build or layout, or in the
|
||||
/// completion handlers of futures that execute in response to user input.
|
||||
///
|
||||
/// Generally, it is better to use [expect], which does include checks to ensure
|
||||
/// that asynchronous APIs are not being called.
|
||||
void expectSync(
|
||||
dynamic actual,
|
||||
dynamic matcher, {
|
||||
String reason,
|
||||
}) {
|
||||
test_package.expect(actual, matcher, reason: reason);
|
||||
}
|
||||
|
||||
/// Just like [expect], but returns a [Future] that completes when the matcher
|
||||
/// has finished matching.
|
||||
///
|
||||
/// See [test_package.expectLater] for details.
|
||||
///
|
||||
/// If the matcher fails asynchronously, that failure is piped to the returned
|
||||
/// future where it can be handled by user code. If it is not handled by user
|
||||
/// code, the test will fail.
|
||||
Future<void> expectLater(
|
||||
dynamic actual,
|
||||
dynamic matcher, {
|
||||
String reason,
|
||||
dynamic skip, // true or a String
|
||||
}) {
|
||||
// We can't wrap the delegate in a guard, or we'll hit async barriers in
|
||||
// [TestWidgetsFlutterBinding] while we're waiting for the matcher to complete
|
||||
TestAsyncUtils.guardSync();
|
||||
return test_package
|
||||
.expectLater(actual, matcher, reason: reason, skip: skip)
|
||||
.then<void>((dynamic value) => null);
|
||||
}
|
||||
|
||||
/// Class that programmatically interacts with widgets and the test environment.
|
||||
///
|
||||
/// For convenience, instances of this class (such as the one provided by
|
||||
/// `testWidget`) can be used as the `vsync` for `AnimationController` objects.
|
||||
class WidgetTester extends WidgetController
|
||||
implements HitTestDispatcher, TickerProvider {
|
||||
WidgetTester._(TestWidgetsFlutterBinding binding) : super(binding) {
|
||||
if (binding is LiveTestWidgetsFlutterBinding)
|
||||
binding.deviceEventDispatcher = this;
|
||||
}
|
||||
|
||||
/// The binding instance used by the testing framework.
|
||||
@override
|
||||
TestWidgetsFlutterBinding get binding => super.binding;
|
||||
|
||||
/// Renders the UI from the given [widget].
|
||||
///
|
||||
/// Calls [runApp] with the given widget, then triggers a frame and flushes
|
||||
/// microtasks, by calling [pump] with the same `duration` (if any). The
|
||||
/// supplied [EnginePhase] is the final phase reached during the pump pass; if
|
||||
/// not supplied, the whole pass is executed.
|
||||
///
|
||||
/// Subsequent calls to this is different from [pump] in that it forces a full
|
||||
/// rebuild of the tree, even if [widget] is the same as the previous call.
|
||||
/// [pump] will only rebuild the widgets that have changed.
|
||||
///
|
||||
/// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
|
||||
/// this method works when the test is run with `flutter run`.
|
||||
Future<void> pumpWidget(
|
||||
Widget widget, [
|
||||
Duration duration,
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
]) {
|
||||
return TestAsyncUtils.guard<void>(() {
|
||||
binding.attachRootWidget(widget);
|
||||
binding.scheduleFrame();
|
||||
return binding.pump(duration, phase);
|
||||
});
|
||||
}
|
||||
|
||||
/// Triggers a frame after `duration` amount of time.
|
||||
///
|
||||
/// This makes the framework act as if the application had janked (missed
|
||||
/// frames) for `duration` amount of time, and then received a v-sync signal
|
||||
/// to paint the application.
|
||||
///
|
||||
/// This is a convenience function that just calls
|
||||
/// [TestWidgetsFlutterBinding.pump].
|
||||
///
|
||||
/// See also [LiveTestWidgetsFlutterBindingFramePolicy], which affects how
|
||||
/// this method works when the test is run with `flutter run`.
|
||||
@override
|
||||
Future<void> pump([
|
||||
Duration duration,
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
]) {
|
||||
return TestAsyncUtils.guard<void>(() => binding.pump(duration, phase));
|
||||
}
|
||||
|
||||
/// Repeatedly calls [pump] with the given `duration` until there are no
|
||||
/// longer any frames scheduled. This will call [pump] at least once, even if
|
||||
/// no frames are scheduled when the function is called, to flush any pending
|
||||
/// microtasks which may themselves schedule a frame.
|
||||
///
|
||||
/// This essentially waits for all animations to have completed.
|
||||
///
|
||||
/// If it takes longer that the given `timeout` to settle, then the test will
|
||||
/// fail (this method will throw an exception). In particular, this means that
|
||||
/// if there is an infinite animation in progress (for example, if there is an
|
||||
/// indeterminate progress indicator spinning), this method will throw.
|
||||
///
|
||||
/// The default timeout is ten minutes, which is longer than most reasonable
|
||||
/// finite animations would last.
|
||||
///
|
||||
/// If the function returns, it returns the number of pumps that it performed.
|
||||
///
|
||||
/// In general, it is better practice to figure out exactly why each frame is
|
||||
/// needed, and then to [pump] exactly as many frames as necessary. This will
|
||||
/// help catch regressions where, for instance, an animation is being started
|
||||
/// one frame later than it should.
|
||||
///
|
||||
/// Alternatively, one can check that the return value from this function
|
||||
/// matches the expected number of pumps.
|
||||
Future<int> pumpAndSettle([
|
||||
Duration duration = const Duration(milliseconds: 100),
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
Duration timeout = const Duration(minutes: 10),
|
||||
]) {
|
||||
assert(duration != null);
|
||||
assert(duration > Duration.zero);
|
||||
assert(timeout != null);
|
||||
assert(timeout > Duration.zero);
|
||||
assert(() {
|
||||
final WidgetsBinding binding = this.binding;
|
||||
if (binding is LiveTestWidgetsFlutterBinding &&
|
||||
binding.framePolicy ==
|
||||
LiveTestWidgetsFlutterBindingFramePolicy.benchmark) {
|
||||
throw 'When using LiveTestWidgetsFlutterBindingFramePolicy.benchmark, '
|
||||
'hasScheduledFrame is never set to true. This means that pumpAndSettle() '
|
||||
'cannot be used, because it has no way to know if the application has '
|
||||
'stopped registering new frames.';
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
int count = 0;
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
final DateTime endTime = binding.clock.fromNowBy(timeout);
|
||||
do {
|
||||
if (binding.clock.now().isAfter(endTime))
|
||||
throw FlutterError('pumpAndSettle timed out');
|
||||
await binding.pump(duration, phase);
|
||||
count += 1;
|
||||
} while (binding.hasScheduledFrame);
|
||||
}).then<int>((_) => count);
|
||||
}
|
||||
|
||||
/// Runs a [callback] that performs real asynchronous work.
|
||||
///
|
||||
/// This is intended for callers who need to call asynchronous methods where
|
||||
/// the methods spawn isolates or OS threads and thus cannot be executed
|
||||
/// synchronously by calling [pump].
|
||||
///
|
||||
/// If callers were to run these types of asynchronous tasks directly in
|
||||
/// their test methods, they run the possibility of encountering deadlocks.
|
||||
///
|
||||
/// If [callback] completes successfully, this will return the future
|
||||
/// returned by [callback].
|
||||
///
|
||||
/// If [callback] completes with an error, the error will be caught by the
|
||||
/// Flutter framework and made available via [takeException], and this method
|
||||
/// will return a future that completes will `null`.
|
||||
///
|
||||
/// Re-entrant calls to this method are not allowed; callers of this method
|
||||
/// are required to wait for the returned future to complete before calling
|
||||
/// this method again. Attempts to do otherwise will result in a
|
||||
/// [TestFailure] error being thrown.
|
||||
Future<T> runAsync<T>(
|
||||
Future<T> callback(), {
|
||||
Duration additionalTime = const Duration(milliseconds: 250),
|
||||
}) =>
|
||||
binding.runAsync<T>(callback, additionalTime: additionalTime);
|
||||
|
||||
/// Whether there are any any transient callbacks scheduled.
|
||||
///
|
||||
/// This essentially checks whether all animations have completed.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [pumpAndSettle], which essentially calls [pump] until there are no
|
||||
/// scheduled frames.
|
||||
/// * [SchedulerBinding.transientCallbackCount], which is the value on which
|
||||
/// this is based.
|
||||
/// * [SchedulerBinding.hasScheduledFrame], which is true whenever a frame is
|
||||
/// pending. [SchedulerBinding.hasScheduledFrame] is made true when a
|
||||
/// widget calls [State.setState], even if there are no transient callbacks
|
||||
/// scheduled. This is what [pumpAndSettle] uses.
|
||||
bool get hasRunningAnimations => binding.transientCallbackCount > 0;
|
||||
|
||||
@override
|
||||
HitTestResult hitTestOnBinding(Offset location) {
|
||||
location = binding.localToGlobal(location);
|
||||
return super.hitTestOnBinding(location);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> sendEventToBinding(PointerEvent event, HitTestResult result) {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
binding.dispatchEvent(event, result, source: TestBindingEventSource.test);
|
||||
});
|
||||
}
|
||||
|
||||
/// Handler for device events caught by the binding in live test mode.
|
||||
@override
|
||||
void dispatchEvent(PointerEvent event, HitTestResult result) {
|
||||
if (event is PointerDownEvent) {
|
||||
final RenderObject innerTarget = result.path
|
||||
.firstWhere(
|
||||
(HitTestEntry candidate) => candidate.target is RenderObject,
|
||||
)
|
||||
.target;
|
||||
final Element innerTargetElement = collectAllElementsFrom(
|
||||
binding.renderViewElement,
|
||||
skipOffstage: true,
|
||||
).lastWhere(
|
||||
(Element element) => element.renderObject == innerTarget,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (innerTargetElement == null) {
|
||||
debugPrint(
|
||||
'No widgets found at ${binding.globalToLocal(event.position)}.');
|
||||
return;
|
||||
}
|
||||
final List<Element> candidates = <Element>[];
|
||||
innerTargetElement.visitAncestorElements((Element element) {
|
||||
candidates.add(element);
|
||||
return true;
|
||||
});
|
||||
assert(candidates.isNotEmpty);
|
||||
String descendantText;
|
||||
int numberOfWithTexts = 0;
|
||||
int numberOfTypes = 0;
|
||||
int totalNumber = 0;
|
||||
debugPrint(
|
||||
'Some possible finders for the widgets at ${binding.globalToLocal(event.position)}:');
|
||||
for (Element element in candidates) {
|
||||
if (totalNumber >
|
||||
13) // an arbitrary number of finders that feels useful without being overwhelming
|
||||
break;
|
||||
totalNumber += 1; // optimistically assume we'll be able to describe it
|
||||
|
||||
if (element.widget is Tooltip) {
|
||||
final Tooltip widget = element.widget;
|
||||
final Iterable<Element> matches =
|
||||
find.byTooltip(widget.message).evaluate();
|
||||
if (matches.length == 1) {
|
||||
debugPrint(' find.byTooltip(\'${widget.message}\')');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (element.widget is Text) {
|
||||
assert(descendantText == null);
|
||||
final Text widget = element.widget;
|
||||
final Iterable<Element> matches = find.text(widget.data).evaluate();
|
||||
descendantText = widget.data;
|
||||
if (matches.length == 1) {
|
||||
debugPrint(' find.text(\'${widget.data}\')');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (element.widget.key is ValueKey<dynamic>) {
|
||||
final ValueKey<dynamic> key = element.widget.key;
|
||||
String keyLabel;
|
||||
if (key is ValueKey<int> ||
|
||||
key is ValueKey<double> ||
|
||||
key is ValueKey<bool>) {
|
||||
keyLabel = 'const ${element.widget.key.runtimeType}(${key.value})';
|
||||
} else if (key is ValueKey<String>) {
|
||||
keyLabel = 'const Key(\'${key.value}\')';
|
||||
}
|
||||
if (keyLabel != null) {
|
||||
final Iterable<Element> matches = find.byKey(key).evaluate();
|
||||
if (matches.length == 1) {
|
||||
debugPrint(' find.byKey($keyLabel)');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isPrivate(element.widget.runtimeType)) {
|
||||
if (numberOfTypes < 5) {
|
||||
final Iterable<Element> matches =
|
||||
find.byType(element.widget.runtimeType).evaluate();
|
||||
if (matches.length == 1) {
|
||||
debugPrint(' find.byType(${element.widget.runtimeType})');
|
||||
numberOfTypes += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (descendantText != null && numberOfWithTexts < 5) {
|
||||
final Iterable<Element> matches = find
|
||||
.widgetWithText(element.widget.runtimeType, descendantText)
|
||||
.evaluate();
|
||||
if (matches.length == 1) {
|
||||
debugPrint(
|
||||
' find.widgetWithText(${element.widget.runtimeType}, \'$descendantText\')');
|
||||
numberOfWithTexts += 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isPrivate(element.runtimeType)) {
|
||||
final Iterable<Element> matches =
|
||||
find.byElementType(element.runtimeType).evaluate();
|
||||
if (matches.length == 1) {
|
||||
debugPrint(' find.byElementType(${element.runtimeType})');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
totalNumber -=
|
||||
1; // if we got here, we didn't actually find something to say about it
|
||||
}
|
||||
if (totalNumber == 0)
|
||||
debugPrint(' <could not come up with any unique finders>');
|
||||
}
|
||||
}
|
||||
|
||||
bool _isPrivate(Type type) {
|
||||
// used above so that we don't suggest matchers for private types
|
||||
return '_'.matchAsPrefix(type.toString()) != null;
|
||||
}
|
||||
|
||||
/// Returns the exception most recently caught by the Flutter framework.
|
||||
///
|
||||
/// See [TestWidgetsFlutterBinding.takeException] for details.
|
||||
dynamic takeException() {
|
||||
return binding.takeException();
|
||||
}
|
||||
|
||||
/// Acts as if the application went idle.
|
||||
///
|
||||
/// Runs all remaining microtasks, including those scheduled as a result of
|
||||
/// running them, until there are no more microtasks scheduled.
|
||||
///
|
||||
/// Does not run timers. May result in an infinite loop or run out of memory
|
||||
/// if microtasks continue to recursively schedule new microtasks.
|
||||
Future<void> idle() {
|
||||
return TestAsyncUtils.guard<void>(() => binding.idle());
|
||||
}
|
||||
|
||||
Set<Ticker> _tickers;
|
||||
|
||||
@override
|
||||
Ticker createTicker(TickerCallback onTick) {
|
||||
_tickers ??= Set<_TestTicker>();
|
||||
final _TestTicker result = _TestTicker(onTick, _removeTicker);
|
||||
_tickers.add(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void _removeTicker(_TestTicker ticker) {
|
||||
assert(_tickers != null);
|
||||
assert(_tickers.contains(ticker));
|
||||
_tickers.remove(ticker);
|
||||
}
|
||||
|
||||
/// Throws an exception if any tickers created by the [WidgetTester] are still
|
||||
/// active when the method is called.
|
||||
///
|
||||
/// An argument can be specified to provide a string that will be used in the
|
||||
/// error message. It should be an adverbial phrase describing the current
|
||||
/// situation, such as "at the end of the test".
|
||||
void verifyTickersWereDisposed([String when = 'when none should have been']) {
|
||||
assert(when != null);
|
||||
if (_tickers != null) {
|
||||
for (Ticker ticker in _tickers) {
|
||||
if (ticker.isActive) {
|
||||
throw FlutterError('A Ticker was active $when.\n'
|
||||
'All Tickers must be disposed. Tickers used by AnimationControllers '
|
||||
'should be disposed by calling dispose() on the AnimationController itself. '
|
||||
'Otherwise, the ticker will leak.\n'
|
||||
'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _endOfTestVerifications() {
|
||||
verifyTickersWereDisposed('at the end of the test');
|
||||
_verifySemanticsHandlesWereDisposed();
|
||||
}
|
||||
|
||||
void _verifySemanticsHandlesWereDisposed() {
|
||||
assert(_lastRecordedSemanticsHandles != null);
|
||||
if (binding.pipelineOwner.debugOutstandingSemanticsHandles >
|
||||
_lastRecordedSemanticsHandles) {
|
||||
throw FlutterError(
|
||||
'A SemanticsHandle was active at the end of the test.\n'
|
||||
'All SemanticsHandle instances must be disposed by calling dispose() on '
|
||||
'the SemanticsHandle. If your test uses SemanticsTester, it is '
|
||||
'sufficient to call dispose() on SemanticsTester. Otherwise, the '
|
||||
'existing handle will leak into another test and alter its behavior.');
|
||||
}
|
||||
_lastRecordedSemanticsHandles = null;
|
||||
}
|
||||
|
||||
int _lastRecordedSemanticsHandles;
|
||||
|
||||
void _recordNumberOfSemanticsHandles() {
|
||||
_lastRecordedSemanticsHandles =
|
||||
binding.pipelineOwner.debugOutstandingSemanticsHandles;
|
||||
}
|
||||
|
||||
/// Returns the TestTextInput singleton.
|
||||
///
|
||||
/// Typical app tests will not need to use this value. To add text to widgets
|
||||
/// like [TextField] or [TextFormField], call [enterText].
|
||||
TestTextInput get testTextInput => binding.testTextInput;
|
||||
|
||||
/// Give the text input widget specified by [finder] the focus, as if the
|
||||
/// onscreen keyboard had appeared.
|
||||
///
|
||||
/// Implies a call to [pump].
|
||||
///
|
||||
/// The widget specified by [finder] must be an [EditableText] or have
|
||||
/// an [EditableText] descendant. For example `find.byType(TextField)`
|
||||
/// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
|
||||
///
|
||||
/// Tests that just need to add text to widgets like [TextField]
|
||||
/// or [TextFormField] only need to call [enterText].
|
||||
Future<void> showKeyboard(Finder finder) async {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
final EditableTextState editable = state<EditableTextState>(
|
||||
find.descendant(
|
||||
of: finder,
|
||||
matching: find.byType(EditableText),
|
||||
matchRoot: true,
|
||||
),
|
||||
);
|
||||
binding.focusedEditable = editable;
|
||||
await pump();
|
||||
});
|
||||
}
|
||||
|
||||
/// Give the text input widget specified by [finder] the focus and
|
||||
/// enter [text] as if it been provided by the onscreen keyboard.
|
||||
///
|
||||
/// The widget specified by [finder] must be an [EditableText] or have
|
||||
/// an [EditableText] descendant. For example `find.byType(TextField)`
|
||||
/// or `find.byType(TextFormField)`, or `find.byType(EditableText)`.
|
||||
///
|
||||
/// To just give [finder] the focus without entering any text,
|
||||
/// see [showKeyboard].
|
||||
Future<void> enterText(Finder finder, String text) async {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
await showKeyboard(finder);
|
||||
testTextInput.enterText(text);
|
||||
await idle();
|
||||
});
|
||||
}
|
||||
|
||||
/// Makes an effort to dismiss the current page with a Material [Scaffold] or
|
||||
/// a [CupertinoPageScaffold].
|
||||
///
|
||||
/// Will throw an error if there is no back button in the page.
|
||||
Future<void> pageBack() async {
|
||||
return TestAsyncUtils.guard<void>(() async {
|
||||
Finder backButton = find.byTooltip('Back');
|
||||
// TODO(yjbanov): implement Cupertino widgets
|
||||
// if (backButton.evaluate().isEmpty) {
|
||||
// backButton = find.byType(CupertinoNavigationBarBackButton);
|
||||
// }
|
||||
|
||||
expectSync(backButton, findsOneWidget,
|
||||
reason: 'One back button expected on screen');
|
||||
|
||||
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.
|
||||
///
|
||||
/// 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) {
|
||||
renderObject = renderObject?.parent;
|
||||
result = renderObject?.debugSemantics;
|
||||
}
|
||||
if (result == null) throw StateError('No Semantics data found.');
|
||||
return result;
|
||||
}
|
||||
|
||||
/// DEPRECATED: use [getSemantics] instead.
|
||||
@Deprecated('use getSemantics instead')
|
||||
SemanticsData getSemanticsData(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) {
|
||||
renderObject = renderObject?.parent;
|
||||
result = renderObject?.debugSemantics;
|
||||
}
|
||||
if (result == null) throw StateError('No Semantics data found.');
|
||||
return result.getSemanticsData();
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Shorthand for `Scrollable.ensureVisible(tester.element(finder))`
|
||||
Future<void> ensureVisible(Finder finder) =>
|
||||
Scrollable.ensureVisible(element(finder));
|
||||
}
|
||||
|
||||
typedef _TickerDisposeCallback = void Function(_TestTicker ticker);
|
||||
|
||||
class _TestTicker extends Ticker {
|
||||
_TestTicker(TickerCallback onTick, this._onDispose) : super(onTick);
|
||||
|
||||
_TickerDisposeCallback _onDispose;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_onDispose != null) _onDispose(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
393
packages/flutter_web_test/lib/src/window.dart
Normal file
393
packages/flutter_web_test/lib/src/window.dart
Normal file
@ -0,0 +1,393 @@
|
||||
// Copyright 2018 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
// Synced 2019-05-30T14:20:57.825725.
|
||||
|
||||
import 'dart:typed_data' show ByteData;
|
||||
import 'package:flutter_web_ui/ui.dart'
|
||||
hide window, webOnlyScheduleFrameCallback;
|
||||
import 'package:flutter_web_ui/ui.dart' as ui;
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// [Window] that wraps another [Window] and allows faking of some properties
|
||||
/// for testing purposes.
|
||||
///
|
||||
/// Tests for certain widgets, e.g., [MaterialApp], might require faking certain
|
||||
/// properties of a [Window]. [TestWindow] facilitates the faking of these properties
|
||||
/// by overidding the properties of a real [Window] with desired fake values. The
|
||||
/// binding used within tests, [TestWidgetsFlutterBinding], contains a [TestWindow]
|
||||
/// that is used by all tests.
|
||||
///
|
||||
/// ## Sample Code
|
||||
///
|
||||
/// A test can utilize a [TestWindow] in the following way:
|
||||
///
|
||||
/// ```dart
|
||||
/// testWidgets('your test name here', (WidgetTester tester) async {
|
||||
/// // Retrieve the TestWidgetsFlutterBinding.
|
||||
/// final TestWidgetsFlutterBinding testBinding = tester.binding;
|
||||
///
|
||||
/// // Fake the desired properties of the TestWindow. All code running
|
||||
/// // within this test will perceive the following fake text scale
|
||||
/// // factor as the real text scale factor of the window.
|
||||
/// testBinding.window.textScaleFactorFakeValue = 2.5;
|
||||
///
|
||||
/// // Test code that depends on text scale factor here.
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// The [TestWidgetsFlutterBinding] is recreated for each test and
|
||||
/// therefore any fake values defined in one test will not persist
|
||||
/// to the next.
|
||||
///
|
||||
/// If a test needs to override a real [Window] property and then later
|
||||
/// return to using the real [Window] property, [TestWindow] provides
|
||||
/// methods to clear each individual test value, e.g., [clearLocaleTestValue()].
|
||||
///
|
||||
/// To clear all fake test values in a [TestWindow], consider using [clearAllTestValues()].
|
||||
class TestWindow implements Window {
|
||||
/// Constructs a [TestWindow] that defers all behavior to the given [window] unless
|
||||
/// explicitly overidden for test purposes.
|
||||
TestWindow({
|
||||
@required Window window,
|
||||
}) : _window = window;
|
||||
|
||||
/// The [Window] that is wrapped by this [TestWindow].
|
||||
final Window _window;
|
||||
|
||||
@override
|
||||
double get devicePixelRatio => _devicePixelRatio ?? _window.devicePixelRatio;
|
||||
|
||||
double _devicePixelRatio;
|
||||
|
||||
/// Hides the real device pixel ratio and reports the given [devicePixelRatio] instead.
|
||||
set devicePixelRatioTestValue(double devicePixelRatio) {
|
||||
_devicePixelRatio = devicePixelRatio;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test device pixel ratio and returns to using the real device pixel ratio.
|
||||
void clearDevicePixelRatioTestValue() {
|
||||
_devicePixelRatio = null;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
Size get physicalSize => _physicalSizeTestValue ?? _window.physicalSize;
|
||||
|
||||
Size _physicalSizeTestValue;
|
||||
|
||||
/// Hides the real physical size and reports the given [physicalSizeTestValue] instead.
|
||||
set physicalSizeTestValue(Size physicalSizeTestValue) {
|
||||
_physicalSizeTestValue = physicalSizeTestValue;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test physical size and returns to using the real physical size.
|
||||
void clearPhysicalSizeTestValue() {
|
||||
_physicalSizeTestValue = null;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
void setIsolateDebugName(String name) {}
|
||||
|
||||
@override
|
||||
WindowPadding get viewInsets => _viewInsetsTestValue ?? _window.viewInsets;
|
||||
|
||||
WindowPadding _viewInsetsTestValue;
|
||||
|
||||
/// Hides the real view insets and reports the given [viewInsetsTestValue] instead.
|
||||
set viewInsetsTestValue(WindowPadding viewInsetsTestValue) {
|
||||
_viewInsetsTestValue = viewInsetsTestValue;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test view insets and returns to using the real view insets.
|
||||
void clearViewInsetsTestValue() {
|
||||
_viewInsetsTestValue = null;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
WindowPadding get padding => _paddingTestValue ?? _window.padding;
|
||||
|
||||
WindowPadding _paddingTestValue;
|
||||
|
||||
/// Hides the real padding and reports the given [paddingTestValue] instead.
|
||||
set paddingTestValue(WindowPadding paddingTestValue) {
|
||||
_paddingTestValue = paddingTestValue;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test padding and returns to using the real padding.
|
||||
void clearPaddingTestValue() {
|
||||
_paddingTestValue = null;
|
||||
onMetricsChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
VoidCallback get onMetricsChanged => _window.onMetricsChanged;
|
||||
@override
|
||||
set onMetricsChanged(VoidCallback callback) {
|
||||
_window.onMetricsChanged = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
Locale get locale => _localeTestValue ?? _window.locale;
|
||||
Locale _localeTestValue;
|
||||
|
||||
/// Hides the real locale and reports the given [localeTestValue] instead.
|
||||
set localeTestValue(Locale localeTestValue) {
|
||||
_localeTestValue = localeTestValue;
|
||||
onLocaleChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test locale and returns to using the real locale.
|
||||
void clearLocaleTestValue() {
|
||||
_localeTestValue = null;
|
||||
onLocaleChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
List<Locale> get locales => _localesTestValue ?? _window.locales;
|
||||
List<Locale> _localesTestValue;
|
||||
|
||||
/// Hides the real locales and reports the given [localesTestValue] instead.
|
||||
set localesTestValue(List<Locale> localesTestValue) {
|
||||
_localesTestValue = localesTestValue;
|
||||
onLocaleChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test locales and returns to using the real locales.
|
||||
void clearLocalesTestValue() {
|
||||
_localesTestValue = null;
|
||||
onLocaleChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
VoidCallback get onLocaleChanged => _window.onLocaleChanged;
|
||||
@override
|
||||
set onLocaleChanged(VoidCallback callback) {
|
||||
_window.onLocaleChanged = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
double get textScaleFactor =>
|
||||
_textScaleFactorTestValue ?? _window.textScaleFactor;
|
||||
double _textScaleFactorTestValue;
|
||||
|
||||
/// Hides the real text scale factor and reports the given [textScaleFactorTestValue] instead.
|
||||
set textScaleFactorTestValue(double textScaleFactorTestValue) {
|
||||
_textScaleFactorTestValue = textScaleFactorTestValue;
|
||||
onTextScaleFactorChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test text scale factor and returns to using the real text scale factor.
|
||||
void clearTextScaleFactorTestValue() {
|
||||
_textScaleFactorTestValue = null;
|
||||
onTextScaleFactorChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
Brightness get platformBrightness =>
|
||||
_platformBrightnessTestValue ?? _window.platformBrightness;
|
||||
Brightness _platformBrightnessTestValue;
|
||||
@override
|
||||
VoidCallback get onPlatformBrightnessChanged =>
|
||||
_window.onPlatformBrightnessChanged;
|
||||
@override
|
||||
set onPlatformBrightnessChanged(VoidCallback callback) {
|
||||
_window.onPlatformBrightnessChanged = callback;
|
||||
}
|
||||
|
||||
/// Hides the real text scale factor and reports the given [platformBrightnessTestValue] instead.
|
||||
set platformBrightnessTestValue(Brightness platformBrightnessTestValue) {
|
||||
_platformBrightnessTestValue = platformBrightnessTestValue;
|
||||
onPlatformBrightnessChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test platform brightness and returns to using the real platform brightness.
|
||||
void clearPlatformBrightnessTestValue() {
|
||||
_platformBrightnessTestValue = null;
|
||||
onPlatformBrightnessChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get alwaysUse24HourFormat =>
|
||||
_alwaysUse24HourFormatTestValue ?? _window.alwaysUse24HourFormat;
|
||||
bool _alwaysUse24HourFormatTestValue;
|
||||
|
||||
/// Hides the real clock format and reports the given [alwaysUse24HourFormatTestValue] instead.
|
||||
set alwaysUse24HourFormatTestValue(bool alwaysUse24HourFormatTestValue) {
|
||||
_alwaysUse24HourFormatTestValue = alwaysUse24HourFormatTestValue;
|
||||
}
|
||||
|
||||
/// Deletes any existing test clock format and returns to using the real clock format.
|
||||
void clearAlwaysUse24HourTestValue() {
|
||||
_alwaysUse24HourFormatTestValue = null;
|
||||
}
|
||||
|
||||
@override
|
||||
VoidCallback get onTextScaleFactorChanged => _window.onTextScaleFactorChanged;
|
||||
@override
|
||||
set onTextScaleFactorChanged(VoidCallback callback) {
|
||||
_window.onTextScaleFactorChanged = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
FrameCallback get onBeginFrame => _window.onBeginFrame;
|
||||
@override
|
||||
set onBeginFrame(FrameCallback callback) {
|
||||
_window.onBeginFrame = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
VoidCallback get onDrawFrame => _window.onDrawFrame;
|
||||
@override
|
||||
set onDrawFrame(VoidCallback callback) {
|
||||
_window.onDrawFrame = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
PointerDataPacketCallback get onPointerDataPacket =>
|
||||
_window.onPointerDataPacket;
|
||||
@override
|
||||
set onPointerDataPacket(PointerDataPacketCallback callback) {
|
||||
_window.onPointerDataPacket = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
String get defaultRouteName =>
|
||||
_defaultRouteNameTestValue ?? _window.defaultRouteName;
|
||||
String _defaultRouteNameTestValue;
|
||||
|
||||
/// Hides the real default route name and reports the given [defaultRouteNameTestValue] instead.
|
||||
set defaultRouteNameTestValue(String defaultRouteNameTestValue) {
|
||||
_defaultRouteNameTestValue = defaultRouteNameTestValue;
|
||||
}
|
||||
|
||||
/// Deletes any existing test default route name and returns to using the real default route name.
|
||||
void clearDefaultRouteNameTestValue() {
|
||||
_defaultRouteNameTestValue = null;
|
||||
}
|
||||
|
||||
@override
|
||||
void scheduleFrame() {
|
||||
_window.scheduleFrame();
|
||||
}
|
||||
|
||||
@override
|
||||
void render(Scene scene) {
|
||||
_window.render(scene);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get semanticsEnabled =>
|
||||
_semanticsEnabledTestValue ?? _window.semanticsEnabled;
|
||||
bool _semanticsEnabledTestValue;
|
||||
|
||||
/// Hides the real semantics enabled and reports the given [semanticsEnabledTestValue] instead.
|
||||
set semanticsEnabledTestValue(bool semanticsEnabledTestValue) {
|
||||
_semanticsEnabledTestValue = semanticsEnabledTestValue;
|
||||
onSemanticsEnabledChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test semantics enabled and returns to using the real semantics enabled.
|
||||
void clearSemanticsEnabledTestValue() {
|
||||
_semanticsEnabledTestValue = null;
|
||||
onSemanticsEnabledChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
VoidCallback get onSemanticsEnabledChanged =>
|
||||
_window.onSemanticsEnabledChanged;
|
||||
@override
|
||||
set onSemanticsEnabledChanged(VoidCallback callback) {
|
||||
_window.onSemanticsEnabledChanged = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
SemanticsActionCallback get onSemanticsAction => _window.onSemanticsAction;
|
||||
@override
|
||||
set onSemanticsAction(SemanticsActionCallback callback) {
|
||||
_window.onSemanticsAction = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
AccessibilityFeatures get accessibilityFeatures =>
|
||||
_accessibilityFeaturesTestValue ?? _window.accessibilityFeatures;
|
||||
AccessibilityFeatures _accessibilityFeaturesTestValue;
|
||||
|
||||
/// Hides the real accessibility features and reports the given [accessibilityFeaturesTestValue] instead.
|
||||
set accessibilityFeaturesTestValue(
|
||||
AccessibilityFeatures accessibilityFeaturesTestValue) {
|
||||
_accessibilityFeaturesTestValue = accessibilityFeaturesTestValue;
|
||||
onAccessibilityFeaturesChanged();
|
||||
}
|
||||
|
||||
/// Deletes any existing test accessibility features and returns to using the real accessibility features.
|
||||
void clearAccessibilityFeaturesTestValue() {
|
||||
_accessibilityFeaturesTestValue = null;
|
||||
onAccessibilityFeaturesChanged();
|
||||
}
|
||||
|
||||
@override
|
||||
VoidCallback get onAccessibilityFeaturesChanged =>
|
||||
_window.onAccessibilityFeaturesChanged;
|
||||
@override
|
||||
set onAccessibilityFeaturesChanged(VoidCallback callback) {
|
||||
_window.onAccessibilityFeaturesChanged = callback;
|
||||
}
|
||||
|
||||
@override
|
||||
void updateSemantics(SemanticsUpdate update) {
|
||||
_window.updateSemantics(update);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendPlatformMessage(
|
||||
String name, ByteData data, PlatformMessageResponseCallback callback) {
|
||||
_window.sendPlatformMessage(name, data, callback);
|
||||
}
|
||||
|
||||
@override
|
||||
PlatformMessageCallback get onPlatformMessage => _window.onPlatformMessage;
|
||||
@override
|
||||
set onPlatformMessage(PlatformMessageCallback callback) {
|
||||
_window.onPlatformMessage = callback;
|
||||
}
|
||||
|
||||
String get initialLifecycleState {
|
||||
return _initialLifecycleState;
|
||||
}
|
||||
|
||||
String _initialLifecycleState;
|
||||
|
||||
/// Delete any test value properties that have been set on this [TestWindow]
|
||||
/// and return to reporting the real [Window] values for all [Window] properties.
|
||||
///
|
||||
/// If desired, clearing of properties can be done on an individual basis, e.g.,
|
||||
/// [clearLocaleTestValue()].
|
||||
void clearAllTestValues() {
|
||||
clearAccessibilityFeaturesTestValue();
|
||||
clearAlwaysUse24HourTestValue();
|
||||
clearDefaultRouteNameTestValue();
|
||||
clearDevicePixelRatioTestValue();
|
||||
clearPlatformBrightnessTestValue();
|
||||
clearLocaleTestValue();
|
||||
clearLocalesTestValue();
|
||||
clearPaddingTestValue();
|
||||
clearPhysicalSizeTestValue();
|
||||
clearSemanticsEnabledTestValue();
|
||||
clearTextScaleFactorTestValue();
|
||||
clearViewInsetsTestValue();
|
||||
}
|
||||
}
|
||||
|
||||
VoidCallback get webOnlyScheduleFrameCallback =>
|
||||
ui.webOnlyScheduleFrameCallback;
|
||||
set webOnlyScheduleFrameCallback(VoidCallback callback) {
|
||||
ui.webOnlyScheduleFrameCallback = callback;
|
||||
}
|
Reference in New Issue
Block a user