custom_painter_test.dart 27.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
import 'dart:ui';
6 7 8

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
9
import 'package:flutter_test/flutter_test.dart';
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

import 'semantics_tester.dart';

void main() {
  group(CustomPainter, () {
    setUp(() {
      debugResetSemanticsIdCounter();
      _PainterWithSemantics.shouldRebuildSemanticsCallCount = 0;
      _PainterWithSemantics.buildSemanticsCallCount = 0;
      _PainterWithSemantics.semanticsBuilderCallCount = 0;
    });

    _defineTests();
  });
}

void _defineTests() {
  testWidgets('builds no semantics by default', (WidgetTester tester) async {
28
    final SemanticsTester semanticsTester = SemanticsTester(tester);
29

30 31
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithoutSemantics(),
32 33 34
    ));

    expect(semanticsTester, hasSemantics(
35
      TestSemantics.root(),
36 37 38 39 40 41
    ));

    semanticsTester.dispose();
  });

  testWidgets('provides foreground semantics', (WidgetTester tester) async {
42
    final SemanticsTester semanticsTester = SemanticsTester(tester);
43

44 45
    await tester.pumpWidget(CustomPaint(
      foregroundPainter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
46
        semantics: const CustomPainterSemantics(
47
          rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
48
          properties: SemanticsProperties(
49 50 51 52 53 54 55 56
            label: 'foreground',
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
    ));

    expect(semanticsTester, hasSemantics(
57
      TestSemantics.root(
58
        children: <TestSemantics>[
59
          TestSemantics.rootChild(
60 61 62
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
63
              TestSemantics(
64 65
                id: 2,
                label: 'foreground',
Dan Field's avatar
Dan Field committed
66
                rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
67 68 69 70 71 72 73 74 75 76 77
              ),
            ],
          ),
        ],
      ),
    ));

    semanticsTester.dispose();
  });

  testWidgets('provides background semantics', (WidgetTester tester) async {
78
    final SemanticsTester semanticsTester = SemanticsTester(tester);
79

80 81
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
82
        semantics: const CustomPainterSemantics(
83
          rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
84
          properties: SemanticsProperties(
85 86 87 88 89 90 91 92
            label: 'background',
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
    ));

    expect(semanticsTester, hasSemantics(
93
      TestSemantics.root(
94
        children: <TestSemantics>[
95
          TestSemantics.rootChild(
96 97 98
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
99
              TestSemantics(
100 101
                id: 2,
                label: 'background',
Dan Field's avatar
Dan Field committed
102
                rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
103 104 105 106 107 108 109 110 111 112 113
              ),
            ],
          ),
        ],
      ),
    ));

    semanticsTester.dispose();
  });

  testWidgets('combines background, child and foreground semantics', (WidgetTester tester) async {
114
    final SemanticsTester semanticsTester = SemanticsTester(tester);
115

116 117
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
118
        semantics: const CustomPainterSemantics(
119
          rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
120
          properties: SemanticsProperties(
121 122 123 124 125
            label: 'background',
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
126
      foregroundPainter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
127
        semantics: const CustomPainterSemantics(
128
          rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
129
          properties: SemanticsProperties(
130 131 132 133 134
            label: 'foreground',
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
135 136 137 138
      child: Semantics(
        container: true,
        child: const Text('Hello', textDirection: TextDirection.ltr),
      ),
139 140 141
    ));

    expect(semanticsTester, hasSemantics(
142
      TestSemantics.root(
143
        children: <TestSemantics>[
144
          TestSemantics.rootChild(
145 146 147
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
148
              TestSemantics(
149 150
                id: 3,
                label: 'background',
Dan Field's avatar
Dan Field committed
151
                rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
152
              ),
153
              TestSemantics(
154 155
                id: 2,
                label: 'Hello',
Dan Field's avatar
Dan Field committed
156
                rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
157
              ),
158
              TestSemantics(
159 160
                id: 4,
                label: 'foreground',
Dan Field's avatar
Dan Field committed
161
                rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
162 163 164 165 166 167 168 169 170 171 172
              ),
            ],
          ),
        ],
      ),
    ));

    semanticsTester.dispose();
  });

  testWidgets('applies $SemanticsProperties', (WidgetTester tester) async {
173
    final SemanticsTester semanticsTester = SemanticsTester(tester);
174

175 176
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
177 178
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
179
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
180
          properties: SemanticsProperties(
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
            checked: false,
            selected: false,
            button: false,
            label: 'label-before',
            value: 'value-before',
            increasedValue: 'increase-before',
            decreasedValue: 'decrease-before',
            hint: 'hint-before',
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
    ));

    expect(semanticsTester, hasSemantics(
196
      TestSemantics.root(
197
        children: <TestSemantics>[
198
          TestSemantics.rootChild(
199 200 201
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
202
              TestSemantics(
Dan Field's avatar
Dan Field committed
203
                rect: const Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
                id: 2,
                flags: 1,
                label: 'label-before',
                value: 'value-before',
                increasedValue: 'increase-before',
                decreasedValue: 'decrease-before',
                hint: 'hint-before',
                textDirection: TextDirection.rtl,
              ),
            ],
          ),
        ],
      ),
    ));

219 220 221
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
        semantics: CustomPainterSemantics(
222
          key: const ValueKey<int>(1),
Dan Field's avatar
Dan Field committed
223
          rect: const Rect.fromLTRB(5.0, 6.0, 7.0, 8.0),
224
          properties: SemanticsProperties(
225 226 227 228 229 230 231 232 233
            checked: true,
            selected: true,
            button: true,
            label: 'label-after',
            value: 'value-after',
            increasedValue: 'increase-after',
            decreasedValue: 'decrease-after',
            hint: 'hint-after',
            textDirection: TextDirection.ltr,
234 235 236 237 238 239 240 241
            onScrollDown: () { },
            onLongPress: () { },
            onDecrease: () { },
            onIncrease: () { },
            onScrollLeft: () { },
            onScrollRight: () { },
            onScrollUp: () { },
            onTap: () { },
242 243 244 245 246 247
          ),
        ),
      ),
    ));

    expect(semanticsTester, hasSemantics(
248
      TestSemantics.root(
249
        children: <TestSemantics>[
250
          TestSemantics.rootChild(
251 252 253
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
254
              TestSemantics(
Dan Field's avatar
Dan Field committed
255
                rect: const Rect.fromLTRB(5.0, 6.0, 7.0, 8.0),
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
                actions: 255,
                id: 2,
                flags: 15,
                label: 'label-after',
                value: 'value-after',
                increasedValue: 'increase-after',
                decreasedValue: 'decrease-after',
                hint: 'hint-after',
                textDirection: TextDirection.ltr,
              ),
            ],
          ),
        ],
      ),
    ));

    semanticsTester.dispose();
  });

275
  testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async {
276 277
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
278 279
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
280
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
281
          properties: SemanticsProperties(
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
            checked: false,
            selected: false,
            button: false,
            label: 'label-before',
            value: 'value-before',
            increasedValue: 'increase-before',
            decreasedValue: 'decrease-before',
            hint: 'hint-before',
            textDirection: TextDirection.rtl,
          ),
        ),
      ),
    ));

    // Start with semantics off.
    expect(tester.binding.pipelineOwner.semanticsOwner, isNull);

    // Semantics on
300
    SemanticsTester semantics = SemanticsTester(tester);
301 302 303 304 305 306 307 308 309
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);

    // Semantics off
    semantics.dispose();
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNull);

    // Semantics on
310
    semantics = SemanticsTester(tester);
311 312 313 314
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);

    semantics.dispose();
315
  }, semanticsEnabled: false);
316

317
  testWidgets('Supports all actions', (WidgetTester tester) async {
318
    final SemanticsTester semantics = SemanticsTester(tester);
319 320
    final List<SemanticsAction> performedActions = <SemanticsAction>[];

321 322 323
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
        semantics: CustomPainterSemantics(
324
          key: const ValueKey<int>(1),
Dan Field's avatar
Dan Field committed
325
          rect: const Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
326
          properties: SemanticsProperties(
327
            onDismiss: () => performedActions.add(SemanticsAction.dismiss),
328 329 330 331 332 333 334 335 336 337 338 339 340
            onTap: () => performedActions.add(SemanticsAction.tap),
            onLongPress: () => performedActions.add(SemanticsAction.longPress),
            onScrollLeft: () => performedActions.add(SemanticsAction.scrollLeft),
            onScrollRight: () => performedActions.add(SemanticsAction.scrollRight),
            onScrollUp: () => performedActions.add(SemanticsAction.scrollUp),
            onScrollDown: () => performedActions.add(SemanticsAction.scrollDown),
            onIncrease: () => performedActions.add(SemanticsAction.increase),
            onDecrease: () => performedActions.add(SemanticsAction.decrease),
            onCopy: () => performedActions.add(SemanticsAction.copy),
            onCut: () => performedActions.add(SemanticsAction.cut),
            onPaste: () => performedActions.add(SemanticsAction.paste),
            onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
            onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
341 342
            onMoveCursorForwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByWord),
            onMoveCursorBackwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByWord),
343
            onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
344
            onSetText: (String text) => performedActions.add(SemanticsAction.setText),
345 346 347 348 349 350 351
            onDidGainAccessibilityFocus: () => performedActions.add(SemanticsAction.didGainAccessibilityFocus),
            onDidLoseAccessibilityFocus: () => performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
          ),
        ),
      ),
    ));
    final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
352
      ..remove(SemanticsAction.customAction) // customAction is not user-exposed.
353
      ..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
354 355

    const int expectedId = 2;
356
    final TestSemantics expectedSemantics = TestSemantics.root(
357
      children: <TestSemantics>[
358
        TestSemantics.rootChild(
359 360
          id: 1,
          children: <TestSemantics>[
361
            TestSemantics.rootChild(
362 363
              id: expectedId,
              rect: TestSemantics.fullScreen,
364
              actions: allActions.fold<int>(0, (int previous, SemanticsAction action) => previous | action.index),
365
            ),
366
          ],
367 368 369 370 371 372
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));

    // Do the actions work?
373
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
374
    int expectedLength = 1;
375
    for (final SemanticsAction action in allActions) {
376 377 378
      switch (action) {
        case SemanticsAction.moveCursorBackwardByCharacter:
        case SemanticsAction.moveCursorForwardByCharacter:
379 380
        case SemanticsAction.moveCursorBackwardByWord:
        case SemanticsAction.moveCursorForwardByWord:
381 382 383 384 385 386 387 388
          semanticsOwner.performAction(expectedId, action, true);
          break;
        case SemanticsAction.setSelection:
          semanticsOwner.performAction(expectedId, action, <String, int>{
            'base': 4,
            'extent': 5,
          });
          break;
389 390 391
        case SemanticsAction.setText:
          semanticsOwner.performAction(expectedId, action, 'text');
          break;
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
        case SemanticsAction.copy:
        case SemanticsAction.customAction:
        case SemanticsAction.cut:
        case SemanticsAction.decrease:
        case SemanticsAction.didGainAccessibilityFocus:
        case SemanticsAction.didLoseAccessibilityFocus:
        case SemanticsAction.dismiss:
        case SemanticsAction.increase:
        case SemanticsAction.longPress:
        case SemanticsAction.paste:
        case SemanticsAction.scrollDown:
        case SemanticsAction.scrollLeft:
        case SemanticsAction.scrollRight:
        case SemanticsAction.scrollUp:
        case SemanticsAction.showOnScreen:
        case SemanticsAction.tap:
408
          semanticsOwner.performAction(expectedId, action);
409
          break;
410 411 412 413 414 415 416
      }
      expect(performedActions.length, expectedLength);
      expect(performedActions.last, action);
      expectedLength += 1;
    }

    semantics.dispose();
417
  });
418

419
  testWidgets('Supports all flags', (WidgetTester tester) async {
420
    final SemanticsTester semantics = SemanticsTester(tester);
421
    // checked state and toggled state are mutually exclusive.
422 423
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
424 425
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
426
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
427
          properties: SemanticsProperties(
428 429 430
            enabled: true,
            checked: true,
            selected: true,
431
            hidden: true,
432
            button: true,
433
            slider: true,
434
            keyboardKey: true,
435
            link: true,
436
            textField: true,
437
            readOnly: true,
438
            focused: true,
439
            focusable: true,
440 441
            inMutuallyExclusiveGroup: true,
            header: true,
442
            obscured: true,
443
            multiline: true,
444 445
            scopesRoute: true,
            namesRoute: true,
446 447
            image: true,
            liveRegion: true,
448
            toggled: true,
449 450 451 452
          ),
        ),
      ),
    ));
453
    List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
454 455
    // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
    // therefore it has to be removed.
456
    flags.remove(SemanticsFlag.hasImplicitScrolling);
457
    TestSemantics expectedSemantics = TestSemantics.root(
458
      children: <TestSemantics>[
459
        TestSemantics.rootChild(
460 461
            id: 1,
            children: <TestSemantics>[
462
              TestSemantics.rootChild(
463
                id: 2,
464
                rect: TestSemantics.fullScreen,
465
                flags: flags,
466
              ),
467
            ],
468 469 470 471 472
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));

473 474
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
475 476
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
477
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
478
          properties: SemanticsProperties(
479
            enabled: true,
480
            checked: true,
481 482 483 484
            toggled: true,
            selected: true,
            hidden: true,
            button: true,
485
            slider: true,
486
            keyboardKey: true,
487
            link: true,
488
            textField: true,
489
            readOnly: true,
490
            focused: true,
491
            focusable: true,
492 493 494
            inMutuallyExclusiveGroup: true,
            header: true,
            obscured: true,
495
            multiline: true,
496 497 498 499 500 501 502 503 504
            scopesRoute: true,
            namesRoute: true,
            image: true,
            liveRegion: true,
          ),
        ),
      ),
    ));
    flags = SemanticsFlag.values.values.toList();
505 506
    // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
    // therefore it has to be removed.
507
    flags.remove(SemanticsFlag.hasImplicitScrolling);
508
    expectedSemantics = TestSemantics.root(
509
      children: <TestSemantics>[
510
        TestSemantics.rootChild(
511 512
            id: 1,
            children: <TestSemantics>[
513
              TestSemantics.rootChild(
514 515 516 517
                id: 2,
                rect: TestSemantics.fullScreen,
                flags: flags,
              ),
518
            ],
519 520 521 522
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
523
    semantics.dispose();
524
  });
525

526 527
  group('diffing', () {
    testWidgets('complains about duplicate keys', (WidgetTester tester) async {
528 529 530
      final SemanticsTester semanticsTester = SemanticsTester(tester);
      await tester.pumpWidget(CustomPaint(
        painter: _SemanticsDiffTest(<String>[
531 532 533 534 535 536 537 538
          'a-k',
          'a-k',
        ]),
      ));
      expect(tester.takeException(), isFlutterError);
      semanticsTester.dispose();
    });

539
    _testDiff('adds one item to an empty list', (_DiffTester tester) async {
540 541 542 543 544 545
      await tester.diff(
        from: <String>[],
        to: <String>['a'],
      );
    });

546
    _testDiff('removes the last item from the list', (_DiffTester tester) async {
547 548 549 550 551 552
      await tester.diff(
        from: <String>['a'],
        to: <String>[],
      );
    });

553
    _testDiff('appends one item at the end of a non-empty list', (_DiffTester tester) async {
554 555 556 557 558 559
      await tester.diff(
        from: <String>['a'],
        to: <String>['a', 'b'],
      );
    });

560
    _testDiff('prepends one item at the beginning of a non-empty list', (_DiffTester tester) async {
561 562 563 564 565 566
      await tester.diff(
        from: <String>['b'],
        to: <String>['a', 'b'],
      );
    });

567
    _testDiff('inserts one item in the middle of a list', (_DiffTester tester) async {
568 569 570 571 572 573 574 575 576 577 578 579 580
      await tester.diff(
        from: <String>[
          'a-k',
          'c-k',
        ],
        to: <String>[
          'a-k',
          'b-k',
          'c-k',
        ],
      );
    });

581
    _testDiff('removes one item from the middle of a list', (_DiffTester tester) async {
582 583 584 585 586 587 588 589 590 591 592 593 594
      await tester.diff(
        from: <String>[
          'a-k',
          'b-k',
          'c-k',
        ],
        to: <String>[
          'a-k',
          'c-k',
        ],
      );
    });

595
    _testDiff('swaps two items', (_DiffTester tester) async {
596 597 598 599 600 601 602 603 604 605 606 607
      await tester.diff(
        from: <String>[
          'a-k',
          'b-k',
        ],
        to: <String>[
          'b-k',
          'a-k',
        ],
      );
    });

608
    _testDiff('finds and moved one keyed item', (_DiffTester tester) async {
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
      await tester.diff(
        from: <String>[
          'a-k',
          'b',
          'c',
        ],
        to: <String>[
          'b',
          'c',
          'a-k',
        ],
      );
    });
  });

  testWidgets('rebuilds semantics upon resize', (WidgetTester tester) async {
625
    final SemanticsTester semanticsTester = SemanticsTester(tester);
626

627
    final _PainterWithSemantics painter = _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
628
      semantics: const CustomPainterSemantics(
629
        rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
630
        properties: SemanticsProperties(
631 632 633 634 635 636
          label: 'background',
          textDirection: TextDirection.rtl,
        ),
      ),
    );

637
    final CustomPaint paint = CustomPaint(painter: painter);
638

639
    await tester.pumpWidget(SizedBox(
640 641 642 643 644 645 646 647
      height: 20.0,
      width: 20.0,
      child: paint,
    ));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

648
    await tester.pumpWidget(SizedBox(
649 650 651 652 653 654 655 656
      height: 20.0,
      width: 20.0,
      child: paint,
    ));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

657
    await tester.pumpWidget(SizedBox(
658 659 660 661 662 663 664 665 666 667 668 669
      height: 40.0,
      width: 40.0,
      child: paint,
    ));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 2);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

    semanticsTester.dispose();
  });

  testWidgets('does not rebuild when shouldRebuildSemantics is false', (WidgetTester tester) async {
670
    final SemanticsTester semanticsTester = SemanticsTester(tester);
671

Dan Field's avatar
Dan Field committed
672
    const CustomPainterSemantics testSemantics = CustomPainterSemantics(
673
      rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
674
      properties: SemanticsProperties(
675 676 677 678 679
        label: 'background',
        textDirection: TextDirection.rtl,
      ),
    );

680
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
681 682 683 684 685 686
      semantics: testSemantics,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

687
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
688 689 690 691 692 693
      semantics: testSemantics,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

Dan Field's avatar
Dan Field committed
694
    const CustomPainterSemantics testSemantics2 = CustomPainterSemantics(
695
      rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
696
      properties: SemanticsProperties(
697 698 699 700 701
        label: 'background',
        textDirection: TextDirection.rtl,
      ),
    );

702
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
703 704 705
      semantics: testSemantics2,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 2);
Dan Field's avatar
Dan Field committed
706 707
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
708 709 710 711 712

    semanticsTester.dispose();
  });
}

713
void _testDiff(String description, Future<void> Function(_DiffTester tester) testFunction) {
714
  testWidgets(description, (WidgetTester tester) async {
715
    await testFunction(_DiffTester(tester));
716 717 718 719 720 721 722 723 724 725
  });
}

class _DiffTester {
  _DiffTester(this.tester);

  final WidgetTester tester;

  /// Creates an initial semantics list using the `from` list, then updates the
  /// list to the `to` list. This causes [RenderCustomPaint] to diff the two
726
  /// lists and apply the changes. This method asserts the changes were
727 728 729 730
  /// applied correctly, specifically:
  ///
  /// - checks that initial and final configurations are in the desired states.
  /// - checks that keyed nodes have stable IDs.
731
  Future<void> diff({ required List<String> from, required List<String> to }) async {
732
    final SemanticsTester semanticsTester = SemanticsTester(tester);
733 734

    TestSemantics createExpectations(List<String> labels) {
735
      return TestSemantics.root(
736
        children: <TestSemantics>[
737
          TestSemantics.rootChild(
738
            rect: TestSemantics.fullScreen,
739
            children: <TestSemantics>[
740
              for (final String label in labels)
741 742 743 744 745
                TestSemantics(
                  rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
                  label: label,
                ),
            ],
746 747 748 749 750
          ),
        ],
      );
    }

751 752
    await tester.pumpWidget(CustomPaint(
      painter: _SemanticsDiffTest(from),
753 754 755
    ));
    expect(semanticsTester, hasSemantics(createExpectations(from), ignoreId: true));

756
    SemanticsNode root = RendererBinding.instance.renderView.debugSemantics!;
757 758 759 760
    final Map<Key, int> idAssignments = <Key, int>{};
    root.visitChildren((SemanticsNode firstChild) {
      firstChild.visitChildren((SemanticsNode node) {
        if (node.key != null) {
761
          idAssignments[node.key!] = node.id;
762 763 764 765 766 767
        }
        return true;
      });
      return true;
    });

768 769
    await tester.pumpWidget(CustomPaint(
      painter: _SemanticsDiffTest(to),
770 771 772 773
    ));
    await tester.pumpAndSettle();
    expect(semanticsTester, hasSemantics(createExpectations(to), ignoreId: true));

774
    root = RendererBinding.instance.renderView.debugSemantics!;
775 776 777 778
    root.visitChildren((SemanticsNode firstChild) {
      firstChild.visitChildren((SemanticsNode node) {
        if (node.key != null && idAssignments[node.key] != null) {
          expect(idAssignments[node.key], node.id, reason:
779
            'Node with key ${node.key} was previously assigned ID ${idAssignments[node.key]}. '
780
            'After diffing the child list, its ID changed to ${node.id}. IDs must be stable.',
781
          );
782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
        }
        return true;
      });
      return true;
    });

    semanticsTester.dispose();
  }
}

class _SemanticsDiffTest extends CustomPainter {
  _SemanticsDiffTest(this.data);

  final List<String> data;

  @override
  void paint(Canvas canvas, Size size) {
    // We don't test painting.
  }

  @override
  SemanticsBuilderCallback get semanticsBuilder => buildSemantics;

  List<CustomPainterSemantics> buildSemantics(Size size) {
    final List<CustomPainterSemantics> semantics = <CustomPainterSemantics>[];
807
    for (final String label in data) {
808
      Key? key;
809
      if (label.endsWith('-k')) {
810
        key = ValueKey<String>(label);
811 812
      }
      semantics.add(
813
        CustomPainterSemantics(
Dan Field's avatar
Dan Field committed
814
          rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
815
          key: key,
816
          properties: SemanticsProperties(
817 818 819 820 821 822 823 824 825 826 827 828 829 830
            label: label,
            textDirection: TextDirection.rtl,
          ),
        ),
      );
    }
    return semantics;
  }

  @override
  bool shouldRepaint(_SemanticsDiffTest oldPainter) => true;
}

class _PainterWithSemantics extends CustomPainter {
831
  _PainterWithSemantics({ required this.semantics });
832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877

  final CustomPainterSemantics semantics;

  static int semanticsBuilderCallCount = 0;
  static int buildSemanticsCallCount = 0;
  static int shouldRebuildSemanticsCallCount = 0;

  @override
  void paint(Canvas canvas, Size size) {
    // We don't test painting.
  }

  @override
  SemanticsBuilderCallback get semanticsBuilder {
    semanticsBuilderCallCount += 1;
    return buildSemantics;
  }

  List<CustomPainterSemantics> buildSemantics(Size size) {
    buildSemanticsCallCount += 1;
    return <CustomPainterSemantics>[semantics];
  }

  @override
  bool shouldRepaint(_PainterWithSemantics oldPainter) {
    return true;
  }

  @override
  bool shouldRebuildSemantics(_PainterWithSemantics oldPainter) {
    shouldRebuildSemanticsCallCount += 1;
    return !identical(oldPainter.semantics, semantics);
  }
}

class _PainterWithoutSemantics extends CustomPainter {
  _PainterWithoutSemantics();

  @override
  void paint(Canvas canvas, Size size) {
    // We don't test painting.
  }

  @override
  bool shouldRepaint(_PainterWithSemantics oldPainter) => true;
}