custom_painter_test.dart 26.3 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
// @dart = 2.8

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

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

33 34
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithoutSemantics(),
35 36 37
    ));

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

    semanticsTester.dispose();
  });

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

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

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

    semanticsTester.dispose();
  });

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

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

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

    semanticsTester.dispose();
  });

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

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

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

    semanticsTester.dispose();
  });

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

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

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

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

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

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

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

    semantics.dispose();
320
  }, semanticsEnabled: false);
321

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

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

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

    // Do the actions work?
    final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner;
    int expectedLength = 1;
379
    for (final SemanticsAction action in allActions) {
380 381 382
      switch (action) {
        case SemanticsAction.moveCursorBackwardByCharacter:
        case SemanticsAction.moveCursorForwardByCharacter:
383 384
        case SemanticsAction.moveCursorBackwardByWord:
        case SemanticsAction.moveCursorForwardByWord:
385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403
          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();
  });

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

456 457
    await tester.pumpWidget(CustomPaint(
      painter: _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
458 459
        semantics: const CustomPainterSemantics(
          key: ValueKey<int>(1),
460
          rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
Dan Field's avatar
Dan Field committed
461
          properties: SemanticsProperties(
462
            enabled: true,
463
            checked: true,
464 465 466 467
            toggled: true,
            selected: true,
            hidden: true,
            button: true,
468
            link: true,
469
            textField: true,
470
            readOnly: true,
471
            focused: true,
472
            focusable: true,
473 474 475
            inMutuallyExclusiveGroup: true,
            header: true,
            obscured: true,
476
            multiline: true,
477 478 479 480 481 482 483 484 485
            scopesRoute: true,
            namesRoute: true,
            image: true,
            liveRegion: true,
          ),
        ),
      ),
    ));
    flags = SemanticsFlag.values.values.toList();
486 487
    // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
    // therefore it has to be removed.
488
    flags.remove(SemanticsFlag.hasImplicitScrolling);
489
    expectedSemantics = TestSemantics.root(
490
      children: <TestSemantics>[
491
        TestSemantics.rootChild(
492 493
            id: 1,
            children: <TestSemantics>[
494
              TestSemantics.rootChild(
495 496 497 498
                id: 2,
                rect: TestSemantics.fullScreen,
                flags: flags,
              ),
499
            ],
500 501 502 503
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));
504
    semantics.dispose();
505
  });
506

507 508
  group('diffing', () {
    testWidgets('complains about duplicate keys', (WidgetTester tester) async {
509 510 511
      final SemanticsTester semanticsTester = SemanticsTester(tester);
      await tester.pumpWidget(CustomPaint(
        painter: _SemanticsDiffTest(<String>[
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 601 602 603 604 605
          '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 {
606
    final SemanticsTester semanticsTester = SemanticsTester(tester);
607

608
    final _PainterWithSemantics painter = _PainterWithSemantics(
Dan Field's avatar
Dan Field committed
609
      semantics: const CustomPainterSemantics(
610
        rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
611
        properties: SemanticsProperties(
612 613 614 615 616 617
          label: 'background',
          textDirection: TextDirection.rtl,
        ),
      ),
    );

618
    final CustomPaint paint = CustomPaint(painter: painter);
619

620
    await tester.pumpWidget(SizedBox(
621 622 623 624 625 626 627 628
      height: 20.0,
      width: 20.0,
      child: paint,
    ));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

629
    await tester.pumpWidget(SizedBox(
630 631 632 633 634 635 636 637
      height: 20.0,
      width: 20.0,
      child: paint,
    ));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 0);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

638
    await tester.pumpWidget(SizedBox(
639 640 641 642 643 644 645 646 647 648 649 650
      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 {
651
    final SemanticsTester semanticsTester = SemanticsTester(tester);
652

Dan Field's avatar
Dan Field committed
653
    const CustomPainterSemantics testSemantics = CustomPainterSemantics(
654
      rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
Dan Field's avatar
Dan Field committed
655
      properties: SemanticsProperties(
656 657 658 659 660
        label: 'background',
        textDirection: TextDirection.rtl,
      ),
    );

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

668
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
669 670 671 672 673 674
      semantics: testSemantics,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);

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

683
    await tester.pumpWidget(CustomPaint(painter: _PainterWithSemantics(
684 685 686
      semantics: testSemantics2,
    )));
    expect(_PainterWithSemantics.shouldRebuildSemanticsCallCount, 2);
Dan Field's avatar
Dan Field committed
687 688
    expect(_PainterWithSemantics.buildSemanticsCallCount, 1);
    expect(_PainterWithSemantics.semanticsBuilderCallCount, 4);
689 690 691 692 693

    semanticsTester.dispose();
  });
}

694
void testDiff(String description, Future<void> Function(_DiffTester tester) testFunction) {
695
  testWidgets(description, (WidgetTester tester) async {
696
    await testFunction(_DiffTester(tester));
697 698 699 700 701 702 703 704 705 706
  });
}

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

    TestSemantics createExpectations(List<String> labels) {
716
      return TestSemantics.root(
717
        children: <TestSemantics>[
718
          TestSemantics.rootChild(
719
            rect: TestSemantics.fullScreen,
720
            children: <TestSemantics>[
721
              for (final String label in labels)
722 723 724 725 726
                TestSemantics(
                  rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
                  label: label,
                ),
            ],
727 728 729 730 731
          ),
        ],
      );
    }

732 733
    await tester.pumpWidget(CustomPaint(
      painter: _SemanticsDiffTest(from),
734 735 736 737 738 739 740 741 742 743 744 745 746 747 748
    ));
    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;
    });

749 750
    await tester.pumpWidget(CustomPaint(
      painter: _SemanticsDiffTest(to),
751 752 753 754 755 756 757 758 759
    ));
    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:
760
            'Node with key ${node.key} was previously assigned ID ${idAssignments[node.key]}. '
761
            'After diffing the child list, its ID changed to ${node.id}. IDs must be stable.',
762
          );
763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787
        }
        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>[];
788
    for (final String label in data) {
789 790
      Key key;
      if (label.endsWith('-k')) {
791
        key = ValueKey<String>(label);
792 793
      }
      semantics.add(
794
        CustomPainterSemantics(
Dan Field's avatar
Dan Field committed
795
          rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
796
          key: key,
797
          properties: SemanticsProperties(
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 854 855 856 857 858
            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;
}