• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:ui' show SemanticsFlag;
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/physics.dart';
9import 'package:flutter/rendering.dart';
10import 'package:flutter_test/flutter_test.dart';
11import 'package:meta/meta.dart';
12
13export 'dart:ui' show SemanticsFlag, SemanticsAction;
14export 'package:flutter/rendering.dart' show SemanticsData;
15
16const String _matcherHelp = 'Try dumping the semantics with debugDumpSemanticsTree(DebugSemanticsDumpOrder.inverseHitTest) from the package:flutter/rendering.dart library to see what the semantics tree looks like.';
17
18/// Test semantics data that is compared against real semantics tree.
19///
20/// Useful with [hasSemantics] and [SemanticsTester] to test the contents of the
21/// semantics tree.
22class TestSemantics {
23  /// Creates an object with some test semantics data.
24  ///
25  /// The [id] field is required. The root node has an id of zero. Other nodes
26  /// are given a unique id when they are created, in a predictable fashion, and
27  /// so these values can be hard-coded.
28  ///
29  /// The [rect] field is required and has no default. Convenient values are
30  /// available:
31  ///
32  ///  * [TestSemantics.rootRect]: 2400x1600, the test screen's size in physical
33  ///    pixels, useful for the node with id zero.
34  ///
35  ///  * [TestSemantics.fullScreen] 800x600, the test screen's size in logical
36  ///    pixels, useful for other full-screen widgets.
37  TestSemantics({
38    this.id,
39    this.flags = 0,
40    this.actions = 0,
41    this.label = '',
42    this.value = '',
43    this.increasedValue = '',
44    this.decreasedValue = '',
45    this.hint = '',
46    this.textDirection,
47    this.rect,
48    this.transform,
49    this.elevation,
50    this.thickness,
51    this.textSelection,
52    this.children = const <TestSemantics>[],
53    this.scrollIndex,
54    this.scrollChildren,
55    Iterable<SemanticsTag> tags,
56  }) : assert(flags is int || flags is List<SemanticsFlag>),
57       assert(actions is int || actions is List<SemanticsAction>),
58       assert(label != null),
59       assert(value != null),
60       assert(increasedValue != null),
61       assert(decreasedValue != null),
62       assert(hint != null),
63       assert(children != null),
64       tags = tags?.toSet() ?? <SemanticsTag>{};
65
66  /// Creates an object with some test semantics data, with the [id] and [rect]
67  /// set to the appropriate values for the root node.
68  TestSemantics.root({
69    this.flags = 0,
70    this.actions = 0,
71    this.label = '',
72    this.value = '',
73    this.increasedValue = '',
74    this.decreasedValue = '',
75    this.hint = '',
76    this.textDirection,
77    this.transform,
78    this.textSelection,
79    this.children = const <TestSemantics>[],
80    this.scrollIndex,
81    this.scrollChildren,
82    Iterable<SemanticsTag> tags,
83  }) : id = 0,
84       assert(flags is int || flags is List<SemanticsFlag>),
85       assert(actions is int || actions is List<SemanticsAction>),
86       assert(label != null),
87       assert(increasedValue != null),
88       assert(decreasedValue != null),
89       assert(value != null),
90       assert(hint != null),
91       rect = TestSemantics.rootRect,
92       elevation = 0.0,
93       thickness = 0.0,
94       assert(children != null),
95       tags = tags?.toSet() ?? <SemanticsTag>{};
96
97  /// Creates an object with some test semantics data, with the [id] and [rect]
98  /// set to the appropriate values for direct children of the root node.
99  ///
100  /// The [transform] is set to a 3.0 scale (to account for the
101  /// [Window.devicePixelRatio] being 3.0 on the test pseudo-device).
102  ///
103  /// The [rect] field is required and has no default. The
104  /// [TestSemantics.fullScreen] property may be useful as a value; it describes
105  /// an 800x600 rectangle, which is the test screen's size in logical pixels.
106  TestSemantics.rootChild({
107    this.id,
108    this.flags = 0,
109    this.actions = 0,
110    this.label = '',
111    this.hint = '',
112    this.value = '',
113    this.increasedValue = '',
114    this.decreasedValue = '',
115    this.textDirection,
116    this.rect,
117    Matrix4 transform,
118    this.elevation,
119    this.thickness,
120    this.textSelection,
121    this.children = const <TestSemantics>[],
122    this.scrollIndex,
123    this.scrollChildren,
124    Iterable<SemanticsTag> tags,
125  }) : assert(flags is int || flags is List<SemanticsFlag>),
126       assert(actions is int || actions is List<SemanticsAction>),
127       assert(label != null),
128       assert(value != null),
129       assert(increasedValue != null),
130       assert(decreasedValue != null),
131       assert(hint != null),
132       transform = _applyRootChildScale(transform),
133       assert(children != null),
134       tags = tags?.toSet() ?? <SemanticsTag>{};
135
136  /// The unique identifier for this node.
137  ///
138  /// The root node has an id of zero. Other nodes are given a unique id when
139  /// they are created.
140  final int id;
141
142  /// The [SemanticsFlag]s set on this node.
143  ///
144  /// There are two ways to specify this property: as an `int` that encodes the
145  /// flags as a bit field, or as a `List<SemanticsFlag>` that are _on_.
146  ///
147  /// Using `List<SemanticsFlag>` is recommended due to better readability.
148  final dynamic flags;
149
150  /// The [SemanticsAction]s set on this node.
151  ///
152  /// There are two ways to specify this property: as an `int` that encodes the
153  /// actions as a bit field, or as a `List<SemanticsAction>`.
154  ///
155  /// Using `List<SemanticsAction>` is recommended due to better readability.
156  ///
157  /// The tester does not check the function corresponding to the action, but
158  /// only its existence.
159  final dynamic actions;
160
161  /// A textual description of this node.
162  final String label;
163
164  /// A textual description for the value of this node.
165  final String value;
166
167  /// What [value] will become after [SemanticsAction.increase] has been
168  /// performed.
169  final String increasedValue;
170
171  /// What [value] will become after [SemanticsAction.decrease] has been
172  /// performed.
173  final String decreasedValue;
174
175  /// A brief textual description of the result of the action that can be
176  /// performed on this node.
177  final String hint;
178
179  /// The reading direction of the [label].
180  ///
181  /// Even if this is not set, the [hasSemantics] matcher will verify that if a
182  /// label is present on the [SemanticsNode], a [SemanticsNode.textDirection]
183  /// is also set.
184  final TextDirection textDirection;
185
186  /// The bounding box for this node in its coordinate system.
187  ///
188  /// Convenient values are available:
189  ///
190  ///  * [TestSemantics.rootRect]: 2400x1600, the test screen's size in physical
191  ///    pixels, useful for the node with id zero.
192  ///
193  ///  * [TestSemantics.fullScreen] 800x600, the test screen's size in logical
194  ///    pixels, useful for other full-screen widgets.
195  final Rect rect;
196
197  /// The test screen's size in physical pixels, typically used as the [rect]
198  /// for the node with id zero.
199  ///
200  /// See also [new TestSemantics.root], which uses this value to describe the
201  /// root node.
202  static const Rect rootRect = Rect.fromLTWH(0.0, 0.0, 2400.0, 1800.0);
203
204  /// The test screen's size in logical pixels, useful for the [rect] of
205  /// full-screen widgets other than the root node.
206  static const Rect fullScreen = Rect.fromLTWH(0.0, 0.0, 800.0, 600.0);
207
208  /// The transform from this node's coordinate system to its parent's coordinate system.
209  ///
210  /// By default, the transform is null, which represents the identity
211  /// transformation (i.e., that this node has the same coordinate system as its
212  /// parent).
213  final Matrix4 transform;
214
215  /// The elevation of this node relative to the parent node.
216  ///
217  /// See also:
218  ///
219  ///  * [SemanticsConfiguration.elevation] for a detailed discussion regarding
220  ///    elevation and semantics.
221  final double elevation;
222
223  /// The extend that this node occupies in z-direction starting at [elevation].
224  ///
225  /// See also:
226  ///
227  ///  * [SemanticsConfiguration.thickness] for a more detailed definition.
228  final double thickness;
229
230  /// The index of the first visible semantic node within a scrollable.
231  final int scrollIndex;
232
233  /// The total number of semantic nodes within a scrollable.
234  final int scrollChildren;
235
236  final TextSelection textSelection;
237
238  static Matrix4 _applyRootChildScale(Matrix4 transform) {
239    final Matrix4 result = Matrix4.diagonal3Values(3.0, 3.0, 1.0);
240    if (transform != null)
241      result.multiply(transform);
242    return result;
243  }
244
245  /// The children of this node.
246  final List<TestSemantics> children;
247
248  /// The tags of this node.
249  final Set<SemanticsTag> tags;
250
251  bool _matches(
252    SemanticsNode node,
253    Map<dynamic, dynamic> matchState, {
254    bool ignoreRect = false,
255    bool ignoreTransform = false,
256    bool ignoreId = false,
257    DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.inverseHitTest,
258  }) {
259    bool fail(String message) {
260      matchState[TestSemantics] = '$message';
261      return false;
262    }
263
264    if (node == null)
265      return fail('could not find node with id $id.');
266    if (!ignoreId && id != node.id)
267      return fail('expected node id $id but found id ${node.id}.');
268
269    final SemanticsData nodeData = node.getSemanticsData();
270
271    final int flagsBitmask = flags is int
272      ? flags
273      : flags.fold<int>(0, (int bitmask, SemanticsFlag flag) => bitmask | flag.index);
274    if (flagsBitmask != nodeData.flags)
275      return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.');
276
277    final int actionsBitmask = actions is int
278        ? actions
279        : actions.fold<int>(0, (int bitmask, SemanticsAction action) => bitmask | action.index);
280    if (actionsBitmask != nodeData.actions)
281      return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
282
283    if (label != nodeData.label)
284      return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".');
285    if (value != nodeData.value)
286      return fail('expected node id $id to have value "$value" but found value "${nodeData.value}".');
287    if (increasedValue != nodeData.increasedValue)
288      return fail('expected node id $id to have increasedValue "$increasedValue" but found value "${nodeData.increasedValue}".');
289    if (decreasedValue != nodeData.decreasedValue)
290      return fail('expected node id $id to have decreasedValue "$decreasedValue" but found value "${nodeData.decreasedValue}".');
291    if (hint != nodeData.hint)
292      return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".');
293    if (textDirection != null && textDirection != nodeData.textDirection)
294      return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".');
295    if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '' || node.increasedValue != '' || node.decreasedValue != '') && nodeData.textDirection == null)
296      return fail('expected node id $id, which has a label, value, or hint, to have a textDirection, but it did not.');
297    if (!ignoreRect && rect != nodeData.rect)
298      return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
299    if (!ignoreTransform && transform != nodeData.transform)
300      return fail('expected node id $id to have transform $transform but found transform:\n${nodeData.transform}.');
301    if (elevation != null && elevation != nodeData.elevation) {
302      return fail('expected node id $id to have elevation $elevation but found elevation:\n${nodeData.elevation}.');
303    }
304    if (thickness != null && thickness != nodeData.thickness) {
305      return fail('expected node id $id to have thickness $thickness but found thickness:\n${nodeData.thickness}.');
306    }
307    if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) {
308      return fail('expected node id $id to have textSelection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
309    }
310    if (scrollIndex != null && scrollIndex != nodeData.scrollIndex) {
311      return fail('expected node id $id to have scrollIndex $scrollIndex but found scrollIndex ${nodeData.scrollIndex}.');
312    }
313    if (scrollChildren != null && scrollChildren != nodeData.scrollChildCount) {
314      return fail('expected node id $id to have scrollIndex $scrollChildren but found scrollIndex ${nodeData.scrollChildCount}.');
315    }
316    final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
317    if (children.length != childrenCount)
318      return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $childrenCount.');
319
320    if (children.isEmpty)
321      return true;
322    bool result = true;
323    final Iterator<TestSemantics> it = children.iterator;
324    for (final SemanticsNode child in node.debugListChildrenInOrder(childOrder)) {
325      it.moveNext();
326      final bool childMatches = it.current._matches(
327        child,
328        matchState,
329        ignoreRect: ignoreRect,
330        ignoreTransform: ignoreTransform,
331        ignoreId: ignoreId,
332        childOrder: childOrder,
333      );
334      if (!childMatches) {
335        result = false;
336        return false;
337      }
338    }
339    if (it.moveNext()) {
340      return false;
341    }
342    return result;
343  }
344
345  @override
346  String toString([ int indentAmount = 0 ]) {
347    final String indent = '  ' * indentAmount;
348    final StringBuffer buf = StringBuffer();
349    buf.writeln('$indent$runtimeType(');
350    if (id != null)
351      buf.writeln('$indent  id: $id,');
352    if (flags is int && flags != 0 || flags is List<SemanticsFlag> && flags.isNotEmpty)
353      buf.writeln('$indent  flags: ${SemanticsTester._flagsToSemanticsFlagExpression(flags)},');
354    if (actions is int && actions != 0 || actions is List<SemanticsAction> && actions.isNotEmpty)
355      buf.writeln('$indent  actions: ${SemanticsTester._actionsToSemanticsActionExpression(actions)},');
356    if (label != null && label != '')
357      buf.writeln('$indent  label: \'$label\',');
358    if (value != null && value != '')
359      buf.writeln('$indent  value: \'$value\',');
360    if (increasedValue != null && increasedValue != '')
361      buf.writeln('$indent  increasedValue: \'$increasedValue\',');
362    if (decreasedValue != null && decreasedValue != '')
363      buf.writeln('$indent  decreasedValue: \'$decreasedValue\',');
364    if (hint != null && hint != '')
365      buf.writeln('$indent  hint: \'$hint\',');
366    if (textDirection != null)
367      buf.writeln('$indent  textDirection: $textDirection,');
368    if (textSelection?.isValid == true)
369      buf.writeln('$indent  textSelection:\n[${textSelection.start}, ${textSelection.end}],');
370    if (scrollIndex != null)
371      buf.writeln('$indent scrollIndex: $scrollIndex,');
372    if (rect != null)
373      buf.writeln('$indent  rect: $rect,');
374    if (transform != null)
375      buf.writeln('$indent  transform:\n${transform.toString().trim().split('\n').map<String>((String line) => '$indent    $line').join('\n')},');
376    if (elevation != null)
377      buf.writeln('$indent  elevation: $elevation,');
378    if (thickness != null)
379      buf.writeln('$indent  thickness: $thickness,');
380    buf.writeln('$indent  children: <TestSemantics>[');
381    for (TestSemantics child in children) {
382      buf.writeln('${child.toString(indentAmount + 2)},');
383    }
384    buf.writeln('$indent  ],');
385    buf.write('$indent)');
386    return buf.toString();
387  }
388}
389
390/// Ensures that the given widget tester has a semantics tree to test.
391///
392/// Useful with [hasSemantics] to test the contents of the semantics tree.
393class SemanticsTester {
394  /// Creates a semantics tester for the given widget tester.
395  ///
396  /// You should call [dispose] at the end of a test that creates a semantics
397  /// tester.
398  SemanticsTester(this.tester) {
399    _semanticsHandle = tester.binding.pipelineOwner.ensureSemantics();
400
401    // This _extra_ clean-up is needed for the case when a test fails and
402    // therefore fails to call dispose() explicitly. The test is still required
403    // to call dispose() explicitly, because the semanticsOwner check is
404    // performed irrespective of whether the owner was created via
405    // SemanticsTester or directly. When the test succeeds, this tear-down
406    // becomes a no-op.
407    addTearDown(dispose);
408  }
409
410  /// The widget tester that this object is testing the semantics of.
411  final WidgetTester tester;
412  SemanticsHandle _semanticsHandle;
413
414  /// Release resources held by this semantics tester.
415  ///
416  /// Call this function at the end of any test that uses a semantics tester. It
417  /// is OK to call this function multiple times. If the resources have already
418  /// been released, the subsequent calls have no effect.
419  @mustCallSuper
420  void dispose() {
421    _semanticsHandle?.dispose();
422    _semanticsHandle = null;
423  }
424
425  @override
426  String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode}';
427
428  /// Returns all semantics nodes in the current semantics tree whose properties
429  /// match the non-null arguments.
430  ///
431  /// If multiple arguments are non-null, each of the returned nodes must match
432  /// on all of them.
433  ///
434  /// If `ancestor` is not null, only the descendants of it are returned.
435  Iterable<SemanticsNode> nodesWith({
436    String label,
437    String value,
438    String hint,
439    TextDirection textDirection,
440    List<SemanticsAction> actions,
441    List<SemanticsFlag> flags,
442    double scrollPosition,
443    double scrollExtentMax,
444    double scrollExtentMin,
445    SemanticsNode ancestor,
446  }) {
447    bool checkNode(SemanticsNode node) {
448      if (label != null && node.label != label)
449        return false;
450      if (value != null && node.value != value)
451        return false;
452      if (hint != null && node.hint != hint)
453        return false;
454      if (textDirection != null && node.textDirection != textDirection)
455        return false;
456      if (actions != null) {
457        final int expectedActions = actions.fold<int>(0, (int value, SemanticsAction action) => value | action.index);
458        final int actualActions = node.getSemanticsData().actions;
459        if (expectedActions != actualActions)
460          return false;
461      }
462      if (flags != null) {
463        final int expectedFlags = flags.fold<int>(0, (int value, SemanticsFlag flag) => value | flag.index);
464        final int actualFlags = node.getSemanticsData().flags;
465        if (expectedFlags != actualFlags)
466          return false;
467      }
468      if (scrollPosition != null && !nearEqual(node.scrollPosition, scrollPosition, 0.1))
469        return false;
470      if (scrollExtentMax != null && !nearEqual(node.scrollExtentMax, scrollExtentMax, 0.1))
471        return false;
472      if (scrollExtentMin != null && !nearEqual(node.scrollExtentMin, scrollExtentMin, 0.1))
473        return false;
474      return true;
475    }
476
477    final List<SemanticsNode> result = <SemanticsNode>[];
478    bool visit(SemanticsNode node) {
479      if (checkNode(node)) {
480        result.add(node);
481      }
482      node.visitChildren(visit);
483      return true;
484    }
485    if (ancestor != null) {
486      visit(ancestor);
487    } else {
488      visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode);
489    }
490    return result;
491  }
492
493  /// Generates an expression that creates a [TestSemantics] reflecting the
494  /// current tree of [SemanticsNode]s.
495  ///
496  /// Use this method to generate code for unit tests. It works similar to
497  /// screenshot testing. The very first time you add semantics to a widget you
498  /// verify manually that the widget behaves correctly. You then use this
499  /// method to generate test code for this widget.
500  ///
501  /// Example:
502  ///
503  /// ```dart
504  /// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
505  ///   var semantics = SemanticsTester(tester);
506  ///   await tester.pumpWidget(MyWidget());
507  ///   print(semantics.generateTestSemanticsExpressionForCurrentSemanticsTree());
508  ///   semantics.dispose();
509  /// });
510  /// ```
511  ///
512  /// You can now copy the code printed to the console into a unit test:
513  ///
514  /// ```dart
515  /// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
516  ///   var semantics = SemanticsTester(tester);
517  ///   await tester.pumpWidget(MyWidget());
518  ///   expect(semantics, hasSemantics(
519  ///     // Generated code:
520  ///     TestSemantics(
521  ///       ... properties and child nodes ...
522  ///     ),
523  ///     ignoreRect: true,
524  ///     ignoreTransform: true,
525  ///     ignoreId: true,
526  ///   ));
527  ///   semantics.dispose();
528  /// });
529  /// ```
530  ///
531  /// At this point the unit test should automatically pass because it was
532  /// generated from the actual [SemanticsNode]s. Next time the semantics tree
533  /// changes, the test code may either be updated manually, or regenerated and
534  /// replaced using this method again.
535  ///
536  /// Avoid submitting huge piles of generated test code. This will make test
537  /// code hard to review and it will make it tempting to regenerate test code
538  /// every time and ignore potential regressions. Make sure you do not
539  /// over-test. Prefer breaking your widgets into smaller widgets and test them
540  /// individually.
541  String generateTestSemanticsExpressionForCurrentSemanticsTree(DebugSemanticsDumpOrder childOrder) {
542    final SemanticsNode node = tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode;
543    return _generateSemanticsTestForNode(node, 0, childOrder);
544  }
545
546  static String _flagsToSemanticsFlagExpression(dynamic flags) {
547    Iterable<SemanticsFlag> list;
548    if (flags is int) {
549      list = SemanticsFlag.values.values
550          .where((SemanticsFlag flag) => (flag.index & flags) != 0);
551    } else {
552      list = flags;
553    }
554    return '<SemanticsFlag>[${list.join(', ')}]';
555  }
556
557  static String _tagsToSemanticsTagExpression(Set<SemanticsTag> tags) {
558    return '<SemanticsTag>[${tags.map<String>((SemanticsTag tag) => 'const SemanticsTag(\'${tag.name}\')').join(', ')}]';
559  }
560
561  static String _actionsToSemanticsActionExpression(dynamic actions) {
562    Iterable<SemanticsAction> list;
563    if (actions is int) {
564      list = SemanticsAction.values.values
565          .where((SemanticsAction action) => (action.index & actions) != 0);
566    } else {
567      list = actions;
568    }
569    return '<SemanticsAction>[${list.join(', ')}]';
570  }
571
572  /// Recursively generates [TestSemantics] code for [node] and its children,
573  /// indenting the expression by `indentAmount`.
574  static String _generateSemanticsTestForNode(SemanticsNode node, int indentAmount, DebugSemanticsDumpOrder childOrder) {
575    if (node == null)
576      return 'null';
577    final String indent = '  ' * indentAmount;
578    final StringBuffer buf = StringBuffer();
579    final SemanticsData nodeData = node.getSemanticsData();
580    final bool isRoot = node.id == 0;
581    buf.writeln('TestSemantics${isRoot ? '.root': ''}(');
582    if (!isRoot)
583      buf.writeln('  id: ${node.id},');
584    if (nodeData.tags != null)
585      buf.writeln('  tags: ${_tagsToSemanticsTagExpression(nodeData.tags)},');
586    if (nodeData.flags != 0)
587      buf.writeln('  flags: ${_flagsToSemanticsFlagExpression(nodeData.flags)},');
588    if (nodeData.actions != 0)
589      buf.writeln('  actions: ${_actionsToSemanticsActionExpression(nodeData.actions)},');
590    if (node.label != null && node.label.isNotEmpty) {
591      final String escapedLabel = node.label.replaceAll('\n', r'\n');
592      if (escapedLabel != node.label) {
593        buf.writeln('  label: r\'$escapedLabel\',');
594      } else {
595        buf.writeln('  label: \'$escapedLabel\',');
596      }
597    }
598    if (node.value != null && node.value.isNotEmpty)
599      buf.writeln('  value: \'${node.value}\',');
600    if (node.increasedValue != null && node.increasedValue.isNotEmpty)
601      buf.writeln('  increasedValue: \'${node.increasedValue}\',');
602    if (node.decreasedValue != null && node.decreasedValue.isNotEmpty)
603      buf.writeln('  decreasedValue: \'${node.decreasedValue}\',');
604    if (node.hint != null && node.hint.isNotEmpty)
605      buf.writeln('  hint: \'${node.hint}\',');
606    if (node.textDirection != null)
607      buf.writeln('  textDirection: ${node.textDirection},');
608    if (node.hasChildren) {
609      buf.writeln('  children: <TestSemantics>[');
610      for (final SemanticsNode child in node.debugListChildrenInOrder(childOrder)) {
611        buf
612          ..write(_generateSemanticsTestForNode(child, 2, childOrder))
613          ..writeln(',');
614      }
615      buf.writeln('  ],');
616    }
617
618    buf.write(')');
619    return buf.toString().split('\n').map<String>((String l) => '$indent$l').join('\n');
620  }
621}
622
623class _HasSemantics extends Matcher {
624  const _HasSemantics(
625    this._semantics, {
626    @required this.ignoreRect,
627    @required this.ignoreTransform,
628    @required this.ignoreId,
629    @required this.childOrder,
630  }) : assert(_semantics != null),
631       assert(ignoreRect != null),
632       assert(ignoreId != null),
633       assert(ignoreTransform != null),
634       assert(childOrder != null);
635
636  final TestSemantics _semantics;
637  final bool ignoreRect;
638  final bool ignoreTransform;
639  final bool ignoreId;
640  final DebugSemanticsDumpOrder childOrder;
641
642  @override
643  bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
644    final bool doesMatch = _semantics._matches(
645      item.tester.binding.pipelineOwner.semanticsOwner?.rootSemanticsNode,
646      matchState,
647      ignoreTransform: ignoreTransform,
648      ignoreRect: ignoreRect,
649      ignoreId: ignoreId,
650      childOrder: childOrder,
651    );
652    if (!doesMatch) {
653      matchState['would-match'] = item.generateTestSemanticsExpressionForCurrentSemanticsTree(childOrder);
654    }
655    if (item.tester.binding.pipelineOwner.semanticsOwner == null) {
656      matchState['additional-notes'] = '(Check that the SemanticsTester has not been disposed early.)';
657    }
658    return doesMatch;
659  }
660
661  @override
662  Description describe(Description description) {
663    return description.add('semantics node matching:\n$_semantics');
664  }
665
666  String _indent(String text) {
667    return text.toString().trimRight().split('\n').map<String>((String line) => '  $line').join('\n');
668  }
669
670  @override
671  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
672    Description result = mismatchDescription
673      .add('${matchState[TestSemantics]}\n')
674      .add('Current SemanticsNode tree:\n')
675      .add(_indent(RendererBinding.instance?.renderView?.debugSemantics?.toStringDeep(childOrder: childOrder)))
676      .add('\n')
677      .add('The semantics tree would have matched the following configuration:\n')
678      .add(_indent(matchState['would-match']));
679    if (matchState.containsKey('additional-notes')) {
680      result = result
681        .add('\n')
682        .add(matchState['additional-notes']);
683    }
684    return result;
685  }
686}
687
688/// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics.
689Matcher hasSemantics(
690  TestSemantics semantics, {
691  bool ignoreRect = false,
692  bool ignoreTransform = false,
693  bool ignoreId = false,
694  DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder,
695}) {
696  return _HasSemantics(
697    semantics,
698    ignoreRect: ignoreRect,
699    ignoreTransform: ignoreTransform,
700    ignoreId: ignoreId,
701    childOrder: childOrder,
702  );
703}
704
705class _IncludesNodeWith extends Matcher {
706  const _IncludesNodeWith({
707    this.label,
708    this.value,
709    this.hint,
710    this.textDirection,
711    this.actions,
712    this.flags,
713    this.scrollPosition,
714    this.scrollExtentMax,
715    this.scrollExtentMin,
716}) : assert(label != null || value != null || actions != null || flags != null || scrollPosition != null || scrollExtentMax != null || scrollExtentMin != null);
717
718  final String label;
719  final String value;
720  final String hint;
721  final TextDirection textDirection;
722  final List<SemanticsAction> actions;
723  final List<SemanticsFlag> flags;
724  final double scrollPosition;
725  final double scrollExtentMax;
726  final double scrollExtentMin;
727
728  @override
729  bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
730    return item.nodesWith(
731      label: label,
732      value: value,
733      hint: hint,
734      textDirection: textDirection,
735      actions: actions,
736      flags: flags,
737      scrollPosition: scrollPosition,
738      scrollExtentMax: scrollExtentMax,
739      scrollExtentMin: scrollExtentMin,
740    ).isNotEmpty;
741  }
742
743  @override
744  Description describe(Description description) {
745    return description.add('includes node with $_configAsString');
746  }
747
748  @override
749  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
750    return mismatchDescription.add('could not find node with $_configAsString.\n$_matcherHelp');
751  }
752
753  String get _configAsString {
754    final List<String> strings = <String>[];
755    if (label != null)
756      strings.add('label "$label"');
757    if (value != null)
758      strings.add('value "$value"');
759    if (hint != null)
760      strings.add('hint "$hint"');
761    if (textDirection != null)
762      strings.add(' (${describeEnum(textDirection)})');
763    if (actions != null)
764      strings.add('actions "${actions.join(', ')}"');
765    if (flags != null)
766      strings.add('flags "${flags.join(', ')}"');
767    if (scrollPosition != null)
768      strings.add('scrollPosition "$scrollPosition"');
769    if (scrollExtentMax != null)
770      strings.add('scrollExtentMax "$scrollExtentMax"');
771    if (scrollExtentMin != null)
772      strings.add('scrollExtentMin "$scrollExtentMin"');
773    return strings.join(', ');
774  }
775}
776
777/// Asserts that a node in the semantics tree of [SemanticsTester] has `label`,
778/// `textDirection`, and `actions`.
779///
780/// If null is provided for an argument, it will match against any value.
781Matcher includesNodeWith({
782  String label,
783  String value,
784  String hint,
785  TextDirection textDirection,
786  List<SemanticsAction> actions,
787  List<SemanticsFlag> flags,
788  double scrollPosition,
789  double scrollExtentMax,
790  double scrollExtentMin,
791}) {
792  return _IncludesNodeWith(
793    label: label,
794    value: value,
795    hint: hint,
796    textDirection: textDirection,
797    actions: actions,
798    flags: flags,
799    scrollPosition: scrollPosition,
800    scrollExtentMax: scrollExtentMax,
801    scrollExtentMin: scrollExtentMin,
802  );
803}
804