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