semantics_test.dart 30 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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_test/flutter_test.dart';
7
import 'package:vector_math/vector_math_64.dart';
8

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

11 12
const int kMaxFrameworkAccessibilityIdentifier = (1<<16) - 1;

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
      node.tags!.add(tag2);
34 35
      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
      final SemanticsNode node = SemanticsNode()
Dan Field's avatar
Dan Field committed
42
        ..rect = const 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
Dan Field's avatar
Dan Field committed
58
            ..rect = const 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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
    test('SemanticsConfiguration can set both string label/value/hint and attributed version', () {
      final SemanticsConfiguration config = SemanticsConfiguration();
      config.label = 'label1';
      expect(config.label, 'label1');
      expect(config.attributedLabel.string, 'label1');
      expect(config.attributedLabel.attributes.isEmpty, isTrue);

      config.attributedLabel = AttributedString(
        'label2',
        attributes: <StringAttribute>[
          SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
        ]
      );
      expect(config.label, 'label2');
      expect(config.attributedLabel.string, 'label2');
      expect(config.attributedLabel.attributes.length, 1);
      expect(config.attributedLabel.attributes[0] is SpellOutStringAttribute, isTrue);
      expect(config.attributedLabel.attributes[0].range, const TextRange(start: 0, end: 1));

      config.label = 'label3';
      expect(config.label, 'label3');
      expect(config.attributedLabel.string, 'label3');
      expect(config.attributedLabel.attributes.isEmpty, isTrue);

      config.value = 'value1';
      expect(config.value, 'value1');
      expect(config.attributedValue.string, 'value1');
      expect(config.attributedValue.attributes.isEmpty, isTrue);

      config.attributedValue = AttributedString(
          'value2',
          attributes: <StringAttribute>[
            SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
          ]
      );
      expect(config.value, 'value2');
      expect(config.attributedValue.string, 'value2');
      expect(config.attributedValue.attributes.length, 1);
      expect(config.attributedValue.attributes[0] is SpellOutStringAttribute, isTrue);
      expect(config.attributedValue.attributes[0].range, const TextRange(start: 0, end: 1));

      config.value = 'value3';
      expect(config.value, 'value3');
      expect(config.attributedValue.string, 'value3');
      expect(config.attributedValue.attributes.isEmpty, isTrue);

      config.hint = 'hint1';
      expect(config.hint, 'hint1');
      expect(config.attributedHint.string, 'hint1');
      expect(config.attributedHint.attributes.isEmpty, isTrue);

      config.attributedHint = AttributedString(
          'hint2',
          attributes: <StringAttribute>[
            SpellOutStringAttribute(range: const TextRange(start: 0, end:1)),
          ]
      );
      expect(config.hint, 'hint2');
      expect(config.attributedHint.string, 'hint2');
      expect(config.attributedHint.attributes.length, 1);
      expect(config.attributedHint.attributes[0] is SpellOutStringAttribute, isTrue);
      expect(config.attributedHint.attributes[0].range, const TextRange(start: 0, end: 1));

      config.hint = 'hint3';
      expect(config.hint, 'hint3');
      expect(config.attributedHint.string, 'hint3');
      expect(config.attributedHint.attributes.isEmpty, isTrue);
    });

135 136 137 138 139 140 141 142 143 144 145
    test('mutate existing semantic node list errors', () {
      final SemanticsNode node = SemanticsNode()
        ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0);

      final SemanticsConfiguration config = SemanticsConfiguration()
        ..isSemanticBoundary = true
        ..isMergingSemanticsOfDescendants = true;

      final List<SemanticsNode> children = <SemanticsNode>[
        SemanticsNode()
          ..isMergedIntoParent = true
146
          ..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
147 148 149 150
      ];

      node.updateWith(
        config: config,
151
        childrenInInversePaintOrder: children,
152 153
      );

154 155 156 157
      children.add(
        SemanticsNode()
          ..isMergedIntoParent = true
          ..rect = const Rect.fromLTRB(42.0, 42.0, 10.0, 10.0),
158 159 160
      );

      {
161
        late FlutterError error;
162 163 164
        try {
          node.updateWith(
            config: config,
165
            childrenInInversePaintOrder: children,
166 167 168 169 170 171 172 173
          );
        } on FlutterError catch (e) {
          error = e;
        }
        expect(error.toString(), equalsIgnoringHashCodes(
          'Failed to replace child semantics nodes because the list of `SemanticsNode`s was mutated.\n'
          'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.\n'
          'Error details:\n'
174
          "The list's length has changed from 1 to 2.",
175 176 177
        ));
        expect(
          error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
178
          'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.',
179 180 181 182
        );
      }

      {
183
        late FlutterError error;
184 185 186 187 188 189
        final List<SemanticsNode> modifiedChildren = <SemanticsNode>[
          SemanticsNode()
            ..isMergedIntoParent = true
            ..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
          SemanticsNode()
            ..isMergedIntoParent = true
190
            ..rect = const Rect.fromLTRB(10.0, 10.0, 20.0, 20.0),
191 192 193 194 195 196 197 198 199 200 201 202 203 204
        ];
        node.updateWith(
          config: config,
          childrenInInversePaintOrder: modifiedChildren,
        );
        try {
          modifiedChildren[0] = SemanticsNode()
            ..isMergedIntoParent = true
            ..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0);
          modifiedChildren[1] = SemanticsNode()
            ..isMergedIntoParent = true
            ..rect = const Rect.fromLTRB(40.0, 14.0, 20.0, 20.0);
          node.updateWith(
            config: config,
205
            childrenInInversePaintOrder: modifiedChildren,
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
          );
        } on FlutterError catch (e) {
          error = e;
        }
        expect(error.toStringDeep(), equalsIgnoringHashCodes(
          'FlutterError\n'
          '   Failed to replace child semantics nodes because the list of\n'
          '   `SemanticsNode`s was mutated.\n'
          '   Instead of mutating the existing list, create a new list\n'
          '   containing the desired `SemanticsNode`s.\n'
          '   Error details:\n'
          '   Child node at position 0 was replaced:\n'
          '   Previous child: SemanticsNode#6(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(0.0, 0.0, 20.0, 20.0))\n'
          '   New child: SemanticsNode#4(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(5.0, 5.0, 10.0, 10.0))\n'
          '\n'
          '   Child node at position 1 was replaced:\n'
          '   Previous child: SemanticsNode#7(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(40.0, 14.0, 20.0, 20.0))\n'
223
          '   New child: SemanticsNode#5(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(10.0, 10.0, 20.0, 20.0))\n',
224 225 226 227
        ));

        expect(
          error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
228
          'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.',
229 230 231 232 233 234
        );
        // Two previous children and two new children.
        expect(error.diagnostics.where((DiagnosticsNode node) => node.value is SemanticsNode).length, 4);
      }
    });

235
    test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () {
236 237 238
      renderer.pipelineOwner.ensureSemantics();

      TestRender middle;
239
      final TestRender root = TestRender(
240
        hasTapAction: true,
241
        isSemanticBoundary: true,
242
        child: TestRender(
243
          hasLongPressAction: true,
244
          isSemanticBoundary: false,
245
          child: middle = TestRender(
246
            hasScrollLeftAction: true,
247
            isSemanticBoundary: false,
248
            child: TestRender(
249
              hasScrollRightAction: true,
250
              isSemanticBoundary: false,
251
              child: TestRender(
252
                hasScrollUpAction: true,
253
                isSemanticBoundary: true,
254 255 256 257
              ),
            ),
          ),
        ),
258 259 260 261 262 263
      );

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

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

266 267 268
      middle
        ..hasScrollLeftAction = false
        ..hasScrollDownAction = true;
269
      middle.markNeedsSemanticsUpdate();
270 271 272 273

      pumpFrame(phase: EnginePhase.flushSemantics);

      expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollDown.index | SemanticsAction.scrollRight.index;
274
      expect(root.debugSemantics!.getSemanticsData().actions, expectedActions);
275
    });
276
  });
277 278

  test('toStringDeep() does not throw with transform == null', () {
279
    final SemanticsNode child1 = SemanticsNode()
Dan Field's avatar
Dan Field committed
280
      ..rect = const Rect.fromLTRB(0.0, 0.0, 5.0, 5.0);
281
    final SemanticsNode child2 = SemanticsNode()
Dan Field's avatar
Dan Field committed
282
      ..rect = const Rect.fromLTRB(5.0, 0.0, 10.0, 5.0);
283
    final SemanticsNode root = SemanticsNode()
Dan Field's avatar
Dan Field committed
284
      ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
285 286 287 288
    root.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
    );
289 290 291 292 293

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

294
    expect(
295
      root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversalOrder),
296 297 298 299 300 301 302 303 304 305 306 307 308
      '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'
309
      '     Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n',
310 311 312
    );
  });

313 314 315 316 317 318 319
  test('Incompatible OrdinalSortKey throw AssertionError when compared', () {
    // Different types.
    expect(() {
      const OrdinalSortKey(0.0).compareTo(const CustomSortKey(0.0));
    }, throwsAssertionError);
  });

320
  test('OrdinalSortKey compares correctly when names are the same', () {
321 322 323 324 325
    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)],
326 327 328 329
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'a'), OrdinalSortKey(0.0, name: 'a')],
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'a'), OrdinalSortKey(1.0, name: 'a')],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'a'), OrdinalSortKey(0.0, name: 'a')],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'a'), OrdinalSortKey(1.0, name: 'a')],
330
    ];
331
    final List<int> expectedResults = <int>[0, -1, 1, 0, 0, -1, 1, 0];
332
    assert(tests.length == expectedResults.length);
333
    final List<int> results = <int>[
334
      for (final List<SemanticsSortKey> tuple in tests) tuple[0].compareTo(tuple[1]),
335
    ];
336
    expect(results, orderedEquals(expectedResults));
337 338 339

    // Differing types should throw an assertion.
    expect(() => const OrdinalSortKey(0.0).compareTo(const CustomSortKey(0.0)), throwsAssertionError);
340 341
  });

342
  test('OrdinalSortKey compares correctly when the names are different', () {
343
    const List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359
      <SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(0.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(0.0), OrdinalSortKey(1.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(0.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(1.0), OrdinalSortKey(1.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(0.0)],
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(1.0)],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(0.0)],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(1.0)],
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(0.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'foo'), OrdinalSortKey(1.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(0.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'foo'), OrdinalSortKey(1.0, name: 'bar')],
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'bar'), OrdinalSortKey(0.0, name: 'foo')],
      <SemanticsSortKey>[OrdinalSortKey(0.0, name: 'bar'), OrdinalSortKey(1.0, name: 'foo')],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'bar'), OrdinalSortKey(0.0, name: 'foo')],
      <SemanticsSortKey>[OrdinalSortKey(1.0, name: 'bar'), OrdinalSortKey(1.0, name: 'foo')],
360
    ];
361
    final List<int> expectedResults = <int>[ -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1];
362
    assert(tests.length == expectedResults.length);
363
    final List<int> results = <int>[
364
      for (final List<SemanticsSortKey> tuple in tests) tuple[0].compareTo(tuple[1]),
365
    ];
366 367 368
    expect(results, orderedEquals(expectedResults));
  });

369
  test('toStringDeep respects childOrder parameter', () {
370
    final SemanticsNode child1 = SemanticsNode()
Dan Field's avatar
Dan Field committed
371
      ..rect = const Rect.fromLTRB(15.0, 0.0, 20.0, 5.0);
372
    final SemanticsNode child2 = SemanticsNode()
Dan Field's avatar
Dan Field committed
373
      ..rect = const Rect.fromLTRB(10.0, 0.0, 15.0, 5.0);
374
    final SemanticsNode root = SemanticsNode()
Dan Field's avatar
Dan Field committed
375
      ..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 5.0);
376 377 378 379
    root.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
    );
380
    expect(
381
      root.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversalOrder),
382 383 384 385 386
      'SemanticsNode#3\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 20.0, 5.0)\n'
      ' │\n'
387
      ' ├─SemanticsNode#1\n'
388 389
      ' │   STALE\n'
      ' │   owner: null\n'
390
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
391
      ' │\n'
392
      ' └─SemanticsNode#2\n'
393 394
      '     STALE\n'
      '     owner: null\n'
395
      '     Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
396 397 398 399
    );

    expect(
      root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
400 401 402 403 404 405 406 407 408 409 410 411 412
      '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'
413
      '     Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
414 415
    );

416
    final SemanticsNode child3 = SemanticsNode()
Dan Field's avatar
Dan Field committed
417
      ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
418 419 420
    child3.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[
421
        SemanticsNode()
Dan Field's avatar
Dan Field committed
422
          ..rect = const Rect.fromLTRB(5.0, 0.0, 10.0, 5.0),
423
        SemanticsNode()
Dan Field's avatar
Dan Field committed
424
          ..rect = const Rect.fromLTRB(0.0, 0.0, 5.0, 5.0),
425 426
      ],
    );
427

428
    final SemanticsNode rootComplex = SemanticsNode()
Dan Field's avatar
Dan Field committed
429
      ..rect = const Rect.fromLTRB(0.0, 0.0, 25.0, 5.0);
430 431
    rootComplex.updateWith(
        config: null,
432
        childrenInInversePaintOrder: <SemanticsNode>[child1, child2, child3],
433
    );
434 435

    expect(
436
      rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.traversalOrder),
437 438 439 440 441
      'SemanticsNode#7\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 25.0, 5.0)\n'
      ' │\n'
442 443 444 445
      ' ├─SemanticsNode#1\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
446 447 448 449 450 451
      ' │\n'
      ' ├─SemanticsNode#2\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n'
      ' │\n'
452 453 454 455 456 457 458 459 460 461 462 463 464
      ' └─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'
465
      '       Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
466 467 468 469
    );

    expect(
      rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
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
      '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'
498
      '       Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
499 500 501 502
    );
  });

  test('debug properties', () {
503
    final SemanticsNode minimalProperties = SemanticsNode();
504
    expect(
505
      minimalProperties.toStringDeep(),
506 507
      'SemanticsNode#1\n'
      '   Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
508
      '   invisible\n',
509 510
    );

511 512
    expect(
      minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
513 514 515 516 517
      'SemanticsNode#1\n'
      '   owner: null\n'
      '   isMergedIntoParent: false\n'
      '   mergeAllDescendantsIntoThisNode: false\n'
      '   Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
518
      '   tags: null\n'
519
      '   actions: []\n'
520
      '   customActions: []\n'
521 522
      '   flags: []\n'
      '   invisible\n'
523
      '   isHidden: false\n'
524 525 526 527 528 529
      '   label: ""\n'
      '   value: ""\n'
      '   increasedValue: ""\n'
      '   decreasedValue: ""\n'
      '   hint: ""\n'
      '   textDirection: null\n'
530
      '   sortKey: null\n'
531
      '   platformViewId: null\n'
532 533
      '   maxValueLength: null\n'
      '   currentValueLength: null\n'
534 535
      '   scrollChildren: null\n'
      '   scrollIndex: null\n'
536 537 538
      '   scrollExtentMin: null\n'
      '   scrollPosition: null\n'
      '   scrollExtentMax: null\n'
539
      '   elevation: 0.0\n'
540
      '   thickness: 0.0\n',
541 542
    );

543
    final SemanticsConfiguration config = SemanticsConfiguration()
544
      ..isSemanticBoundary = true
545
      ..isMergingSemanticsOfDescendants = true
546 547 548
      ..onScrollUp = () { }
      ..onLongPress = () { }
      ..onShowOnScreen = () { }
549 550
      ..isChecked = false
      ..isSelected = true
551
      ..isButton = true
552
      ..label = 'Use all the properties'
553
      ..textDirection = TextDirection.rtl
554
      ..sortKey = const OrdinalSortKey(1.0);
555
    final SemanticsNode allProperties = SemanticsNode()
Dan Field's avatar
Dan Field committed
556
      ..rect = const Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
557
      ..transform = Matrix4.translation(Vector3(10.0, 10.0, 0.0))
558
      ..updateWith(config: config, childrenInInversePaintOrder: null);
559 560
    expect(
      allProperties.toStringDeep(),
561
      equalsIgnoringHashCodes(
562 563 564 565 566 567 568 569 570 571
        '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'
        '   sortKey: OrdinalSortKey#19df5(order: 1.0)\n',
572
      ),
573 574 575
    );
    expect(
      allProperties.getSemanticsData().toString(),
576
      '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)',
577 578
    );

579
    final SemanticsNode scaled = SemanticsNode()
Dan Field's avatar
Dan Field committed
580
      ..rect = const Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
581
      ..transform = Matrix4.diagonal3(Vector3(10.0, 10.0, 1.0));
582 583
    expect(
      scaled.toStringDeep(),
584 585 586 587
      'SemanticsNode#3\n'
      '   STALE\n'
      '   owner: null\n'
      '   Rect.fromLTRB(50.0, 10.0, 70.0, 40.0) scaled by 10.0x\n',
588 589 590 591
    );
    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])',
592 593
    );
  });
594

595
  test('Custom actions debug properties', () {
596
    final SemanticsConfiguration configuration = SemanticsConfiguration();
597 598 599
    const CustomSemanticsAction action1 = CustomSemanticsAction(label: 'action1');
    const CustomSemanticsAction action2 = CustomSemanticsAction(label: 'action2');
    const CustomSemanticsAction action3 = CustomSemanticsAction(label: 'action3');
600
    configuration.customSemanticsActions = <CustomSemanticsAction, VoidCallback>{
601 602 603
      action1: () { },
      action2: () { },
      action3: () { },
604
    };
605
    final SemanticsNode actionNode = SemanticsNode();
606 607 608 609 610 611 612 613 614 615
    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'
616
      '   tags: null\n'
617 618 619 620 621 622 623 624 625 626 627 628
      '   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'
629
      '   platformViewId: null\n'
630 631
      '   maxValueLength: null\n'
      '   currentValueLength: null\n'
632 633
      '   scrollChildren: null\n'
      '   scrollIndex: null\n'
634 635 636
      '   scrollExtentMin: null\n'
      '   scrollPosition: null\n'
      '   scrollExtentMax: null\n'
637
      '   elevation: 0.0\n'
638
      '   thickness: 0.0\n',
639
    );
640 641
  });

642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661
  test('Attributed String can concate', () {
    final AttributedString string1 = AttributedString(
      'string1',
      attributes: <StringAttribute>[
        SpellOutStringAttribute(range: const TextRange(start:0, end:4)),
      ]
    );
    final AttributedString string2 = AttributedString(
        'string2',
        attributes: <StringAttribute>[
          LocaleStringAttribute(locale: const Locale('es', 'MX'), range: const TextRange(start:0, end:4)),
        ]
    );
    final AttributedString result = string1 + string2;
    expect(result.string, 'string1string2');
    expect(result.attributes.length, 2);
    expect(result.attributes[0].range, const TextRange(start:0, end:4));
    expect(result.attributes[0] is SpellOutStringAttribute, isTrue);
  });

662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679
  test('Semantics id does not repeat', () {
    final SemanticsOwner owner = SemanticsOwner();
    const int expectId = 1400;
    SemanticsNode? nodeToRemove;
    for (int i = 0; i < kMaxFrameworkAccessibilityIdentifier; i++) {
      final SemanticsNode node = SemanticsNode();
      node.attach(owner);
      if (node.id == expectId) {
        nodeToRemove = node;
      }
    }
    nodeToRemove!.detach();
    final SemanticsNode newNode = SemanticsNode();
    newNode.attach(owner);
    // Id is reused.
    expect(newNode.id, expectId);
  });

680 681 682
  test('Tags show up in debug properties', () {
    final SemanticsNode actionNode = SemanticsNode()
      ..tags = <SemanticsTag>{RenderViewport.useTwoPaneSemantics};
683

684 685 686 687
    expect(
      actionNode.toStringDeep(),
      contains('\n   tags: RenderViewport.twoPane\n'),
    );
688 689
  });

690
  test('SemanticsConfiguration getter/setter', () {
691
    final SemanticsConfiguration config = SemanticsConfiguration();
692
    const CustomSemanticsAction customAction = CustomSemanticsAction(label: 'test');
693 694 695

    expect(config.isSemanticBoundary, isFalse);
    expect(config.isButton, isFalse);
696
    expect(config.isLink, isFalse);
697
    expect(config.isMergingSemanticsOfDescendants, isFalse);
698 699
    expect(config.isEnabled, null);
    expect(config.isChecked, null);
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715
    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);
716
    expect(config.customSemanticsActions[customAction], isNull);
717 718 719

    config.isSemanticBoundary = true;
    config.isButton = true;
720
    config.isLink = true;
721
    config.isMergingSemanticsOfDescendants = true;
722
    config.isEnabled = true;
723 724 725 726 727 728
    config.isChecked = true;
    config.isSelected = true;
    config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
    config.isFocused = true;
    config.isTextField = true;

729 730 731 732 733 734 735 736 737 738 739 740
    void onShowOnScreen() { }
    void onScrollDown() { }
    void onScrollUp() { }
    void onScrollLeft() { }
    void onScrollRight() { }
    void onLongPress() { }
    void onDecrease() { }
    void onIncrease() { }
    void onMoveCursorForwardByCharacter(bool _) { }
    void onMoveCursorBackwardByCharacter(bool _) { }
    void onTap() { }
    void onCustomAction() { }
741 742 743 744 745 746 747 748 749 750 751 752

    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;
753
    config.customSemanticsActions[customAction] = onCustomAction;
754 755 756

    expect(config.isSemanticBoundary, isTrue);
    expect(config.isButton, isTrue);
757
    expect(config.isLink, isTrue);
758
    expect(config.isMergingSemanticsOfDescendants, isTrue);
759
    expect(config.isEnabled, isTrue);
760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776
    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));
777
    expect(config.customSemanticsActions[customAction], same(onCustomAction));
778
  });
779 780 781 782
}

class TestRender extends RenderProxyBox {

783
  TestRender({
784 785 786 787 788 789
    this.hasTapAction = false,
    this.hasLongPressAction = false,
    this.hasScrollLeftAction = false,
    this.hasScrollRightAction = false,
    this.hasScrollUpAction = false,
    this.hasScrollDownAction = false,
790 791
    this.isSemanticBoundary = false,
    RenderBox? child,
792 793 794 795 796 797 798 799 800
  }) : super(child);

  bool hasTapAction;
  bool hasLongPressAction;
  bool hasScrollLeftAction;
  bool hasScrollRightAction;
  bool hasScrollUpAction;
  bool hasScrollDownAction;
  bool isSemanticBoundary;
801 802

  @override
803 804
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
805

806 807 808 809 810 811 812 813 814 815 816 817 818
    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 = () { };
819
  }
820
}
821 822

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