custom_painter_test.dart 26.3 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
            button: true,
416
            link: true,
417
            textField: true,
418
            readOnly: true,
419
            focused: true,
420
            focusable: true,
421 422
            inMutuallyExclusiveGroup: true,
            header: true,
423
            obscured: true,
424
            multiline: true,
425 426
            scopesRoute: true,
            namesRoute: true,
427 428
            image: true,
            liveRegion: true,
429
            toggled: true,
430 431 432 433
          ),
        ),
      ),
    ));
434
    List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
435
    print('flags: $flags');
436 437
    // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
    // therefore it has to be removed.
438
    flags.remove(SemanticsFlag.hasImplicitScrolling);
439
    TestSemantics expectedSemantics = TestSemantics.root(
440
      children: <TestSemantics>[
441
        TestSemantics.rootChild(
442 443
            id: 1,
            children: <TestSemantics>[
444
              TestSemantics.rootChild(
445
                id: 2,
446
                rect: TestSemantics.fullScreen,
447
                flags: flags,
448
              ),
449
            ],
450 451 452 453 454
        ),
      ],
    );
    expect(semantics, hasSemantics(expectedSemantics, ignoreRect: true, ignoreTransform: true));

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

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

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

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

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

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

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

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

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

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

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

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

    semanticsTester.dispose();
  });
}

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

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

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

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

748 749
    await tester.pumpWidget(CustomPaint(
      painter: _SemanticsDiffTest(to),
750 751 752 753 754 755 756 757 758
    ));
    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:
759
            'Node with key ${node.key} was previously assigned ID ${idAssignments[node.key]}. '
760
            'After diffing the child list, its ID changed to ${node.id}. IDs must be stable.',
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 786 787 788 789
        }
        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')) {
790
        key = ValueKey<String>(label);
791 792
      }
      semantics.add(
793
        CustomPainterSemantics(
Dan Field's avatar
Dan Field committed
794
          rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
795
          key: key,
796
          properties: SemanticsProperties(
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 854 855 856 857
            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;
}