semantics_tester.dart 27.6 KB
Newer Older
1 2 3 4
// 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.

5
import 'dart:ui' show SemanticsFlag;
6

7
import 'package:flutter/foundation.dart';
8
import 'package:flutter/physics.dart';
9 10 11 12 13 14
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';

export 'package:flutter/rendering.dart' show SemanticsData;

Ian Hickson's avatar
Ian Hickson committed
15 16
const 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 19 20 21
/// Test semantics data that is compared against real semantics tree.
///
/// Useful with [hasSemantics] and [SemanticsTester] to test the contents of the
/// semantics tree.
class TestSemantics {
22
  /// Creates an object with some test semantics data.
23
  ///
24 25 26 27 28 29 30 31 32 33 34 35
  /// The [id] field is required. The root node has an id of zero. Other nodes
  /// are given a unique id when they are created, in a predictable fashion, and
  /// so these values can be hard-coded.
  ///
  /// The [rect] field is required and has no default. Convenient values are
  /// available:
  ///
  ///  * [TestSemantics.rootRect]: 2400x1600, the test screen's size in physical
  ///    pixels, useful for the node with id zero.
  ///
  ///  * [TestSemantics.fullScreen] 800x600, the test screen's size in logical
  ///    pixels, useful for other full-screen widgets.
36
  TestSemantics({
37
    this.id,
38 39 40
    this.flags: 0,
    this.actions: 0,
    this.label: '',
41
    this.value: '',
42 43
    this.increasedValue: '',
    this.decreasedValue: '',
44
    this.hint: '',
Ian Hickson's avatar
Ian Hickson committed
45
    this.textDirection,
46 47
    this.nextNodeId: -1,
    this.previousNodeId: -1,
48
    this.rect,
49
    this.transform,
50
    this.textSelection,
51
    this.children: const <TestSemantics>[],
52
    Iterable<SemanticsTag> tags,
53
  }) : assert(flags is int || flags is List<SemanticsFlag>),
54
       assert(actions is int || actions is List<SemanticsAction>),
55
       assert(label != null),
56
       assert(value != null),
57 58
       assert(increasedValue != null),
       assert(decreasedValue != null),
59
       assert(hint != null),
60
       assert(children != null),
61 62
       assert(nextNodeId != null),
       assert(previousNodeId != null),
63
       tags = tags?.toSet() ?? new Set<SemanticsTag>();
64 65 66 67 68 69 70

  /// Creates an object with some test semantics data, with the [id] and [rect]
  /// set to the appropriate values for the root node.
  TestSemantics.root({
    this.flags: 0,
    this.actions: 0,
    this.label: '',
71
    this.value: '',
72 73
    this.increasedValue: '',
    this.decreasedValue: '',
74
    this.hint: '',
Ian Hickson's avatar
Ian Hickson committed
75
    this.textDirection,
76
    this.transform,
77
    this.textSelection,
78
    this.children: const <TestSemantics>[],
79
    Iterable<SemanticsTag> tags,
80
  }) : id = 0,
81
       assert(flags is int || flags is List<SemanticsFlag>),
82
       assert(actions is int || actions is List<SemanticsAction>),
83
       assert(label != null),
84 85
       assert(increasedValue != null),
       assert(decreasedValue != null),
86 87
       assert(value != null),
       assert(hint != null),
88
       rect = TestSemantics.rootRect,
89
       assert(children != null),
90 91
       nextNodeId = null,
       previousNodeId = null,
92
       tags = tags?.toSet() ?? new Set<SemanticsTag>();
93 94 95 96 97 98 99 100 101 102 103

  /// Creates an object with some test semantics data, with the [id] and [rect]
  /// set to the appropriate values for direct children of the root node.
  ///
  /// The [transform] is set to a 3.0 scale (to account for the
  /// [Window.devicePixelRatio] being 3.0 on the test pseudo-device).
  ///
  /// The [rect] field is required and has no default. The
  /// [TestSemantics.fullScreen] property may be useful as a value; it describes
  /// an 800x600 rectangle, which is the test screen's size in logical pixels.
  TestSemantics.rootChild({
104
    this.id,
105 106 107
    this.flags: 0,
    this.actions: 0,
    this.label: '',
108 109
    this.hint: '',
    this.value: '',
110 111
    this.increasedValue: '',
    this.decreasedValue: '',
Ian Hickson's avatar
Ian Hickson committed
112
    this.textDirection,
113 114
    this.nextNodeId: -1,
    this.previousNodeId: -1,
115
    this.rect,
116
    Matrix4 transform,
117
    this.textSelection,
118
    this.children: const <TestSemantics>[],
119
    Iterable<SemanticsTag> tags,
120
  }) : assert(flags is int || flags is List<SemanticsFlag>),
121
       assert(actions is int || actions is List<SemanticsAction>),
122
       assert(label != null),
123
       assert(value != null),
124 125
       assert(increasedValue != null),
       assert(decreasedValue != null),
126
       assert(hint != null),
127
       transform = _applyRootChildScale(transform),
128
       assert(children != null),
129 130
       assert(nextNodeId != null),
       assert(previousNodeId != null),
131
       tags = tags?.toSet() ?? new Set<SemanticsTag>();
132 133 134 135 136 137 138

  /// The unique identifier for this node.
  ///
  /// The root node has an id of zero. Other nodes are given a unique id when
  /// they are created.
  final int id;

139
  /// The [SemanticsFlag]s set on this node.
140 141
  ///
  /// There are two ways to specify this property: as an `int` that encodes the
142
  /// flags as a bit field, or as a `List<SemanticsFlag>` that are _on_.
143
  ///
144
  /// Using `List<SemanticsFlag>` is recommended due to better readability.
145
  final dynamic flags;
146

147 148 149 150 151 152 153 154 155 156
  /// The [SemanticsAction]s set on this node.
  ///
  /// There are two ways to specify this property: as an `int` that encodes the
  /// actions as a bit field, or as a `List<SemanticsAction>`.
  ///
  /// Using `List<SemanticsAction>` is recommended due to better readability.
  ///
  /// The tester does not check the function corresponding to the action, but
  /// only its existence.
  final dynamic actions;
157 158 159 160

  /// A textual description of this node.
  final String label;

161 162 163
  /// A textual description for the value of this node.
  final String value;

164 165 166 167 168 169 170 171
  /// What [value] will become after [SemanticsAction.increase] has been
  /// performed.
  final String increasedValue;

  /// What [value] will become after [SemanticsAction.decrease] has been
  /// performed.
  final String decreasedValue;

172 173 174 175
  /// A brief textual description of the result of the action that can be
  /// performed on this node.
  final String hint;

Ian Hickson's avatar
Ian Hickson committed
176 177 178 179 180 181 182
  /// The reading direction of the [label].
  ///
  /// Even if this is not set, the [hasSemantics] matcher will verify that if a
  /// label is present on the [SemanticsNode], a [SemanticsNode.textDirection]
  /// is also set.
  final TextDirection textDirection;

183 184 185 186
  /// The ID of the node that is next in the semantics traversal order after
  /// this node.
  final int nextNodeId;

187
  /// The ID of the node that is previous in the semantics traversal order before
188
  /// this node.
189
  final int previousNodeId;
190

191
  /// The bounding box for this node in its coordinate system.
192
  ///
193 194 195 196 197 198 199
  /// Convenient values are available:
  ///
  ///  * [TestSemantics.rootRect]: 2400x1600, the test screen's size in physical
  ///    pixels, useful for the node with id zero.
  ///
  ///  * [TestSemantics.fullScreen] 800x600, the test screen's size in logical
  ///    pixels, useful for other full-screen widgets.
200 201
  final Rect rect;

202 203 204 205 206 207 208 209 210 211 212
  /// The test screen's size in physical pixels, typically used as the [rect]
  /// for the node with id zero.
  ///
  /// See also [new TestSemantics.root], which uses this value to describe the
  /// root node.
  static final Rect rootRect = new Rect.fromLTWH(0.0, 0.0, 2400.0, 1800.0);

  /// The test screen's size in logical pixels, useful for the [rect] of
  /// full-screen widgets other than the root node.
  static final Rect fullScreen = new Rect.fromLTWH(0.0, 0.0, 800.0, 600.0);

213 214 215
  /// The transform from this node's coordinate system to its parent's coordinate system.
  ///
  /// By default, the transform is null, which represents the identity
216
  /// transformation (i.e., that this node has the same coordinate system as its
217 218 219
  /// parent).
  final Matrix4 transform;

220 221
  final TextSelection textSelection;

222 223 224 225 226 227 228
  static Matrix4 _applyRootChildScale(Matrix4 transform) {
    final Matrix4 result = new Matrix4.diagonal3Values(3.0, 3.0, 1.0);
    if (transform != null)
      result.multiply(transform);
    return result;
  }

229 230 231
  /// The children of this node.
  final List<TestSemantics> children;

232 233
  /// The tags of this node.
  final Set<SemanticsTag> tags;
234

235
  bool _matches(SemanticsNode node, Map<dynamic, dynamic> matchState, { bool ignoreRect: false, bool ignoreTransform: false, bool ignoreId: false }) {
236
    final SemanticsData nodeData = node.getSemanticsData();
Ian Hickson's avatar
Ian Hickson committed
237 238

    bool fail(String message) {
239
      matchState[TestSemantics] = '$message';
240 241
      return false;
    }
Ian Hickson's avatar
Ian Hickson committed
242 243 244

    if (node == null)
      return fail('could not find node with id $id.');
245
    if (!ignoreId && id != node.id)
Ian Hickson's avatar
Ian Hickson committed
246
      return fail('expected node id $id but found id ${node.id}.');
247 248 249

    final int flagsBitmask = flags is int
      ? flags
250
      : flags.fold<int>(0, (int bitmask, SemanticsFlag flag) => bitmask | flag.index);
251
    if (flagsBitmask != nodeData.flags)
Ian Hickson's avatar
Ian Hickson committed
252
      return fail('expected node id $id to have flags $flags but found flags ${nodeData.flags}.');
253 254 255 256 257

    final int actionsBitmask = actions is int
        ? actions
        : actions.fold<int>(0, (int bitmask, SemanticsAction action) => bitmask | action.index);
    if (actionsBitmask != nodeData.actions)
Ian Hickson's avatar
Ian Hickson committed
258
      return fail('expected node id $id to have actions $actions but found actions ${nodeData.actions}.');
259

Ian Hickson's avatar
Ian Hickson committed
260 261
    if (label != nodeData.label)
      return fail('expected node id $id to have label "$label" but found label "${nodeData.label}".');
262 263
    if (value != nodeData.value)
      return fail('expected node id $id to have value "$value" but found value "${nodeData.value}".');
264 265 266 267
    if (increasedValue != nodeData.increasedValue)
      return fail('expected node id $id to have increasedValue "$increasedValue" but found value "${nodeData.increasedValue}".');
    if (decreasedValue != nodeData.decreasedValue)
      return fail('expected node id $id to have decreasedValue "$decreasedValue" but found value "${nodeData.decreasedValue}".');
268 269
    if (hint != nodeData.hint)
      return fail('expected node id $id to have hint "$hint" but found hint "${nodeData.hint}".');
Ian Hickson's avatar
Ian Hickson committed
270 271
    if (textDirection != null && textDirection != nodeData.textDirection)
      return fail('expected node id $id to have textDirection "$textDirection" but found "${nodeData.textDirection}".');
272
    if (!ignoreId && nextNodeId != nodeData.nextNodeId)
273
      return fail('expected node id $id to have nextNodeId "$nextNodeId" but found "${nodeData.nextNodeId}".');
274
    if (!ignoreId && previousNodeId != nodeData.previousNodeId)
275
      return fail('expected node id $id to have previousNodeId "$previousNodeId" but found "${nodeData.previousNodeId}".');
276
    if ((nodeData.label != '' || nodeData.value != '' || nodeData.hint != '' || node.increasedValue != '' || node.decreasedValue != '') && nodeData.textDirection == null)
277
      return fail('expected node id $id, which has a label, value, or hint, to have a textDirection, but it did not.');
Ian Hickson's avatar
Ian Hickson committed
278 279 280 281
    if (!ignoreRect && rect != nodeData.rect)
      return fail('expected node id $id to have rect $rect but found rect ${nodeData.rect}.');
    if (!ignoreTransform && transform != nodeData.transform)
      return fail('expected node id $id to have transform $transform but found transform:\n${nodeData.transform}.');
282
    if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) {
283
      return fail('expected node id $id to have textSelection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
284
    }
Ian Hickson's avatar
Ian Hickson committed
285 286 287 288
    final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
    if (children.length != childrenCount)
      return fail('expected node id $id to have ${children.length} child${ children.length == 1 ? "" : "ren" } but found $childrenCount.');

289 290 291
    if (children.isEmpty)
      return true;
    bool result = true;
292
    final Iterator<TestSemantics> it = children.iterator;
293 294
    node.visitChildren((SemanticsNode node) {
      it.moveNext();
295
      if (!it.current._matches(node, matchState, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform, ignoreId: ignoreId)) {
296 297 298 299 300 301 302
        result = false;
        return false;
      }
      return true;
    });
    return result;
  }
Ian Hickson's avatar
Ian Hickson committed
303 304

  @override
305 306 307
  String toString([int indentAmount = 0]) {
    final String indent = '  ' * indentAmount;
    final StringBuffer buf = new StringBuffer();
308
    buf.writeln('${indent}new $runtimeType(');
309
    if (id != null)
310
      buf.writeln('$indent  id: $id,');
311 312
    if (flags is int && flags != 0 || flags is List<SemanticsFlag> && flags.isNotEmpty)
      buf.writeln('$indent  flags: ${SemanticsTester._flagsToSemanticsFlagExpression(flags)},');
313 314 315 316 317 318 319 320 321 322 323 324
    if (actions is int && actions != 0 || actions is List<SemanticsAction> && actions.isNotEmpty)
      buf.writeln('$indent  actions: ${SemanticsTester._actionsToSemanticsActionExpression(actions)},');
    if (label != null && label != '')
      buf.writeln('$indent  label: \'$label\',');
    if (value != null && value != '')
      buf.writeln('$indent  value: \'$value\',');
    if (increasedValue != null && increasedValue != '')
      buf.writeln('$indent  increasedValue: \'$increasedValue\',');
    if (decreasedValue != null && decreasedValue != '')
      buf.writeln('$indent  decreasedValue: \'$decreasedValue\',');
    if (hint != null && hint != '')
      buf.writeln('$indent  hint: \'$hint\',');
325
    if (textDirection != null)
326
      buf.writeln('$indent  textDirection: $textDirection,');
327 328
    if (nextNodeId != null)
      buf.writeln('$indent  nextNodeId: $nextNodeId,');
329 330
    if (previousNodeId != null)
      buf.writeln('$indent  previousNodeId: $previousNodeId,');
331 332
    if (textSelection?.isValid == true)
      buf.writeln('$indent  textSelection:\n[${textSelection.start}, ${textSelection.end}],');
333
    if (rect != null)
334
      buf.writeln('$indent  rect: $rect,');
335
    if (transform != null)
336 337
      buf.writeln('$indent  transform:\n${transform.toString().trim().split('\n').map((String line) => '$indent    $line').join('\n')},');
    buf.writeln('$indent  children: <TestSemantics>[');
338
    for (TestSemantics child in children) {
339
      buf.writeln('${child.toString(indentAmount + 2)},');
340
    }
341 342
    buf.writeln('$indent  ],');
    buf.write('$indent)');
343
    return buf.toString();
Ian Hickson's avatar
Ian Hickson committed
344
  }
345 346 347 348 349 350 351 352 353 354 355 356
}

/// Ensures that the given widget tester has a semantics tree to test.
///
/// Useful with [hasSemantics] to test the contents of the semantics tree.
class SemanticsTester {
  /// Creates a semantics tester for the given widget tester.
  ///
  /// You should call [dispose] at the end of a test that creates a semantics
  /// tester.
  SemanticsTester(this.tester) {
    _semanticsHandle = tester.binding.pipelineOwner.ensureSemantics();
357 358 359 360 361 362 363 364

    // This _extra_ clean-up is needed for the case when a test fails and
    // therefore fails to call dispose() explicitly. The test is still required
    // to call dispose() explicitly, because the semanticsOwner check is
    // performed irrespective of whether the owner was created via
    // SemanticsTester or directly. When the test succeeds, this tear-down
    // becomes a no-op.
    addTearDown(dispose);
365 366 367 368 369 370 371 372
  }

  /// The widget tester that this object is testing the semantics of.
  final WidgetTester tester;
  SemanticsHandle _semanticsHandle;

  /// Release resources held by this semantics tester.
  ///
373 374 375
  /// Call this function at the end of any test that uses a semantics tester. It
  /// is OK to call this function multiple times. If the resources have already
  /// been released, the subsequent calls have no effect.
376 377
  @mustCallSuper
  void dispose() {
378
    _semanticsHandle?.dispose();
379 380 381 382
    _semanticsHandle = null;
  }

  @override
Ian Hickson's avatar
Ian Hickson committed
383
  String toString() => 'SemanticsTester for ${tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode}';
384

385 386 387 388 389 390 391
  /// Returns all semantics nodes in the current semantics tree whose properties
  /// match the non-null arguments.
  ///
  /// If multiple arguments are non-null, each of the returned nodes must match
  /// on all of them.
  ///
  /// If `ancestor` is not null, only the descendants of it are returned.
392 393 394 395 396
  Iterable<SemanticsNode> nodesWith({
    String label,
    String value,
    TextDirection textDirection,
    List<SemanticsAction> actions,
397
    List<SemanticsFlag> flags,
398 399 400
    double scrollPosition,
    double scrollExtentMax,
    double scrollExtentMin,
401
    SemanticsNode ancestor,
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
  }) {
    bool checkNode(SemanticsNode node) {
      if (label != null && node.label != label)
        return false;
      if (value != null && node.value != value)
        return false;
      if (textDirection != null && node.textDirection != textDirection)
        return false;
      if (actions != null) {
        final int expectedActions = actions.fold(0, (int value, SemanticsAction action) => value | action.index);
        final int actualActions = node.getSemanticsData().actions;
        if (expectedActions != actualActions)
          return false;
      }
      if (flags != null) {
417
        final int expectedFlags = flags.fold(0, (int value, SemanticsFlag flag) => value | flag.index);
418 419 420 421
        final int actualFlags = node.getSemanticsData().flags;
        if (expectedFlags != actualFlags)
          return false;
      }
422 423 424 425 426 427
      if (scrollPosition != null && !nearEqual(node.scrollPosition, scrollPosition, 0.1))
        return false;
      if (scrollExtentMax != null && !nearEqual(node.scrollExtentMax, scrollExtentMax, 0.1))
        return false;
      if (scrollExtentMin != null && !nearEqual(node.scrollExtentMin, scrollExtentMin, 0.1))
        return false;
428 429 430 431 432 433 434 435 436 437 438
      return true;
    }

    final List<SemanticsNode> result = <SemanticsNode>[];
    bool visit(SemanticsNode node) {
      if (checkNode(node)) {
        result.add(node);
      }
      node.visitChildren(visit);
      return true;
    }
439 440 441 442 443
    if (ancestor != null) {
      visit(ancestor);
    } else {
      visit(tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode);
    }
444 445
    return result;
  }
446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498

  /// Generates an expression that creates a [TestSemantics] reflecting the
  /// current tree of [SemanticsNode]s.
  ///
  /// Use this method to generate code for unit tests. It works similar to
  /// screenshot testing. The very first time you add semantics to a widget you
  /// verify manually that the widget behaves correctly. You then use ths method
  /// to generate test code for this widget.
  ///
  /// Example:
  ///
  /// ```dart
  /// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
  ///   var semantics = new SemanticsTester(tester);
  ///   await tester.pumpWidget(new MyWidget());
  ///   print(semantics.generateTestSemanticsExpressionForCurrentSemanticsTree());
  ///   semantics.dispose();
  /// });
  /// ```
  ///
  /// You can now copy the code printed to the console into a unit test:
  ///
  /// ```dart
  /// testWidgets('generate code for MyWidget', (WidgetTester tester) async {
  ///   var semantics = new SemanticsTester(tester);
  ///   await tester.pumpWidget(new MyWidget());
  ///   expect(semantics, hasSemantics(
  ///     // Generated code:
  ///     new TestSemantics(
  ///       ... properties and child nodes ...
  ///     ),
  ///     ignoreRect: true,
  ///     ignoreTransform: true,
  ///     ignoreId: true,
  ///   ));
  ///   semantics.dispose();
  /// });
  ///
  /// At this point the unit test should automatically pass because it was
  /// generated from the actual [SemanticsNode]s. Next time the semantics tree
  /// changes, the test code may either be updated manually, or regenerated and
  /// replaced using this method again.
  ///
  /// Avoid submitting huge piles of generated test code. This will make test
  /// code hard to review and it will make it tempting to regenerate test code
  /// every time and ignore potential regressions. Make sure you do not
  /// over-test. Prefer breaking your widgets into smaller widgets and test them
  /// individually.
  String generateTestSemanticsExpressionForCurrentSemanticsTree() {
    final SemanticsNode node = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
    return _generateSemanticsTestForNode(node, 0);
  }

499 500
  static String _flagsToSemanticsFlagExpression(dynamic flags) {
    Iterable<SemanticsFlag> list;
501
    if (flags is int) {
502 503
      list = SemanticsFlag.values.values
          .where((SemanticsFlag flag) => (flag.index & flags) != 0);
504 505 506
    } else {
      list = flags;
    }
507
    return '<SemanticsFlag>[${list.join(', ')}]';
508 509
  }

510 511 512 513
  static String _tagsToSemanticsTagExpression(Set<SemanticsTag> tags) {
    return '<SemanticsTag>[${tags.map((SemanticsTag tag) => 'const SemanticsTag(\'${tag.name}\')').join(', ')}]';
  }

514 515 516 517 518 519 520 521 522
  static String _actionsToSemanticsActionExpression(dynamic actions) {
    Iterable<SemanticsAction> list;
    if (actions is int) {
      list = SemanticsAction.values.values
          .where((SemanticsAction action) => (action.index & actions) != 0);
    } else {
      list = actions;
    }
    return '<SemanticsAction>[${list.join(', ')}]';
523 524 525 526
  }

  /// Recursively generates [TestSemantics] code for [node] and its children,
  /// indenting the expression by `indentAmount`.
527
  static String _generateSemanticsTestForNode(SemanticsNode node, int indentAmount) {
528 529 530
    final String indent = '  ' * indentAmount;
    final StringBuffer buf = new StringBuffer();
    final SemanticsData nodeData = node.getSemanticsData();
531 532 533 534 535 536
    final bool isRoot = node.id == 0;
    buf.writeln('new TestSemantics${isRoot ? '.root': ''}(');
    if (!isRoot)
      buf.writeln('  id: ${node.id},');
    if (nodeData.tags != null)
      buf.writeln('  tags: ${_tagsToSemanticsTagExpression(nodeData.tags)},');
537
    if (nodeData.flags != 0)
538
      buf.writeln('  flags: ${_flagsToSemanticsFlagExpression(nodeData.flags)},');
539
    if (nodeData.actions != 0)
540
      buf.writeln('  actions: ${_actionsToSemanticsActionExpression(nodeData.actions)},');
541 542
    if (node.label != null && node.label.isNotEmpty) {
      final String escapedLabel = node.label.replaceAll('\n', r'\n');
543
      if (escapedLabel != node.label) {
544 545 546 547 548
        buf.writeln('  label: r\'$escapedLabel\',');
      } else {
        buf.writeln('  label: \'$escapedLabel\',');
      }
    }
549
    if (node.value != null && node.value.isNotEmpty)
550
      buf.writeln('  value: \'${node.value}\',');
551
    if (node.increasedValue != null && node.increasedValue.isNotEmpty)
552
      buf.writeln('  increasedValue: \'${node.increasedValue}\',');
553
    if (node.decreasedValue != null && node.decreasedValue.isNotEmpty)
554
      buf.writeln('  decreasedValue: \'${node.decreasedValue}\',');
555
    if (node.hint != null && node.hint.isNotEmpty)
556
      buf.writeln('  hint: \'${node.hint}\',');
557 558
    if (node.textDirection != null)
      buf.writeln('  textDirection: ${node.textDirection},');
559
    if (node.nextNodeId != null && node.nextNodeId != -1)
560
      buf.writeln('  nextNodeId: ${node.nextNodeId},');
561
    if (node.previousNodeId != null && node.previousNodeId != -1)
562
      buf.writeln('  previousNodeId: ${node.previousNodeId},');
563 564 565 566 567 568 569 570 571 572 573 574 575 576 577

    if (node.hasChildren) {
      buf.writeln('  children: <TestSemantics>[');
      node.visitChildren((SemanticsNode child) {
        buf
          ..write(_generateSemanticsTestForNode(child, 2))
          ..writeln(',');
        return true;
      });
      buf.writeln('  ],');
    }

    buf.write(')');
    return buf.toString().split('\n').map((String l) => '$indent$l').join('\n');
  }
578 579 580
}

class _HasSemantics extends Matcher {
581
  const _HasSemantics(this._semantics, { this.ignoreRect: false, this.ignoreTransform: false, this.ignoreId: false }) : assert(_semantics != null), assert(ignoreRect != null), assert(ignoreId != null), assert(ignoreTransform != null);
582 583

  final TestSemantics _semantics;
584 585
  final bool ignoreRect;
  final bool ignoreTransform;
586
  final bool ignoreId;
587 588

  @override
589
  bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
590 591 592 593 594
    final bool doesMatch = _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState, ignoreTransform: ignoreTransform, ignoreRect: ignoreRect, ignoreId: ignoreId);
    if (!doesMatch) {
      matchState['would-match'] = item.generateTestSemanticsExpressionForCurrentSemanticsTree();
    }
    return doesMatch;
595 596 597 598
  }

  @override
  Description describe(Description description) {
599
    return description.add('semantics node matching:\n$_semantics');
600 601 602 603
  }

  @override
  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
604 605
    return mismatchDescription
        .add('${matchState[TestSemantics]}\n')
606 607 608 609
        .add('Current SemanticsNode tree:\n')
        .add(RendererBinding.instance?.renderView?.debugSemantics?.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest))
        .add('The semantics tree would have matched the following configuration:\n')
        .add(matchState['would-match']);
610 611 612 613
  }
}

/// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics.
614 615 616
Matcher hasSemantics(TestSemantics semantics, {
  bool ignoreRect: false,
  bool ignoreTransform: false,
617 618
  bool ignoreId: false,
}) => new _HasSemantics(semantics, ignoreRect: ignoreRect, ignoreTransform: ignoreTransform, ignoreId: ignoreId);
619

620 621 622
class _IncludesNodeWith extends Matcher {
  const _IncludesNodeWith({
    this.label,
623
    this.value,
Ian Hickson's avatar
Ian Hickson committed
624
    this.textDirection,
625
    this.actions,
626
    this.flags,
627 628 629 630
    this.scrollPosition,
    this.scrollExtentMax,
    this.scrollExtentMin,
}) : assert(label != null || value != null || actions != null || flags != null || scrollPosition != null || scrollExtentMax != null || scrollExtentMin != null);
631

632
  final String label;
633
  final String value;
Ian Hickson's avatar
Ian Hickson committed
634
  final TextDirection textDirection;
635
  final List<SemanticsAction> actions;
636
  final List<SemanticsFlag> flags;
637 638 639
  final double scrollPosition;
  final double scrollExtentMax;
  final double scrollExtentMin;
640 641 642

  @override
  bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
643 644 645 646 647 648
    return item.nodesWith(
      label: label,
      value: value,
      textDirection: textDirection,
      actions: actions,
      flags: flags,
649 650 651
      scrollPosition: scrollPosition,
      scrollExtentMax: scrollExtentMax,
      scrollExtentMin: scrollExtentMin,
652
    ).isNotEmpty;
653 654
  }

655 656
  @override
  Description describe(Description description) {
657
    return description.add('includes node with $_configAsString');
658 659 660 661
  }

  @override
  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
662 663 664 665
    return mismatchDescription.add('could not find node with $_configAsString.\n$_matcherHelp');
  }

  String get _configAsString {
666 667 668
    final List<String> strings = <String>[];
    if (label != null)
      strings.add('label "$label"');
669 670
    if (value != null)
      strings.add('value "$value"');
671 672 673 674 675 676
    if (textDirection != null)
      strings.add(' (${describeEnum(textDirection)})');
    if (actions != null)
      strings.add('actions "${actions.join(', ')}"');
    if (flags != null)
      strings.add('flags "${flags.join(', ')}"');
677 678 679 680 681 682
    if (scrollPosition != null)
      strings.add('scrollPosition "$scrollPosition"');
    if (scrollExtentMax != null)
      strings.add('scrollExtentMax "$scrollExtentMax"');
    if (scrollExtentMin != null)
      strings.add('scrollExtentMin "$scrollExtentMin"');
683
    return strings.join(', ');
684 685 686
  }
}

Ian Hickson's avatar
Ian Hickson committed
687 688
/// Asserts that a node in the semantics tree of [SemanticsTester] has `label`,
/// `textDirection`, and `actions`.
689
///
Ian Hickson's avatar
Ian Hickson committed
690
/// If null is provided for an argument, it will match against any value.
691 692
Matcher includesNodeWith({
  String label,
693
  String value,
694 695
  TextDirection textDirection,
  List<SemanticsAction> actions,
696
  List<SemanticsFlag> flags,
697 698 699
  double scrollPosition,
  double scrollExtentMax,
  double scrollExtentMin,
700
}) {
Ian Hickson's avatar
Ian Hickson committed
701 702
  return new _IncludesNodeWith(
    label: label,
703
    value: value,
Ian Hickson's avatar
Ian Hickson committed
704 705
    textDirection: textDirection,
    actions: actions,
706
    flags: flags,
707 708 709
    scrollPosition: scrollPosition,
    scrollExtentMax: scrollExtentMax,
    scrollExtentMin: scrollExtentMin,
Ian Hickson's avatar
Ian Hickson committed
710 711
  );
}