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

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

import '../rendering/mock_canvas.dart';
import '../widgets/semantics_tester.dart';

void main() {
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
  testWidgets('OutlineButton defaults', (WidgetTester tester) async {
    final Finder rawButtonMaterial = find.descendant(
      of: find.byType(OutlineButton),
      matching: find.byType(Material),
    );

    // Enabled OutlineButton
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: OutlineButton(
          onPressed: () { },
          child: const Text('button'),
        ),
      ),
    );
    Material material = tester.widget<Material>(rawButtonMaterial);
    expect(material.animationDuration, const Duration(milliseconds: 75));
    expect(material.borderOnForeground, true);
    expect(material.borderRadius, null);
    expect(material.clipBehavior, Clip.none);
    expect(material.color, const Color(0x00000000));
    expect(material.elevation, 0.0);
    expect(material.shadowColor, const Color(0xff000000));
    expect(material.textStyle.color, const Color(0xdd000000));
    expect(material.textStyle.fontFamily, 'Roboto');
    expect(material.textStyle.fontSize, 14);
    expect(material.textStyle.fontWeight, FontWeight.w500);
    expect(material.type, MaterialType.button);

    final Offset center = tester.getCenter(find.byType(OutlineButton));
    await tester.startGesture(center);
    await tester.pumpAndSettle();

    // No change vs enabled and not pressed.
    material = tester.widget<Material>(rawButtonMaterial);
    expect(material.animationDuration, const Duration(milliseconds: 75));
    expect(material.borderOnForeground, true);
    expect(material.borderRadius, null);
    expect(material.clipBehavior, Clip.none);
    expect(material.color, const Color(0x00000000));
    expect(material.elevation, 0.0);
    expect(material.shadowColor, const Color(0xff000000));
    expect(material.textStyle.color, const Color(0xdd000000));
    expect(material.textStyle.fontFamily, 'Roboto');
    expect(material.textStyle.fontSize, 14);
    expect(material.textStyle.fontWeight, FontWeight.w500);
    expect(material.type, MaterialType.button);

    // Disabled OutlineButton
    await tester.pumpWidget(
      const Directionality(
        textDirection: TextDirection.ltr,
        child: OutlineButton(
          onPressed: null,
          child: Text('button'),
        ),
      ),
    );
    material = tester.widget<Material>(rawButtonMaterial);
    expect(material.animationDuration, const Duration(milliseconds: 75));
    expect(material.borderOnForeground, true);
    expect(material.borderRadius, null);
    expect(material.clipBehavior, Clip.none);
    expect(material.color, const Color(0x00000000));
    expect(material.elevation, 0.0);
    expect(material.shadowColor, const Color(0xff000000));
    expect(material.textStyle.color, const Color(0x61000000));
    expect(material.textStyle.fontFamily, 'Roboto');
    expect(material.textStyle.fontSize, 14);
    expect(material.textStyle.fontWeight, FontWeight.w500);
    expect(material.type, MaterialType.button);
  });

  testWidgets('Does OutlineButton work with hover', (WidgetTester tester) async {
    const Color hoverColor = Color(0xff001122);

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: OutlineButton(
          hoverColor: hoverColor,
          onPressed: () { },
          child: const Text('button'),
        ),
      ),
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    await gesture.addPointer();
    await gesture.moveTo(tester.getCenter(find.byType(OutlineButton)));
    await tester.pumpAndSettle();
    final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
    expect(inkFeatures, paints..rect(color: hoverColor));

    gesture.removePointer();
  });

  testWidgets('Does OutlineButton work with focus', (WidgetTester tester) async {
    const Color focusColor = Color(0xff001122);

    final FocusNode focusNode = FocusNode(debugLabel: 'OutlineButton Node');
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: OutlineButton(
          focusColor: focusColor,
          focusNode: focusNode,
          onPressed: () { },
          child: const Text('button'),
        ),
      ),
    );

    FocusManager.instance.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
    focusNode.requestFocus();
    await tester.pumpAndSettle();

    final RenderObject inkFeatures = tester.allRenderObjects.firstWhere((RenderObject object) => object.runtimeType.toString() == '_RenderInkFeatures');
    expect(inkFeatures, paints..rect(color: focusColor));
  });

136 137 138
  testWidgets('OutlineButton implements debugFillProperties', (WidgetTester tester) async {
    final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
    OutlineButton(
139
      onPressed: () {},
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
      textColor: const Color(0xFF00FF00),
      disabledTextColor: const Color(0xFFFF0000),
      color: const Color(0xFF000000),
      highlightColor: const Color(0xFF1565C0),
      splashColor: const Color(0xFF9E9E9E),
      child: const Text('Hello'),
    ).debugFillProperties(builder);

    final List<String> description = builder.properties
      .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info))
      .map((DiagnosticsNode node) => node.toString()).toList();

    expect(description, <String>[
      'textColor: Color(0xff00ff00)',
      'disabledTextColor: Color(0xffff0000)',
      'color: Color(0xff000000)',
      'highlightColor: Color(0xff1565c0)',
      'splashColor: Color(0xff9e9e9e)',
    ]);
  });

  testWidgets('Default OutlineButton meets a11y contrast guidelines', (WidgetTester tester) async {
162 163
    final FocusNode focusNode = FocusNode();

164 165 166 167 168 169
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: OutlineButton(
              child: const Text('OutlineButton'),
170
              onPressed: () {},
171
              focusNode: focusNode,
172 173 174 175 176 177 178 179 180
            ),
          ),
        ),
      ),
    );

    // Default, not disabled.
    await expectLater(tester, meetsGuideline(textContrastGuideline));

181 182 183 184 185 186 187 188 189 190 191
    // Focused.
    focusNode.requestFocus();
    await tester.pumpAndSettle();
    await expectLater(tester, meetsGuideline(textContrastGuideline));

    // Hovered.
    final Offset center = tester.getCenter(find.byType(OutlineButton));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
192
    addTearDown(gesture.removePointer);
193 194 195 196
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    await expectLater(tester, meetsGuideline(textContrastGuideline));

197
    // Highlighted (pressed).
198 199 200 201 202 203
    await gesture.down(center);
    await tester.pump(); // Start the splash and highlight animations.
    await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
    await expectLater(tester, meetsGuideline(textContrastGuideline));
  },
    semanticsEnabled: true,
204
    skip: isBrowser,
205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
  );

  testWidgets('OutlineButton with colored theme meets a11y contrast guidelines', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();

    final ColorScheme colorScheme = ColorScheme.fromSwatch(primarySwatch: Colors.blue);

    Color getTextColor(Set<MaterialState> states) {
      final Set<MaterialState> interactiveStates = <MaterialState>{
        MaterialState.pressed,
        MaterialState.hovered,
        MaterialState.focused,
      };
      if (states.any(interactiveStates.contains)) {
        return Colors.blue[900];
      }
      return Colors.blue[800];
    }

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: ButtonTheme(
              colorScheme: colorScheme,
              textTheme: ButtonTextTheme.primary,
              child: OutlineButton(
                child: const Text('OutlineButton'),
                onPressed: () {},
                focusNode: focusNode,
                textColor: MaterialStateColor.resolveWith(getTextColor),
              ),
            ),
          ),
        ),
      ),
    );

    // Default, not disabled.
    await expectLater(tester, meetsGuideline(textContrastGuideline));

    // Focused.
    focusNode.requestFocus();
    await tester.pumpAndSettle();
    await expectLater(tester, meetsGuideline(textContrastGuideline));

    // Hovered.
252
    final Offset center = tester.getCenter(find.byType(OutlineButton));
253 254 255 256
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
257
    addTearDown(gesture.removePointer);
258 259 260 261 262 263
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    await expectLater(tester, meetsGuideline(textContrastGuideline));

    // Highlighted (pressed).
    await gesture.down(center);
264 265 266 267
    await tester.pump(); // Start the splash and highlight animations.
    await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
    await expectLater(tester, meetsGuideline(textContrastGuideline));
  },
268
    skip: isBrowser,
269 270
    semanticsEnabled: true,
  );
271

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

275 276 277 278
    const Color pressedColor = Color(0x00000001);
    const Color hoverColor = Color(0x00000002);
    const Color focusedColor = Color(0x00000003);
    const Color defaultColor = Color(0x00000004);
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325

    Color getTextColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        return pressedColor;
      }
      if (states.contains(MaterialState.hovered)) {
        return hoverColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusedColor;
      }
      return defaultColor;
    }

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: OutlineButton(
              child: const Text('OutlineButton'),
              onPressed: () {},
              focusNode: focusNode,
              textColor: MaterialStateColor.resolveWith(getTextColor),
            ),
          ),
        ),
      ),
    );

    Color textColor() {
      return tester.renderObject<RenderParagraph>(find.text('OutlineButton')).text.style.color;
    }

    // Default, not disabled.
    expect(textColor(), equals(defaultColor));

    // Focused.
    focusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(textColor(), focusedColor);

    // Hovered.
    final Offset center = tester.getCenter(find.byType(OutlineButton));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
326
    addTearDown(gesture.removePointer);
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(textColor(), hoverColor);

    // Highlighted (pressed).
    await gesture.down(center);
    await tester.pump(); // Start the splash and highlight animations.
    await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
    expect(textColor(), pressedColor);
  });

  testWidgets('OutlineButton uses stateful color for icon color in different states', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();
    final Key buttonKey = UniqueKey();

342 343 344 345
    const Color pressedColor = Color(0x00000001);
    const Color hoverColor = Color(0x00000002);
    const Color focusedColor = Color(0x00000003);
    const Color defaultColor = Color(0x00000004);
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391

    Color getTextColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        return pressedColor;
      }
      if (states.contains(MaterialState.hovered)) {
        return hoverColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusedColor;
      }
      return defaultColor;
    }

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: OutlineButton.icon(
              key: buttonKey,
              icon: const Icon(Icons.add),
              label: const Text('OutlineButton'),
              onPressed: () {},
              focusNode: focusNode,
              textColor: MaterialStateColor.resolveWith(getTextColor),
            ),
          ),
        ),
      ),
    );

    Color iconColor() => _iconStyle(tester, Icons.add).color;
    // Default, not disabled.
    expect(iconColor(), equals(defaultColor));

    // Focused.
    focusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(iconColor(), focusedColor);

    // Hovered.
    final Offset center = tester.getCenter(find.byKey(buttonKey));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
392
    addTearDown(gesture.removePointer);
393 394 395 396 397 398 399 400 401 402 403 404 405 406
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(iconColor(), hoverColor);

    // Highlighted (pressed).
    await gesture.down(center);
    await tester.pump(); // Start the splash and highlight animations.
    await tester.pump(const Duration(milliseconds: 800)); // Wait for splash and highlight to be well under way.
    expect(iconColor(), pressedColor);
  });

  testWidgets('OutlineButton ignores disabled text color if text color is stateful', (WidgetTester tester) async {
    final FocusNode focusNode = FocusNode();

407 408 409
    const Color disabledColor = Color(0x00000001);
    const Color defaultColor = Color(0x00000002);
    const Color unusedDisabledTextColor = Color(0x00000003);
410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442

    Color getTextColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return disabledColor;
      }
      return defaultColor;
    }

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: OutlineButton(
              onPressed: null,
              child: const Text('OutlineButton'),
              focusNode: focusNode,
              textColor: MaterialStateColor.resolveWith(getTextColor),
              disabledTextColor: unusedDisabledTextColor,
            ),
          ),
        ),
      ),
    );

    Color textColor() {
      return tester.renderObject<RenderParagraph>(find.text('OutlineButton')).text.style.color;
    }

    // Disabled.
    expect(textColor(), equals(disabledColor));
    expect(textColor(), isNot(unusedDisabledTextColor));
  });

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
  testWidgets('OutlineButton uses stateful color for border 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);

    Color getBorderColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        return pressedColor;
      }
      if (states.contains(MaterialState.hovered)) {
        return hoverColor;
      }
      if (states.contains(MaterialState.focused)) {
        return focusedColor;
      }
      return defaultColor;
    }

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: OutlineButton(
              child: const Text('OutlineButton'),
              onPressed: () {},
              focusNode: focusNode,
              borderSide: BorderSide(color: MaterialStateColor.resolveWith(getBorderColor)),
            ),
          ),
        ),
      ),
    );

    final Finder outlineButton = find.byType(OutlineButton);

    // Default, not disabled.
    expect(outlineButton, paints..path(color: defaultColor));

    // Focused.
    focusNode.requestFocus();
    await tester.pumpAndSettle();
    expect(outlineButton, paints..path(color: focusedColor));

    // Hovered.
    final Offset center = tester.getCenter(find.byType(OutlineButton));
    final TestGesture gesture = await tester.createGesture(
      kind: PointerDeviceKind.mouse,
    );
    await gesture.addPointer();
495
    addTearDown(gesture.removePointer);
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
    await gesture.moveTo(center);
    await tester.pumpAndSettle();
    expect(outlineButton, paints..path(color: hoverColor));

    // Highlighted (pressed).
    await gesture.down(center);
    await tester.pumpAndSettle();
    expect(outlineButton, paints..path(color: pressedColor));
  });

  testWidgets('OutlineButton ignores highlightBorderColor if border color is stateful', (WidgetTester tester) async {
    const Color pressedColor = Color(0x00000001);
    const Color defaultColor = Color(0x00000002);
    const Color ignoredPressedColor = Color(0x00000003);

    Color getBorderColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.pressed)) {
        return pressedColor;
      }
      return defaultColor;
    }

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: OutlineButton(
              child: const Text('OutlineButton'),
              onPressed: () {},
              borderSide: BorderSide(color: MaterialStateColor.resolveWith(getBorderColor)),
              highlightedBorderColor: ignoredPressedColor,
            ),
          ),
        ),
      ),
    );

    final Finder outlineButton = find.byType(OutlineButton);

    // Default, not disabled.
    expect(outlineButton, paints..path(color: defaultColor));

    // Highlighted (pressed).
    await tester.press(outlineButton);
    await tester.pumpAndSettle();
    expect(outlineButton, paints..path(color: pressedColor));
  });

  testWidgets('OutlineButton ignores disabledBorderColor if border color is stateful', (WidgetTester tester) async {
    const Color disabledColor = Color(0x00000001);
    const Color defaultColor = Color(0x00000002);
    const Color ignoredDisabledColor = Color(0x00000003);

    Color getBorderColor(Set<MaterialState> states) {
      if (states.contains(MaterialState.disabled)) {
        return disabledColor;
      }
      return defaultColor;
    }

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Center(
            child: OutlineButton(
              child: const Text('OutlineButton'),
              onPressed: null,
              borderSide: BorderSide(color: MaterialStateColor.resolveWith(getBorderColor)),
              highlightedBorderColor: ignoredDisabledColor,
            ),
          ),
        ),
      ),
    );

    // Disabled.
    expect(find.byType(OutlineButton), paints..path(color: disabledColor));
  });

575
  testWidgets('OutlineButton onPressed and onLongPress callbacks are correctly called when non-null', (WidgetTester tester) async {
576

577 578 579 580
    bool wasPressed;
    Finder outlineButton;

    Widget buildFrame({ VoidCallback onPressed, VoidCallback onLongPress }) {
581
      return Directionality(
582
        textDirection: TextDirection.ltr,
583 584 585 586
        child: OutlineButton(
          child: const Text('button'),
          onPressed: onPressed,
          onLongPress: onLongPress,
587 588 589 590
        ),
      );
    }

591 592
    // onPressed not null, onLongPress null.
    wasPressed = false;
593
    await tester.pumpWidget(
594
      buildFrame(onPressed: () { wasPressed = true; }, onLongPress: null),
595
    );
596 597 598 599
    outlineButton = find.byType(OutlineButton);
    expect(tester.widget<OutlineButton>(outlineButton).enabled, true);
    await tester.tap(outlineButton);
    expect(wasPressed, true);
600

601 602
    // onPressed null, onLongPress not null.
    wasPressed = false;
603
    await tester.pumpWidget(
604
      buildFrame(onPressed: null, onLongPress: () { wasPressed = true; }),
605
    );
606 607 608 609 610 611 612 613 614 615
    outlineButton = find.byType(OutlineButton);
    expect(tester.widget<OutlineButton>(outlineButton).enabled, true);
    await tester.longPress(outlineButton);
    expect(wasPressed, true);

    // onPressed null, onLongPress null.
    await tester.pumpWidget(
      buildFrame(onPressed: null, onLongPress: null),
    );
    outlineButton = find.byType(OutlineButton);
616 617 618
    expect(tester.widget<OutlineButton>(outlineButton).enabled, false);
  });

619
  testWidgets("Outline button doesn't crash if disabled during a gesture", (WidgetTester tester) async {
620 621 622 623 624 625 626 627 628 629 630 631
    Widget buildFrame(VoidCallback onPressed) {
      return Directionality(
        textDirection: TextDirection.ltr,
        child: Theme(
          data: ThemeData(),
          child: Center(
            child: OutlineButton(onPressed: onPressed),
          ),
        ),
      );
    }

632
    await tester.pumpWidget(buildFrame(() {}));
633 634 635 636 637
    await tester.press(find.byType(OutlineButton));
    await tester.pumpAndSettle();
    await tester.pumpWidget(buildFrame(null));
    await tester.pumpAndSettle();
  });
638

639
  testWidgets('OutlineButton shape and border component overrides', (WidgetTester tester) async {
640 641 642
    const Color fillColor = Color(0xFF00FF00);
    const Color borderColor = Color(0xFFFF0000);
    const Color highlightedBorderColor = Color(0xFF0000FF);
643
    const Color disabledBorderColor = Color(0xFFFF00FF);
644 645
    const double borderWidth = 4.0;

646
    Widget buildFrame({ VoidCallback onPressed }) {
647
      return Directionality(
648
        textDirection: TextDirection.ltr,
649 650 651
        child: Theme(
          data: ThemeData(materialTapTargetSize: MaterialTapTargetSize.shrinkWrap),
          child: Container(
652
            alignment: Alignment.topLeft,
653
            child: OutlineButton(
654
              shape: const RoundedRectangleBorder(), // default border radius is 0
655
              clipBehavior: Clip.antiAlias,
656
              color: fillColor,
657 658 659 660
              // Causes the button to be filled with the theme's canvasColor
              // instead of Colors.transparent before the button material's
              // elevation is animated to 2.0.
              highlightElevation: 2.0,
661
              highlightedBorderColor: highlightedBorderColor,
662
              disabledBorderColor: disabledBorderColor,
663 664 665 666
              borderSide: const BorderSide(
                width: borderWidth,
                color: borderColor,
              ),
667 668
              onPressed: onPressed,
              child: const Text('button'),
669 670 671
            ),
          ),
        ),
672 673
      );
    }
674

Dan Field's avatar
Dan Field committed
675
    const Rect clipRect = Rect.fromLTRB(0.0, 0.0, 116.0, 36.0);
676
    final Path clipPath = Path()..addRect(clipRect);
677 678 679 680 681 682 683 684 685 686 687

    final Finder outlineButton = find.byType(OutlineButton);

    // Pump a button with a null onPressed callback to make it disabled.
    await tester.pumpWidget(
      buildFrame(onPressed: null),
    );

    // Expect that the button is disabled and painted with the disabled border color.
    expect(tester.widget<OutlineButton>(outlineButton).enabled, false);
    expect(
688
      outlineButton,
689 690
      paints
        ..path(color: disabledBorderColor, strokeWidth: borderWidth));
691
    _checkPhysicalLayer(
692
      tester.element(outlineButton),
693
      const Color(0x00000000),
694 695 696
      clipPath: clipPath,
      clipRect: clipRect,
    );
697 698 699

    // Pump a new button with a no-op onPressed callback to make it enabled.
    await tester.pumpWidget(
700
      buildFrame(onPressed: () {}),
701 702 703 704 705
    );

    // Wait for the border color to change from disabled to enabled.
    await tester.pumpAndSettle();

706
    // Expect that the button is enabled and painted with the enabled border color.
707
    expect(tester.widget<OutlineButton>(outlineButton).enabled, true);
708 709 710
    expect(
      outlineButton,
      paints
711
        ..path(color: borderColor, strokeWidth: borderWidth));
712
    // initially, the interior of the button is transparent
713
    _checkPhysicalLayer(
714 715 716 717 718
      tester.element(outlineButton),
      fillColor.withAlpha(0x00),
      clipPath: clipPath,
      clipRect: clipRect,
    );
719 720 721 722 723 724 725 726 727 728

    final Offset center = tester.getCenter(outlineButton);
    final TestGesture gesture = await tester.startGesture(center);
    await tester.pump(); // start gesture
    // Wait for the border's color to change to highlightedBorderColor and
    // the fillColor to become opaque.
    await tester.pump(const Duration(milliseconds: 200));
    expect(
      outlineButton,
      paints
729
        ..path(color: highlightedBorderColor, strokeWidth: borderWidth));
730
    _checkPhysicalLayer(
731 732 733 734 735
      tester.element(outlineButton),
      fillColor.withAlpha(0xFF),
      clipPath: clipPath,
      clipRect: clipRect,
    );
736 737 738 739 740 741 742

    // Tap gesture completes, button returns to its initial configuration.
    await gesture.up();
    await tester.pumpAndSettle();
    expect(
      outlineButton,
      paints
743
        ..path(color: borderColor, strokeWidth: borderWidth));
744
    _checkPhysicalLayer(
745 746 747 748 749
      tester.element(outlineButton),
      fillColor.withAlpha(0x00),
      clipPath: clipPath,
      clipRect: clipRect,
    );
750
  }, skip: isBrowser);
751

752
  testWidgets('OutlineButton has no clip by default', (WidgetTester tester) async {
753
    final GlobalKey buttonKey = GlobalKey();
754
    await tester.pumpWidget(
755
      Directionality(
756
        textDirection: TextDirection.ltr,
757 758 759
        child: Material(
          child: Center(
            child: OutlineButton(
760 761 762
              key: buttonKey,
              onPressed: () {},
              child: const Text('ABC'),
763 764 765 766 767 768 769 770
            ),
          ),
        ),
      ),
    );

    expect(
        tester.renderObject(find.byKey(buttonKey)),
771
        paintsExactlyCountTimes(#clipPath, 0),
772
    );
773
  });
774

775
  testWidgets('OutlineButton contributes semantics', (WidgetTester tester) async {
776
    final SemanticsTester semantics = SemanticsTester(tester);
777
    await tester.pumpWidget(
778
      Directionality(
779
        textDirection: TextDirection.ltr,
780 781 782
        child: Material(
          child: Center(
            child: OutlineButton(
783
              onPressed: () {},
784
              child: const Text('ABC'),
785 786 787 788 789 790 791
            ),
          ),
        ),
      ),
    );

    expect(semantics, hasSemantics(
792
      TestSemantics.root(
793
        children: <TestSemantics>[
794
          TestSemantics.rootChild(
795 796 797 798
            actions: <SemanticsAction>[
              SemanticsAction.tap,
            ],
            label: 'ABC',
Dan Field's avatar
Dan Field committed
799
            rect: const Rect.fromLTRB(0.0, 0.0, 88.0, 48.0),
800
            transform: Matrix4.translationValues(356.0, 276.0, 0.0),
801 802
            flags: <SemanticsFlag>[
              SemanticsFlag.hasEnabledState,
803
              SemanticsFlag.isButton,
804
              SemanticsFlag.isEnabled,
805
              SemanticsFlag.isFocusable,
806
            ],
807
          ),
808 809 810 811 812 813 814 815 816 817
        ],
      ),
      ignoreId: true,
    ));

    semantics.dispose();
  });

  testWidgets('OutlineButton scales textScaleFactor', (WidgetTester tester) async {
    await tester.pumpWidget(
818
      Directionality(
819
        textDirection: TextDirection.ltr,
820 821
        child: Material(
          child: MediaQuery(
822
            data: const MediaQueryData(textScaleFactor: 1.0),
823 824
            child: Center(
              child: OutlineButton(
825
                onPressed: () {},
826 827 828 829 830 831 832 833
                child: const Text('ABC'),
              ),
            ),
          ),
        ),
      ),
    );

834
    expect(tester.getSize(find.byType(OutlineButton)), equals(const Size(88.0, 48.0)));
835 836 837 838
    expect(tester.getSize(find.byType(Text)), equals(const Size(42.0, 14.0)));

    // textScaleFactor expands text, but not button.
    await tester.pumpWidget(
839
      Directionality(
840
        textDirection: TextDirection.ltr,
841 842
        child: Material(
          child: MediaQuery(
843
            data: const MediaQueryData(textScaleFactor: 1.3),
844 845
            child: Center(
              child: FlatButton(
846
                onPressed: () {},
847 848 849 850 851 852 853 854
                child: const Text('ABC'),
              ),
            ),
          ),
        ),
      ),
    );

855
    expect(tester.getSize(find.byType(FlatButton)), equals(const Size(88.0, 48.0)));
856
    // Scaled text rendering is different on Linux and Mac by one pixel.
857
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
858 859 860 861 862
    expect(tester.getSize(find.byType(Text)).width, isIn(<double>[54.0, 55.0]));
    expect(tester.getSize(find.byType(Text)).height, isIn(<double>[18.0, 19.0]));

    // Set text scale large enough to expand text and button.
    await tester.pumpWidget(
863
      Directionality(
864
        textDirection: TextDirection.ltr,
865 866
        child: Material(
          child: MediaQuery(
867
            data: const MediaQueryData(textScaleFactor: 3.0),
868 869
            child: Center(
              child: FlatButton(
870
                onPressed: () {},
871 872 873 874 875 876 877 878 879
                child: const Text('ABC'),
              ),
            ),
          ),
        ),
      ),
    );

    // Scaled text rendering is different on Linux and Mac by one pixel.
880
    // TODO(gspencergoog): Figure out why this is, and fix it. https://github.com/flutter/flutter/issues/12357
881
    expect(tester.getSize(find.byType(FlatButton)).width, isIn(<double>[158.0, 159.0]));
882
    expect(tester.getSize(find.byType(FlatButton)).height, equals(48.0));
883 884
    expect(tester.getSize(find.byType(Text)).width, isIn(<double>[126.0, 127.0]));
    expect(tester.getSize(find.byType(Text)).height, equals(42.0));
885
  }, skip: isBrowser);
886

887 888 889 890 891 892 893
  testWidgets('OutlineButton pressed fillColor default', (WidgetTester tester) async {
    Widget buildFrame(ThemeData theme) {
      return MaterialApp(
        theme: theme,
        home: Scaffold(
          body: Center(
            child: OutlineButton(
894
              onPressed: () {},
895 896 897 898
              // Causes the button to be filled with the theme's canvasColor
              // instead of Colors.transparent before the button material's
              // elevation is animated to 2.0.
              highlightElevation: 2.0,
899 900 901 902 903 904 905 906 907
              child: const Text('Hello'),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame(ThemeData.dark()));
    final Finder button = find.byType(OutlineButton);
908
    final Element buttonElement = tester.element(button);
909 910 911 912 913 914 915
    final Offset center = tester.getCenter(button);

    // Default value for dark Theme.of(context).canvasColor as well as
    // the OutlineButton fill color when the button has been pressed.
    Color fillColor = Colors.grey[850];

    // Initially the interior of the button is transparent.
916
    _checkPhysicalLayer(buttonElement, fillColor.withAlpha(0x00));
917 918 919 920 921

    // Tap-press gesture on the button triggers the fill animation.
    TestGesture gesture = await tester.startGesture(center);
    await tester.pump(); // Start the button fill animation.
    await tester.pump(const Duration(milliseconds: 200)); // Animation is complete.
922
    _checkPhysicalLayer(buttonElement, fillColor.withAlpha(0xFF));
923 924 925 926

    // Tap gesture completes, button returns to its initial configuration.
    await gesture.up();
    await tester.pumpAndSettle();
927
    _checkPhysicalLayer(buttonElement, fillColor.withAlpha(0x00));
928 929 930 931 932 933 934 935 936

    await tester.pumpWidget(buildFrame(ThemeData.light()));
    await tester.pumpAndSettle(); // Finish the theme change animation.

    // Default value for light Theme.of(context).canvasColor as well as
    // the OutlineButton fill color when the button has been pressed.
    fillColor = Colors.grey[50];

    // Initially the interior of the button is transparent.
937
    // expect(button, paints..path(color: fillColor.withAlpha(0x00)));
938 939 940 941 942

    // Tap-press gesture on the button triggers the fill animation.
    gesture = await tester.startGesture(center);
    await tester.pump(); // Start the button fill animation.
    await tester.pump(const Duration(milliseconds: 200)); // Animation is complete.
943
    _checkPhysicalLayer(buttonElement, fillColor.withAlpha(0xFF));
944 945 946 947

    // Tap gesture completes, button returns to its initial configuration.
    await gesture.up();
    await tester.pumpAndSettle();
948
    _checkPhysicalLayer(buttonElement, fillColor.withAlpha(0x00));
949
  });
950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980

  testWidgets('OutlineButton onPressed and onLongPress callbacks are distinctly recognized', (WidgetTester tester) async {
    bool didPressButton = false;
    bool didLongPressButton = false;

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: OutlineButton(
          onPressed: () {
            didPressButton = true;
          },
          onLongPress: () {
            didLongPressButton = true;
          },
          child: const Text('button'),
        ),
      ),
    );

    final Finder outlineButton = find.byType(OutlineButton);
    expect(tester.widget<OutlineButton>(outlineButton).enabled, true);

    expect(didPressButton, isFalse);
    await tester.tap(outlineButton);
    expect(didPressButton, isTrue);

    expect(didLongPressButton, isFalse);
    await tester.longPress(outlineButton);
    expect(didLongPressButton, isTrue);
  });
981 982 983

  testWidgets('OutlineButton responds to density changes.', (WidgetTester tester) async {
    const Key key = Key('test');
984
    const Key childKey = Key('test child');
985 986 987 988 989 990 991 992 993 994 995

    Future<void> buildTest(VisualDensity visualDensity, {bool useText = false}) async {
      return await tester.pumpWidget(
        MaterialApp(
          home: Directionality(
            textDirection: TextDirection.rtl,
            child: Center(
              child: OutlineButton(
                visualDensity: visualDensity,
                key: key,
                onPressed: () {},
996
                child: useText ? const Text('Text', key: childKey) : Container(key: childKey, width: 100, height: 100, color: const Color(0xffff0000)),
997 998 999 1000 1001 1002 1003 1004 1005
              ),
            ),
          ),
        ),
      );
    }

    await buildTest(const VisualDensity());
    final RenderBox box = tester.renderObject(find.byKey(key));
1006
    Rect childRect = tester.getRect(find.byKey(childKey));
1007 1008
    await tester.pumpAndSettle();
    expect(box.size, equals(const Size(132, 100)));
1009
    expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
1010 1011 1012

    await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0));
    await tester.pumpAndSettle();
1013
    childRect = tester.getRect(find.byKey(childKey));
1014
    expect(box.size, equals(const Size(156, 124)));
1015
    expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
1016 1017 1018

    await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0));
    await tester.pumpAndSettle();
1019
    childRect = tester.getRect(find.byKey(childKey));
1020
    expect(box.size, equals(const Size(108, 100)));
1021
    expect(childRect, equals(const Rect.fromLTRB(350, 250, 450, 350)));
1022 1023 1024

    await buildTest(const VisualDensity(), useText: true);
    await tester.pumpAndSettle();
1025
    childRect = tester.getRect(find.byKey(childKey));
1026
    expect(box.size, equals(const Size(88, 48)));
1027
    expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
1028 1029 1030

    await buildTest(const VisualDensity(horizontal: 3.0, vertical: 3.0), useText: true);
    await tester.pumpAndSettle();
1031
    childRect = tester.getRect(find.byKey(childKey));
1032
    expect(box.size, equals(const Size(112, 60)));
1033
    expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
1034 1035 1036

    await buildTest(const VisualDensity(horizontal: -3.0, vertical: -3.0), useText: true);
    await tester.pumpAndSettle();
1037
    childRect = tester.getRect(find.byKey(childKey));
1038
    expect(box.size, equals(const Size(76, 36)));
1039
    expect(childRect, equals(const Rect.fromLTRB(372.0, 293.0, 428.0, 307.0)));
1040
  });
1041
}
1042 1043 1044 1045 1046

PhysicalModelLayer _findPhysicalLayer(Element element) {
  expect(element, isNotNull);
  RenderObject object = element.renderObject;
  while (object != null && object is! RenderRepaintBoundary && object is! RenderView) {
1047
    object = object.parent as RenderObject;
1048 1049
  }
  expect(object.debugLayer, isNotNull);
Dan Field's avatar
Dan Field committed
1050
  expect(object.debugLayer.firstChild, isA<PhysicalModelLayer>());
1051 1052 1053
  final PhysicalModelLayer layer = object.debugLayer.firstChild as PhysicalModelLayer;
  final Layer child = layer.firstChild;
  return child is PhysicalModelLayer ? child : layer;
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064
}

void _checkPhysicalLayer(Element element, Color expectedColor, { Path clipPath, Rect clipRect }) {
  final PhysicalModelLayer expectedLayer = _findPhysicalLayer(element);
  expect(expectedLayer.elevation, 0.0);
  expect(expectedLayer.color, expectedColor);
  if (clipPath != null) {
    expect(clipRect, isNotNull);
    expect(expectedLayer.clipPath, coversSameAreaAs(clipPath, areaToCompare: clipRect.inflate(10.0)));
  }
}
1065 1066 1067 1068 1069 1070 1071

TextStyle _iconStyle(WidgetTester tester, IconData icon) {
  final RichText iconRichText = tester.widget<RichText>(
    find.descendant(of: find.byIcon(icon), matching: find.byType(RichText)),
  );
  return iconRichText.text.style;
}