Add:创建 flutter go web 版

This commit is contained in:
ryan
2019-08-13 20:38:46 +08:00
parent 64513b59c1
commit da67ccf5a8
1656 changed files with 483653 additions and 0 deletions

View 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);
}

View 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._();

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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;
}
}

View 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;
}
}

File diff suppressed because it is too large Load Diff

View 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;

View 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;
}

View 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;
}

View File

@ -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>[]);

View 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;
});
}
}

View 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;
}
}

View 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();
}
}

View 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;
}