theme_test.dart 30.4 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 'dart:ui' as ui;
xster's avatar
xster committed
6
import 'package:flutter/cupertino.dart';
7
import 'package:flutter/material.dart';
8
import 'package:flutter/rendering.dart';
9 10 11
import 'package:flutter_test/flutter_test.dart';

void main() {
12 13
  const TextTheme defaultGeometryTheme = Typography.englishLike2014;

14
  test('ThemeDataTween control test', () {
15
    final ThemeData light = ThemeData.light();
16
    final ThemeData dark = ThemeData.dark();
17
    final ThemeDataTween tween = ThemeDataTween(begin: light, end: dark);
18 19 20
    expect(tween.lerp(0.25), equals(ThemeData.lerp(light, dark, 0.25)));
  });

21
  testWidgets('PopupMenu inherits app theme', (WidgetTester tester) async {
22
    final Key popupMenuButtonKey = UniqueKey();
23
    await tester.pumpWidget(
24 25 26 27
      MaterialApp(
        theme: ThemeData(brightness: Brightness.dark),
        home: Scaffold(
          appBar: AppBar(
28
            actions: <Widget>[
29
              PopupMenuButton<String>(
30 31 32
                key: popupMenuButtonKey,
                itemBuilder: (BuildContext context) {
                  return <PopupMenuItem<String>>[
33
                    const PopupMenuItem<String>(child: Text('menuItem')),
34
                  ];
35
                },
36
              ),
37 38 39
            ],
          ),
        ),
40
      ),
41 42 43 44 45
    );

    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pump(const Duration(seconds: 1));

46
    expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.dark));
47 48
  });

49
  testWidgets('Theme overrides selection style', (WidgetTester tester) async {
50
    final Key key = UniqueKey();
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
    const Color defaultSelectionColor = Color(0x11111111);
    const Color defaultCursorColor = Color(0x22222222);
    const Color themeSelectionColor = Color(0x33333333);
    const Color themeCursorColor = Color(0x44444444);
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(brightness: Brightness.dark),
        home: Scaffold(
          body: DefaultSelectionStyle(
            selectionColor: defaultSelectionColor,
            cursorColor: defaultCursorColor,
            child: Theme(
              data: ThemeData(
                textSelectionTheme: const TextSelectionThemeData(
                  selectionColor: themeSelectionColor,
                  cursorColor: themeCursorColor,
                ),
              ),
69 70 71
              child: TextField(
                key: key,
              ),
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
            )
          ),
        ),
      ),
    );
    // Finds RenderEditable.
    final RenderObject root = tester.renderObject(find.byType(EditableText));
    late RenderEditable renderEditable;
    void recursiveFinder(RenderObject child) {
      if (child is RenderEditable) {
        renderEditable = child;
        return;
      }
      child.visitChildren(recursiveFinder);
    }
    root.visitChildren(recursiveFinder);
88 89 90 91 92 93

    // Focus text field so it has a selection color. The selection color is null
    // on an unfocused text field.
    await tester.tap(find.byKey(key));
    await tester.pump();

94 95 96 97
    expect(renderEditable.selectionColor, themeSelectionColor);
    expect(tester.widget<EditableText>(find.byType(EditableText)).cursorColor, themeCursorColor);
  });

98
  testWidgets('Fallback theme', (WidgetTester tester) async {
99
    late BuildContext capturedContext;
100
    await tester.pumpWidget(
101
      Builder(
102 103
        builder: (BuildContext context) {
          capturedContext = context;
104
          return Container();
105 106
        },
      ),
107 108
    );

109
    expect(Theme.of(capturedContext), equals(ThemeData.localize(ThemeData.fallback(), defaultGeometryTheme)));
110 111
  });

112
  testWidgets('ThemeData.localize memoizes the result', (WidgetTester tester) async {
113 114
    final ThemeData light = ThemeData.light();
    final ThemeData dark = ThemeData.dark();
115 116 117

    // Same input, same output.
    expect(
118 119
      ThemeData.localize(light, defaultGeometryTheme),
      same(ThemeData.localize(light, defaultGeometryTheme)),
120 121 122 123
    );

    // Different text geometry, different output.
    expect(
124 125
      ThemeData.localize(light, defaultGeometryTheme),
      isNot(same(ThemeData.localize(light, Typography.tall2014))),
126 127 128 129
    );

    // Different base theme, different output.
    expect(
130 131
      ThemeData.localize(light, defaultGeometryTheme),
      isNot(same(ThemeData.localize(dark, defaultGeometryTheme))),
132 133 134
    );
  });

135 136 137 138 139
  testWidgets('ThemeData with null typography uses proper defaults', (WidgetTester tester) async {
    expect(ThemeData().typography, Typography.material2014());
    expect(ThemeData(useMaterial3: true).typography, Typography.material2021());
  });

140 141
  testWidgets('PopupMenu inherits shadowed app theme', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/5572
142
    final Key popupMenuButtonKey = UniqueKey();
143
    await tester.pumpWidget(
144 145 146 147 148 149
      MaterialApp(
        theme: ThemeData(brightness: Brightness.dark),
        home: Theme(
          data: ThemeData(brightness: Brightness.light),
          child: Scaffold(
            appBar: AppBar(
150
              actions: <Widget>[
151
                PopupMenuButton<String>(
152 153 154
                  key: popupMenuButtonKey,
                  itemBuilder: (BuildContext context) {
                    return <PopupMenuItem<String>>[
155
                      const PopupMenuItem<String>(child: Text('menuItem')),
156
                    ];
157
                  },
158
                ),
159 160 161 162
              ],
            ),
          ),
        ),
163
      ),
164 165 166 167 168
    );

    await tester.tap(find.byKey(popupMenuButtonKey));
    await tester.pump(const Duration(seconds: 1));

169
    expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.light));
170 171 172
  });

  testWidgets('DropdownMenu inherits shadowed app theme', (WidgetTester tester) async {
173
    final Key dropdownMenuButtonKey = UniqueKey();
174
    await tester.pumpWidget(
175 176 177 178 179 180
      MaterialApp(
        theme: ThemeData(brightness: Brightness.dark),
        home: Theme(
          data: ThemeData(brightness: Brightness.light),
          child: Scaffold(
            appBar: AppBar(
181
              actions: <Widget>[
182
                DropdownButton<String>(
183
                  key: dropdownMenuButtonKey,
184
                  onChanged: (String? newValue) { },
185
                  value: 'menuItem',
186
                  items: const <DropdownMenuItem<String>>[
187
                    DropdownMenuItem<String>(
188
                      value: 'menuItem',
189
                      child: Text('menuItem'),
190 191
                    ),
                  ],
192 193 194 195 196
                ),
              ],
            ),
          ),
        ),
197
      ),
198 199 200 201 202
    );

    await tester.tap(find.byKey(dropdownMenuButtonKey));
    await tester.pump(const Duration(seconds: 1));

203
    for (final Element item in tester.elementList(find.text('menuItem'))) {
204
      expect(Theme.of(item).brightness, equals(Brightness.light));
205
    }
206 207 208 209
  });

  testWidgets('ModalBottomSheet inherits shadowed app theme', (WidgetTester tester) async {
    await tester.pumpWidget(
210 211 212 213 214 215 216
      MaterialApp(
        theme: ThemeData(brightness: Brightness.dark),
        home: Theme(
          data: ThemeData(brightness: Brightness.light),
          child: Scaffold(
            body: Center(
              child: Builder(
217
                builder: (BuildContext context) {
218
                  return ElevatedButton(
219
                    onPressed: () {
220
                      showModalBottomSheet<void>(
221
                        context: context,
222
                        builder: (BuildContext context) => const Text('bottomSheet'),
223 224
                      );
                    },
225
                    child: const Text('SHOW'),
226
                  );
227
                },
228 229 230 231
              ),
            ),
          ),
        ),
232
      ),
233 234 235
    );

    await tester.tap(find.text('SHOW'));
236 237
    await tester.pump(); // start animation
    await tester.pump(const Duration(seconds: 1)); // end animation
238
    expect(Theme.of(tester.element(find.text('bottomSheet'))).brightness, equals(Brightness.light));
239 240 241
  });

  testWidgets('Dialog inherits shadowed app theme', (WidgetTester tester) async {
242
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
243
    await tester.pumpWidget(
244 245 246 247 248
      MaterialApp(
        theme: ThemeData(brightness: Brightness.dark),
        home: Theme(
          data: ThemeData(brightness: Brightness.light),
          child: Scaffold(
249
            key: scaffoldKey,
250 251
            body: Center(
              child: Builder(
252
                builder: (BuildContext context) {
253
                  return ElevatedButton(
254
                    onPressed: () {
255
                      showDialog<void>(
256
                        context: context,
257
                        builder: (BuildContext context) => const Text('dialog'),
258 259
                      );
                    },
260
                    child: const Text('SHOW'),
261
                  );
262
                },
263 264 265 266
              ),
            ),
          ),
        ),
267
      ),
268 269 270 271
    );

    await tester.tap(find.text('SHOW'));
    await tester.pump(const Duration(seconds: 1));
272
    expect(Theme.of(tester.element(find.text('dialog'))).brightness, equals(Brightness.light));
273 274
  });

275
  testWidgets("Scaffold inherits theme's scaffoldBackgroundColor", (WidgetTester tester) async {
276
    const Color green = Color(0xFF00FF00);
277 278

    await tester.pumpWidget(
279 280 281 282 283
      MaterialApp(
        theme: ThemeData(scaffoldBackgroundColor: green),
        home: Scaffold(
          body: Center(
            child: Builder(
284
              builder: (BuildContext context) {
285
                return GestureDetector(
286
                  onTap: () {
287
                    showDialog<void>(
288
                      context: context,
289 290
                      builder: (BuildContext context) {
                        return const Scaffold(
291
                          body: SizedBox(
292 293 294 295 296
                            width: 200.0,
                            height: 200.0,
                          ),
                        );
                      },
297 298
                    );
                  },
299
                  child: const Text('SHOW'),
300 301 302 303 304
                );
              },
            ),
          ),
        ),
305
      ),
306 307 308 309 310
    );

    await tester.tap(find.text('SHOW'));
    await tester.pump(const Duration(seconds: 1));

311
    final List<Material> materials = tester.widgetList<Material>(find.byType(Material)).toList();
312 313 314 315
    expect(materials.length, equals(2));
    expect(materials[0].color, green); // app scaffold
    expect(materials[1].color, green); // dialog scaffold
  });
316 317 318

  testWidgets('IconThemes are applied', (WidgetTester tester) async {
    await tester.pumpWidget(
319 320
      MaterialApp(
        theme: ThemeData(iconTheme: const IconThemeData(color: Colors.green, size: 10.0)),
321
        home: const Icon(Icons.computer),
322
      ),
323 324 325 326
    );

    RenderParagraph glyphText = tester.renderObject(find.byType(RichText));

327 328
    expect(glyphText.text.style!.color, Colors.green);
    expect(glyphText.text.style!.fontSize, 10.0);
329 330

    await tester.pumpWidget(
331 332
      MaterialApp(
        theme: ThemeData(iconTheme: const IconThemeData(color: Colors.orange, size: 20.0)),
333 334 335 336 337 338 339
        home: const Icon(Icons.computer),
      ),
    );
    await tester.pump(const Duration(milliseconds: 100)); // Halfway through the theme transition

    glyphText = tester.renderObject(find.byType(RichText));

340 341
    expect(glyphText.text.style!.color, Color.lerp(Colors.green, Colors.orange, 0.5));
    expect(glyphText.text.style!.fontSize, 15.0);
342 343 344 345

    await tester.pump(const Duration(milliseconds: 100)); // Finish the transition
    glyphText = tester.renderObject(find.byType(RichText));

346 347
    expect(glyphText.text.style!.color, Colors.orange);
    expect(glyphText.text.style!.fontSize, 20.0);
348
  });
349 350

  testWidgets(
Ian Hickson's avatar
Ian Hickson committed
351
    'Same ThemeData reapplied does not trigger descendants rebuilds',
352 353
    (WidgetTester tester) async {
      testBuildCalled = 0;
354
      ThemeData themeData = ThemeData(primaryColor: const Color(0xFF000000));
355

356
      Widget buildTheme() {
357
        return Theme(
358 359
          data: themeData,
          child: const Test(),
360 361 362 363
        );
      }

      await tester.pumpWidget(buildTheme());
364 365 366
      expect(testBuildCalled, 1);

      // Pump the same widgets again.
367
      await tester.pumpWidget(buildTheme());
368 369 370 371
      // No repeated build calls to the child since it's the same theme data.
      expect(testBuildCalled, 1);

      // New instance of theme data but still the same content.
372
      themeData = ThemeData(primaryColor: const Color(0xFF000000));
373
      await tester.pumpWidget(buildTheme());
374 375 376 377
      // Still no repeated calls.
      expect(testBuildCalled, 1);

      // Different now.
378
      themeData = ThemeData(primaryColor: const Color(0xFF222222));
379
      await tester.pumpWidget(buildTheme());
380 381 382 383
      // Should call build again.
      expect(testBuildCalled, 2);
    },
  );
384 385

  testWidgets('Text geometry set in Theme has higher precedence than that of Localizations', (WidgetTester tester) async {
386
    const double kMagicFontSize = 4321.0;
387
    final ThemeData fallback = ThemeData.fallback();
388 389
    final ThemeData customTheme = fallback.copyWith(
      primaryTextTheme: fallback.primaryTextTheme.copyWith(
390
        bodyText2: fallback.primaryTextTheme.bodyText2!.copyWith(
391
          fontSize: kMagicFontSize,
392
        ),
393 394
      ),
    );
395
    expect(customTheme.primaryTextTheme.bodyText2!.fontSize, kMagicFontSize);
396

397
    late double actualFontSize;
398
    await tester.pumpWidget(Directionality(
399
      textDirection: TextDirection.ltr,
400
      child: Theme(
401
        data: customTheme,
402
        child: Builder(builder: (BuildContext context) {
403
          final ThemeData theme = Theme.of(context);
404
          actualFontSize = theme.primaryTextTheme.bodyText2!.fontSize!;
405
          return Text(
406
            'A',
407
            style: theme.primaryTextTheme.bodyText2,
408 409 410 411 412
          );
        }),
      ),
    ));

413
    expect(actualFontSize, kMagicFontSize);
414
  });
415 416

  testWidgets('Default Theme provides all basic TextStyle properties', (WidgetTester tester) async {
417
    late ThemeData theme;
418
    await tester.pumpWidget(Directionality(
419
      textDirection: TextDirection.ltr,
420
      child: Builder(
421
        builder: (BuildContext context) {
422
          theme = Theme.of(context);
423 424 425 426 427 428 429
          return const Text('A');
        },
      ),
    ));

    List<TextStyle> extractStyles(TextTheme textTheme) {
      return <TextStyle>[
430 431 432 433 434 435 436 437 438 439 440 441 442
        textTheme.displayLarge!,
        textTheme.displayMedium!,
        textTheme.displaySmall!,
        textTheme.headlineLarge!,
        textTheme.headlineMedium!,
        textTheme.headlineSmall!,
        textTheme.titleLarge!,
        textTheme.titleMedium!,
        textTheme.bodyLarge!,
        textTheme.bodyMedium!,
        textTheme.bodySmall!,
        textTheme.labelLarge!,
        textTheme.labelMedium!,
443 444 445
      ];
    }

446 447
    for (final TextTheme textTheme in <TextTheme>[theme.textTheme, theme.primaryTextTheme, theme.accentTextTheme]) {
      for (final TextStyle style in extractStyles(textTheme).map<TextStyle>((TextStyle style) => _TextStyleProxy(style))) {
448 449 450 451 452 453 454 455 456 457 458 459 460 461
        expect(style.inherit, false);
        expect(style.color, isNotNull);
        expect(style.fontFamily, isNotNull);
        expect(style.fontSize, isNotNull);
        expect(style.fontWeight, isNotNull);
        expect(style.fontStyle, null);
        expect(style.letterSpacing, null);
        expect(style.wordSpacing, null);
        expect(style.textBaseline, isNotNull);
        expect(style.height, null);
        expect(style.decoration, TextDecoration.none);
        expect(style.decorationColor, null);
        expect(style.decorationStyle, null);
        expect(style.debugLabel, isNotNull);
462 463
        expect(style.locale, null);
        expect(style.background, null);
464 465 466
      }
    }

467
    expect(theme.textTheme.displayLarge!.debugLabel, '(englishLike displayLarge 2014).merge(blackMountainView displayLarge)');
468
  });
xster's avatar
xster committed
469 470

  group('Cupertino theme', () {
471 472 473 474
    late int buildCount;
    CupertinoThemeData? actualTheme;
    IconThemeData? actualIconTheme;
    BuildContext? context;
xster's avatar
xster committed
475 476

    final Widget singletonThemeSubtree = Builder(
477
      builder: (BuildContext localContext) {
xster's avatar
xster committed
478
        buildCount++;
479 480 481
        actualTheme = CupertinoTheme.of(localContext);
        actualIconTheme = IconTheme.of(localContext);
        context = localContext;
xster's avatar
xster committed
482 483 484 485 486
        return const Placeholder();
      },
    );

    Future<CupertinoThemeData> testTheme(WidgetTester tester, ThemeData theme) async {
487
      await tester.pumpWidget(Theme(data: theme, child: singletonThemeSubtree));
488
      return actualTheme!;
xster's avatar
xster committed
489 490 491 492 493
    }

    setUp(() {
      buildCount = 0;
      actualTheme = null;
494
      actualIconTheme = null;
495
      context = null;
xster's avatar
xster committed
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
    });

    testWidgets('Default theme has defaults', (WidgetTester tester) async {
      final CupertinoThemeData theme = await testTheme(tester, ThemeData.light());

      expect(theme.brightness, Brightness.light);
      expect(theme.primaryColor, Colors.blue);
      expect(theme.scaffoldBackgroundColor, Colors.grey[50]);
      expect(theme.primaryContrastingColor, Colors.white);
      expect(theme.textTheme.textStyle.fontFamily, '.SF Pro Text');
      expect(theme.textTheme.textStyle.fontSize, 17.0);
    });

    testWidgets('Dark theme has defaults', (WidgetTester tester) async {
      final CupertinoThemeData theme = await testTheme(tester, ThemeData.dark());

      expect(theme.brightness, Brightness.dark);
      expect(theme.primaryColor, Colors.blue);
      expect(theme.primaryContrastingColor, Colors.white);
      expect(theme.scaffoldBackgroundColor, Colors.grey[850]);
      expect(theme.textTheme.textStyle.fontFamily, '.SF Pro Text');
      expect(theme.textTheme.textStyle.fontSize, 17.0);
    });

520 521
    testWidgets('MaterialTheme overrides the brightness', (WidgetTester tester) async {
      await testTheme(tester, ThemeData.dark());
522
      expect(CupertinoTheme.brightnessOf(context!), Brightness.dark);
523 524

      await testTheme(tester, ThemeData.light());
525
      expect(CupertinoTheme.brightnessOf(context!), Brightness.light);
526 527 528 529 530 531

      // Overridable by cupertinoOverrideTheme.
      await testTheme(tester, ThemeData(
        brightness: Brightness.light,
        cupertinoOverrideTheme: const CupertinoThemeData(brightness: Brightness.dark),
      ));
532
      expect(CupertinoTheme.brightnessOf(context!), Brightness.dark);
533 534 535 536 537

      await testTheme(tester, ThemeData(
        brightness: Brightness.dark,
        cupertinoOverrideTheme: const CupertinoThemeData(brightness: Brightness.light),
      ));
538
      expect(CupertinoTheme.brightnessOf(context!), Brightness.light);
539 540
    });

xster's avatar
xster committed
541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
    testWidgets('Can override material theme', (WidgetTester tester) async {
      final CupertinoThemeData theme = await testTheme(tester, ThemeData(
        cupertinoOverrideTheme: const CupertinoThemeData(
          scaffoldBackgroundColor: CupertinoColors.lightBackgroundGray,
        ),
      ));

      expect(theme.brightness, Brightness.light);
      // We took the scaffold background override but the rest are still cascaded
      // to the material theme.
      expect(theme.primaryColor, Colors.blue);
      expect(theme.primaryContrastingColor, Colors.white);
      expect(theme.scaffoldBackgroundColor, CupertinoColors.lightBackgroundGray);
      expect(theme.textTheme.textStyle.fontFamily, '.SF Pro Text');
      expect(theme.textTheme.textStyle.fontSize, 17.0);
    });

    testWidgets('Can override properties that are independent of material', (WidgetTester tester) async {
      final CupertinoThemeData theme = await testTheme(tester, ThemeData(
        cupertinoOverrideTheme: const CupertinoThemeData(
          // The bar colors ignore all things material except brightness.
          barBackgroundColor: CupertinoColors.black,
        ),
      ));

      expect(theme.primaryColor, Colors.blue);
      // MaterialBasedCupertinoThemeData should also function like a normal CupertinoThemeData.
      expect(theme.barBackgroundColor, CupertinoColors.black);
    });

    testWidgets('Changing material theme triggers rebuilds', (WidgetTester tester) async {
      CupertinoThemeData theme = await testTheme(tester, ThemeData(
        primarySwatch: Colors.red,
      ));

      expect(buildCount, 1);
      expect(theme.primaryColor, Colors.red);

      theme = await testTheme(tester, ThemeData(
        primarySwatch: Colors.orange,
      ));

      expect(buildCount, 2);
      expect(theme.primaryColor, Colors.orange);
    });

587 588
    testWidgets(
      "CupertinoThemeData does not override material theme's icon theme",
589 590 591 592 593 594
      (WidgetTester tester) async {
        const Color materialIconColor = Colors.blue;
        const Color cupertinoIconColor = Colors.black;

        await testTheme(tester, ThemeData(
            iconTheme: const IconThemeData(color: materialIconColor),
595
            cupertinoOverrideTheme: const CupertinoThemeData(primaryColor: cupertinoIconColor),
596 597 598
        ));

        expect(buildCount, 1);
599
        expect(actualIconTheme!.color, materialIconColor);
600 601
      },
    );
602

xster's avatar
xster committed
603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
    testWidgets(
      'Changing cupertino theme override triggers rebuilds',
      (WidgetTester tester) async {
        CupertinoThemeData theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.purple,
          cupertinoOverrideTheme: const CupertinoThemeData(
            primaryColor: CupertinoColors.activeOrange,
          ),
        ));

        expect(buildCount, 1);
        expect(theme.primaryColor, CupertinoColors.activeOrange);

        theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.purple,
          cupertinoOverrideTheme: const CupertinoThemeData(
            primaryColor: CupertinoColors.activeGreen,
          ),
        ));

        expect(buildCount, 2);
        expect(theme.primaryColor, CupertinoColors.activeGreen);
      },
    );

    testWidgets(
      'Cupertino theme override blocks derivative changes',
      (WidgetTester tester) async {
        CupertinoThemeData theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.purple,
          cupertinoOverrideTheme: const CupertinoThemeData(
            primaryColor: CupertinoColors.activeOrange,
          ),
        ));

        expect(buildCount, 1);
        expect(theme.primaryColor, CupertinoColors.activeOrange);

        // Change the upstream material primary color.
        theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.blue,
          cupertinoOverrideTheme: const CupertinoThemeData(
            // But the primary material color is preempted by the override.
            primaryColor: CupertinoColors.activeOrange,
          ),
        ));

650
        expect(buildCount, 2);
xster's avatar
xster committed
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
        expect(theme.primaryColor, CupertinoColors.activeOrange);
      },
    );

    testWidgets(
      'Cupertino overrides do not block derivatives triggering rebuilds when derivatives are not overridden',
      (WidgetTester tester) async {
        CupertinoThemeData theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.purple,
          cupertinoOverrideTheme: const CupertinoThemeData(
            primaryContrastingColor: CupertinoColors.destructiveRed,
          ),
        ));

        expect(buildCount, 1);
        expect(theme.textTheme.actionTextStyle.color, Colors.purple);
        expect(theme.primaryContrastingColor, CupertinoColors.destructiveRed);

        theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.green,
          cupertinoOverrideTheme: const CupertinoThemeData(
            primaryContrastingColor: CupertinoColors.destructiveRed,
          ),
        ));

        expect(buildCount, 2);
        expect(theme.textTheme.actionTextStyle.color, Colors.green);
        expect(theme.primaryContrastingColor, CupertinoColors.destructiveRed);
      },
    );

    testWidgets(
      'copyWith only copies the overrides, not the material or cupertino derivatives',
      (WidgetTester tester) async {
        final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.purple,
          cupertinoOverrideTheme: const CupertinoThemeData(
            primaryContrastingColor: CupertinoColors.activeOrange,
          ),
        ));

        final CupertinoThemeData copiedTheme = originalTheme.copyWith(
          barBackgroundColor: CupertinoColors.destructiveRed,
        );

        final CupertinoThemeData theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.blue,
          cupertinoOverrideTheme: copiedTheme,
        ));

        expect(theme.primaryColor, Colors.blue);
        expect(theme.primaryContrastingColor, CupertinoColors.activeOrange);
        expect(theme.barBackgroundColor, CupertinoColors.destructiveRed);
      },
    );

    testWidgets(
      "Material themes with no cupertino overrides can also be copyWith'ed",
      (WidgetTester tester) async {
        final CupertinoThemeData originalTheme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.purple,
        ));

        final CupertinoThemeData copiedTheme = originalTheme.copyWith(
          primaryContrastingColor: CupertinoColors.destructiveRed,
        );

        final CupertinoThemeData theme = await testTheme(tester, ThemeData(
          primarySwatch: Colors.blue,
          cupertinoOverrideTheme: copiedTheme,
        ));

        expect(theme.primaryColor, Colors.blue);
        expect(theme.primaryContrastingColor, CupertinoColors.destructiveRed);
      },
    );
  });
728 729
}

730
int testBuildCalled = 0;
731
class Test extends StatefulWidget {
732
  const Test({ super.key });
733 734

  @override
735
  State<Test> createState() => _TestState();
736 737 738 739 740 741
}

class _TestState extends State<Test> {
  @override
  Widget build(BuildContext context) {
    testBuildCalled += 1;
742 743
    return Container(
      decoration: BoxDecoration(
744
        color: Theme.of(context).primaryColor,
745 746 747
      ),
    );
  }
748
}
749 750 751 752

/// This class exists only to make sure that we test all the properties of the
/// [TextStyle] class. If a property is added/removed/renamed, the analyzer will
/// complain that this class has incorrect overrides.
753
// ignore: avoid_implementing_value_types
754 755 756 757 758 759
class _TextStyleProxy implements TextStyle {
  _TextStyleProxy(this._delegate);

  final TextStyle _delegate;

  // Do make sure that all the properties correctly forward to the _delegate.
760
  @override
761
  Color? get color => _delegate.color;
762
  @override
763
  Color? get backgroundColor => _delegate.backgroundColor;
764
  @override
765
  String? get debugLabel => _delegate.debugLabel;
766
  @override
767
  TextDecoration? get decoration => _delegate.decoration;
768
  @override
769
  Color? get decorationColor => _delegate.decorationColor;
770
  @override
771
  TextDecorationStyle? get decorationStyle => _delegate.decorationStyle;
772
  @override
773
  double? get decorationThickness => _delegate.decorationThickness;
774
  @override
775
  String? get fontFamily => _delegate.fontFamily;
776
  @override
777
  List<String>? get fontFamilyFallback => _delegate.fontFamilyFallback;
778
  @override
779
  double? get fontSize => _delegate.fontSize;
780
  @override
781
  FontStyle? get fontStyle => _delegate.fontStyle;
782
  @override
783
  FontWeight? get fontWeight => _delegate.fontWeight;
784
  @override
785
  double? get height => _delegate.height;
786
  @override
787 788
  TextLeadingDistribution? get leadingDistribution => _delegate.leadingDistribution;
  @override
789
  Locale? get locale => _delegate.locale;
790
  @override
791
  ui.Paint? get foreground => _delegate.foreground;
792
  @override
793
  ui.Paint? get background => _delegate.background;
794 795 796
  @override
  bool get inherit => _delegate.inherit;
  @override
797
  double? get letterSpacing => _delegate.letterSpacing;
798
  @override
799
  TextBaseline? get textBaseline => _delegate.textBaseline;
800
  @override
801
  double? get wordSpacing => _delegate.wordSpacing;
802
  @override
803
  List<Shadow>? get shadows => _delegate.shadows;
804
  @override
805
  List<ui.FontFeature>? get fontFeatures => _delegate.fontFeatures;
806
  @override
807 808
  List<ui.FontVariation>? get fontVariations => _delegate.fontVariations;
  @override
809
  TextOverflow? get overflow => _delegate.overflow;
810

811
  @override
812
  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) =>
813 814
      super.toString();

815
  @override
816
  DiagnosticsNode toDiagnosticsNode({ String? name, DiagnosticsTreeStyle? style }) {
817
    throw UnimplementedError();
818 819 820 821
  }

  @override
  String toStringShort() {
822
    throw UnimplementedError();
823 824 825
  }

  @override
826
  TextStyle apply({
827 828 829 830 831
    Color? color,
    Color? backgroundColor,
    TextDecoration? decoration,
    Color? decorationColor,
    TextDecorationStyle? decorationStyle,
832 833
    double decorationThicknessFactor = 1.0,
    double decorationThicknessDelta = 0.0,
834 835
    String? fontFamily,
    List<String>? fontFamilyFallback,
836 837 838
    double fontSizeFactor = 1.0,
    double fontSizeDelta = 0.0,
    int fontWeightDelta = 0,
839
    FontStyle? fontStyle,
840 841 842 843 844
    double letterSpacingFactor = 1.0,
    double letterSpacingDelta = 0.0,
    double wordSpacingFactor = 1.0,
    double wordSpacingDelta = 0.0,
    double heightFactor = 1.0,
845
    double heightDelta = 0.0,
846
    TextLeadingDistribution? leadingDistribution,
847 848 849 850
    TextBaseline? textBaseline,
    Locale? locale,
    List<ui.Shadow>? shadows,
    List<ui.FontFeature>? fontFeatures,
851
    List<ui.FontVariation>? fontVariations,
852
    TextOverflow? overflow,
853
    String? package,
854
  }) {
855
    throw UnimplementedError();
856 857 858 859
  }

  @override
  RenderComparison compareTo(TextStyle other) {
860
    throw UnimplementedError();
861 862 863
  }

  @override
864
  TextStyle copyWith({
865 866 867 868 869 870 871 872 873 874 875 876
    bool? inherit,
    Color? color,
    Color? backgroundColor,
    String? fontFamily,
    List<String>? fontFamilyFallback,
    double? fontSize,
    FontWeight? fontWeight,
    FontStyle? fontStyle,
    double? letterSpacing,
    double? wordSpacing,
    TextBaseline? textBaseline,
    double? height,
877
    TextLeadingDistribution? leadingDistribution,
878 879 880 881 882
    Locale? locale,
    ui.Paint? foreground,
    ui.Paint? background,
    List<Shadow>? shadows,
    List<ui.FontFeature>? fontFeatures,
883
    List<ui.FontVariation>? fontVariations,
884 885 886 887 888
    TextDecoration? decoration,
    Color? decorationColor,
    TextDecorationStyle? decorationStyle,
    double? decorationThickness,
    String? debugLabel,
889
    TextOverflow? overflow,
890
    String? package,
891
  }) {
892
    throw UnimplementedError();
893 894 895
  }

  @override
896
  void debugFillProperties(DiagnosticPropertiesBuilder properties, { String prefix = '' }) {
897
    throw UnimplementedError();
898 899 900
  }

  @override
901
  ui.ParagraphStyle getParagraphStyle({
902 903
    TextAlign? textAlign,
    TextDirection? textDirection,
904
    double textScaleFactor = 1.0,
905 906 907 908 909 910 911 912 913 914
    String? ellipsis,
    int? maxLines,
    ui.TextHeightBehavior? textHeightBehavior,
    Locale? locale,
    String? fontFamily,
    double? fontSize,
    FontWeight? fontWeight,
    FontStyle? fontStyle,
    double? height,
    StrutStyle? strutStyle,
915
  }) {
916
    throw UnimplementedError();
917 918 919
  }

  @override
920
  ui.TextStyle getTextStyle({ double textScaleFactor = 1.0 }) {
921
    throw UnimplementedError();
922 923 924
  }

  @override
925
  TextStyle merge(TextStyle? other) {
926
    throw UnimplementedError();
927 928
  }
}