semantics_test.dart 32.2 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:ui';

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

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

13 14
const int kMaxFrameworkAccessibilityIdentifier = (1<<16) - 1;

15
void main() {
16 17
  TestRenderingFlutterBinding.ensureInitialized();

18 19 20 21
  setUp(() {
    debugResetSemanticsIdCounter();
  });

22
  group('SemanticsNode', () {
23 24 25
    const SemanticsTag tag1 = SemanticsTag('Tag One');
    const SemanticsTag tag2 = SemanticsTag('Tag Two');
    const SemanticsTag tag3 = SemanticsTag('Tag Three');
26 27

    test('tagging', () {
28
      final SemanticsNode node = SemanticsNode();
29

30 31
      expect(node.isTagged(tag1), isFalse);
      expect(node.isTagged(tag2), isFalse);
32

33
      node.tags = <SemanticsTag>{tag1};
34 35
      expect(node.isTagged(tag1), isTrue);
      expect(node.isTagged(tag2), isFalse);
36

37
      node.tags!.add(tag2);
38 39
      expect(node.isTagged(tag1), isTrue);
      expect(node.isTagged(tag2), isTrue);
40 41 42
    });

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

45
      final SemanticsNode node = SemanticsNode()
Dan Field's avatar
Dan Field committed
46
        ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0)
47
        ..tags = tags;
48

49
      expect(node.getSemanticsData().tags, tags);
50

51
      tags.add(tag3);
52

53
      final SemanticsConfiguration config = SemanticsConfiguration()
54
        ..isSemanticBoundary = true
55
        ..isMergingSemanticsOfDescendants = true;
56

57 58 59
      node.updateWith(
        config: config,
        childrenInInversePaintOrder: <SemanticsNode>[
60
          SemanticsNode()
61
            ..isMergedIntoParent = true
Dan Field's avatar
Dan Field committed
62
            ..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0)
63 64 65
            ..tags = tags,
        ],
      );
66

67
      expect(node.getSemanticsData().tags, tags);
68
    });
69

70 71 72 73 74 75
    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);
76 77 78 79
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#1(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label1")',
      );
80 81 82 83 84 85 86 87 88 89 90 91

      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));
92 93 94 95
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#2(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label2" [SpellOutStringAttribute(TextRange(start: 0, end: 1))])',
      );
96 97 98 99 100

      config.label = 'label3';
      expect(config.label, 'label3');
      expect(config.attributedLabel.string, 'label3');
      expect(config.attributedLabel.attributes.isEmpty, isTrue);
101 102 103 104
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#3(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3")',
      );
105 106 107 108 109

      config.value = 'value1';
      expect(config.value, 'value1');
      expect(config.attributedValue.string, 'value1');
      expect(config.attributedValue.attributes.isEmpty, isTrue);
110 111 112 113
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#4(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value1")',
      );
114 115 116 117 118 119 120 121 122 123 124 125

      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));
126 127 128 129
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#5(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value2" [SpellOutStringAttribute(TextRange(start: 0, end: 1))])',
      );
130 131 132 133 134

      config.value = 'value3';
      expect(config.value, 'value3');
      expect(config.attributedValue.string, 'value3');
      expect(config.attributedValue.attributes.isEmpty, isTrue);
135 136 137 138
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#6(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3")',
      );
139 140 141 142 143

      config.hint = 'hint1';
      expect(config.hint, 'hint1');
      expect(config.attributedHint.string, 'hint1');
      expect(config.attributedHint.attributes.isEmpty, isTrue);
144 145 146 147
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#7(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3", hint: "hint1")',
      );
148 149 150 151 152 153 154 155 156 157 158 159

      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));
160 161 162 163
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#8(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3", hint: "hint2" [SpellOutStringAttribute(TextRange(start: 0, end: 1))])',
      );
164 165 166 167 168

      config.hint = 'hint3';
      expect(config.hint, 'hint3');
      expect(config.attributedHint.string, 'hint3');
      expect(config.attributedHint.attributes.isEmpty, isTrue);
169 170 171 172
      expect(
        (SemanticsNode()..updateWith(config: config)).toString(),
        'SemanticsNode#9(STALE, owner: null, Rect.fromLTRB(0.0, 0.0, 0.0, 0.0), invisible, label: "label3", value: "value3", hint: "hint3")',
      );
173 174
    });

175 176 177 178 179 180 181 182 183 184 185
    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
186
          ..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
187 188 189 190
      ];

      node.updateWith(
        config: config,
191
        childrenInInversePaintOrder: children,
192 193
      );

194 195 196 197
      children.add(
        SemanticsNode()
          ..isMergedIntoParent = true
          ..rect = const Rect.fromLTRB(42.0, 42.0, 10.0, 10.0),
198 199 200
      );

      {
201
        late FlutterError error;
202 203 204
        try {
          node.updateWith(
            config: config,
205
            childrenInInversePaintOrder: children,
206 207 208 209 210 211 212 213
          );
        } 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'
214
          "The list's length has changed from 1 to 2.",
215 216 217
        ));
        expect(
          error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
218
          'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.',
219 220 221 222
        );
      }

      {
223
        late FlutterError error;
224 225 226 227 228 229
        final List<SemanticsNode> modifiedChildren = <SemanticsNode>[
          SemanticsNode()
            ..isMergedIntoParent = true
            ..rect = const Rect.fromLTRB(5.0, 5.0, 10.0, 10.0),
          SemanticsNode()
            ..isMergedIntoParent = true
230
            ..rect = const Rect.fromLTRB(10.0, 10.0, 20.0, 20.0),
231 232 233 234 235 236 237 238 239 240 241 242 243 244
        ];
        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,
245
            childrenInInversePaintOrder: modifiedChildren,
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
          );
        } 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'
263
          '   New child: SemanticsNode#5(STALE, owner: null, merged up ⬆️, Rect.fromLTRB(10.0, 10.0, 20.0, 20.0))\n',
264 265 266 267
        ));

        expect(
          error.diagnostics.singleWhere((DiagnosticsNode node) => node.level == DiagnosticLevel.hint).toString(),
268
          'Instead of mutating the existing list, create a new list containing the desired `SemanticsNode`s.',
269 270 271 272 273 274
        );
        // Two previous children and two new children.
        expect(error.diagnostics.where((DiagnosticsNode node) => node.value is SemanticsNode).length, 4);
      }
    });

275
    test('after markNeedsSemanticsUpdate() all render objects between two semantic boundaries are asked for annotations', () {
276 277
      final SemanticsHandle handle = TestRenderingFlutterBinding.instance.ensureSemantics();
      addTearDown(handle.dispose);
278 279

      TestRender middle;
280
      final TestRender root = TestRender(
281
        hasTapAction: true,
282
        isSemanticBoundary: true,
283
        child: TestRender(
284
          hasLongPressAction: true,
285
          child: middle = TestRender(
286
            hasScrollLeftAction: true,
287
            child: TestRender(
288
              hasScrollRightAction: true,
289
              child: TestRender(
290
                hasScrollUpAction: true,
291
                isSemanticBoundary: true,
292 293 294 295
              ),
            ),
          ),
        ),
296 297 298 299 300 301
      );

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

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

304 305 306
      middle
        ..hasScrollLeftAction = false
        ..hasScrollDownAction = true;
307
      middle.markNeedsSemanticsUpdate();
308 309 310 311

      pumpFrame(phase: EnginePhase.flushSemantics);

      expectedActions = SemanticsAction.tap.index | SemanticsAction.longPress.index | SemanticsAction.scrollDown.index | SemanticsAction.scrollRight.index;
312
      expect(root.debugSemantics!.getSemanticsData().actions, expectedActions);
313
    });
314
  });
315 316

  test('toStringDeep() does not throw with transform == null', () {
317
    final SemanticsNode child1 = SemanticsNode()
Dan Field's avatar
Dan Field committed
318
      ..rect = const Rect.fromLTRB(0.0, 0.0, 5.0, 5.0);
319
    final SemanticsNode child2 = SemanticsNode()
Dan Field's avatar
Dan Field committed
320
      ..rect = const Rect.fromLTRB(5.0, 0.0, 10.0, 5.0);
321
    final SemanticsNode root = SemanticsNode()
Dan Field's avatar
Dan Field committed
322
      ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
323 324 325 326
    root.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
    );
327 328 329 330 331

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

332
    expect(
333
      root.toStringDeep(),
334 335 336 337 338 339 340 341 342 343 344 345 346
      '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'
347
      '     Rect.fromLTRB(5.0, 0.0, 10.0, 5.0)\n',
348 349 350
    );
  });

351 352 353 354 355 356 357
  test('Incompatible OrdinalSortKey throw AssertionError when compared', () {
    // Different types.
    expect(() {
      const OrdinalSortKey(0.0).compareTo(const CustomSortKey(0.0));
    }, throwsAssertionError);
  });

358
  test('OrdinalSortKey compares correctly when names are the same', () {
359 360 361 362 363
    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)],
364 365 366 367
      <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')],
368
    ];
369
    final List<int> expectedResults = <int>[0, -1, 1, 0, 0, -1, 1, 0];
370
    assert(tests.length == expectedResults.length);
371
    final List<int> results = <int>[
372
      for (final List<SemanticsSortKey> tuple in tests) tuple[0].compareTo(tuple[1]),
373
    ];
374
    expect(results, orderedEquals(expectedResults));
375 376 377

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

380
  test('OrdinalSortKey compares correctly when the names are different', () {
381
    const List<List<SemanticsSortKey>> tests = <List<SemanticsSortKey>>[
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397
      <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')],
398
    ];
399
    final List<int> expectedResults = <int>[ -1, -1, -1, -1, 1, 1, 1, 1, 1, 1, 1, 1, -1, -1, -1, -1];
400
    assert(tests.length == expectedResults.length);
401
    final List<int> results = <int>[
402
      for (final List<SemanticsSortKey> tuple in tests) tuple[0].compareTo(tuple[1]),
403
    ];
404 405 406
    expect(results, orderedEquals(expectedResults));
  });

407
  test('toStringDeep respects childOrder parameter', () {
408
    final SemanticsNode child1 = SemanticsNode()
Dan Field's avatar
Dan Field committed
409
      ..rect = const Rect.fromLTRB(15.0, 0.0, 20.0, 5.0);
410
    final SemanticsNode child2 = SemanticsNode()
Dan Field's avatar
Dan Field committed
411
      ..rect = const Rect.fromLTRB(10.0, 0.0, 15.0, 5.0);
412
    final SemanticsNode root = SemanticsNode()
Dan Field's avatar
Dan Field committed
413
      ..rect = const Rect.fromLTRB(0.0, 0.0, 20.0, 5.0);
414 415 416 417
    root.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[child1, child2],
    );
418
    expect(
419
      root.toStringDeep(),
420 421 422 423 424
      'SemanticsNode#3\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 20.0, 5.0)\n'
      ' │\n'
425
      ' ├─SemanticsNode#1\n'
426 427
      ' │   STALE\n'
      ' │   owner: null\n'
428
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
429
      ' │\n'
430
      ' └─SemanticsNode#2\n'
431 432
      '     STALE\n'
      '     owner: null\n'
433
      '     Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
434 435 436 437
    );

    expect(
      root.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
438 439 440 441 442 443 444 445 446 447 448 449 450
      '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'
451
      '     Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n',
452 453
    );

454
    final SemanticsNode child3 = SemanticsNode()
Dan Field's avatar
Dan Field committed
455
      ..rect = const Rect.fromLTRB(0.0, 0.0, 10.0, 5.0);
456 457 458
    child3.updateWith(
      config: null,
      childrenInInversePaintOrder: <SemanticsNode>[
459
        SemanticsNode()
Dan Field's avatar
Dan Field committed
460
          ..rect = const Rect.fromLTRB(5.0, 0.0, 10.0, 5.0),
461
        SemanticsNode()
Dan Field's avatar
Dan Field committed
462
          ..rect = const Rect.fromLTRB(0.0, 0.0, 5.0, 5.0),
463 464
      ],
    );
465

466
    final SemanticsNode rootComplex = SemanticsNode()
Dan Field's avatar
Dan Field committed
467
      ..rect = const Rect.fromLTRB(0.0, 0.0, 25.0, 5.0);
468 469
    rootComplex.updateWith(
        config: null,
470
        childrenInInversePaintOrder: <SemanticsNode>[child1, child2, child3],
471
    );
472 473

    expect(
474
      rootComplex.toStringDeep(),
475 476 477 478 479
      'SemanticsNode#7\n'
      ' │ STALE\n'
      ' │ owner: null\n'
      ' │ Rect.fromLTRB(0.0, 0.0, 25.0, 5.0)\n'
      ' │\n'
480 481 482 483
      ' ├─SemanticsNode#1\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(15.0, 0.0, 20.0, 5.0)\n'
484 485 486 487 488 489
      ' │\n'
      ' ├─SemanticsNode#2\n'
      ' │   STALE\n'
      ' │   owner: null\n'
      ' │   Rect.fromLTRB(10.0, 0.0, 15.0, 5.0)\n'
      ' │\n'
490 491 492 493 494 495 496 497 498 499 500 501 502
      ' └─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'
503
      '       Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
504 505 506 507
    );

    expect(
      rootComplex.toStringDeep(childOrder: DebugSemanticsDumpOrder.inverseHitTest),
508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535
      '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'
536
      '       Rect.fromLTRB(0.0, 0.0, 5.0, 5.0)\n',
537 538 539 540
    );
  });

  test('debug properties', () {
541
    final SemanticsNode minimalProperties = SemanticsNode();
542
    expect(
543
      minimalProperties.toStringDeep(),
544 545
      'SemanticsNode#1\n'
      '   Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
546
      '   invisible\n',
547 548
    );

549 550
    expect(
      minimalProperties.toStringDeep(minLevel: DiagnosticLevel.hidden),
551 552 553 554 555
      'SemanticsNode#1\n'
      '   owner: null\n'
      '   isMergedIntoParent: false\n'
      '   mergeAllDescendantsIntoThisNode: false\n'
      '   Rect.fromLTRB(0.0, 0.0, 0.0, 0.0)\n'
556
      '   tags: null\n'
557
      '   actions: []\n'
558
      '   customActions: []\n'
559 560
      '   flags: []\n'
      '   invisible\n'
561
      '   isHidden: false\n'
562 563 564 565 566
      '   label: ""\n'
      '   value: ""\n'
      '   increasedValue: ""\n'
      '   decreasedValue: ""\n'
      '   hint: ""\n'
567
      '   tooltip: ""\n'
568
      '   textDirection: null\n'
569
      '   sortKey: null\n'
570
      '   platformViewId: null\n'
571 572
      '   maxValueLength: null\n'
      '   currentValueLength: null\n'
573 574
      '   scrollChildren: null\n'
      '   scrollIndex: null\n'
575 576 577
      '   scrollExtentMin: null\n'
      '   scrollPosition: null\n'
      '   scrollExtentMax: null\n'
578
      '   elevation: 0.0\n'
579
      '   thickness: 0.0\n',
580 581
    );

582
    final SemanticsConfiguration config = SemanticsConfiguration()
583
      ..isSemanticBoundary = true
584
      ..isMergingSemanticsOfDescendants = true
585 586 587
      ..onScrollUp = () { }
      ..onLongPress = () { }
      ..onShowOnScreen = () { }
588 589
      ..isChecked = false
      ..isSelected = true
590
      ..isButton = true
591
      ..label = 'Use all the properties'
592
      ..textDirection = TextDirection.rtl
593
      ..sortKey = const OrdinalSortKey(1.0);
594
    final SemanticsNode allProperties = SemanticsNode()
Dan Field's avatar
Dan Field committed
595
      ..rect = const Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
596
      ..transform = Matrix4.translation(Vector3(10.0, 10.0, 0.0))
597
      ..updateWith(config: config);
598 599
    expect(
      allProperties.toStringDeep(),
600
      equalsIgnoringHashCodes(
601 602 603 604 605 606 607 608 609 610
        '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',
611
      ),
612 613 614
    );
    expect(
      allProperties.getSemanticsData().toString(),
615
      '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)',
616 617
    );

618
    final SemanticsNode scaled = SemanticsNode()
Dan Field's avatar
Dan Field committed
619
      ..rect = const Rect.fromLTWH(50.0, 10.0, 20.0, 30.0)
620
      ..transform = Matrix4.diagonal3(Vector3(10.0, 10.0, 1.0));
621 622
    expect(
      scaled.toStringDeep(),
623 624 625 626
      'SemanticsNode#3\n'
      '   STALE\n'
      '   owner: null\n'
      '   Rect.fromLTRB(50.0, 10.0, 70.0, 40.0) scaled by 10.0x\n',
627 628 629 630
    );
    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])',
631 632
    );
  });
633

634
  test('Custom actions debug properties', () {
635
    final SemanticsConfiguration configuration = SemanticsConfiguration();
636 637 638
    const CustomSemanticsAction action1 = CustomSemanticsAction(label: 'action1');
    const CustomSemanticsAction action2 = CustomSemanticsAction(label: 'action2');
    const CustomSemanticsAction action3 = CustomSemanticsAction(label: 'action3');
639
    configuration.customSemanticsActions = <CustomSemanticsAction, VoidCallback>{
640 641 642
      action1: () { },
      action2: () { },
      action3: () { },
643
    };
644
    final SemanticsNode actionNode = SemanticsNode();
645 646 647 648 649 650 651 652 653 654
    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'
655
      '   tags: null\n'
656 657 658 659 660 661 662 663 664 665
      '   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'
666
      '   tooltip: ""\n'
667 668
      '   textDirection: null\n'
      '   sortKey: null\n'
669
      '   platformViewId: null\n'
670 671
      '   maxValueLength: null\n'
      '   currentValueLength: null\n'
672 673
      '   scrollChildren: null\n'
      '   scrollIndex: null\n'
674 675 676
      '   scrollExtentMin: null\n'
      '   scrollPosition: null\n'
      '   scrollExtentMax: null\n'
677
      '   elevation: 0.0\n'
678
      '   thickness: 0.0\n',
679
    );
680 681
  });

Lioness100's avatar
Lioness100 committed
682
  test('Attributed String can concat', () {
683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699
    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);
700
    expect(result.toString(), "AttributedString('string1string2', attributes: [SpellOutStringAttribute(TextRange(start: 0, end: 4)), LocaleStringAttribute(TextRange(start: 7, end: 11), es-MX)])");
701 702
  });

703
  test('Semantics id does not repeat', () {
704 705 706
    final SemanticsOwner owner = SemanticsOwner(
      onSemanticsUpdate: (SemanticsUpdate update) {},
    );
707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722
    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);
  });

723 724 725
  test('Tags show up in debug properties', () {
    final SemanticsNode actionNode = SemanticsNode()
      ..tags = <SemanticsTag>{RenderViewport.useTwoPaneSemantics};
726

727 728 729 730
    expect(
      actionNode.toStringDeep(),
      contains('\n   tags: RenderViewport.twoPane\n'),
    );
731 732
  });

733
  test('SemanticsConfiguration getter/setter', () {
734
    final SemanticsConfiguration config = SemanticsConfiguration();
735
    const CustomSemanticsAction customAction = CustomSemanticsAction(label: 'test');
736 737 738

    expect(config.isSemanticBoundary, isFalse);
    expect(config.isButton, isFalse);
739
    expect(config.isLink, isFalse);
740
    expect(config.isMergingSemanticsOfDescendants, isFalse);
741 742
    expect(config.isEnabled, null);
    expect(config.isChecked, null);
743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758
    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);
759
    expect(config.customSemanticsActions[customAction], isNull);
760 761 762

    config.isSemanticBoundary = true;
    config.isButton = true;
763
    config.isLink = true;
764
    config.isMergingSemanticsOfDescendants = true;
765
    config.isEnabled = true;
766 767 768 769 770 771
    config.isChecked = true;
    config.isSelected = true;
    config.isBlockingSemanticsOfPreviouslyPaintedNodes = true;
    config.isFocused = true;
    config.isTextField = true;

772 773 774 775 776 777 778 779 780 781 782 783
    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() { }
784 785 786 787 788 789 790 791 792 793 794 795

    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;
796
    config.customSemanticsActions[customAction] = onCustomAction;
797 798 799

    expect(config.isSemanticBoundary, isTrue);
    expect(config.isButton, isTrue);
800
    expect(config.isLink, isTrue);
801
    expect(config.isMergingSemanticsOfDescendants, isTrue);
802
    expect(config.isEnabled, isTrue);
803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819
    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));
820
    expect(config.customSemanticsActions[customAction], same(onCustomAction));
821
  });
822 823 824 825
}

class TestRender extends RenderProxyBox {

826
  TestRender({
827 828 829 830 831 832
    this.hasTapAction = false,
    this.hasLongPressAction = false,
    this.hasScrollLeftAction = false,
    this.hasScrollRightAction = false,
    this.hasScrollUpAction = false,
    this.hasScrollDownAction = false,
833 834
    this.isSemanticBoundary = false,
    RenderBox? child,
835 836 837 838 839 840 841 842 843
  }) : super(child);

  bool hasTapAction;
  bool hasLongPressAction;
  bool hasScrollLeftAction;
  bool hasScrollRightAction;
  bool hasScrollUpAction;
  bool hasScrollDownAction;
  bool isSemanticBoundary;
844 845

  @override
846 847
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
848

849
    config.isSemanticBoundary = isSemanticBoundary;
850
    if (hasTapAction) {
851
      config.onTap = () { };
852 853
    }
    if (hasLongPressAction) {
854
      config.onLongPress = () { };
855 856
    }
    if (hasScrollLeftAction) {
857
      config.onScrollLeft = () { };
858 859
    }
    if (hasScrollRightAction) {
860
      config.onScrollRight = () { };
861 862
    }
    if (hasScrollUpAction) {
863
      config.onScrollUp = () { };
864 865
    }
    if (hasScrollDownAction) {
866
      config.onScrollDown = () { };
867
    }
868
  }
869
}
870 871

class CustomSortKey extends OrdinalSortKey {
872
  const CustomSortKey(super.order, {super.name});
873
}