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

import 'dart:async';
6
import 'dart:ui';
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

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 {
29
    final SemanticsTester semanticsTester = SemanticsTester(tester);
30

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

    expect(semanticsTester, hasSemantics(
36
      TestSemantics.root(
37 38 39 40 41 42 43 44
        children: const <TestSemantics>[],
      ),
    ));

    semanticsTester.dispose();
  });

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

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

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

    semanticsTester.dispose();
  });

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

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

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

    semanticsTester.dispose();
  });

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

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

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

    semanticsTester.dispose();
  });

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

178 179
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
180 181
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
182
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
183
          properties: SemanticsProperties(
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
            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(
199
      TestSemantics.root(
200
        children: <TestSemantics>[
201
          TestSemantics.rootChild(
202 203 204
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
205
              TestSemantics(
Dan Field's avatar
Dan Field committed
206
                rect: const Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
                id: 2,
                flags: 1,
                label: 'label-before',
                value: 'value-before',
                increasedValue: 'increase-before',
                decreasedValue: 'decrease-before',
                hint: 'hint-before',
                textDirection: TextDirection.rtl,
              ),
            ],
          ),
        ],
      ),
    ));

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

    expect(semanticsTester, hasSemantics(
251
      TestSemantics.root(
252
        children: <TestSemantics>[
253
          TestSemantics.rootChild(
254 255 256
            id: 1,
            rect: TestSemantics.fullScreen,
            children: <TestSemantics>[
257
              TestSemantics(
Dan Field's avatar
Dan Field committed
258
                rect: const Rect.fromLTRB(5.0, 6.0, 7.0, 8.0),
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
                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();
  });

278
  testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async {
279 280
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
281 282
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
283
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
284
          properties: SemanticsProperties(
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
            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
303
    SemanticsTester semantics = SemanticsTester(tester);
304 305 306 307 308 309 310 311 312
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);

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

    // Semantics on
313
    semantics = SemanticsTester(tester);
314 315 316 317
    await tester.pumpAndSettle();
    expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull);

    semantics.dispose();
318
  }, semanticsEnabled: false);
319

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

324 325 326
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
        semantics: CustomPainterSemantics(
327
          key: const ValueKey<int>(1),
Dan Field's avatar
Dan Field committed
328
          rect: const Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
329
          properties: SemanticsProperties(
330
            onDismiss: () => performedActions.add(SemanticsAction.dismiss),
331 332 333 334 335 336 337 338 339 340 341 342 343
            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),
344 345
            onMoveCursorForwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByWord),
            onMoveCursorBackwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByWord),
346 347 348 349 350 351 352 353
            onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
            onDidGainAccessibilityFocus: () => performedActions.add(SemanticsAction.didGainAccessibilityFocus),
            onDidLoseAccessibilityFocus: () => performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
          ),
        ),
      ),
    ));
    final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
354
      ..remove(SemanticsAction.customAction) // customAction is not user-exposed.
355
      ..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
356 357

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

    // Do the actions work?
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
    int expectedLength = 1;
    for (SemanticsAction action in allActions) {
      switch (action) {
        case SemanticsAction.moveCursorBackwardByCharacter:
        case SemanticsAction.moveCursorForwardByCharacter:
381 382
        case SemanticsAction.moveCursorBackwardByWord:
        case SemanticsAction.moveCursorForwardByWord:
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
          semanticsOwner.performAction(expectedId, action, true);
          break;
        case SemanticsAction.setSelection:
          semanticsOwner.performAction(expectedId, action, <String, int>{
            'base': 4,
            'extent': 5,
          });
          break;
        default:
          semanticsOwner.performAction(expectedId, action);
      }
      expect(performedActions.length, expectedLength);
      expect(performedActions.last, action);
      expectedLength += 1;
    }

    semantics.dispose();
  });

402
  testWidgets('Supports all flags', (WidgetTester tester) async {
403
    final SemanticsTester semantics = SemanticsTester(tester);
404
    // checked state and toggled state are mutually exclusive.
405 406
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
407 408
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
409
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
410
          properties: SemanticsProperties(
411 412 413
            enabled: true,
            checked: true,
            selected: true,
414
            hidden: true,
415 416
            button: true,
            textField: true,
417
            readOnly: true,
418 419 420
            focused: true,
            inMutuallyExclusiveGroup: true,
            header: true,
421
            obscured: true,
422
            multiline: true,
423 424
            scopesRoute: true,
            namesRoute: true,
425 426
            image: true,
            liveRegion: true,
427
            toggled: true,
428 429 430 431
          ),
        ),
      ),
    ));
432
    List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
433 434
    // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
    // therefore it has to be removed.
435
    flags.remove(SemanticsFlag.hasImplicitScrolling);
436
    TestSemantics expectedSemantics = TestSemantics.root(
437
      children: <TestSemantics>[
438
        TestSemantics.rootChild(
439 440
            id: 1,
            children: <TestSemantics>[
441
              TestSemantics.rootChild(
442
                id: 2,
443
                rect: TestSemantics.fullScreen,
444
                flags: flags,
445
              ),
446
            ],
447 448 449 450 451
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));

452 453
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
454 455
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
456
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
457
          properties: SemanticsProperties(
458
            enabled: true,
459
            checked: true,
460 461 462 463 464
            toggled: true,
            selected: true,
            hidden: true,
            button: true,
            textField: true,
465
            readOnly: true,
466 467 468 469
            focused: true,
            inMutuallyExclusiveGroup: true,
            header: true,
            obscured: true,
470
            multiline: true,
471 472 473 474 475 476 477 478 479
            scopesRoute: true,
            namesRoute: true,
            image: true,
            liveRegion: true,
          ),
        ),
      ),
    ));
    flags = SemanticsFlag.values.values.toList();
480 481
    // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
    // therefore it has to be removed.
482
    flags.remove(SemanticsFlag.hasImplicitScrolling);
483

484
    expectedSemantics = TestSemantics.root(
485
      children: <TestSemantics>[
486
        TestSemantics.rootChild(
487 488
            id: 1,
            children: <TestSemantics>[
489
              TestSemantics.rootChild(
490 491 492 493
                id: 2,
                rect: TestSemantics.fullScreen,
                flags: flags,
              ),
494
            ],
495 496 497 498
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
499
    semantics.dispose();
500
  }, skip: isBrowser);
501

502 503
  group('diffing', () {
    testWidgets('complains about duplicate keys', (WidgetTester tester) async {
504 505 506
      final SemanticsTester semanticsTester = SemanticsTester(tester);
      await tester.pumpWidget(CustomPaint(
        painter: _SemanticsDiffTest(<String>[
507 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 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600
          'a-k',
          'a-k',
        ]),
      ));
      expect(tester.takeException(), isFlutterError);
      semanticsTester.dispose();
    });

    testDiff('adds one item to an empty list', (_DiffTester tester) async {
      await tester.diff(
        from: <String>[],
        to: <String>['a'],
      );
    });

    testDiff('removes the last item from the list', (_DiffTester tester) async {
      await tester.diff(
        from: <String>['a'],
        to: <String>[],
      );
    });

    testDiff('appends one item at the end of a non-empty list', (_DiffTester tester) async {
      await tester.diff(
        from: <String>['a'],
        to: <String>['a', 'b'],
      );
    });

    testDiff('prepends one item at the beginning of a non-empty list', (_DiffTester tester) async {
      await tester.diff(
        from: <String>['b'],
        to: <String>['a', 'b'],
      );
    });

    testDiff('inserts one item in the middle of a list', (_DiffTester tester) async {
      await tester.diff(
        from: <String>[
          'a-k',
          'c-k',
        ],
        to: <String>[
          'a-k',
          'b-k',
          'c-k',
        ],
      );
    });

    testDiff('removes one item from the middle of a list', (_DiffTester tester) async {
      await tester.diff(
        from: <String>[
          'a-k',
          'b-k',
          'c-k',
        ],
        to: <String>[
          'a-k',
          'c-k',
        ],
      );
    });

    testDiff('swaps two items', (_DiffTester tester) async {
      await tester.diff(
        from: <String>[
          'a-k',
          'b-k',
        ],
        to: <String>[
          'b-k',
          'a-k',
        ],
      );
    });

    testDiff('finds and moved one keyed item', (_DiffTester tester) async {
      await tester.diff(
        from: <String>[
          'a-k',
          'b',
          'c',
        ],
        to: <String>[
          'b',
          'c',
          'a-k',
        ],
      );
    });
  });

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

603
    final _PainterWithSemantics painter = _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
604
      semantics: const CustomPainterSemantics(
605
        rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
606
        properties: SemanticsProperties(
607 608 609 610 611 612
          label: 'background',
          textDirection: TextDirection.rtl,
        ),
      ),
    );

613
    final CustomPaint paint = CustomPaint(painter: painter);
614

615
    await tester.pumpWidget(SizedBox(
616 617 618 619 620 621 622 623
      height: 20.0,
      width: 20.0,
      child: paint,
    ));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

624
    await tester.pumpWidget(SizedBox(
625 626 627 628 629 630 631 632
      height: 20.0,
      width: 20.0,
      child: paint,
    ));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

633
    await tester.pumpWidget(SizedBox(
634 635 636 637 638 639 640 641 642 643 644 645
      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 {
646
    final SemanticsTester semanticsTester = SemanticsTester(tester);
647

Dan Field's avatar
Dan Field committed
648
    const CustomPainterSemantics testSemantics = CustomPainterSemantics(
649
      rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
650
      properties: SemanticsProperties(
651 652 653 654 655
        label: 'background',
        textDirection: TextDirection.rtl,
      ),
    );

656
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
657 658 659 660 661 662
      semantics: testSemantics,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

663
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
664 665 666 667 668 669
      semantics: testSemantics,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

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

678
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
679 680 681
      semantics: testSemantics2,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 2);
Dan Field's avatar
Dan Field committed
682 683
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
684 685 686 687 688

    semanticsTester.dispose();
  });
}

689
void testDiff(String description, Future<void> Function(_DiffTester tester) testFunction) {
690
  testWidgets(description, (WidgetTester tester) async {
691
    await testFunction(_DiffTester(tester));
692 693 694 695 696 697 698 699 700 701
  });
}

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
702
  /// lists and apply the changes. This method asserts the changes were
703 704 705 706
  /// applied correctly, specifically:
  ///
  /// - checks that initial and final configurations are in the desired states.
  /// - checks that keyed nodes have stable IDs.
707
  Future<void> diff({ List<String> from, List<String> to }) async {
708
    final SemanticsTester semanticsTester = SemanticsTester(tester);
709 710

    TestSemantics createExpectations(List<String> labels) {
711
      return TestSemantics.root(
712
        children: <TestSemantics>[
713
          TestSemantics.rootChild(
714
            rect: TestSemantics.fullScreen,
715 716 717 718 719 720 721
            children: <TestSemantics>[
              for (String label in labels)
                TestSemantics(
                  rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
                  label: label,
                ),
            ],
722 723 724 725 726
          ),
        ],
      );
    }

727 728
    await tester.pumpWidget(CustomPaint(
      painter: _SemanticsDiffTest(from),
729 730 731 732 733 734 735 736 737 738 739 740 741 742 743
    ));
    expect(semanticsTester, hasSemantics(createExpectations(from), ignoreId: true));

    SemanticsNode root = RendererBinding.instance?.renderView?.debugSemantics;
    final Map<Key, int> idAssignments = <Key, int>{};
    root.visitChildren((SemanticsNode firstChild) {
      firstChild.visitChildren((SemanticsNode node) {
        if (node.key != null) {
          idAssignments[node.key] = node.id;
        }
        return true;
      });
      return true;
    });

744 745
    await tester.pumpWidget(CustomPaint(
      painter: _SemanticsDiffTest(to),
746 747 748 749 750 751 752 753 754
    ));
    await tester.pumpAndSettle();
    expect(semanticsTester, hasSemantics(createExpectations(to), ignoreId: true));

    root = RendererBinding.instance?.renderView?.debugSemantics;
    root.visitChildren((SemanticsNode firstChild) {
      firstChild.visitChildren((SemanticsNode node) {
        if (node.key != null && idAssignments[node.key] != null) {
          expect(idAssignments[node.key], node.id, reason:
755
            'Node with key ${node.key} was previously assigned ID ${idAssignments[node.key]}. '
756
            'After diffing the child list, its ID changed to ${node.id}. IDs must be stable.',
757
          );
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785
        }
        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>[];
    for (String label in data) {
      Key key;
      if (label.endsWith('-k')) {
786
        key = ValueKey<String>(label);
787 788
      }
      semantics.add(
789
        CustomPainterSemantics(
Dan Field's avatar
Dan Field committed
790
          rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
791
          key: key,
792
          properties: SemanticsProperties(
793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853
            label: label,
            textDirection: TextDirection.rtl,
          ),
        ),
      );
    }
    return semantics;
  }

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

class _PainterWithSemantics extends CustomPainter {
  _PainterWithSemantics({ this.semantics });

  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;
}