semantics_test.dart 19.9 KB
Newer Older
1 2 3 4 5
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/rendering.dart';
6
import 'package:flutter/semantics.dart';
7
import 'package:vector_math/vector_math_64.dart';
8
import 'package:flutter_test/flutter_test.dart';
9

10
import '../rendering/rendering_tester.dart';
11

12 13

void main() {
14 15 16 17
  setUp(() {
    debugResetSemanticsIdCounter();
  });

18
  group('SemanticsNode', () {
19 20 21
    const SemanticsTag tag1 = SemanticsTag('Tag One');
    const SemanticsTag tag2 = SemanticsTag('Tag Two');
    const SemanticsTag tag3 = SemanticsTag('Tag Three');
22 23

    test('tagging', () {
24
      final SemanticsNode node = SemanticsNode();
25

26 27
      expect(node.isTagged(tag1), isFalse);
      expect(node.isTagged(tag2), isFalse);
28

29
      node.tags = <SemanticsTag>{tag1};
30 31
      expect(node.isTagged(tag1), isTrue);
      expect(node.isTagged(tag2), isFalse);
32

33 34 35
      node.tags.add(tag2);
      expect(node.isTagged(tag1), isTrue);
      expect(node.isTagged(tag2), isTrue);
36 37 38
    });

    test('getSemanticsData includes tags', () {
39
      final Set<SemanticsTag> tags = <SemanticsTag>{tag1, tag2};
40

41 42
      final SemanticsNode node = SemanticsNode()
        ..rect = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0)
43
        ..tags = tags;
44

45
      expect(node.getSemanticsData().tags, tags);
46

47
      tags.add(tag3);
48

49
      final SemanticsConfiguration config = SemanticsConfiguration()
50
        ..isSemanticBoundary = true
51
        ..isMergingSemanticsOfDescendants = true;
52

53 54 55
      node.updateWith(
        config: config,
        childrenInInversePaintOrder: <SemanticsNode>[
56
          SemanticsNode()
57
            ..isMergedIntoParent = true
58
            ..rect = Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
59 60 61
            ..tags = tags,
        ],
      );
62

63
      expect(node.getSemanticsData().tags, tags);
64
    });
65

66
    test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () {
67 68 69
      renderer.pipelineOwner.ensureSemantics();

      TestRender middle;
70
      final TestRender root = TestRender(
71
        hasTapAction: true,
72
        isSemanticBoundary: true,
73
        child: TestRender(
74
          hasLongPressAction: true,
75
          isSemanticBoundary: false,
76
          child: middle = TestRender(
77
            hasScrollLeftAction: true,
78
            isSemanticBoundary: false,
79
            child: TestRender(
80
              hasScrollRightAction: true,
81
              isSemanticBoundary: false,
82
              child: TestRender(
83
                hasScrollUpAction: true,
84
                isSemanticBoundary: true,
85 86 87 88
              ),
            ),
          ),
        ),
89 90 91 92 93 94 95 96
      );

      layout(root);
      pumpFrame(phase: EnginePhase.flushSemantics);

      int expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollLeft.index | SemanticsAction.scrollRight.index;
      expect(root.debugSemantics.getSemanticsData().actions, expectedActions);

97 98 99
      middle
        ..hasScrollLeftAction = false
        ..hasScrollDownAction = true;
100
      middle.markNeedsSemanticsUpdate();
101 102 103 104 105 106

      pumpFrame(phase: EnginePhase.flushSemantics);

      expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollDown.index | SemanticsAction.scrollRight.index;
      expect(root.debugSemantics.getSemanticsData().actions, expectedActions);
    });
107
  });
108 109

  test('toStringDeep() does not throw with transform == null', () {
110 111 112 113 114 115
    final SemanticsNode child1 = SemanticsNode()
      ..rect = Rect.fromLTRB(0.0, 0.0, 5.0, 5.0);
    final SemanticsNode child2 = SemanticsNode()
      ..rect = Rect.fromLTRB(5.0, 0.0, 10.0, 5.0);
    final SemanticsNode root = SemanticsNode()
      ..rect = Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
116 117 118 119
    root.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
    );
120 121 122 123 124

    expect(root.transform, isNull);
    expect(child1.transform, isNull);
    expect(child2.transform, isNull);

125
    expect(
126
      root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversalOrder),
127 128 129 130 131 132 133 134 135 136 137 138 139
      'SemanticsNode#3\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 10.0, 5.0)\n'
      ' │\n'
      ' ├─SemanticsNode#1\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n'
      ' │\n'
      ' └─SemanticsNode#2\n'
      '     STALE\n'
      '     owner: null\n'
140
      '     Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n',
141 142 143
    );
  });

144 145 146 147 148 149 150 151 152 153 154 155
  test('Incompatible OrdinalSortKey throw AssertionError when compared', () {
    // Different types.
    expect(() {
      const OrdinalSortKey(0.0).compareTo(const CustomSortKey(0.0));
    }, throwsAssertionError);

    // Different names.
    expect(() {
      const OrdinalSortKey(0.0, name: 'a').compareTo(const OrdinalSortKey(0.0, name: 'b'));
    }, throwsAssertionError);
  });

156
  test('OrdinalSortKey compares correctly', () {
157 158 159 160 161
    const List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
      <SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(0.0)],
      <SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(1.0)],
      <SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(0.0)],
      <SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(1.0)],
162
    ];
163
    final List<int> expectedResults = <int>[0, -1, 1, 0];
164 165 166 167 168 169 170 171 172
    assert(tests.length == expectedResults.length);
    final List<int> results = <int>[];
    for (List<SemanticsSortKey> tuple in tests) {
      results.add(tuple[0].compareTo(tuple[1]));
    }
    expect(results, orderedEquals(expectedResults));
  });

  test('OrdinalSortKey compares correctly', () {
173 174 175 176 177
    const List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
      <SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(0.0)],
      <SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(1.0)],
      <SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(0.0)],
      <SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(1.0)],
178
    ];
179
    final List<int> expectedResults = <int>[0, -1, 1, 0];
180 181 182 183 184 185 186 187
    assert(tests.length == expectedResults.length);
    final List<int> results = <int>[];
    for (List<SemanticsSortKey> tuple in tests) {
      results.add(tuple[0].compareTo(tuple[1]));
    }
    expect(results, orderedEquals(expectedResults));
  });

188
  test('toStringDeep respects childOrder parameter', () {
189 190 191 192 193 194
    final SemanticsNode child1 = SemanticsNode()
      ..rect = Rect.fromLTRB(15.0, 0.0, 20.0, 5.0);
    final SemanticsNode child2 = SemanticsNode()
      ..rect = Rect.fromLTRB(10.0, 0.0, 15.0, 5.0);
    final SemanticsNode root = SemanticsNode()
      ..rect = Rect.fromLTRB(0.0, 0.0, 20.0, 5.0);
195 196 197 198
    root.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
    );
199
    expect(
200
      root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversalOrder),
201 202 203 204 205
      'SemanticsNode#3\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 20.0, 5.0)\n'
      ' │\n'
206
      ' ├─SemanticsNode#1\n'
207 208
      ' │   STALE\n'
      ' │   owner: null\n'
209
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
210
      ' │\n'
211
      ' └─SemanticsNode#2\n'
212 213
      '     STALE\n'
      '     owner: null\n'
214
      '     Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
215 216 217 218
    );

    expect(
      root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
219 220 221 222 223 224 225 226 227 228 229 230 231
      'SemanticsNode#3\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 20.0, 5.0)\n'
      ' │\n'
      ' ├─SemanticsNode#1\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
      ' │\n'
      ' └─SemanticsNode#2\n'
      '     STALE\n'
      '     owner: null\n'
232
      '     Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
233 234
    );

235 236
    final SemanticsNode child3 = SemanticsNode()
      ..rect = Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
237 238 239
    child3.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[
240 241 242 243
        SemanticsNode()
          ..rect = Rect.fromLTRB(5.0, 0.0, 10.0, 5.0),
        SemanticsNode()
          ..rect = Rect.fromLTRB(0.0, 0.0, 5.0, 5.0),
244 245
      ],
    );
246

247 248
    final SemanticsNode rootComplex = SemanticsNode()
      ..rect = Rect.fromLTRB(0.0, 0.0, 25.0, 5.0);
249 250
    rootComplex.updateWith(
        config: null,
251
        childrenInInversePaintOrder: <SemanticsNode>[child1, child2, child3],
252
    );
253 254

    expect(
255
      rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversalOrder),
256 257 258 259 260
      'SemanticsNode#7\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 25.0, 5.0)\n'
      ' │\n'
261 262 263 264
      ' ├─SemanticsNode#1\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
265 266 267 268 269 270
      ' │\n'
      ' ├─SemanticsNode#2\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n'
      ' │\n'
271 272 273 274 275 276 277 278 279 280 281 282 283
      ' └─SemanticsNode#4\n'
      '   │ STALE\n'
      '   │ owner: null\n'
      '   │ Rect.fromLTRB(0.0, 0.0, 10.0, 5.0)\n'
      '   │\n'
      '   ├─SemanticsNode#5\n'
      '   │   STALE\n'
      '   │   owner: null\n'
      '   │   Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n'
      '   │\n'
      '   └─SemanticsNode#6\n'
      '       STALE\n'
      '       owner: null\n'
284
      '       Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
285 286 287 288
    );

    expect(
      rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
      'SemanticsNode#7\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 25.0, 5.0)\n'
      ' │\n'
      ' ├─SemanticsNode#1\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
      ' │\n'
      ' ├─SemanticsNode#2\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n'
      ' │\n'
      ' └─SemanticsNode#4\n'
      '   │ STALE\n'
      '   │ owner: null\n'
      '   │ Rect.fromLTRB(0.0, 0.0, 10.0, 5.0)\n'
      '   │\n'
      '   ├─SemanticsNode#5\n'
      '   │   STALE\n'
      '   │   owner: null\n'
      '   │   Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n'
      '   │\n'
      '   └─SemanticsNode#6\n'
      '       STALE\n'
      '       owner: null\n'
317
      '       Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
318 319 320 321
    );
  });

  test('debug properties', () {
322
    final SemanticsNode minimalProperties = SemanticsNode();
323
    expect(
324
      minimalProperties.toStringDeep(),
325 326
      'SemanticsNode#1\n'
      '   Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
327
      '   invisible\n',
328 329
    );

330 331
    expect(
      minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
332 333 334 335 336 337
      'SemanticsNode#1\n'
      '   owner: null\n'
      '   isMergedIntoParent: false\n'
      '   mergeAllDescendantsIntoThisNode: false\n'
      '   Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
      '   actions: []\n'
338
      '   customActions: []\n'
339 340
      '   flags: []\n'
      '   invisible\n'
341
      '   isHidden: false\n'
342 343 344 345 346 347
      '   label: ""\n'
      '   value: ""\n'
      '   increasedValue: ""\n'
      '   decreasedValue: ""\n'
      '   hint: ""\n'
      '   textDirection: null\n'
348
      '   sortKey: null\n'
349
      '   platformViewId: null\n'
350 351
      '   scrollChildren: null\n'
      '   scrollIndex: null\n'
352 353 354
      '   scrollExtentMin: null\n'
      '   scrollPosition: null\n'
      '   scrollExtentMax: null\n'
355
      '   elevation: 0.0\n'
356
      '   thicknes: 0.0\n',
357 358
    );

359
    final SemanticsConfiguration config = SemanticsConfiguration()
360
      ..isSemanticBoundary = true
361
      ..isMergingSemanticsOfDescendants = true
362 363 364
      ..onScrollUp = () { }
      ..onLongPress = () { }
      ..onShowOnScreen = () { }
365 366
      ..isChecked = false
      ..isSelected = true
367
      ..isButton = true
368
      ..label = 'Use all the properties'
369
      ..textDirection = TextDirection.rtl
370
      ..sortKey = const OrdinalSortKey(1.0);
371 372 373
    final SemanticsNode allProperties = SemanticsNode()
      ..rect = Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
      ..transform = Matrix4.translation(Vector3(10.0, 10.0, 0.0))
374
      ..updateWith(config: config, childrenInInversePaintOrder: null);
375 376
    expect(
      allProperties.toStringDeep(),
377 378 379 380 381 382 383 384 385 386
      equalsIgnoringHashCodes(
          'SemanticsNode#2\n'
          '   STALE\n'
          '   owner: null\n'
          '   merge boundary ⛔️\n'
          '   Rect.fromLTRB(60.0, 20.0, 80.0, 50.0)\n'
          '   actions: longPress, scrollUp, showOnScreen\n'
          '   flags: hasCheckedState, isSelected, isButton\n'
          '   label: "Use all the properties"\n'
          '   textDirection: rtl\n'
387
          '   sortKey: OrdinalSortKey#19df5(order: 1.0)\n'
388
      ),
389 390 391
    );
    expect(
      allProperties.getSemanticsData().toString(),
392
      'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [1.0,0.0,0.0,10.0; 0.0,1.0,0.0,10.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0], actions: [longPress, scrollUp, showOnScreen], flags: [hasCheckedState, isSelected, isButton], label: "Use all the properties", textDirection: rtl)',
393 394
    );

395 396 397
    final SemanticsNode scaled = SemanticsNode()
      ..rect = Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
      ..transform = Matrix4.diagonal3(Vector3(10.0, 10.0, 1.0));
398 399
    expect(
      scaled.toStringDeep(),
400 401 402 403
      'SemanticsNode#3\n'
      '   STALE\n'
      '   owner: null\n'
      '   Rect.fromLTRB(50.0, 10.0, 70.0, 40.0) scaled by 10.0x\n',
404 405 406 407
    );
    expect(
      scaled.getSemanticsData().toString(),
      'SemanticsData(Rect.fromLTRB(50.0, 10.0, 70.0, 40.0), [10.0,0.0,0.0,0.0; 0.0,10.0,0.0,0.0; 0.0,0.0,1.0,0.0; 0.0,0.0,0.0,1.0])',
408 409
    );
  });
410

411
  test('Custom actions debug properties', () {
412
    final SemanticsConfiguration configuration = SemanticsConfiguration();
413 414 415
    const CustomSemanticsAction action1 = CustomSemanticsAction(label: 'action1');
    const CustomSemanticsAction action2 = CustomSemanticsAction(label: 'action2');
    const CustomSemanticsAction action3 = CustomSemanticsAction(label: 'action3');
416
    configuration.customSemanticsActions = <CustomSemanticsAction, VoidCallback>{
417 418 419
      action1: () { },
      action2: () { },
      action3: () { },
420
    };
421
    final SemanticsNode actionNode = SemanticsNode();
422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443
    actionNode.updateWith(config: configuration);

    expect(
      actionNode.toStringDeep(minLevel: DiagnosticLevel.hidden),
      'SemanticsNode#1\n'
      '   STALE\n'
      '   owner: null\n'
      '   isMergedIntoParent: false\n'
      '   mergeAllDescendantsIntoThisNode: false\n'
      '   Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
      '   actions: customAction\n'
      '   customActions: action1, action2, action3\n'
      '   flags: []\n'
      '   invisible\n'
      '   isHidden: false\n'
      '   label: ""\n'
      '   value: ""\n'
      '   increasedValue: ""\n'
      '   decreasedValue: ""\n'
      '   hint: ""\n'
      '   textDirection: null\n'
      '   sortKey: null\n'
444
      '   platformViewId: null\n'
445 446
      '   scrollChildren: null\n'
      '   scrollIndex: null\n'
447 448 449
      '   scrollExtentMin: null\n'
      '   scrollPosition: null\n'
      '   scrollExtentMax: null\n'
450
      '   elevation: 0.0\n'
451
      '   thicknes: 0.0\n',
452 453 454 455
    );

  });

456
  test('SemanticsConfiguration getter/setter', () {
457
    final SemanticsConfiguration config = SemanticsConfiguration();
458
    const CustomSemanticsAction customAction = CustomSemanticsAction(label: 'test');
459 460 461 462

    expect(config.isSemanticBoundary, isFalse);
    expect(config.isButton, isFalse);
    expect(config.isMergingSemanticsOfDescendants, isFalse);
463 464
    expect(config.isEnabled, null);
    expect(config.isChecked, null);
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480
    expect(config.isSelected, isFalse);
    expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isFalse);
    expect(config.isFocused, isFalse);
    expect(config.isTextField, isFalse);

    expect(config.onShowOnScreen, isNull);
    expect(config.onScrollDown, isNull);
    expect(config.onScrollUp, isNull);
    expect(config.onScrollLeft, isNull);
    expect(config.onScrollRight, isNull);
    expect(config.onLongPress, isNull);
    expect(config.onDecrease, isNull);
    expect(config.onIncrease, isNull);
    expect(config.onMoveCursorForwardByCharacter, isNull);
    expect(config.onMoveCursorBackwardByCharacter, isNull);
    expect(config.onTap, isNull);
481
    expect(config.customSemanticsActions[customAction], isNull);
482 483 484 485

    config.isSemanticBoundary = true;
    config.isButton = true;
    config.isMergingSemanticsOfDescendants = true;
486
    config.isEnabled = true;
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503
    config.isChecked = true;
    config.isSelected = true;
    config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
    config.isFocused = true;
    config.isTextField = true;

    final VoidCallback onShowOnScreen = () { };
    final VoidCallback onScrollDown = () { };
    final VoidCallback onScrollUp = () { };
    final VoidCallback onScrollLeft = () { };
    final VoidCallback onScrollRight = () { };
    final VoidCallback onLongPress = () { };
    final VoidCallback onDecrease = () { };
    final VoidCallback onIncrease = () { };
    final MoveCursorHandler onMoveCursorForwardByCharacter = (bool _) { };
    final MoveCursorHandler onMoveCursorBackwardByCharacter = (bool _) { };
    final VoidCallback onTap = () { };
504
    final VoidCallback onCustomAction = () { };
505 506 507 508 509 510 511 512 513 514 515 516

    config.onShowOnScreen = onShowOnScreen;
    config.onScrollDown = onScrollDown;
    config.onScrollUp = onScrollUp;
    config.onScrollLeft = onScrollLeft;
    config.onScrollRight = onScrollRight;
    config.onLongPress = onLongPress;
    config.onDecrease = onDecrease;
    config.onIncrease = onIncrease;
    config.onMoveCursorForwardByCharacter = onMoveCursorForwardByCharacter;
    config.onMoveCursorBackwardByCharacter = onMoveCursorBackwardByCharacter;
    config.onTap = onTap;
517
    config.customSemanticsActions[customAction] = onCustomAction;
518 519 520 521

    expect(config.isSemanticBoundary, isTrue);
    expect(config.isButton, isTrue);
    expect(config.isMergingSemanticsOfDescendants, isTrue);
522
    expect(config.isEnabled, isTrue);
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
    expect(config.isChecked, isTrue);
    expect(config.isSelected, isTrue);
    expect(config.isBlockingSemanticsOfPreviouslyPaintedNodes, isTrue);
    expect(config.isFocused, isTrue);
    expect(config.isTextField, isTrue);

    expect(config.onShowOnScreen, same(onShowOnScreen));
    expect(config.onScrollDown, same(onScrollDown));
    expect(config.onScrollUp, same(onScrollUp));
    expect(config.onScrollLeft, same(onScrollLeft));
    expect(config.onScrollRight, same(onScrollRight));
    expect(config.onLongPress, same(onLongPress));
    expect(config.onDecrease, same(onDecrease));
    expect(config.onIncrease, same(onIncrease));
    expect(config.onMoveCursorForwardByCharacter, same(onMoveCursorForwardByCharacter));
    expect(config.onMoveCursorBackwardByCharacter, same(onMoveCursorBackwardByCharacter));
    expect(config.onTap, same(onTap));
540
    expect(config.customSemanticsActions[customAction], same(onCustomAction));
541
  });
542 543 544 545
}

class TestRender extends RenderProxyBox {

546
  TestRender({
547 548 549 550 551 552
    this.hasTapAction = false,
    this.hasLongPressAction = false,
    this.hasScrollLeftAction = false,
    this.hasScrollRightAction = false,
    this.hasScrollUpAction = false,
    this.hasScrollDownAction = false,
553
    this.isSemanticBoundary,
554
    RenderObject child,
555 556 557 558 559 560 561 562 563
  }) : super(child);

  bool hasTapAction;
  bool hasLongPressAction;
  bool hasScrollLeftAction;
  bool hasScrollRightAction;
  bool hasScrollUpAction;
  bool hasScrollDownAction;
  bool isSemanticBoundary;
564 565 566


  @override
567 568
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
569

570 571 572 573 574 575 576 577 578 579 580 581 582
    config.isSemanticBoundary = isSemanticBoundary;
    if (hasTapAction)
      config.onTap = () { };
    if (hasLongPressAction)
      config.onLongPress = () { };
    if (hasScrollLeftAction)
      config.onScrollLeft = () { };
    if (hasScrollRightAction)
      config.onScrollRight = () { };
    if (hasScrollUpAction)
      config.onScrollUp = () { };
    if (hasScrollDownAction)
      config.onScrollDown = () { };
583
  }
584
}
585 586 587

class CustomSortKey extends OrdinalSortKey {
  const CustomSortKey(double order, {String name}) : super(order, name: name);
588
}