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

5
import 'package:flutter/gestures.dart';
6
import 'package:flutter/material.dart';
7
import 'package:flutter/rendering.dart';
8
import 'package:flutter/scheduler.dart';
9
import 'package:flutter_test/flutter_test.dart';
10

11
import '../rendering/mock_canvas.dart';
12
import '../widgets/semantics_tester.dart';
13 14
import 'feedback_tester.dart';

15
Finder findRenderChipElement() {
16
  return find.byElementPredicate((Element e) => '${e.renderObject.runtimeType}' == '_RenderChip');
17
}
18

19 20 21 22 23 24 25 26 27
RenderBox getMaterialBox(WidgetTester tester) {
  return tester.firstRenderObject<RenderBox>(
    find.descendant(
      of: find.byType(RawChip),
      matching: find.byType(CustomPaint),
    ),
  );
}

28 29 30 31 32 33 34 35 36
Material getMaterial(WidgetTester tester) {
  return tester.widget<Material>(
    find.descendant(
      of: find.byType(RawChip),
      matching: find.byType(Material),
    ),
  );
}

37 38 39 40 41 42 43 44 45 46
IconThemeData getIconData(WidgetTester tester) {
  final IconTheme iconTheme = tester.firstWidget(
    find.descendant(
      of: find.byType(RawChip),
      matching: find.byType(IconTheme),
    ),
  );
  return iconTheme.data;
}

47
DefaultTextStyle getLabelStyle(WidgetTester tester, String labelText) {
48
  return tester.widget(
49 50
    find.ancestor(
      of: find.text(labelText),
51
      matching: find.byType(DefaultTextStyle),
52
    ).first,
53 54 55
  );
}

56 57 58 59 60 61 62 63
dynamic getRenderChip(WidgetTester tester) {
  if (!tester.any(findRenderChipElement())) {
    return null;
  }
  final Element element = tester.element(findRenderChipElement());
  return element.renderObject;
}

64
// ignore: avoid_dynamic_calls
65
double getSelectProgress(WidgetTester tester) => getRenderChip(tester)?.checkmarkAnimation?.value as double;
66
// ignore: avoid_dynamic_calls
67
double getAvatarDrawerProgress(WidgetTester tester) => getRenderChip(tester)?.avatarDrawerAnimation?.value as double;
68
// ignore: avoid_dynamic_calls
69
double getDeleteDrawerProgress(WidgetTester tester) => getRenderChip(tester)?.deleteDrawerAnimation?.value as double;
70

71
/// Adds the basic requirements for a Chip.
72
Widget wrapForChip({
73
  required Widget child,
74 75
  TextDirection textDirection = TextDirection.ltr,
  double textScaleFactor = 1.0,
76
  Brightness brightness = Brightness.light,
77
}) {
78
  return MaterialApp(
79
    theme: ThemeData(brightness: brightness),
80
    home: Directionality(
81
      textDirection: textDirection,
82
      child: MediaQuery(
83
        data: MediaQueryData.fromWindow(WidgetsBinding.instance.window).copyWith(textScaleFactor: textScaleFactor),
84
        child: Material(child: child),
85 86 87 88 89
      ),
    ),
  );
}

90 91 92 93
/// Tests that a [Chip] that has its size constrained by its parent is
/// further constraining the size of its child, the label widget.
/// Optionally, adding an avatar or delete icon to the chip should not
/// cause the chip or label to exceed its constrained height.
94
Future<void> testConstrainedLabel(
95
  WidgetTester tester, {
96 97
  CircleAvatar? avatar,
  VoidCallback? onDeleted,
98 99 100 101 102
}) async {
  const double labelWidth = 100.0;
  const double labelHeight = 50.0;
  const double chipParentWidth = 75.0;
  const double chipParentHeight = 25.0;
103
  final Key labelKey = UniqueKey();
104 105

  await tester.pumpWidget(
106
    wrapForChip(
107
      child: Center(
108
        child: SizedBox(
109 110
          width: chipParentWidth,
          height: chipParentHeight,
111
          child: Chip(
112
            avatar: avatar,
113
            label: SizedBox(
114 115 116
              key: labelKey,
              width: labelWidth,
              height: labelHeight,
117
            ),
118
            onDeleted: onDeleted,
119 120 121
          ),
        ),
      ),
122 123
    ),
  );
124

125 126 127
  final Size labelSize = tester.getSize(find.byKey(labelKey));
  expect(labelSize.width, lessThan(chipParentWidth));
  expect(labelSize.height, lessThanOrEqualTo(chipParentHeight));
128

129 130 131 132
  final Size chipSize = tester.getSize(find.byType(Chip));
  expect(chipSize.width, chipParentWidth);
  expect(chipSize.height, chipParentHeight);
}
133

134
void doNothing() {}
135

136
Widget chipWithOptionalDeleteButton({
137 138
  Key? deleteButtonKey,
  Key? labelKey,
139
  required bool deletable,
140
  TextDirection textDirection = TextDirection.ltr,
141 142
  bool useDeleteButtonTooltip = true,
  String? chipTooltip,
143
  String? deleteButtonTooltipMessage,
144
  VoidCallback? onPressed = doNothing,
145
}) {
146
  return wrapForChip(
147 148 149 150
    textDirection: textDirection,
    child: Wrap(
      children: <Widget>[
        RawChip(
151
          tooltip: chipTooltip,
152
          onPressed: onPressed,
153
          onDeleted: deletable ? doNothing : null,
154
          deleteIcon: Icon(Icons.close, key: deleteButtonKey),
155
          useDeleteButtonTooltip: useDeleteButtonTooltip,
156
          deleteButtonTooltipMessage: deleteButtonTooltipMessage,
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177
          label: Text(
            deletable
              ? 'Chip with Delete Button'
              : 'Chip without Delete Button',
            key: labelKey,
          ),
        ),
      ],
    ),
  );
}

bool offsetsAreClose(Offset a, Offset b) => (a - b).distance < 1.0;
bool radiiAreClose(double a, double b) => (a - b).abs() < 1.0;

// Ripple pattern matches if there exists at least one ripple
// with the [expectedCenter] and [expectedRadius].
// This ensures the existence of a ripple.
PaintPattern ripplePattern(Offset expectedCenter, double expectedRadius) {
  return paints
    ..something((Symbol method, List<dynamic> arguments) {
178
        if (method != #drawCircle) {
179
          return false;
180
        }
181 182
        final Offset center = arguments[0] as Offset;
        final double radius = arguments[1] as double;
183 184 185 186 187 188 189 190 191 192 193
        return offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius);
      }
    );
}

// Unique ripple pattern matches if there does not exist ripples
// other than ones with the [expectedCenter] and [expectedRadius].
// This ensures the nonexistence of two different ripples.
PaintPattern uniqueRipplePattern(Offset expectedCenter, double expectedRadius) {
  return paints
    ..everything((Symbol method, List<dynamic> arguments) {
194
        if (method != #drawCircle) {
195
          return true;
196
        }
197 198
        final Offset center = arguments[0] as Offset;
        final double radius = arguments[1] as double;
199
        if (offsetsAreClose(center, expectedCenter) && radiiAreClose(radius, expectedRadius)) {
200
          return true;
201
        }
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
        throw '''
              Expected: center == $expectedCenter, radius == $expectedRadius
              Found: center == $center radius == $radius''';
      }
    );
}

// Finds any container of a tooltip.
Finder findTooltipContainer(String tooltipText) {
  return find.ancestor(
    of: find.text(tooltipText),
    matching: find.byType(Container),
  );
}

217
void main() {
218
  testWidgets('Chip defaults', (WidgetTester tester) async {
219 220
    late TextTheme textTheme;

221 222 223 224 225
    Widget buildFrame(Brightness brightness) {
      return MaterialApp(
        theme: ThemeData(brightness: brightness),
        home: Scaffold(
          body: Center(
226 227 228 229 230 231 232 233 234
            child: Builder(
              builder: (BuildContext context) {
                textTheme = Theme.of(context).textTheme;
                return Chip(
                  avatar: const CircleAvatar(child: Text('A')),
                  label: const Text('Chip A'),
                  onDeleted: () { },
                );
              },
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(Brightness.light));
    expect(getMaterialBox(tester), paints..path(color: const Color(0x1f000000)));
    expect(tester.getSize(find.byType(Chip)), const Size(156.0, 48.0));
    expect(getMaterial(tester).color, null);
    expect(getMaterial(tester).elevation, 0);
    expect(getMaterial(tester).shape, const StadiumBorder());
    expect(getIconData(tester).color?.value, 0xffffffff);
    expect(getIconData(tester).opacity, null);
    expect(getIconData(tester).size, null);
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265

    TextStyle labelStyle = getLabelStyle(tester, 'Chip A').style;
    expect(labelStyle.color?.value, 0xde000000);
    expect(labelStyle.fontFamily, textTheme.bodyText1?.fontFamily);
    expect(labelStyle.fontFamilyFallback, textTheme.bodyText1?.fontFamilyFallback);
    expect(labelStyle.fontFeatures, textTheme.bodyText1?.fontFeatures);
    expect(labelStyle.fontSize, textTheme.bodyText1?.fontSize);
    expect(labelStyle.fontStyle, textTheme.bodyText1?.fontStyle);
    expect(labelStyle.fontWeight, textTheme.bodyText1?.fontWeight);
    expect(labelStyle.height, textTheme.bodyText1?.height);
    expect(labelStyle.inherit, textTheme.bodyText1?.inherit);
    expect(labelStyle.leadingDistribution, textTheme.bodyText1?.leadingDistribution);
    expect(labelStyle.letterSpacing, textTheme.bodyText1?.letterSpacing);
    expect(labelStyle.overflow, textTheme.bodyText1?.overflow);
    expect(labelStyle.textBaseline, textTheme.bodyText1?.textBaseline);
    expect(labelStyle.wordSpacing, textTheme.bodyText1?.wordSpacing);
266 267 268 269 270 271 272 273 274 275 276

    await tester.pumpWidget(buildFrame(Brightness.dark));
    await tester.pumpAndSettle(); // Theme transition animation
    expect(getMaterialBox(tester), paints..path(color: const Color(0x1fffffff)));
    expect(tester.getSize(find.byType(Chip)), const Size(156.0, 48.0));
    expect(getMaterial(tester).color, null);
    expect(getMaterial(tester).elevation, 0);
    expect(getMaterial(tester).shape, const StadiumBorder());
    expect(getIconData(tester).color?.value, 0xffffffff);
    expect(getIconData(tester).opacity, null);
    expect(getIconData(tester).size, null);
277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292

    labelStyle = getLabelStyle(tester, 'Chip A').style;
    expect(labelStyle.color?.value, 0xdeffffff);
    expect(labelStyle.fontFamily, textTheme.bodyText1?.fontFamily);
    expect(labelStyle.fontFamilyFallback, textTheme.bodyText1?.fontFamilyFallback);
    expect(labelStyle.fontFeatures, textTheme.bodyText1?.fontFeatures);
    expect(labelStyle.fontSize, textTheme.bodyText1?.fontSize);
    expect(labelStyle.fontStyle, textTheme.bodyText1?.fontStyle);
    expect(labelStyle.fontWeight, textTheme.bodyText1?.fontWeight);
    expect(labelStyle.height, textTheme.bodyText1?.height);
    expect(labelStyle.inherit, textTheme.bodyText1?.inherit);
    expect(labelStyle.leadingDistribution, textTheme.bodyText1?.leadingDistribution);
    expect(labelStyle.letterSpacing, textTheme.bodyText1?.letterSpacing);
    expect(labelStyle.overflow, textTheme.bodyText1?.overflow);
    expect(labelStyle.textBaseline, textTheme.bodyText1?.textBaseline);
    expect(labelStyle.wordSpacing, textTheme.bodyText1?.wordSpacing);
293 294
  });

295
  testWidgets('Chip control test', (WidgetTester tester) async {
296
    final FeedbackTester feedback = FeedbackTester();
297
    final List<String> deletedChipLabels = <String>[];
298
    await tester.pumpWidget(
299
      wrapForChip(
300
        child: Column(
301
          children: <Widget>[
302
            Chip(
303
              avatar: const CircleAvatar(child: Text('A')),
304 305 306 307 308 309
              label: const Text('Chip A'),
              onDeleted: () {
                deletedChipLabels.add('A');
              },
              deleteButtonTooltipMessage: 'Delete chip A',
            ),
310
            Chip(
311
              avatar: const CircleAvatar(child: Text('B')),
312 313 314 315 316 317 318 319
              label: const Text('Chip B'),
              onDeleted: () {
                deletedChipLabels.add('B');
              },
              deleteButtonTooltipMessage: 'Delete chip B',
            ),
          ],
        ),
320
      ),
321
    );
322

323 324 325
    expect(tester.widget(find.byTooltip('Delete chip A')), isNotNull);
    expect(tester.widget(find.byTooltip('Delete chip B')), isNotNull);

326 327
    expect(feedback.clickSoundCount, 0);

328 329 330
    expect(deletedChipLabels, isEmpty);
    await tester.tap(find.byTooltip('Delete chip A'));
    expect(deletedChipLabels, equals(<String>['A']));
331 332 333 334

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(feedback.clickSoundCount, 1);

335 336 337 338 339 340
    await tester.tap(find.byTooltip('Delete chip B'));
    expect(deletedChipLabels, equals(<String>['A', 'B']));

    await tester.pumpAndSettle(const Duration(seconds: 1));
    expect(feedback.clickSoundCount, 2);

341
    feedback.dispose();
342
  });
343

344
  testWidgets(
345 346 347 348 349 350 351 352
    'Chip does not constrain size of label widget if it does not exceed '
    'the available space',
    (WidgetTester tester) async {
      const double labelWidth = 50.0;
      const double labelHeight = 30.0;
      final Key labelKey = UniqueKey();

      await tester.pumpWidget(
353
        wrapForChip(
354
          child: Center(
355
            child: SizedBox(
356 357 358 359 360
              width: 500.0,
              height: 500.0,
              child: Column(
                children: <Widget>[
                  Chip(
361
                    label: SizedBox(
362 363 364 365
                      key: labelKey,
                      width: labelWidth,
                      height: labelHeight,
                    ),
366
                  ),
367 368
                ],
              ),
369 370 371
            ),
          ),
        ),
372
      );
373

374 375 376 377 378
      final Size labelSize = tester.getSize(find.byKey(labelKey));
      expect(labelSize.width, labelWidth);
      expect(labelSize.height, labelHeight);
    },
  );
379

380
  testWidgets(
381 382 383
    'Chip constrains the size of the label widget when it exceeds the '
    'available space',
    (WidgetTester tester) async {
384
      await testConstrainedLabel(tester);
385 386
    },
  );
387

388
  testWidgets(
389 390 391
    'Chip constrains the size of the label widget when it exceeds the '
    'available space and the avatar is present',
    (WidgetTester tester) async {
392
      await testConstrainedLabel(
393 394 395 396 397
        tester,
        avatar: const CircleAvatar(child: Text('A')),
      );
    },
  );
398

399
  testWidgets(
400 401 402
    'Chip constrains the size of the label widget when it exceeds the '
    'available space and the delete icon is present',
    (WidgetTester tester) async {
403
      await testConstrainedLabel(
404 405 406 407 408
        tester,
        onDeleted: () { },
      );
    },
  );
409

410
  testWidgets(
411 412 413
    'Chip constrains the size of the label widget when it exceeds the '
    'available space and both avatar and delete icons are present',
    (WidgetTester tester) async {
414
      await testConstrainedLabel(
415 416 417 418 419 420
        tester,
        avatar: const CircleAvatar(child: Text('A')),
        onDeleted: () { },
      );
    },
  );
421

422
  testWidgets(
423 424 425 426
    'Chip constrains the avatar, label, and delete icons to the bounds of '
    'the chip when it exceeds the available space',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/11523
427
      Widget chipBuilder (String text, {Widget? avatar, VoidCallback? onDeleted}) {
428 429
        return MaterialApp(
          home: Scaffold(
430
            body: SizedBox(
431 432 433 434 435 436 437 438
              width: 150,
              child: Column(
                children: <Widget>[
                  Chip(
                    avatar: avatar,
                    label: Text(text),
                    onDeleted: onDeleted,
                  ),
439
                ],
440
              ),
441 442
            ),
          ),
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495
        );
      }

      void chipRectContains(Rect chipRect, Rect rect) {
        expect(chipRect.contains(rect.topLeft), true);
        expect(chipRect.contains(rect.topRight), true);
        expect(chipRect.contains(rect.bottomLeft), true);
        expect(chipRect.contains(rect.bottomRight), true);
      }

      Rect chipRect;
      Rect avatarRect;
      Rect labelRect;
      Rect deleteIconRect;
      const String text = 'Very long text that will be clipped';

      await tester.pumpWidget(chipBuilder(text));

      chipRect = tester.getRect(find.byType(Chip));
      labelRect = tester.getRect(find.text(text));
      chipRectContains(chipRect, labelRect);

      await tester.pumpWidget(chipBuilder(
        text,
        avatar: const CircleAvatar(child: Text('A')),
      ));
      await tester.pumpAndSettle();

      chipRect = tester.getRect(find.byType(Chip));
      avatarRect = tester.getRect(find.byType(CircleAvatar));
      chipRectContains(chipRect, avatarRect);

      labelRect = tester.getRect(find.text(text));
      chipRectContains(chipRect, labelRect);

      await tester.pumpWidget(chipBuilder(
        text,
        avatar: const CircleAvatar(child: Text('A')),
        onDeleted: () {},
      ));
      await tester.pumpAndSettle();

      chipRect = tester.getRect(find.byType(Chip));
      avatarRect = tester.getRect(find.byType(CircleAvatar));
      chipRectContains(chipRect, avatarRect);

      labelRect = tester.getRect(find.text(text));
      chipRectContains(chipRect, labelRect);

      deleteIconRect = tester.getRect(find.byIcon(Icons.cancel));
      chipRectContains(chipRect, deleteIconRect);
    },
  );
496

497
  testWidgets('Chip in row works ok', (WidgetTester tester) async {
498
    const TextStyle style = TextStyle(fontFamily: 'Ahem', fontSize: 10.0);
499
    await tester.pumpWidget(
500
      wrapForChip(
501
        child: Row(
502
          children: const <Widget>[
503
            Chip(label: Text('Test'), labelStyle: style),
504
          ],
505 506 507
        ),
      ),
    );
508
    expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0));
509
    expect(tester.getSize(find.byType(Chip)), const Size(64.0, 48.0));
510
    await tester.pumpWidget(
511
      wrapForChip(
512
        child: Row(
513
          children: const <Widget>[
514
            Flexible(child: Chip(label: Text('Test'), labelStyle: style)),
515
          ],
516 517 518 519
        ),
      ),
    );
    expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0));
520
    expect(tester.getSize(find.byType(Chip)), const Size(64.0, 48.0));
521
    await tester.pumpWidget(
522
      wrapForChip(
523
        child: Row(
524
          children: const <Widget>[
525
            Expanded(child: Chip(label: Text('Test'), labelStyle: style)),
526
          ],
527 528 529 530
        ),
      ),
    );
    expect(tester.getSize(find.byType(Text)), const Size(40.0, 10.0));
531
    expect(tester.getSize(find.byType(Chip)), const Size(800.0, 48.0));
532
  });
533

534 535
  testWidgets('Chip responds to materialTapTargetSize', (WidgetTester tester) async {
      await tester.pumpWidget(
536
        wrapForChip(
537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
          child: Column(
            children: const <Widget>[
              Chip(
                label: Text('X'),
                materialTapTargetSize: MaterialTapTargetSize.padded,
              ),
              Chip(
                label: Text('X'),
                materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
              ),
            ],
          ),
        ),
      );
      expect(tester.getSize(find.byType(Chip).first), const Size(48.0, 48.0));
      expect(tester.getSize(find.byType(Chip).last), const Size(38.0, 32.0));
    },
  );

556 557 558 559
  testWidgets('delete button tap target is the right proportion of the chip', (WidgetTester tester) async {
    final UniqueKey deleteKey = UniqueKey();
    bool calledDelete = false;
    await tester.pumpWidget(
560
      wrapForChip(
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
        child: Column(
          children: <Widget>[
            Chip(
              label: const Text('Really Long Label'),
              deleteIcon: Icon(Icons.delete, key: deleteKey),
              onDeleted: () {
                calledDelete = true;
              },
            ),
          ],
        ),
      ),
    );
    await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(24.0, 0.0));
    await tester.pump();
    expect(calledDelete, isTrue);
    calledDelete = false;

    await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(25.0, 0.0));
    await tester.pump();
    expect(calledDelete, isFalse);
    calledDelete = false;

    await tester.pumpWidget(
585
      wrapForChip(
586 587 588 589
        child: Column(
          children: <Widget>[
            Chip(
              label: const SizedBox(), // Short label
590
              deleteIcon: Icon(Icons.cancel, key: deleteKey),
591 592 593 594 595 596 597 598
              onDeleted: () {
                calledDelete = true;
              },
            ),
          ],
        ),
      ),
    );
599 600 601 602 603 604 605

    // Chip width is 48 with padding, 40 without padding, so halfway is at 20. Cancel
    // icon is 24x24, so since 24 > 20 the split location should be halfway across the
    // chip, which is at 12 + 8 = 20 from the right side. Since the split is just
    // slightly less than 50%, 8 from the center of the delete button should hit the
    // chip, not the delete button.
    await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(7.0, 0.0));
606 607 608 609
    await tester.pump();
    expect(calledDelete, isTrue);
    calledDelete = false;

610
    await tester.tapAt(tester.getCenter(find.byKey(deleteKey)) - const Offset(8.0, 0.0));
611 612 613 614
    await tester.pump();
    expect(calledDelete, isFalse);
  });

615
  testWidgets('Chip elements are ordered horizontally for locale', (WidgetTester tester) async {
616 617
    final UniqueKey iconKey = UniqueKey();
    final Widget test = Overlay(
618
      initialEntries: <OverlayEntry>[
619
        OverlayEntry(
620
          builder: (BuildContext context) {
621 622 623
            return Material(
              child: Chip(
                deleteIcon: Icon(Icons.delete, key: iconKey),
624
                onDeleted: () { },
625
                label: const Text('ABC'),
626 627 628 629 630 631 632 633
              ),
            );
          },
        ),
      ],
    );

    await tester.pumpWidget(
634
      wrapForChip(
635 636
        child: test,
        textDirection: TextDirection.rtl,
637 638
      ),
    );
639 640
    await tester.pumpAndSettle(const Duration(milliseconds: 500));
    expect(tester.getCenter(find.text('ABC')).dx, greaterThan(tester.getCenter(find.byKey(iconKey)).dx));
641
    await tester.pumpWidget(
642
      wrapForChip(
643
        child: test,
644 645
      ),
    );
646 647
    await tester.pumpAndSettle(const Duration(milliseconds: 500));
    expect(tester.getCenter(find.text('ABC')).dx, lessThan(tester.getCenter(find.byKey(iconKey)).dx));
648 649
  });

650 651
  testWidgets('Chip responds to textScaleFactor', (WidgetTester tester) async {
    await tester.pumpWidget(
652
      wrapForChip(
653
        child: Column(
654
          children: const <Widget>[
655 656 657
            Chip(
              avatar: CircleAvatar(child: Text('A')),
              label: Text('Chip A'),
658
            ),
659 660 661
            Chip(
              avatar: CircleAvatar(child: Text('B')),
              label: Text('Chip B'),
662 663
            ),
          ],
664 665 666 667 668 669 670 671
        ),
      ),
    );

    // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
    // https://github.com/flutter/flutter/issues/12357
    expect(
      tester.getSize(find.text('Chip A')),
672
      anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)),
673 674 675
    );
    expect(
      tester.getSize(find.text('Chip B')),
676
      anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)),
677
    );
678 679
    expect(tester.getSize(find.byType(Chip).first), anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0)));
    expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0)));
680 681

    await tester.pumpWidget(
682
      wrapForChip(
683
        textScaleFactor: 3.0,
684
        child: Column(
685
          children: const <Widget>[
686 687 688
            Chip(
              avatar: CircleAvatar(child: Text('A')),
              label: Text('Chip A'),
689
            ),
690 691 692
            Chip(
              avatar: CircleAvatar(child: Text('B')),
              label: Text('Chip B'),
693 694
            ),
          ],
695 696 697 698 699 700
        ),
      ),
    );

    // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
    // https://github.com/flutter/flutter/issues/12357
701 702
    expect(tester.getSize(find.text('Chip A')), anyOf(const Size(252.0, 42.0), const Size(251.0, 42.0)));
    expect(tester.getSize(find.text('Chip B')), anyOf(const Size(252.0, 42.0), const Size(251.0, 42.0)));
703
    expect(tester.getSize(find.byType(Chip).first).width, anyOf(310.0, 311.0));
704
    expect(tester.getSize(find.byType(Chip).first).height, equals(50.0));
705
    expect(tester.getSize(find.byType(Chip).last).width, anyOf(310.0, 311.0));
706
    expect(tester.getSize(find.byType(Chip).last).height, equals(50.0));
707 708 709

    // Check that individual text scales are taken into account.
    await tester.pumpWidget(
710
      wrapForChip(
711
        child: Column(
712
          children: const <Widget>[
713 714 715
            Chip(
              avatar: CircleAvatar(child: Text('A')),
              label: Text('Chip A', textScaleFactor: 3.0),
716
            ),
717 718 719
            Chip(
              avatar: CircleAvatar(child: Text('B')),
              label: Text('Chip B'),
720 721
            ),
          ],
722 723 724 725 726 727
        ),
      ),
    );

    // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
    // https://github.com/flutter/flutter/issues/12357
728 729
    expect(tester.getSize(find.text('Chip A')), anyOf(const Size(252.0, 42.0), const Size(251.0, 42.0)));
    expect(tester.getSize(find.text('Chip B')), anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)));
730 731
    expect(tester.getSize(find.byType(Chip).first).width, anyOf(318.0, 319.0));
    expect(tester.getSize(find.byType(Chip).first).height, equals(50.0));
732
    expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0)));
733
  });
734 735

  testWidgets('Labels can be non-text widgets', (WidgetTester tester) async {
736 737
    final Key keyA = GlobalKey();
    final Key keyB = GlobalKey();
738
    await tester.pumpWidget(
739
      wrapForChip(
740
        child: Column(
741
          children: <Widget>[
742
            Chip(
743
              avatar: const CircleAvatar(child: Text('A')),
744
              label: Text('Chip A', key: keyA),
745
            ),
746
            Chip(
747
              avatar: const CircleAvatar(child: Text('B')),
748
              label: SizedBox(key: keyB, width: 10.0, height: 10.0),
749 750
            ),
          ],
751 752 753 754 755 756 757 758
        ),
      ),
    );

    // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs.
    // https://github.com/flutter/flutter/issues/12357
    expect(
      tester.getSize(find.byKey(keyA)),
759
      anyOf(const Size(84.0, 14.0), const Size(83.0, 14.0)),
760 761 762 763
    );
    expect(tester.getSize(find.byKey(keyB)), const Size(10.0, 10.0));
    expect(
      tester.getSize(find.byType(Chip).first),
764
      anyOf(const Size(132.0, 48.0), const Size(131.0, 48.0)),
765
    );
766
    expect(tester.getSize(find.byType(Chip).last), const Size(58.0, 48.0));
767
  });
768

769
  testWidgets('Avatars can be non-circle avatar widgets', (WidgetTester tester) async {
770
    final Key keyA = GlobalKey();
771
    await tester.pumpWidget(
772
      wrapForChip(
773
        child: Column(
774
          children: <Widget>[
775
            Chip(
776
              avatar: SizedBox(key: keyA, width: 20.0, height: 20.0),
777 778 779
              label: const Text('Chip A'),
            ),
          ],
780 781 782 783 784 785 786 787
        ),
      ),
    );

    expect(tester.getSize(find.byKey(keyA)), equals(const Size(20.0, 20.0)));
  });

  testWidgets('Delete icons can be non-icon widgets', (WidgetTester tester) async {
788
    final Key keyA = GlobalKey();
789
    await tester.pumpWidget(
790
      wrapForChip(
791
        child: Column(
792
          children: <Widget>[
793
            Chip(
794
              deleteIcon: SizedBox(key: keyA, width: 20.0, height: 20.0),
795
              label: const Text('Chip A'),
796
              onDeleted: () { },
797 798
            ),
          ],
799 800 801 802 803 804 805
        ),
      ),
    );

    expect(tester.getSize(find.byKey(keyA)), equals(const Size(20.0, 20.0)));
  });

806
  testWidgets('Chip padding - LTR', (WidgetTester tester) async {
807 808
    final GlobalKey keyA = GlobalKey();
    final GlobalKey keyB = GlobalKey();
809
    await tester.pumpWidget(
810
      wrapForChip(
811
        child: Overlay(
812
          initialEntries: <OverlayEntry>[
813
            OverlayEntry(
814
              builder: (BuildContext context) {
815 816 817 818
                return Material(
                  child: Center(
                    child: Chip(
                      avatar: Placeholder(key: keyA),
819
                      label: SizedBox(
820 821 822
                        key: keyB,
                        width: 40.0,
                        height: 40.0,
823
                      ),
824
                      onDeleted: () { },
825
                    ),
826 827 828 829 830
                  ),
                );
              },
            ),
          ],
831 832 833
        ),
      ),
    );
834 835
    expect(tester.getTopLeft(find.byKey(keyA)), const Offset(332.0, 280.0));
    expect(tester.getBottomRight(find.byKey(keyA)), const Offset(372.0, 320.0));
836 837
    expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.0));
    expect(tester.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.0));
838 839
    expect(tester.getTopLeft(find.byType(Icon)), const Offset(439.0, 291.0));
    expect(tester.getBottomRight(find.byType(Icon)), const Offset(457.0, 309.0));
840 841 842
  });

  testWidgets('Chip padding - RTL', (WidgetTester tester) async {
843 844
    final GlobalKey keyA = GlobalKey();
    final GlobalKey keyB = GlobalKey();
845
    await tester.pumpWidget(
846
      wrapForChip(
847
        textDirection: TextDirection.rtl,
848
        child: Overlay(
849
          initialEntries: <OverlayEntry>[
850
            OverlayEntry(
851
              builder: (BuildContext context) {
852 853 854 855
                return Material(
                  child: Center(
                    child: Chip(
                      avatar: Placeholder(key: keyA),
856
                      label: SizedBox(
857 858 859
                        key: keyB,
                        width: 40.0,
                        height: 40.0,
860
                      ),
861
                      onDeleted: () { },
862
                    ),
863 864 865 866 867
                  ),
                );
              },
            ),
          ],
868 869 870
        ),
      ),
    );
871

872 873
    expect(tester.getTopLeft(find.byKey(keyA)), const Offset(428.0, 280.0));
    expect(tester.getBottomRight(find.byKey(keyA)), const Offset(468.0, 320.0));
874 875
    expect(tester.getTopLeft(find.byKey(keyB)), const Offset(380.0, 280.0));
    expect(tester.getBottomRight(find.byKey(keyB)), const Offset(420.0, 320.0));
876 877 878 879 880
    expect(tester.getTopLeft(find.byType(Icon)), const Offset(343.0, 291.0));
    expect(tester.getBottomRight(find.byType(Icon)), const Offset(361.0, 309.0));
  });

  testWidgets('Avatar drawer works as expected on RawChip', (WidgetTester tester) async {
881
    final GlobalKey labelKey = GlobalKey();
882
    Future<void> pushChip({ Widget? avatar }) async {
883
      return tester.pumpWidget(
884
        wrapForChip(
885
          child: Wrap(
886
            children: <Widget>[
887
              RawChip(
888
                avatar: avatar,
889
                label: Text('Chip', key: labelKey),
890 891 892
                shape: const StadiumBorder(),
              ),
            ],
893 894 895 896 897 898 899
          ),
        ),
      );
    }

    // No avatar
    await pushChip();
900
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0)));
901
    final GlobalKey avatarKey = GlobalKey();
902 903 904

    // Add an avatar
    await pushChip(
905
      avatar: Container(
906 907 908 909 910 911 912
        key: avatarKey,
        color: const Color(0xff000000),
        width: 40.0,
        height: 40.0,
      ),
    );
    // Avatar drawer should start out closed.
913
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0)));
914
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
915 916
    expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(-20.0, 12.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
917 918 919

    await tester.pump(const Duration(milliseconds: 20));
    // Avatar drawer should start expanding.
920
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(81.2, epsilon: 0.1));
921
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
922 923
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(-18.8, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(13.2, epsilon: 0.1));
924 925

    await tester.pump(const Duration(milliseconds: 20));
926
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(86.7, epsilon: 0.1));
927
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
928 929
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(-13.3, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(18.6, epsilon: 0.1));
930 931

    await tester.pump(const Duration(milliseconds: 20));
932
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(94.7, epsilon: 0.1));
933
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
934 935
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(-5.3, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(26.7, epsilon: 0.1));
936 937

    await tester.pump(const Duration(milliseconds: 20));
938
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(99.5, epsilon: 0.1));
939
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
940 941
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(-0.5, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(31.5, epsilon: 0.1));
942 943 944 945

    // Wait for being done with animation, and make sure it didn't change
    // height.
    await tester.pumpAndSettle(const Duration(milliseconds: 200));
946
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0)));
947
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
948 949
    expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 12.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 17.0)));
950 951 952 953

    // Remove the avatar again
    await pushChip();
    // Avatar drawer should start out open.
954
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0)));
955
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
956 957
    expect(tester.getTopLeft(find.byKey(avatarKey)), equals(const Offset(4.0, 12.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(36.0, 17.0)));
958 959 960

    await tester.pump(const Duration(milliseconds: 20));
    // Avatar drawer should start contracting.
961
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(102.9, epsilon: 0.1));
962
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
963 964
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(2.9, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(34.9, epsilon: 0.1));
965 966

    await tester.pump(const Duration(milliseconds: 20));
967
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(98.0, epsilon: 0.1));
968
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
969 970
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(-2.0, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(30.0, epsilon: 0.1));
971 972

    await tester.pump(const Duration(milliseconds: 20));
973
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(84.1, epsilon: 0.1));
974
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
975 976
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(-15.9, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(16.1, epsilon: 0.1));
977 978

    await tester.pump(const Duration(milliseconds: 20));
979
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(80.0, epsilon: 0.1));
980
    expect(tester.getSize(find.byKey(avatarKey)), equals(const Size(24.0, 24.0)));
981 982
    expect(tester.getTopLeft(find.byKey(avatarKey)).dx, moreOrLessEquals(-20.0, epsilon: 0.1));
    expect(tester.getTopLeft(find.byKey(labelKey)).dx, moreOrLessEquals(12.0, epsilon: 0.1));
983 984 985 986

    // Wait for being done with animation, make sure it didn't change
    // height, and make sure that the avatar is no longer drawn.
    await tester.pumpAndSettle(const Duration(milliseconds: 200));
987 988
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
989
    expect(find.byKey(avatarKey), findsNothing);
990
  });
991 992

  testWidgets('Delete button drawer works as expected on RawChip', (WidgetTester tester) async {
993 994
    const Key labelKey = Key('label');
    const Key deleteButtonKey = Key('delete');
995
    bool wasDeleted = false;
996
    Future<void> pushChip({ bool deletable = false }) async {
997
      return tester.pumpWidget(
998
        wrapForChip(
999
          child: Wrap(
1000
            children: <Widget>[
1001 1002
              StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
                return RawChip(
1003
                  onDeleted: deletable
1004 1005 1006 1007 1008 1009
                    ? () {
                        setState(() {
                          wasDeleted = true;
                        });
                      }
                    : null,
1010 1011
                  deleteIcon: Container(width: 40.0, height: 40.0, color: Colors.blue, key: deleteButtonKey),
                  label: const Text('Chip', key: labelKey),
1012 1013 1014 1015
                  shape: const StadiumBorder(),
                );
              }),
            ],
1016 1017 1018 1019 1020 1021 1022
          ),
        ),
      );
    }

    // No delete button
    await pushChip();
1023
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0)));
1024 1025 1026 1027

    // Add a delete button
    await pushChip(deletable: true);
    // Delete button drawer should start out closed.
1028
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0)));
1029
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1030 1031
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(52.0, 12.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
1032 1033 1034

    await tester.pump(const Duration(milliseconds: 20));
    // Delete button drawer should start expanding.
1035
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(81.2, epsilon: 0.1));
1036
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1037
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(53.2, epsilon: 0.1));
1038
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
1039 1040

    await tester.pump(const Duration(milliseconds: 20));
1041
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(86.7, epsilon: 0.1));
1042
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1043
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(58.7, epsilon: 0.1));
1044 1045

    await tester.pump(const Duration(milliseconds: 20));
1046
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(94.7, epsilon: 0.1));
1047
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1048
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(66.7, epsilon: 0.1));
1049 1050

    await tester.pump(const Duration(milliseconds: 20));
1051
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(99.5, epsilon: 0.1));
1052
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1053
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(71.5, epsilon: 0.1));
1054 1055 1056 1057

    // Wait for being done with animation, and make sure it didn't change
    // height.
    await tester.pumpAndSettle(const Duration(milliseconds: 200));
1058
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0)));
1059
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1060 1061
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 12.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072

    // Test the tap work for the delete button, but not the rest of the chip.
    expect(wasDeleted, isFalse);
    await tester.tap(find.byKey(labelKey));
    expect(wasDeleted, isFalse);
    await tester.tap(find.byKey(deleteButtonKey));
    expect(wasDeleted, isTrue);

    // Remove the delete button again
    await pushChip();
    // Delete button drawer should start out open.
1073
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(104.0, 48.0)));
1074
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1075 1076
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)), equals(const Offset(76.0, 12.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
1077 1078 1079

    await tester.pump(const Duration(milliseconds: 20));
    // Delete button drawer should start contracting.
1080
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(103.8, epsilon: 0.1));
1081
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1082
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(75.8, epsilon: 0.1));
1083
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
1084 1085

    await tester.pump(const Duration(milliseconds: 20));
1086
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(102.9, epsilon: 0.1));
1087
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1088
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(74.9, epsilon: 0.1));
1089 1090

    await tester.pump(const Duration(milliseconds: 20));
1091
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(101.0, epsilon: 0.1));
1092
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1093
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(73.0, epsilon: 0.1));
1094 1095

    await tester.pump(const Duration(milliseconds: 20));
1096
    expect(tester.getSize(find.byType(RawChip)).width, moreOrLessEquals(97.5, epsilon: 0.1));
1097
    expect(tester.getSize(find.byKey(deleteButtonKey)), equals(const Size(24.0, 24.0)));
1098
    expect(tester.getTopLeft(find.byKey(deleteButtonKey)).dx, moreOrLessEquals(69.5, epsilon: 0.1));
1099 1100 1101 1102

    // Wait for being done with animation, make sure it didn't change
    // height, and make sure that the delete button is no longer drawn.
    await tester.pumpAndSettle(const Duration(milliseconds: 200));
1103 1104
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(80.0, 48.0)));
    expect(tester.getTopLeft(find.byKey(labelKey)), equals(const Offset(12.0, 17.0)));
1105
    expect(find.byKey(deleteButtonKey), findsNothing);
1106
  });
1107

1108 1109 1110 1111 1112 1113
  testWidgets('Delete button takes up at most half of the chip', (WidgetTester tester) async {
    final UniqueKey chipKey = UniqueKey();
    bool chipPressed = false;
    bool deletePressed = false;

    await tester.pumpWidget(
1114
      wrapForChip(
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143
        child: Wrap(
          children: <Widget>[
            RawChip(
              key: chipKey,
              onPressed: () {
                chipPressed = true;
              },
              onDeleted: () {
                deletePressed = true;
              },
              label: const Text(''),
              ),
          ],
        ),
      ),
    );

    await tester.tapAt(tester.getCenter(find.byKey(chipKey)));
    await tester.pump();
    expect(chipPressed, isTrue);
    expect(deletePressed, isFalse);
    chipPressed = false;

    await tester.tapAt(tester.getCenter(find.byKey(chipKey)) + const Offset(1.0, 0.0));
    await tester.pump();
    expect(chipPressed, isFalse);
    expect(deletePressed, isTrue);
  });

1144 1145 1146 1147 1148
  testWidgets('Chip creates centered, unique ripple when label is tapped', (WidgetTester tester) async {
    final UniqueKey labelKey = UniqueKey();
    final UniqueKey deleteButtonKey = UniqueKey();

    await tester.pumpWidget(
1149
      chipWithOptionalDeleteButton(
1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
        labelKey: labelKey,
        deleteButtonKey: deleteButtonKey,
        deletable: true,
      ),
    );

    final RenderBox box = getMaterialBox(tester);

    // Taps at a location close to the center of the label.
    final Offset centerOfLabel = tester.getCenter(find.byKey(labelKey));
    final Offset tapLocationOfLabel = centerOfLabel + const Offset(-10, -10);
    final TestGesture gesture = await tester.startGesture(tapLocationOfLabel);
    await tester.pump();

    // Waits for 100 ms.
    await tester.pump(const Duration(milliseconds: 100));

    // There should be one unique, centered ink ripple.
    expect(box, ripplePattern(const Offset(163.0, 6.0), 20.9));
    expect(box, uniqueRipplePattern(const Offset(163.0, 6.0), 20.9));

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for 100 ms again.
    await tester.pump(const Duration(milliseconds: 100));

    // The ripple should grow, with the same center.
    expect(box, ripplePattern(const Offset(163.0, 6.0), 41.8));
    expect(box, uniqueRipplePattern(const Offset(163.0, 6.0), 41.8));

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for a very long time.
    await tester.pumpAndSettle();

    // There should still be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    await gesture.up();
1191
  });
1192

1193 1194 1195 1196 1197
  testWidgets('Delete button is focusable', (WidgetTester tester) async {
    final GlobalKey labelKey = GlobalKey();
    final GlobalKey deleteButtonKey = GlobalKey();

    await tester.pumpWidget(
1198
      chipWithOptionalDeleteButton(
1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225
        labelKey: labelKey,
        deleteButtonKey: deleteButtonKey,
        deletable: true,
      ),
    );

    Focus.of(deleteButtonKey.currentContext!).requestFocus();
    await tester.pump();

    // They shouldn't have the same focus node.
    expect(Focus.of(deleteButtonKey.currentContext!), isNot(equals(Focus.of(labelKey.currentContext!))));
    expect(Focus.of(deleteButtonKey.currentContext!).hasFocus, isTrue);
    expect(Focus.of(deleteButtonKey.currentContext!).hasPrimaryFocus, isTrue);
    // Delete button is a child widget of the Chip, so the Chip should have focus if
    // the delete button does.
    expect(Focus.of(labelKey.currentContext!).hasFocus, isTrue);
    expect(Focus.of(labelKey.currentContext!).hasPrimaryFocus, isFalse);

    Focus.of(labelKey.currentContext!).requestFocus();
    await tester.pump();

    expect(Focus.of(deleteButtonKey.currentContext!).hasFocus, isFalse);
    expect(Focus.of(deleteButtonKey.currentContext!).hasPrimaryFocus, isFalse);
    expect(Focus.of(labelKey.currentContext!).hasFocus, isTrue);
    expect(Focus.of(labelKey.currentContext!).hasPrimaryFocus, isTrue);
  });

1226 1227 1228 1229 1230
  testWidgets('Delete button creates non-centered, unique ripple when tapped', (WidgetTester tester) async {
    final UniqueKey labelKey = UniqueKey();
    final UniqueKey deleteButtonKey = UniqueKey();

    await tester.pumpWidget(
1231
      chipWithOptionalDeleteButton(
1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249
        labelKey: labelKey,
        deleteButtonKey: deleteButtonKey,
        deletable: true,
      ),
    );

    final RenderBox box = getMaterialBox(tester);

    // Taps at a location close to the center of the delete icon.
    final Offset centerOfDeleteButton = tester.getCenter(find.byKey(deleteButtonKey));
    final Offset tapLocationOfDeleteButton = centerOfDeleteButton + const Offset(-10, -10);
    final TestGesture gesture = await tester.startGesture(tapLocationOfDeleteButton);
    await tester.pump();

    // Waits for 200 ms.
    await tester.pump(const Duration(milliseconds: 100));
    await tester.pump(const Duration(milliseconds: 100));

1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283
    // There should be one unique ink ripple.
    expect(box, ripplePattern(const Offset(3.0, 3.0), 1.44));
    expect(box, uniqueRipplePattern(const Offset(3.0, 3.0), 1.44));

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for 200 ms again.
    await tester.pump(const Duration(milliseconds: 100));
    await tester.pump(const Duration(milliseconds: 100));

    // The ripple should grow, but the center should move,
    // Towards the center of the delete icon.
    expect(box, ripplePattern(const Offset(5.0, 5.0), 4.32));
    expect(box, uniqueRipplePattern(const Offset(5.0, 5.0), 4.32));

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for a very long time.
    // This is pressing and holding the delete button.
    await tester.pumpAndSettle();

    // There should be a tooltip.
    expect(findTooltipContainer('Delete'), findsOneWidget);

    await gesture.up();
  });

  testWidgets('Delete button in a chip with null onPressed creates ripple when tapped', (WidgetTester tester) async {
    final UniqueKey labelKey = UniqueKey();
    final UniqueKey deleteButtonKey = UniqueKey();

    await tester.pumpWidget(
1284
      chipWithOptionalDeleteButton(
1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302
        labelKey: labelKey,
        onPressed: null,
        deleteButtonKey: deleteButtonKey,
        deletable: true,
      ),
    );

    final RenderBox box = getMaterialBox(tester);

    // Taps at a location close to the center of the delete icon.
    final Offset centerOfDeleteButton = tester.getCenter(find.byKey(deleteButtonKey));
    final Offset tapLocationOfDeleteButton = centerOfDeleteButton + const Offset(-10, -10);
    final TestGesture gesture = await tester.startGesture(tapLocationOfDeleteButton);
    await tester.pump();

    // Waits for 200 ms.
    await tester.pump(const Duration(milliseconds: 100));
    await tester.pump(const Duration(milliseconds: 100));
1303 1304

    // There should be one unique ink ripple.
1305 1306
    expect(box, ripplePattern(const Offset(3.0, 3.0), 1.44));
    expect(box, uniqueRipplePattern(const Offset(3.0, 3.0), 1.44));
1307 1308 1309 1310 1311 1312 1313 1314 1315 1316

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for 200 ms again.
    await tester.pump(const Duration(milliseconds: 100));
    await tester.pump(const Duration(milliseconds: 100));

    // The ripple should grow, but the center should move,
    // Towards the center of the delete icon.
1317 1318
    expect(box, ripplePattern(const Offset(5.0, 5.0), 4.32));
    expect(box, uniqueRipplePattern(const Offset(5.0, 5.0), 4.32));
1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for a very long time.
    // This is pressing and holding the delete button.
    await tester.pumpAndSettle();

    // There should be a tooltip.
    expect(findTooltipContainer('Delete'), findsOneWidget);

    await gesture.up();
1331
  });
1332 1333 1334 1335 1336 1337 1338

  testWidgets('RTL delete button responds to tap on the left of the chip', (WidgetTester tester) async {
    // Creates an RTL chip with a delete button.
    final UniqueKey labelKey = UniqueKey();
    final UniqueKey deleteButtonKey = UniqueKey();

    await tester.pumpWidget(
1339
      chipWithOptionalDeleteButton(
1340 1341 1342 1343 1344 1345 1346 1347 1348
        labelKey: labelKey,
        deleteButtonKey: deleteButtonKey,
        deletable: true,
        textDirection: TextDirection.rtl,
      ),
    );

    // Taps at a location close to the center of the delete icon,
    // Which is on the left side of the chip.
1349
    final Offset topLeftOfInkWell = tester.getTopLeft(find.byType(InkWell).first);
1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360
    final Offset tapLocation = topLeftOfInkWell + const Offset(8, 8);
    final TestGesture gesture = await tester.startGesture(tapLocation);
    await tester.pump();

    await tester.pumpAndSettle();

    // The existence of a 'Delete' tooltip indicates the delete icon is tapped,
    // Instead of the label.
    expect(findTooltipContainer('Delete'), findsOneWidget);

    await gesture.up();
1361
  });
1362 1363 1364 1365 1366 1367

  testWidgets('Chip without delete button creates correct ripple', (WidgetTester tester) async {
    // Creates a chip with a delete button.
    final UniqueKey labelKey = UniqueKey();

    await tester.pumpWidget(
1368
      chipWithOptionalDeleteButton(
1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414
        labelKey: labelKey,
        deletable: false,
      ),
    );

    final RenderBox box = getMaterialBox(tester);

    // Taps at a location close to the bottom-right corner of the chip.
    final Offset bottomRightOfInkWell = tester.getBottomRight(find.byType(InkWell));
    final Offset tapLocation = bottomRightOfInkWell + const Offset(-10, -10);
    final TestGesture gesture = await tester.startGesture(tapLocation);
    await tester.pump();

    // Waits for 100 ms.
    await tester.pump(const Duration(milliseconds: 100));

    // There should be exactly one ink-creating widget.
    expect(find.byType(InkWell), findsOneWidget);
    expect(find.byType(InkResponse), findsNothing);

    // There should be one unique, centered ink ripple.
    expect(box, ripplePattern(const Offset(378.0, 22.0), 37.9));
    expect(box, uniqueRipplePattern(const Offset(378.0, 22.0), 37.9));

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for 100 ms again.
    await tester.pump(const Duration(milliseconds: 100));

    // The ripple should grow, with the same center.
    // This indicates that the tap is not on a delete icon.
    expect(box, ripplePattern(const Offset(378.0, 22.0), 75.8));
    expect(box, uniqueRipplePattern(const Offset(378.0, 22.0), 75.8));

    // There should be no tooltip.
    expect(findTooltipContainer('Delete'), findsNothing);

    // Waits for a very long time.
    await tester.pumpAndSettle();

    // There should still be no tooltip.
    // This indicates that the tap is not on a delete icon.
    expect(findTooltipContainer('Delete'), findsNothing);

    await gesture.up();
1415
  });
1416 1417 1418

  testWidgets('Selection with avatar works as expected on RawChip', (WidgetTester tester) async {
    bool selected = false;
1419
    final UniqueKey labelKey = UniqueKey();
1420
    Future<void> pushChip({ Widget? avatar, bool selectable = false }) async {
1421
      return tester.pumpWidget(
1422
        wrapForChip(
1423
          child: Wrap(
1424
            children: <Widget>[
1425 1426
              StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
                return RawChip(
1427 1428
                  avatar: avatar,
                  onSelected: selectable != null
1429 1430 1431 1432 1433 1434
                    ? (bool value) {
                        setState(() {
                          selected = value;
                        });
                      }
                    : null,
1435
                  selected: selected,
1436
                  label: Text('Long Chip Label', key: labelKey),
1437 1438 1439 1440
                  shape: const StadiumBorder(),
                );
              }),
            ],
1441 1442 1443 1444 1445 1446
          ),
        ),
      );
    }

    // With avatar, but not selectable.
1447
    final UniqueKey avatarKey = UniqueKey();
1448
    await pushChip(
1449
      avatar: SizedBox(width: 40.0, height: 40.0, key: avatarKey),
1450
    );
1451
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(258.0, 48.0)));
1452 1453 1454

    // Turn on selection.
    await pushChip(
1455
      avatar: SizedBox(width: 40.0, height: 40.0, key: avatarKey),
1456 1457 1458 1459 1460 1461 1462
      selectable: true,
    );
    await tester.pumpAndSettle();

    // Simulate a tap on the label to select the chip.
    await tester.tap(find.byKey(labelKey));
    expect(selected, equals(true));
1463
    expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
1464 1465
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
1466
    expect(getSelectProgress(tester), moreOrLessEquals(0.002, epsilon: 0.01));
1467 1468 1469
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 50));
1470
    expect(getSelectProgress(tester), moreOrLessEquals(0.54, epsilon: 0.01));
1471 1472 1473 1474 1475 1476 1477 1478 1479 1480
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 100));
    expect(getSelectProgress(tester), equals(1.0));
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pumpAndSettle();
    // Simulate another tap on the label to deselect the chip.
    await tester.tap(find.byKey(labelKey));
    expect(selected, equals(false));
1481
    expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
1482 1483
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 20));
1484
    expect(getSelectProgress(tester), moreOrLessEquals(0.875, epsilon: 0.01));
1485 1486 1487
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 20));
1488
    expect(getSelectProgress(tester), moreOrLessEquals(0.13, epsilon: 0.01));
1489 1490 1491 1492 1493 1494
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 100));
    expect(getSelectProgress(tester), equals(0.0));
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
1495
  });
1496 1497 1498

  testWidgets('Selection without avatar works as expected on RawChip', (WidgetTester tester) async {
    bool selected = false;
1499
    final UniqueKey labelKey = UniqueKey();
1500
    Future<void> pushChip({ bool selectable = false }) async {
1501
      return tester.pumpWidget(
1502
        wrapForChip(
1503
          child: Wrap(
1504
            children: <Widget>[
1505 1506
              StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
                return RawChip(
1507
                  onSelected: selectable != null
1508 1509 1510 1511 1512 1513
                    ? (bool value) {
                        setState(() {
                          selected = value;
                        });
                      }
                    : null,
1514
                  selected: selected,
1515
                  label: Text('Long Chip Label', key: labelKey),
1516 1517 1518 1519
                  shape: const StadiumBorder(),
                );
              }),
            ],
1520 1521 1522 1523 1524 1525 1526
          ),
        ),
      );
    }

    // Without avatar, but not selectable.
    await pushChip();
1527
    expect(tester.getSize(find.byType(RawChip)), equals(const Size(234.0, 48.0)));
1528 1529 1530 1531 1532 1533 1534 1535

    // Turn on selection.
    await pushChip(selectable: true);
    await tester.pumpAndSettle();

    // Simulate a tap on the label to select the chip.
    await tester.tap(find.byKey(labelKey));
    expect(selected, equals(true));
1536
    expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
1537 1538
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
1539 1540
    expect(getSelectProgress(tester), moreOrLessEquals(0.002, epsilon: 0.01));
    expect(getAvatarDrawerProgress(tester), moreOrLessEquals(0.459, epsilon: 0.01));
1541 1542
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 50));
1543 1544
    expect(getSelectProgress(tester), moreOrLessEquals(0.54, epsilon: 0.01));
    expect(getAvatarDrawerProgress(tester), moreOrLessEquals(0.92, epsilon: 0.01));
1545 1546 1547 1548 1549 1550 1551 1552 1553
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 100));
    expect(getSelectProgress(tester), equals(1.0));
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pumpAndSettle();
    // Simulate another tap on the label to deselect the chip.
    await tester.tap(find.byKey(labelKey));
    expect(selected, equals(false));
1554
    expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
1555 1556
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 20));
1557 1558
    expect(getSelectProgress(tester), moreOrLessEquals(0.875, epsilon: 0.01));
    expect(getAvatarDrawerProgress(tester), moreOrLessEquals(0.96, epsilon: 0.01));
1559 1560
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 20));
1561 1562
    expect(getSelectProgress(tester), moreOrLessEquals(0.13, epsilon: 0.01));
    expect(getAvatarDrawerProgress(tester), moreOrLessEquals(0.75, epsilon: 0.01));
1563 1564 1565 1566 1567
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 100));
    expect(getSelectProgress(tester), equals(0.0));
    expect(getAvatarDrawerProgress(tester), equals(0.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
1568
  });
1569 1570 1571

  testWidgets('Activation works as expected on RawChip', (WidgetTester tester) async {
    bool selected = false;
1572
    final UniqueKey labelKey = UniqueKey();
1573
    Future<void> pushChip({ Widget? avatar, bool selectable = false }) async {
1574
      return tester.pumpWidget(
1575
        wrapForChip(
1576
          child: Wrap(
1577
            children: <Widget>[
1578 1579
              StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
                return RawChip(
1580 1581
                  avatar: avatar,
                  onSelected: selectable != null
1582 1583 1584 1585 1586 1587
                    ? (bool value) {
                        setState(() {
                          selected = value;
                        });
                      }
                    : null,
1588
                  selected: selected,
1589
                  label: Text('Long Chip Label', key: labelKey),
1590 1591 1592 1593 1594
                  shape: const StadiumBorder(),
                  showCheckmark: false,
                );
              }),
            ],
1595 1596 1597 1598 1599
          ),
        ),
      );
    }

1600
    final UniqueKey avatarKey = UniqueKey();
1601
    await pushChip(
1602
      avatar: SizedBox(width: 40.0, height: 40.0, key: avatarKey),
1603 1604 1605 1606 1607 1608
      selectable: true,
    );
    await tester.pumpAndSettle();

    await tester.tap(find.byKey(labelKey));
    expect(selected, equals(true));
1609
    expect(SchedulerBinding.instance.transientCallbackCount, equals(2));
1610 1611
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 50));
1612
    expect(getSelectProgress(tester), moreOrLessEquals(0.002, epsilon: 0.01));
1613 1614 1615
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 50));
1616
    expect(getSelectProgress(tester), moreOrLessEquals(0.54, epsilon: 0.01));
1617 1618 1619 1620 1621 1622 1623
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pump(const Duration(milliseconds: 100));
    expect(getSelectProgress(tester), equals(1.0));
    expect(getAvatarDrawerProgress(tester), equals(1.0));
    expect(getDeleteDrawerProgress(tester), equals(0.0));
    await tester.pumpAndSettle();
1624
  });
1625 1626

  testWidgets('Chip uses ThemeData chip theme if present', (WidgetTester tester) async {
1627
    final ThemeData theme = ThemeData(
1628 1629 1630 1631 1632 1633
      platform: TargetPlatform.android,
      primarySwatch: Colors.red,
    );
    final ChipThemeData chipTheme = theme.chipTheme;

    Widget buildChip(ChipThemeData data) {
1634
      return wrapForChip(
1635
        child: Theme(
1636 1637
          data: theme,
          child: const InputChip(
1638
            label: Text('Label'),
1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652
          ),
        ),
      );
    }

    await tester.pumpWidget(buildChip(chipTheme));

    final RenderBox materialBox = tester.firstRenderObject<RenderBox>(
      find.descendant(
        of: find.byType(RawChip),
        matching: find.byType(CustomPaint),
      ),
    );

1653
    expect(materialBox, paints..path(color: chipTheme.disabledColor));
1654 1655
  });

1656 1657 1658
  testWidgets('Chip merges ChipThemeData label style with the provided label style', (WidgetTester tester) async {
    // The font family should be preserved even if the chip overrides some label style properties
    final ThemeData theme = ThemeData(
1659
      fontFamily: 'MyFont',
1660 1661 1662
    );

    Widget buildChip() {
1663
      return wrapForChip(
1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675
        child: Theme(
          data: theme,
          child: const Chip(
            label: Text('Label'),
            labelStyle: TextStyle(fontWeight: FontWeight.w200),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildChip());

1676
    final TextStyle labelStyle = getLabelStyle(tester, 'Label').style;
1677
    expect(labelStyle.inherit, false);
1678 1679 1680 1681
    expect(labelStyle.fontFamily, 'MyFont');
    expect(labelStyle.fontWeight, FontWeight.w200);
  });

1682 1683
  testWidgets('ChipTheme labelStyle with inherit:true', (WidgetTester tester) async {
    Widget buildChip() {
1684
      return wrapForChip(
1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703
        child: Theme(
          data: ThemeData.light().copyWith(
            chipTheme: const ChipThemeData(
              labelStyle: TextStyle(height: 4), // inherit: true
            ),
          ),
          child: const Chip(label: Text('Label')), // labeStyle: null
        ),
      );
    }

    await tester.pumpWidget(buildChip());
    final TextStyle labelStyle = getLabelStyle(tester, 'Label').style;
    expect(labelStyle.inherit, true); // because chipTheme.labelStyle.merge(null)
    expect(labelStyle.height, 4);
  });

  testWidgets('Chip does not merge inherit:false label style with the theme label style', (WidgetTester tester) async {
    Widget buildChip() {
1704
      return wrapForChip(
1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725
        child: Theme(
          data: ThemeData(fontFamily: 'MyFont'),
          child: const DefaultTextStyle(
            style: TextStyle(height: 8),
            child: Chip(
              label: Text('Label'),
              labelStyle: TextStyle(fontWeight: FontWeight.w200, inherit: false),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildChip());
    final TextStyle labelStyle = getLabelStyle(tester, 'Label').style;
    expect(labelStyle.inherit, false);
    expect(labelStyle.fontFamily, null);
    expect(labelStyle.height, null);
    expect(labelStyle.fontWeight, FontWeight.w200);
  });

1726
  testWidgets('Chip size is configurable by ThemeData.materialTapTargetSize', (WidgetTester tester) async {
1727
    final Key key1 = UniqueKey();
1728
    await tester.pumpWidget(
1729
      wrapForChip(
1730 1731 1732 1733
        child: Theme(
          data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.padded),
          child: Center(
            child: RawChip(
1734 1735 1736 1737 1738 1739 1740 1741 1742 1743
              key: key1,
              label: const Text('test'),
            ),
          ),
        ),
      ),
    );

    expect(tester.getSize(find.byKey(key1)), const Size(80.0, 48.0));

1744
    final Key key2 = UniqueKey();
1745
    await tester.pumpWidget(
1746
      wrapForChip(
1747 1748 1749 1750
        child: Theme(
          data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
          child: Center(
            child: RawChip(
1751 1752 1753 1754 1755 1756 1757 1758 1759
              key: key2,
              label: const Text('test'),
            ),
          ),
        ),
      ),
    );

    expect(tester.getSize(find.byKey(key2)), const Size(80.0, 32.0));
1760
  });
1761

1762
  testWidgets('Chip uses the right theme colors for the right components', (WidgetTester tester) async {
1763
    final ThemeData themeData = ThemeData(
1764 1765 1766
      platform: TargetPlatform.android,
      primarySwatch: Colors.blue,
    );
1767 1768 1769 1770 1771
    final ChipThemeData defaultChipTheme = ChipThemeData.fromDefaults(
      brightness: themeData.brightness,
      secondaryColor: Colors.blue,
      labelStyle: themeData.textTheme.bodyText1!,
    );
1772 1773
    bool value = false;
    Widget buildApp({
1774 1775 1776
      ChipThemeData? chipTheme,
      Widget? avatar,
      Widget? deleteIcon,
1777 1778 1779 1780
      bool isSelectable = true,
      bool isPressable = false,
      bool isDeletable = true,
      bool showCheckmark = true,
1781
    }) {
1782
      chipTheme ??= defaultChipTheme;
1783
      return wrapForChip(
1784
        child: Theme(
1785
          data: themeData,
1786
          child: ChipTheme(
1787
            data: chipTheme,
1788 1789
            child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
              return RawChip(
1790
                showCheckmark: showCheckmark,
1791
                onDeleted: isDeletable ? () { } : null,
1792 1793 1794
                avatar: avatar,
                deleteIcon: deleteIcon,
                isEnabled: isSelectable || isPressable,
1795
                shape: chipTheme?.shape,
1796
                selected: isSelectable && value,
1797
                label: Text('$value'),
1798
                onSelected: isSelectable
1799 1800 1801 1802 1803 1804
                  ? (bool newValue) {
                      setState(() {
                        value = newValue;
                      });
                    }
                  : null,
1805
                onPressed: isPressable
1806 1807 1808 1809 1810 1811
                  ? () {
                      setState(() {
                        value = true;
                      });
                    }
                  : null,
1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822
              );
            }),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildApp());

    RenderBox materialBox = getMaterialBox(tester);
    IconThemeData iconData = getIconData(tester);
1823
    DefaultTextStyle labelStyle = getLabelStyle(tester, 'false');
1824 1825

    // Check default theme for enabled widget.
1826
    expect(materialBox, paints..path(color: defaultChipTheme.backgroundColor));
1827 1828 1829 1830 1831
    expect(iconData.color, equals(const Color(0xde000000)));
    expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));
    await tester.tap(find.byType(RawChip));
    await tester.pumpAndSettle();
    materialBox = getMaterialBox(tester);
1832
    expect(materialBox, paints..path(color: defaultChipTheme.selectedColor));
1833 1834 1835 1836
    await tester.tap(find.byType(RawChip));
    await tester.pumpAndSettle();

    // Check default theme with disabled widget.
1837
    await tester.pumpWidget(buildApp(isSelectable: false));
1838 1839
    await tester.pumpAndSettle();
    materialBox = getMaterialBox(tester);
1840
    labelStyle = getLabelStyle(tester, 'false');
1841
    expect(materialBox, paints..path(color: defaultChipTheme.disabledColor));
1842 1843 1844
    expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));

    // Apply a custom theme.
1845 1846 1847 1848
    const Color customColor1 = Color(0xcafefeed);
    const Color customColor2 = Color(0xdeadbeef);
    const Color customColor3 = Color(0xbeefcafe);
    const Color customColor4 = Color(0xaddedabe);
1849
    final ChipThemeData customTheme = defaultChipTheme.copyWith(
1850 1851 1852 1853 1854 1855
      brightness: Brightness.dark,
      backgroundColor: customColor1,
      disabledColor: customColor2,
      selectedColor: customColor3,
      deleteIconColor: customColor4,
    );
1856
    await tester.pumpWidget(buildApp(chipTheme: customTheme));
1857 1858 1859
    await tester.pumpAndSettle();
    materialBox = getMaterialBox(tester);
    iconData = getIconData(tester);
1860
    labelStyle = getLabelStyle(tester, 'false');
1861 1862

    // Check custom theme for enabled widget.
1863
    expect(materialBox, paints..path(color: customTheme.backgroundColor));
1864 1865 1866 1867 1868
    expect(iconData.color, equals(customTheme.deleteIconColor));
    expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));
    await tester.tap(find.byType(RawChip));
    await tester.pumpAndSettle();
    materialBox = getMaterialBox(tester);
1869
    expect(materialBox, paints..path(color: customTheme.selectedColor));
1870 1871 1872 1873 1874
    await tester.tap(find.byType(RawChip));
    await tester.pumpAndSettle();

    // Check custom theme with disabled widget.
    await tester.pumpWidget(buildApp(
1875
      chipTheme: customTheme,
1876 1877 1878 1879
      isSelectable: false,
    ));
    await tester.pumpAndSettle();
    materialBox = getMaterialBox(tester);
1880
    labelStyle = getLabelStyle(tester, 'false');
1881
    expect(materialBox, paints..path(color: customTheme.disabledColor));
1882 1883
    expect(labelStyle.style.color, equals(Colors.black.withAlpha(0xde)));
  });
1884 1885 1886

  group('Chip semantics', () {
    testWidgets('label only', (WidgetTester tester) async {
1887
      final SemanticsTester semanticsTester = SemanticsTester(tester);
1888

1889 1890
      await tester.pumpWidget(const MaterialApp(
        home: Material(
1891 1892
          child: RawChip(
            label: Text('test'),
1893 1894 1895 1896
          ),
        ),
      ));

1897 1898 1899
      expect(
        semanticsTester,
        hasSemantics(
1900
          TestSemantics.root(
1901
            children: <TestSemantics>[
1902
              TestSemantics(
1903 1904
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
1905
                  TestSemantics(
1906
                    children: <TestSemantics>[
1907
                      TestSemantics(
1908 1909 1910 1911 1912
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
1913 1914 1915 1916
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isButton,
                            ],
1917 1918
                          ),
                        ],
1919 1920 1921 1922 1923 1924
                      ),
                    ],
                  ),
                ],
              ),
            ],
1925 1926 1927 1928 1929 1930
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
1931 1932 1933
      semanticsTester.dispose();
    });

1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945
    testWidgets('delete', (WidgetTester tester) async {
      final SemanticsTester semanticsTester = SemanticsTester(tester);

      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: RawChip(
            label: const Text('test'),
            onDeleted: () { },
          ),
        ),
      ));

1946 1947 1948
      expect(
        semanticsTester,
        hasSemantics(
1949 1950 1951 1952 1953 1954 1955 1956
          TestSemantics.root(
            children: <TestSemantics>[
              TestSemantics(
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
                  TestSemantics(
                    children: <TestSemantics>[
                      TestSemantics(
1957
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
1958 1959
                        children: <TestSemantics>[
                          TestSemantics(
1960
                            label: 'test',
1961
                            textDirection: TextDirection.ltr,
1962 1963 1964 1965
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isButton,
                            ],
1966 1967
                            children: <TestSemantics>[
                              TestSemantics(
1968
                                tooltip: 'Delete',
1969 1970 1971 1972
                                actions: <SemanticsAction>[SemanticsAction.tap],
                                textDirection: TextDirection.ltr,
                                flags: <SemanticsFlag>[
                                  SemanticsFlag.isButton,
1973
                                  SemanticsFlag.isFocusable,
1974 1975
                                ],
                              ),
1976 1977 1978 1979 1980 1981 1982 1983 1984
                            ],
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
1985 1986 1987 1988 1989 1990
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
1991 1992 1993
      semanticsTester.dispose();
    });

1994
    testWidgets('with onPressed', (WidgetTester tester) async {
1995
      final SemanticsTester semanticsTester = SemanticsTester(tester);
1996

1997 1998 1999
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: RawChip(
2000
            label: const Text('test'),
2001
            onPressed: () { },
2002 2003 2004 2005
          ),
        ),
      ));

2006 2007 2008
      expect(
        semanticsTester,
        hasSemantics(
2009
          TestSemantics.root(
2010
            children: <TestSemantics>[
2011
              TestSemantics(
2012 2013
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
2014
                  TestSemantics(
2015
                    children: <TestSemantics> [
2016
                      TestSemantics(
2017 2018 2019 2020 2021 2022 2023
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
2024
                              SemanticsFlag.isButton,
2025 2026 2027 2028 2029
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                          ),
2030 2031 2032 2033 2034 2035 2036
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
2037 2038 2039 2040 2041 2042
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
2043 2044 2045 2046 2047 2048

      semanticsTester.dispose();
    });


    testWidgets('with onSelected', (WidgetTester tester) async {
2049
      final SemanticsTester semanticsTester = SemanticsTester(tester);
2050 2051
      bool selected = false;

2052 2053 2054
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: RawChip(
2055 2056 2057 2058 2059 2060 2061 2062 2063
            label: const Text('test'),
            selected: selected,
            onSelected: (bool value) {
              selected = value;
            },
          ),
        ),
      ));

2064 2065 2066
      expect(
        semanticsTester,
        hasSemantics(
2067
          TestSemantics.root(
2068
            children: <TestSemantics>[
2069
              TestSemantics(
2070 2071
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
2072
                  TestSemantics(
2073
                    children: <TestSemantics>[
2074
                      TestSemantics(
2075 2076 2077 2078 2079 2080 2081
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
2082
                              SemanticsFlag.isButton,
2083 2084 2085 2086 2087
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                          ),
2088 2089 2090 2091 2092 2093 2094
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
2095 2096 2097 2098 2099 2100
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
2101 2102

      await tester.tap(find.byType(RawChip));
2103 2104 2105
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: RawChip(
2106 2107 2108 2109 2110 2111 2112 2113 2114 2115
            label: const Text('test'),
            selected: selected,
            onSelected: (bool value) {
              selected = value;
            },
          ),
        ),
      ));

      expect(selected, true);
2116 2117 2118
      expect(
        semanticsTester,
        hasSemantics(
2119
          TestSemantics.root(
2120
            children: <TestSemantics>[
2121
              TestSemantics(
2122 2123
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
2124
                  TestSemantics(
2125
                    children: <TestSemantics>[
2126
                      TestSemantics(
2127 2128 2129 2130 2131 2132 2133
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
2134
                              SemanticsFlag.isButton,
2135 2136 2137 2138 2139 2140
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                              SemanticsFlag.isSelected,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                          ),
2141 2142 2143 2144 2145 2146 2147
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
2148 2149 2150 2151 2152 2153
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
2154 2155 2156 2157 2158

      semanticsTester.dispose();
    });

    testWidgets('disabled', (WidgetTester tester) async {
2159
      final SemanticsTester semanticsTester = SemanticsTester(tester);
2160

2161 2162 2163
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: RawChip(
2164
            isEnabled: false,
2165
            onPressed: () { },
2166 2167 2168 2169 2170
            label: const Text('test'),
          ),
        ),
      ));

2171 2172 2173
      expect(
        semanticsTester,
        hasSemantics(
2174
          TestSemantics.root(
2175
            children: <TestSemantics>[
2176
              TestSemantics(
2177 2178
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
2179
                  TestSemantics(
2180
                    children: <TestSemantics>[
2181
                      TestSemantics(
2182 2183 2184 2185 2186
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
2187 2188 2189 2190
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isButton,
                            ],
2191 2192 2193
                            actions: <SemanticsAction>[],
                          ),
                        ],
2194 2195 2196
                      ),
                    ],
                  ),
2197 2198 2199
                ],
              ),
            ],
2200 2201 2202 2203 2204 2205
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221

      semanticsTester.dispose();
    });

    testWidgets('tapEnabled explicitly false', (WidgetTester tester) async {
      final SemanticsTester semanticsTester = SemanticsTester(tester);

      await tester.pumpWidget(const MaterialApp(
        home: Material(
          child: RawChip(
            tapEnabled: false,
            label: Text('test'),
          ),
        ),
      ));

2222 2223 2224
      expect(
        semanticsTester,
        hasSemantics(
2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247
          TestSemantics.root(
            children: <TestSemantics>[
              TestSemantics(
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
                  TestSemantics(
                    children: <TestSemantics>[
                      TestSemantics(
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
                            flags: <SemanticsFlag>[], // Must not be a button when tapping is disabled.
                            actions: <SemanticsAction>[],
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
2248 2249 2250 2251 2252 2253
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270

      semanticsTester.dispose();
    });

    testWidgets('enabled when tapEnabled and canTap', (WidgetTester tester) async {
      final SemanticsTester semanticsTester = SemanticsTester(tester);

      // These settings make a Chip which can be tapped, both in general and at this moment.
      await tester.pumpWidget(MaterialApp(
        home: Material(
          child: RawChip(
            onPressed: () {},
            label: const Text('test'),
          ),
        ),
      ));

2271 2272 2273
      expect(
        semanticsTester,
        hasSemantics(
2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301
          TestSemantics.root(
            children: <TestSemantics>[
              TestSemantics(
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
                  TestSemantics(
                    children: <TestSemantics>[
                      TestSemantics(
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isButton,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                          ),
                        ],
                      ),
                    ],
                  ),
                ],
              ),
            ],
2302 2303 2304 2305 2306 2307
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322

      semanticsTester.dispose();
    });

    testWidgets('disabled when tapEnabled but not canTap', (WidgetTester tester) async {
      final SemanticsTester semanticsTester = SemanticsTester(tester);
        // These settings make a Chip which _could_ be tapped, but not currently (ensures `canTap == false`).
        await tester.pumpWidget(const MaterialApp(
        home: Material(
          child: RawChip(
            label: Text('test'),
          ),
        ),
      ));

2323 2324 2325
      expect(
        semanticsTester,
        hasSemantics(
2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347
          TestSemantics.root(
            children: <TestSemantics>[
              TestSemantics(
                textDirection: TextDirection.ltr,
                children: <TestSemantics>[
                  TestSemantics(
                    children: <TestSemantics>[
                      TestSemantics(
                        flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
                        children: <TestSemantics>[
                          TestSemantics(
                            label: 'test',
                            textDirection: TextDirection.ltr,
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isButton,
                            ],
                          ),
                        ],
                      ),
                    ],
                  ),
2348 2349 2350
                ],
              ),
            ],
2351 2352 2353 2354 2355 2356
          ),
          ignoreTransform: true,
          ignoreId: true,
          ignoreRect: true,
        ),
      );
2357 2358 2359 2360 2361 2362 2363 2364

      semanticsTester.dispose();
    });
  });

  testWidgets('can be tapped outside of chip delete icon', (WidgetTester tester) async {
    bool deleted = false;
    await tester.pumpWidget(
2365
      wrapForChip(
2366
        child: Row(
2367
          children: <Widget>[
2368
            Chip(
2369
              materialTapTargetSize: MaterialTapTargetSize.padded,
2370
              shape: const RoundedRectangleBorder(),
2371
              avatar: const CircleAvatar(child: Text('A')),
2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387
              label: const Text('Chip A'),
              onDeleted: () {
                deleted = true;
              },
              deleteIcon: const Icon(Icons.delete),
            ),
          ],
        ),
      ),
    );

    await tester.tapAt(tester.getTopRight(find.byType(Chip)) - const Offset(2.0, -2.0));
    await tester.pumpAndSettle();
    expect(deleted, true);
  });

jslavitz's avatar
jslavitz committed
2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402
  testWidgets('Chips can be tapped', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: RawChip(
            label: Text('raw chip'),
          ),
        ),
      ),
    );

    await tester.tap(find.byType(RawChip));
    expect(tester.takeException(), null);
  });

2403
  testWidgets('Chip elevation and shadow color work correctly', (WidgetTester tester) async {
2404 2405 2406 2407 2408 2409 2410
    final ThemeData theme = ThemeData(
      platform: TargetPlatform.android,
      primarySwatch: Colors.red,
    );

    final ChipThemeData chipTheme = theme.chipTheme;

2411
    InputChip inputChip = const InputChip(label: Text('Label'));
2412 2413

    Widget buildChip(ChipThemeData data) {
2414
      return wrapForChip(
2415 2416 2417 2418 2419 2420 2421 2422
        child: Theme(
          data: theme,
          child: inputChip,
        ),
      );
    }

    await tester.pumpWidget(buildChip(chipTheme));
2423 2424
    Material material = getMaterial(tester);
    expect(material.elevation, 0.0);
2425
    expect(material.shadowColor, Colors.black);
2426

2427 2428 2429
    inputChip = const InputChip(
      label: Text('Label'),
      elevation: 4.0,
2430 2431
      shadowColor: Colors.green,
      selectedShadowColor: Colors.blue,
2432
    );
2433 2434 2435

    await tester.pumpWidget(buildChip(chipTheme));
    await tester.pumpAndSettle();
2436 2437
    material = getMaterial(tester);
    expect(material.elevation, 4.0);
2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450
    expect(material.shadowColor, Colors.green);

    inputChip = const InputChip(
      label: Text('Label'),
      selected: true,
      shadowColor: Colors.green,
      selectedShadowColor: Colors.blue,
    );

    await tester.pumpWidget(buildChip(chipTheme));
    await tester.pumpAndSettle();
    material = getMaterial(tester);
    expect(material.shadowColor, Colors.blue);
2451 2452
  });

2453 2454 2455
  testWidgets('can be tapped outside of chip body', (WidgetTester tester) async {
    bool pressed = false;
    await tester.pumpWidget(
2456
      wrapForChip(
2457
        child: Row(
2458
          children: <Widget>[
2459
            InputChip(
2460
              materialTapTargetSize: MaterialTapTargetSize.padded,
2461
              shape: const RoundedRectangleBorder(),
2462
              avatar: const CircleAvatar(child: Text('A')),
2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479
              label: const Text('Chip A'),
              onPressed: () {
                pressed = true;
              },
            ),
          ],
        ),
      ),
    );

    await tester.tapAt(tester.getRect(find.byType(InputChip)).topCenter);
    await tester.pumpAndSettle();
    expect(pressed, true);
  });

  testWidgets('is hitTestable', (WidgetTester tester) async {
    await tester.pumpWidget(
2480
      wrapForChip(
2481
        child: InputChip(
2482
          shape: const RoundedRectangleBorder(),
2483
          avatar: const CircleAvatar(child: Text('A')),
2484
          label: const Text('Chip A'),
2485
          onPressed: () { },
2486 2487 2488 2489
        ),
      ),
    );

2490
    expect(find.byType(InputChip).hitTestable(), findsOneWidget);
2491
  });
2492 2493 2494 2495 2496 2497 2498 2499

  void checkChipMaterialClipBehavior(WidgetTester tester, Clip clipBehavior) {
    final Iterable<Material> materials = tester.widgetList<Material>(find.byType(Material));
    expect(materials.length, 2);
    expect(materials.last.clipBehavior, clipBehavior);
  }

  testWidgets('Chip clipBehavior properly passes through to the Material', (WidgetTester tester) async {
2500
    const Text label = Text('label');
2501
    await tester.pumpWidget(wrapForChip(child: const Chip(label: label)));
2502
    checkChipMaterialClipBehavior(tester, Clip.none);
2503

2504
    await tester.pumpWidget(wrapForChip(child: const Chip(label: label, clipBehavior: Clip.antiAlias)));
2505 2506
    checkChipMaterialClipBehavior(tester, Clip.antiAlias);
  });
2507 2508

  testWidgets('selected chip and avatar draw darkened layer within avatar circle', (WidgetTester tester) async {
2509
    await tester.pumpWidget(wrapForChip(child: const FilterChip(
2510 2511 2512 2513 2514 2515 2516 2517 2518 2519
      avatar: CircleAvatar(child: Text('t')),
      label: Text('test'),
      selected: true,
      onSelected: null,
    )));
    final RenderBox rawChip = tester.firstRenderObject<RenderBox>(
      find.descendant(
        of: find.byType(RawChip),
        matching: find.byWidgetPredicate((Widget widget) {
          return widget.runtimeType.toString() == '_ChipRenderWidget';
2520
        }),
2521
      ),
2522 2523 2524 2525 2526 2527 2528
    );
    const Color selectScrimColor = Color(0x60191919);
    expect(rawChip, paints..path(color: selectScrimColor, includes: <Offset>[
      const Offset(10, 10),
    ], excludes: <Offset>[
      const Offset(4, 4),
    ]));
2529
  });
2530 2531 2532 2533 2534 2535 2536

  testWidgets('Chips should use InkWell instead of InkResponse.', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/28646
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: ActionChip(
2537
            onPressed: () { },
2538 2539 2540 2541 2542 2543 2544
            label: const Text('action chip'),
          ),
        ),
      ),
    );
    expect(find.byType(InkWell), findsOneWidget);
  });
2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556

  testWidgets('Chip uses stateful color for text color in different states', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();

    const Color pressedColor = Color(0x00000001);
    const Color hoverColor = Color(0x00000002);
    const Color focusedColor = Color(0x00000003);
    const Color defaultColor = Color(0x00000004);
    const Color selectedColor = Color(0x00000005);
    const Color disabledColor = Color(0x00000006);

    Color getTextColor(Set<MaterialState> states) {
2557
      if (states.contains(MaterialState.disabled)) {
2558
        return disabledColor;
2559
      }
2560

2561
      if (states.contains(MaterialState.pressed)) {
2562
        return pressedColor;
2563
      }
2564

2565
      if (states.contains(MaterialState.hovered)) {
2566
        return hoverColor;
2567
      }
2568

2569
      if (states.contains(MaterialState.focused)) {
2570
        return focusedColor;
2571
      }
2572

2573
      if (states.contains(MaterialState.selected)) {
2574
        return selectedColor;
2575
      }
2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595

      return defaultColor;
    }

    Widget chipWidget({ bool enabled = true, bool selected = false }) {
      return MaterialApp(
        home: Scaffold(
          body: Focus(
            focusNode: focusNode,
            child: ChoiceChip(
              label: const Text('Chip'),
              selected: selected,
              onSelected: enabled ? (_) {} : null,
              labelStyle: TextStyle(color: MaterialStateColor.resolveWith(getTextColor)),
            ),
          ),
        ),
      );
    }
    Color textColor() {
2596
      return tester.renderObject<RenderParagraph>(find.text('Chip')).text.style!.color!;
2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632
    }

    // Default, not disabled.
    await tester.pumpWidget(chipWidget());
    expect(textColor(), equals(defaultColor));

    // Selected.
    await tester.pumpWidget(chipWidget(selected: true));
    expect(textColor(), selectedColor);

    // Focused.
    final FocusNode chipFocusNode = focusNode.children.first;
    chipFocusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(textColor(), focusedColor);

    // Hovered.
    final Offset center = tester.getCenter(find.byType(ChoiceChip));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(textColor(), hoverColor);

    // Pressed.
    await gesture.down(center);
    await tester.pumpAndSettle();
    expect(textColor(), pressedColor);

    // Disabled.
    await tester.pumpWidget(chipWidget(enabled: false));
    await tester.pumpAndSettle();
    expect(textColor(), disabledColor);
  });
2633

2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646
  testWidgets('Chip uses stateful border side color in different states', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();

    const Color pressedColor = Color(0x00000001);
    const Color hoverColor = Color(0x00000002);
    const Color focusedColor = Color(0x00000003);
    const Color defaultColor = Color(0x00000004);
    const Color selectedColor = Color(0x00000005);
    const Color disabledColor = Color(0x00000006);

    BorderSide getBorderSide(Set<MaterialState> states) {
      Color sideColor = defaultColor;

2647
      if (states.contains(MaterialState.disabled)) {
2648
        sideColor = disabledColor;
2649
      } else if (states.contains(MaterialState.pressed)) {
2650
        sideColor = pressedColor;
2651
      } else if (states.contains(MaterialState.hovered)) {
2652
        sideColor = hoverColor;
2653
      } else if (states.contains(MaterialState.focused)) {
2654
        sideColor = focusedColor;
2655
      } else if (states.contains(MaterialState.selected)) {
2656
        sideColor = selectedColor;
2657
      }
2658

2659
      return BorderSide(color: sideColor);
2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709
    }

    Widget chipWidget({ bool enabled = true, bool selected = false }) {
      return MaterialApp(
        home: Scaffold(
          body: Focus(
            focusNode: focusNode,
            child: ChoiceChip(
              label: const Text('Chip'),
              selected: selected,
              onSelected: enabled ? (_) {} : null,
              side: _MaterialStateBorderSide(getBorderSide),
            ),
          ),
        ),
      );
    }

    // Default, not disabled.
    await tester.pumpWidget(chipWidget());
    expect(find.byType(RawChip), paints..rrect(color: defaultColor));

    // Selected.
    await tester.pumpWidget(chipWidget(selected: true));
    expect(find.byType(RawChip), paints..rrect(color: selectedColor));

    // Focused.
    final FocusNode chipFocusNode = focusNode.children.first;
    chipFocusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: focusedColor));

    // Hovered.
    final Offset center = tester.getCenter(find.byType(ChoiceChip));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: hoverColor));

    // Pressed.
    await gesture.down(center);
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: pressedColor));

    // Disabled.
    await tester.pumpWidget(chipWidget(enabled: false));
    await tester.pumpAndSettle();
2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725
    expect(find.byType(RawChip), paints..rrect(color: disabledColor));
  });

  testWidgets('Chip uses stateful border side color from resolveWith', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();

    const Color pressedColor = Color(0x00000001);
    const Color hoverColor = Color(0x00000002);
    const Color focusedColor = Color(0x00000003);
    const Color defaultColor = Color(0x00000004);
    const Color selectedColor = Color(0x00000005);
    const Color disabledColor = Color(0x00000006);

    BorderSide getBorderSide(Set<MaterialState> states) {
      Color sideColor = defaultColor;

2726
      if (states.contains(MaterialState.disabled)) {
2727
        sideColor = disabledColor;
2728
      } else if (states.contains(MaterialState.pressed)) {
2729
        sideColor = pressedColor;
2730
      } else if (states.contains(MaterialState.hovered)) {
2731
        sideColor = hoverColor;
2732
      } else if (states.contains(MaterialState.focused)) {
2733
        sideColor = focusedColor;
2734
      } else if (states.contains(MaterialState.selected)) {
2735
        sideColor = selectedColor;
2736
      }
2737

2738
      return BorderSide(color: sideColor);
2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807
    }

    Widget chipWidget({ bool enabled = true, bool selected = false }) {
      return MaterialApp(
        home: Scaffold(
          body: Focus(
            focusNode: focusNode,
            child: ChoiceChip(
              label: const Text('Chip'),
              selected: selected,
              onSelected: enabled ? (_) {} : null,
              side: MaterialStateBorderSide.resolveWith(getBorderSide),
            ),
          ),
        ),
      );
    }

    // Default, not disabled.
    await tester.pumpWidget(chipWidget());
    expect(find.byType(RawChip), paints..rrect(color: defaultColor));

    // Selected.
    await tester.pumpWidget(chipWidget(selected: true));
    expect(find.byType(RawChip), paints..rrect(color: selectedColor));

    // Focused.
    final FocusNode chipFocusNode = focusNode.children.first;
    chipFocusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: focusedColor));

    // Hovered.
    final Offset center = tester.getCenter(find.byType(ChoiceChip));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: hoverColor));

    // Pressed.
    await gesture.down(center);
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: pressedColor));

    // Disabled.
    await tester.pumpWidget(chipWidget(enabled: false));
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: disabledColor));

  });

  testWidgets('Chip uses stateful nullable border side color from resolveWith', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();

    const Color pressedColor = Color(0x00000001);
    const Color hoverColor = Color(0x00000002);
    const Color focusedColor = Color(0x00000003);
    const Color defaultColor = Color(0x00000004);
    const Color disabledColor = Color(0x00000006);

    const Color fallbackThemeColor = Color(0x00000007);
    const BorderSide defaultBorderSide = BorderSide(color: fallbackThemeColor, width: 10.0);

    BorderSide? getBorderSide(Set<MaterialState> states) {
      Color sideColor = defaultColor;

2808
      if (states.contains(MaterialState.disabled)) {
2809
        sideColor = disabledColor;
2810
      } else if (states.contains(MaterialState.pressed)) {
2811
        sideColor = pressedColor;
2812
      } else if (states.contains(MaterialState.hovered)) {
2813
        sideColor = hoverColor;
2814
      } else if (states.contains(MaterialState.focused)) {
2815
        sideColor = focusedColor;
2816
      } else if (states.contains(MaterialState.selected)) {
2817
        return null;
2818
      }
2819

2820
      return BorderSide(color: sideColor);
2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877
    }

    Widget chipWidget({ bool enabled = true, bool selected = false }) {
      return MaterialApp(
        home: Scaffold(
          body: Focus(
            focusNode: focusNode,
            child: ChipTheme(
              data: ThemeData.light().chipTheme.copyWith(
                side: defaultBorderSide,
              ),
              child: ChoiceChip(
                label: const Text('Chip'),
                selected: selected,
                onSelected: enabled ? (_) {} : null,
                side: MaterialStateBorderSide.resolveWith(getBorderSide),
              ),
            ),
          ),
        ),
      );
    }

    // Default, not disabled.
    await tester.pumpWidget(chipWidget());
    expect(find.byType(RawChip), paints..rrect(color: defaultColor));

    // Selected.
    await tester.pumpWidget(chipWidget(selected: true));
    // Because the resolver returns `null` for this value, we should fall back
    // to the theme
    expect(find.byType(RawChip), paints..rrect(color: fallbackThemeColor));

    // Focused.
    final FocusNode chipFocusNode = focusNode.children.first;
    chipFocusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: focusedColor));

    // Hovered.
    final Offset center = tester.getCenter(find.byType(ChoiceChip));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: hoverColor));

    // Pressed.
    await gesture.down(center);
    await tester.pumpAndSettle();
    expect(find.byType(RawChip), paints..rrect(color: pressedColor));

    // Disabled.
    await tester.pumpWidget(chipWidget(enabled: false));
    await tester.pumpAndSettle();
2878 2879 2880 2881 2882 2883 2884
    expect(find.byType(RawChip), paints..rrect(color: disabledColor));
  });

  testWidgets('Chip uses stateful shape in different states', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    OutlinedBorder? getShape(Set<MaterialState> states) {

2885
      if (states.contains(MaterialState.disabled)) {
2886
        return const BeveledRectangleBorder();
2887
      } else if (states.contains(MaterialState.pressed)) {
2888
        return const CircleBorder();
2889
      } else if (states.contains(MaterialState.hovered)) {
2890
        return const ContinuousRectangleBorder();
2891
      } else if (states.contains(MaterialState.focused)) {
2892
        return const RoundedRectangleBorder();
2893
      } else if (states.contains(MaterialState.selected)) {
2894
        return const BeveledRectangleBorder();
2895
      }
2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953

      return null;
    }

    Widget chipWidget({ bool enabled = true, bool selected = false }) {
      return MaterialApp(
        home: Scaffold(
          body: Focus(
            focusNode: focusNode,
            child: ChoiceChip(
              selected: selected,
              label: const Text('Chip'),
              shape: _MaterialStateOutlinedBorder(getShape),
              onSelected: enabled ? (_) {} : null,
            ),
          ),
        ),
      );
    }

    // Default, not disabled. Defers to default shape.
    await tester.pumpWidget(chipWidget());
    expect(getMaterial(tester).shape, isA<StadiumBorder>());

    // Selected.
    await tester.pumpWidget(chipWidget(selected: true));
    expect(getMaterial(tester).shape, isA<BeveledRectangleBorder>());

    // Focused.
    final FocusNode chipFocusNode = focusNode.children.first;
    chipFocusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());

    // Hovered.
    final Offset center = tester.getCenter(find.byType(ChoiceChip));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(getMaterial(tester).shape, isA<ContinuousRectangleBorder>());

    // Pressed.
    await gesture.down(center);
    await tester.pumpAndSettle();
    expect(getMaterial(tester).shape, isA<CircleBorder>());

    // Disabled.
    await tester.pumpWidget(chipWidget(enabled: false));
    await tester.pumpAndSettle();
    expect(getMaterial(tester).shape, isA<BeveledRectangleBorder>());
  });

  testWidgets('Chip defers to theme, if shape and side resolves to null', (WidgetTester tester) async {
    const OutlinedBorder themeShape = StadiumBorder();
    const OutlinedBorder selectedShape = RoundedRectangleBorder();
2954 2955
    const BorderSide themeBorderSide = BorderSide(color: Color(0x00000001));
    const BorderSide selectedBorderSide = BorderSide(color: Color(0x00000002));
2956 2957

    OutlinedBorder? getShape(Set<MaterialState> states) {
2958
      if (states.contains(MaterialState.selected)) {
2959
        return selectedShape;
2960
      }
2961 2962 2963 2964
      return null;
    }

    BorderSide? getBorderSide(Set<MaterialState> states) {
2965
      if (states.contains(MaterialState.selected)) {
2966
        return selectedBorderSide;
2967
      }
2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001
      return null;
    }

    Widget chipWidget({ bool enabled = true, bool selected = false }) {
      return MaterialApp(
        theme: ThemeData(
          chipTheme: ThemeData.light().chipTheme.copyWith(
            shape: themeShape,
            side: themeBorderSide,
          ),
        ),
        home: Scaffold(
          body: ChoiceChip(
            selected: selected,
            label: const Text('Chip'),
            shape: _MaterialStateOutlinedBorder(getShape),
            side: _MaterialStateBorderSide(getBorderSide),
            onSelected: enabled ? (_) {} : null,
          ),
        ),
      );
    }

    // Default, not disabled. Defer to theme.
    await tester.pumpWidget(chipWidget());
    expect(getMaterial(tester).shape, isA<StadiumBorder>());
    expect(find.byType(RawChip), paints..rrect(color: themeBorderSide.color));

    // Selected.
    await tester.pumpWidget(chipWidget(selected: true));
    expect(getMaterial(tester).shape, isA<RoundedRectangleBorder>());
    expect(find.byType(RawChip), paints..drrect(color: selectedBorderSide.color));
  });

3002 3003 3004 3005 3006 3007
  testWidgets('Chip responds to density changes.', (WidgetTester tester) async {
    const Key key = Key('test');
    const Key textKey = Key('test text');
    const Key iconKey = Key('test icon');
    const Key avatarKey = Key('test avatar');
    Future<void> buildTest(VisualDensity visualDensity) async {
3008
      return tester.pumpWidget(
3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106
        MaterialApp(
          home: Material(
            child: Center(
              child: Column(
                children: <Widget>[
                  InputChip(
                    visualDensity: visualDensity,
                    key: key,
                    onPressed: () {},
                    onDeleted: () {},
                    label: const Text('Test', key: textKey),
                    deleteIcon: const Icon(Icons.delete, key: iconKey),
                    avatar: const Icon(Icons.play_arrow, key: avatarKey),
                  ),
                ],
              ),
            ),
          ),
        ),
      );
    }

    // The Chips only change in size vertically in response to density, so
    // horizontal changes aren't expected.
    await buildTest(VisualDensity.standard);
    Rect box = tester.getRect(find.byKey(key));
    Rect textBox = tester.getRect(find.byKey(textKey));
    Rect iconBox = tester.getRect(find.byKey(iconKey));
    Rect avatarBox = tester.getRect(find.byKey(avatarKey));
    expect(box.size, equals(const Size(128, 32.0 + 16.0)));
    expect(textBox.size, equals(const Size(56, 14)));
    expect(iconBox.size, equals(const Size(24, 24)));
    expect(avatarBox.size, equals(const Size(24, 24)));
    expect(textBox.top, equals(17));
    expect(box.bottom - textBox.bottom, equals(17));
    expect(textBox.left, equals(372));
    expect(box.right - textBox.right, equals(36));

    // Try decreasing density (with higher density numbers).
    await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
    box = tester.getRect(find.byKey(key));
    textBox = tester.getRect(find.byKey(textKey));
    iconBox = tester.getRect(find.byKey(iconKey));
    avatarBox = tester.getRect(find.byKey(avatarKey));
    expect(box.size, equals(const Size(128, 60)));
    expect(textBox.size, equals(const Size(56, 14)));
    expect(iconBox.size, equals(const Size(24, 24)));
    expect(avatarBox.size, equals(const Size(24, 24)));
    expect(textBox.top, equals(23));
    expect(box.bottom - textBox.bottom, equals(23));
    expect(textBox.left, equals(372));
    expect(box.right - textBox.right, equals(36));

    // Try increasing density (with lower density numbers).
    await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
    box = tester.getRect(find.byKey(key));
    textBox = tester.getRect(find.byKey(textKey));
    iconBox = tester.getRect(find.byKey(iconKey));
    avatarBox = tester.getRect(find.byKey(avatarKey));
    expect(box.size, equals(const Size(128, 36)));
    expect(textBox.size, equals(const Size(56, 14)));
    expect(iconBox.size, equals(const Size(24, 24)));
    expect(avatarBox.size, equals(const Size(24, 24)));
    expect(textBox.top, equals(11));
    expect(box.bottom - textBox.bottom, equals(11));
    expect(textBox.left, equals(372));
    expect(box.right - textBox.right, equals(36));

    // Now test that horizontal and vertical are wired correctly. Negating the
    // horizontal should have no change over what's above.
    await buildTest(const VisualDensity(horizontal: 3.0, vertical: -3.0));
    await tester.pumpAndSettle();
    box = tester.getRect(find.byKey(key));
    textBox = tester.getRect(find.byKey(textKey));
    iconBox = tester.getRect(find.byKey(iconKey));
    avatarBox = tester.getRect(find.byKey(avatarKey));
    expect(box.size, equals(const Size(128, 36)));
    expect(textBox.size, equals(const Size(56, 14)));
    expect(iconBox.size, equals(const Size(24, 24)));
    expect(avatarBox.size, equals(const Size(24, 24)));
    expect(textBox.top, equals(11));
    expect(box.bottom - textBox.bottom, equals(11));
    expect(textBox.left, equals(372));
    expect(box.right - textBox.right, equals(36));

    // Make sure the "Comfortable" setting is the spec'd size
    await buildTest(VisualDensity.comfortable);
    await tester.pumpAndSettle();
    box = tester.getRect(find.byKey(key));
    expect(box.size, equals(const Size(128, 28.0 + 16.0)));

    // Make sure the "Compact" setting is the spec'd size
    await buildTest(VisualDensity.compact);
    await tester.pumpAndSettle();
    box = tester.getRect(find.byKey(key));
    expect(box.size, equals(const Size(128, 24.0 + 16.0)));
  });

3107
  testWidgets('Chip delete button tooltip can be disabled using useDeleteButtonTooltip', (WidgetTester tester) async {
3108
    await tester.pumpWidget(
3109
      chipWithOptionalDeleteButton(
3110
        deletable: true,
3111
        useDeleteButtonTooltip: false,
3112
      ),
3113 3114
    );

3115 3116
    // Tap at the delete icon of the chip, which is at the right side of the
    // chip
3117
    final Offset topRightOfInkwell = tester.getTopLeft(find.byType(InkWell).first);
3118 3119 3120 3121 3122 3123 3124 3125
    final Offset tapLocationOfDeleteButton = topRightOfInkwell + const Offset(8, 8);
    final TestGesture tapGesture = await tester.startGesture(tapLocationOfDeleteButton);

    await tester.pump();

    // Wait for some more time while pressing and holding the delete button
    await tester.pumpAndSettle();

3126
    // There should be no delete button tooltip
3127 3128 3129 3130
    expect(findTooltipContainer('Delete'), findsNothing);

    await tapGesture.up();
  });
3131

3132
  testWidgets('Chip delete button tooltip is disabled if deleteButtonTooltipMessage is empty', (WidgetTester tester) async {
3133 3134
    final UniqueKey deleteButtonKey = UniqueKey();
    await tester.pumpWidget(
3135
      chipWithOptionalDeleteButton(
3136 3137
        deleteButtonKey: deleteButtonKey,
        deletable: true,
3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159
        deleteButtonTooltipMessage: '',
      ),
    );

    // Hover over the delete icon of the chip
    final Offset centerOfDeleteButton = tester.getCenter(find.byKey(deleteButtonKey));
    final TestGesture hoverGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await hoverGesture.moveTo(centerOfDeleteButton);
    addTearDown(hoverGesture.removePointer);

    await tester.pump();

    // Wait for some more time while hovering over the delete button
    await tester.pumpAndSettle();

    // There should be no delete button tooltip
    expect(findTooltipContainer(''), findsNothing);
  });

  testWidgets('Disabling delete button tooltip does not disable chip tooltip', (WidgetTester tester) async {
    final UniqueKey deleteButtonKey = UniqueKey();
    await tester.pumpWidget(
3160
      chipWithOptionalDeleteButton(
3161 3162 3163
        deleteButtonKey: deleteButtonKey,
        deletable: true,
        deleteButtonTooltipMessage: '',
3164 3165 3166 3167
        chipTooltip: 'Chip Tooltip',
      ),
    );

3168
    // Hover over the delete icon of the chip
3169 3170 3171 3172 3173 3174 3175
    final Offset centerOfDeleteButton = tester.getCenter(find.byKey(deleteButtonKey));
    final TestGesture hoverGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await hoverGesture.moveTo(centerOfDeleteButton);
    addTearDown(hoverGesture.removePointer);

    await tester.pump();

3176
    // Wait for some more time while hovering over the delete button
3177 3178
    await tester.pumpAndSettle();

3179
    // There should be no delete button tooltip
3180
    expect(findTooltipContainer(''), findsNothing);
3181 3182 3183 3184
    // There should be a chip tooltip, however.
    expect(findTooltipContainer('Chip Tooltip'), findsOneWidget);
  });

3185
  testWidgets('Triggering delete button tooltip does not trigger Chip tooltip', (WidgetTester tester) async {
3186 3187
    final UniqueKey deleteButtonKey = UniqueKey();
    await tester.pumpWidget(
3188
      chipWithOptionalDeleteButton(
3189 3190 3191 3192 3193 3194
        deleteButtonKey: deleteButtonKey,
        deletable: true,
        chipTooltip: 'Chip Tooltip',
      ),
    );

3195
    // Hover over the delete icon of the chip
3196 3197 3198 3199 3200 3201 3202
    final Offset centerOfDeleteButton = tester.getCenter(find.byKey(deleteButtonKey));
    final TestGesture hoverGesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await hoverGesture.moveTo(centerOfDeleteButton);
    addTearDown(hoverGesture.removePointer);

    await tester.pump();

3203
    // Wait for some more time while hovering over the delete button
3204 3205
    await tester.pumpAndSettle();

3206 3207
    // There should not be a chip tooltip
    expect(findTooltipContainer('Chip Tooltip'), findsNothing);
3208
    // There should be a delete button tooltip
3209 3210 3211
    expect(findTooltipContainer('Delete'), findsOneWidget);
  });

3212
  testWidgets('intrinsicHeight implementation meets constraints', (WidgetTester tester) async {
3213
    // Regression test for https://github.com/flutter/flutter/issues/49478.
3214
    await tester.pumpWidget(wrapForChip(
3215 3216 3217 3218 3219 3220 3221 3222
      child: const Chip(
        label: Text('text'),
        padding: EdgeInsets.symmetric(horizontal: 20),
      ),
    ));

    expect(tester.takeException(), isNull);
  });
3223
}
3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241

class _MaterialStateOutlinedBorder extends StadiumBorder implements MaterialStateOutlinedBorder {
  const _MaterialStateOutlinedBorder(this.resolver);

  final MaterialPropertyResolver<OutlinedBorder?> resolver;

  @override
  OutlinedBorder? resolve(Set<MaterialState> states) => resolver(states);
}

class _MaterialStateBorderSide extends MaterialStateBorderSide {
  const _MaterialStateBorderSide(this.resolver);

  final MaterialPropertyResolver<BorderSide?> resolver;

  @override
  BorderSide? resolve(Set<MaterialState> states) => resolver(states);
}