search_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/foundation.dart';
6
import 'package:flutter/material.dart';
7
import 'package:flutter/services.dart';
8 9
import 'package:flutter_test/flutter_test.dart';

10
import '../widgets/clipboard_utils.dart';
11 12
import '../widgets/semantics_tester.dart';

13
void main() {
14 15 16 17
  TestWidgetsFlutterBinding.ensureInitialized();
  final MockClipboard mockClipboard = MockClipboard();

  setUp(() async {
18
    // Fill the clipboard so that the Paste option is available in the text
19
    // selection menu.
20
    TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, mockClipboard.handleMethodCall);
21 22 23 24
    await Clipboard.setData(const ClipboardData(text: 'Clipboard data'));
  });

  tearDown(() {
25
    TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null);
26 27
  });

28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
  testWidgets('Changing query moves cursor to the end of query', (WidgetTester tester) async {
    final _TestSearchDelegate delegate = _TestSearchDelegate();

    await tester.pumpWidget(TestHomePage(delegate: delegate));
    await tester.tap(find.byTooltip('Search'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 300));

    delegate.query = 'Foo';

    final TextField textField = tester.widget<TextField>(find.byType(TextField));

    expect(
      textField.controller!.selection,
      TextSelection(
        baseOffset: delegate.query.length,
        extentOffset: delegate.query.length,
      ),
    );
  });

49
  testWidgets('Can open and close search', (WidgetTester tester) async {
50
    final _TestSearchDelegate delegate = _TestSearchDelegate();
51 52
    final List<String> selectedResults = <String>[];

53
    await tester.pumpWidget(TestHomePage(
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
      delegate: delegate,
      results: selectedResults,
    ));

    // We are on the homepage
    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('HomeTitle'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);

    // Open search
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('HomeTitle'), findsNothing);
    expect(find.text('Suggestions'), findsOneWidget);
    expect(selectedResults, hasLength(0));

    final TextField textField = tester.widget(find.byType(TextField));
73
    expect(textField.focusNode!.hasFocus, isTrue);
74 75 76 77 78 79 80 81 82 83 84

    // Close search
    await tester.tap(find.byTooltip('Back'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('HomeTitle'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);
    expect(selectedResults, <String>['Result']);
  });

85 86 87
  testWidgets('Can close search with system back button to return null', (WidgetTester tester) async {
    // regression test for https://github.com/flutter/flutter/issues/18145

88
    final _TestSearchDelegate delegate = _TestSearchDelegate();
89
    final List<String?> selectedResults = <String?>[];
90

91
    await tester.pumpWidget(TestHomePage(
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
      delegate: delegate,
      results: selectedResults,
    ));

    // We are on the homepage
    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('HomeTitle'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);

    // Open search
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('HomeTitle'), findsNothing);
    expect(find.text('Suggestions'), findsOneWidget);
108
    expect(find.text('Bottom'), findsOneWidget);
109 110 111

    // Simulate system back button
    final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute'));
112
    await ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
113 114
    await tester.pumpAndSettle();

115
    expect(selectedResults, <String?>[null]);
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130

    // We are on the homepage again
    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('HomeTitle'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);

    // Open search again
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('HomeTitle'), findsNothing);
    expect(find.text('Suggestions'), findsOneWidget);
  });

131
  testWidgets('Hint text color overridden', (WidgetTester tester) async {
132 133
    const String searchHintText = 'Enter search terms';
    final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHintText);
134 135 136 137 138 139 140

    await tester.pumpWidget(TestHomePage(
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

141 142
    final Text hintText = tester.widget(find.text(searchHintText));
    expect(hintText.style!.color, _TestSearchDelegate.hintTextColor);
143 144
  });

145
  testWidgets('Requests suggestions', (WidgetTester tester) async {
146
    final _TestSearchDelegate delegate = _TestSearchDelegate();
147

148
    await tester.pumpWidget(TestHomePage(
149 150 151 152 153 154
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(delegate.query, '');
155 156
    expect(delegate.queriesForSuggestions.last, '');
    expect(delegate.queriesForResults, hasLength(0));
157 158

    // Type W o w into search field
159
    delegate.queriesForSuggestions.clear();
160 161 162 163 164 165 166 167 168 169
    await tester.enterText(find.byType(TextField), 'W');
    await tester.pumpAndSettle();
    expect(delegate.query, 'W');
    await tester.enterText(find.byType(TextField), 'Wo');
    await tester.pumpAndSettle();
    expect(delegate.query, 'Wo');
    await tester.enterText(find.byType(TextField), 'Wow');
    await tester.pumpAndSettle();
    expect(delegate.query, 'Wow');

170 171
    expect(delegate.queriesForSuggestions, <String>['W', 'Wo', 'Wow']);
    expect(delegate.queriesForResults, hasLength(0));
172 173 174
  });

  testWidgets('Shows Results and closes search', (WidgetTester tester) async {
175
    final _TestSearchDelegate delegate = _TestSearchDelegate();
176 177
    final List<String> selectedResults = <String>[];

178
    await tester.pumpWidget(TestHomePage(
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
      delegate: delegate,
      results: selectedResults,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();
    await tester.enterText(find.byType(TextField), 'Wow');
    await tester.pumpAndSettle();
    await tester.tap(find.text('Suggestions'));
    await tester.pumpAndSettle();

    // We are on the results page for Wow
    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('HomeTitle'), findsNothing);
    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Results'), findsOneWidget);

    final TextField textField = tester.widget(find.byType(TextField));
196
    expect(textField.focusNode!.hasFocus, isFalse);
197
    expect(delegate.queriesForResults, <String>['Wow']);
198 199 200 201 202 203 204 205 206 207 208 209

    // Close search
    await tester.tap(find.byTooltip('Back'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('HomeTitle'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Results'), findsNothing);
    expect(selectedResults, <String>['Result']);
  });

210
  testWidgets('Can switch between results and suggestions', (WidgetTester tester) async {
211
    final _TestSearchDelegate delegate = _TestSearchDelegate();
212

213
    await tester.pumpWidget(TestHomePage(
214 215 216
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
217
    await tester.pumpAndSettle();
218 219 220 221 222 223

    // Showing suggestions
    expect(find.text('Suggestions'), findsOneWidget);
    expect(find.text('Results'), findsNothing);

    // Typing query Wow
224
    delegate.queriesForSuggestions.clear();
225
    await tester.enterText(find.byType(TextField), 'Wow');
226
    await tester.pumpAndSettle();
227 228

    expect(delegate.query, 'Wow');
229 230
    expect(delegate.queriesForSuggestions, <String>['Wow']);
    expect(delegate.queriesForResults, hasLength(0));
231 232

    await tester.tap(find.text('Suggestions'));
233
    await tester.pumpAndSettle();
234 235 236 237 238 239

    // Showing Results
    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Results'), findsOneWidget);

    expect(delegate.query, 'Wow');
240 241
    expect(delegate.queriesForSuggestions, <String>['Wow']);
    expect(delegate.queriesForResults, <String>['Wow']);
242 243

    TextField textField = tester.widget(find.byType(TextField));
244
    expect(textField.focusNode!.hasFocus, isFalse);
245

246
    // Tapping search field to go back to suggestions
247
    await tester.tap(find.byType(TextField));
248
    await tester.pumpAndSettle();
249 250

    textField = tester.widget(find.byType(TextField));
251
    expect(textField.focusNode!.hasFocus, isTrue);
252 253 254

    expect(find.text('Suggestions'), findsOneWidget);
    expect(find.text('Results'), findsNothing);
255 256
    expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow']);
    expect(delegate.queriesForResults, <String>['Wow']);
257 258

    await tester.enterText(find.byType(TextField), 'Foo');
259
    await tester.pumpAndSettle();
260 261

    expect(delegate.query, 'Foo');
262 263
    expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']);
    expect(delegate.queriesForResults, <String>['Wow']);
264 265 266

    // Go to results again
    await tester.tap(find.text('Suggestions'));
267
    await tester.pumpAndSettle();
268 269 270 271 272

    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Results'), findsOneWidget);

    expect(delegate.query, 'Foo');
273 274
    expect(delegate.queriesForSuggestions, <String>['Wow', 'Wow', 'Foo']);
    expect(delegate.queriesForResults, <String>['Wow', 'Foo']);
275 276

    textField = tester.widget(find.byType(TextField));
277
    expect(textField.focusNode!.hasFocus, isFalse);
278 279
  });

280
  testWidgets('Fresh search always starts with empty query', (WidgetTester tester) async {
281
    final _TestSearchDelegate delegate = _TestSearchDelegate();
282

283
    await tester.pumpWidget(TestHomePage(
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(delegate.query, '');

    delegate.query = 'Foo';
    await tester.tap(find.byTooltip('Back'));
    await tester.pumpAndSettle();
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(delegate.query, '');
  });

  testWidgets('Initial queries are honored', (WidgetTester tester) async {
301
    final _TestSearchDelegate delegate = _TestSearchDelegate();
302 303 304

    expect(delegate.query, '');

305
    await tester.pumpWidget(TestHomePage(
306 307 308 309 310 311 312 313 314 315 316
      delegate: delegate,
      passInInitialQuery: true,
      initialQuery: 'Foo',
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(delegate.query, 'Foo');
  });

  testWidgets('Initial query null re-used previous query', (WidgetTester tester) async {
317
    final _TestSearchDelegate delegate = _TestSearchDelegate();
318 319 320

    delegate.query = 'Foo';

321
    await tester.pumpWidget(TestHomePage(
322 323 324 325 326 327 328 329 330 331
      delegate: delegate,
      passInInitialQuery: true,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(delegate.query, 'Foo');
  });

  testWidgets('Changing query shows up in search field', (WidgetTester tester) async {
332
    final _TestSearchDelegate delegate = _TestSearchDelegate();
333

334
    await tester.pumpWidget(TestHomePage(
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
      delegate: delegate,
      passInInitialQuery: true,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    delegate.query = 'Foo';

    expect(find.text('Foo'), findsOneWidget);
    expect(find.text('Bar'), findsNothing);

    delegate.query = 'Bar';

    expect(find.text('Foo'), findsNothing);
    expect(find.text('Bar'), findsOneWidget);
  });

  testWidgets('transitionAnimation runs while search fades in/out', (WidgetTester tester) async {
353
    final _TestSearchDelegate delegate = _TestSearchDelegate();
354

355
    await tester.pumpWidget(TestHomePage(
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
      delegate: delegate,
      passInInitialQuery: true,
    ));

    // runs while search fades in
    expect(delegate.transitionAnimation.status, AnimationStatus.dismissed);
    await tester.tap(find.byTooltip('Search'));
    expect(delegate.transitionAnimation.status, AnimationStatus.forward);
    await tester.pumpAndSettle();
    expect(delegate.transitionAnimation.status, AnimationStatus.completed);

    // does not run while switching to results
    await tester.tap(find.text('Suggestions'));
    expect(delegate.transitionAnimation.status, AnimationStatus.completed);
    await tester.pumpAndSettle();
    expect(delegate.transitionAnimation.status, AnimationStatus.completed);

    // runs while search fades out
    await tester.tap(find.byTooltip('Back'));
    expect(delegate.transitionAnimation.status, AnimationStatus.reverse);
    await tester.pumpAndSettle();
    expect(delegate.transitionAnimation.status, AnimationStatus.dismissed);
  });

  testWidgets('Closing nested search returns to search', (WidgetTester tester) async {
381
    final List<String?> nestedSearchResults = <String?>[];
382
    final _TestSearchDelegate nestedSearchDelegate = _TestSearchDelegate(
383 384 385 386 387
      suggestions: 'Nested Suggestions',
      result: 'Nested Result',
    );

    final List<String> selectedResults = <String>[];
388
    final _TestSearchDelegate delegate = _TestSearchDelegate(
389
      actions: <Widget>[
390
        Builder(
391
          builder: (BuildContext context) {
392
            return IconButton(
393 394 395
              tooltip: 'Nested Search',
              icon: const Icon(Icons.search),
              onPressed: () async {
396
                final String? result = await showSearch(
397 398 399 400 401 402 403
                  context: context,
                  delegate: nestedSearchDelegate,
                );
                nestedSearchResults.add(result);
              },
            );
          },
404
        ),
405 406 407
      ],
    );

408
    await tester.pumpWidget(TestHomePage(
409 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 443 444
      delegate: delegate,
      results: selectedResults,
    ));
    expect(find.text('HomeBody'), findsOneWidget);
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('Suggestions'), findsOneWidget);
    expect(find.text('Nested Suggestions'), findsNothing);

    await tester.tap(find.byTooltip('Nested Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Nested Suggestions'), findsOneWidget);

    await tester.tap(find.byTooltip('Back'));
    await tester.pumpAndSettle();
    expect(nestedSearchResults, <String>['Nested Result']);

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('Suggestions'), findsOneWidget);
    expect(find.text('Nested Suggestions'), findsNothing);

    await tester.tap(find.byTooltip('Back'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Nested Suggestions'), findsNothing);
    expect(selectedResults, <String>['Result']);
  });

  testWidgets('Closing search with nested search shown goes back to underlying route', (WidgetTester tester) async {
445 446
    late _TestSearchDelegate delegate;
    final List<String?> nestedSearchResults = <String?>[];
447
    final _TestSearchDelegate nestedSearchDelegate = _TestSearchDelegate(
448 449 450
      suggestions: 'Nested Suggestions',
      result: 'Nested Result',
      actions: <Widget>[
451
        Builder(
452
          builder: (BuildContext context) {
453
            return IconButton(
454 455 456 457 458 459 460
              tooltip: 'Close Search',
              icon: const Icon(Icons.close),
              onPressed: () async {
                delegate.close(context, 'Result Foo');
              },
            );
          },
461
        ),
462 463 464 465
      ],
    );

    final List<String> selectedResults = <String>[];
466
    delegate = _TestSearchDelegate(
467
      actions: <Widget>[
468
        Builder(
469
          builder: (BuildContext context) {
470
            return IconButton(
471 472 473
              tooltip: 'Nested Search',
              icon: const Icon(Icons.search),
              onPressed: () async {
474
                final String? result = await showSearch(
475 476 477 478 479 480 481
                  context: context,
                  delegate: nestedSearchDelegate,
                );
                nestedSearchResults.add(result);
              },
            );
          },
482
        ),
483 484 485
      ],
    );

486
    await tester.pumpWidget(TestHomePage(
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
      delegate: delegate,
      results: selectedResults,
    ));

    expect(find.text('HomeBody'), findsOneWidget);
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('Suggestions'), findsOneWidget);
    expect(find.text('Nested Suggestions'), findsNothing);

    await tester.tap(find.byTooltip('Nested Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Nested Suggestions'), findsOneWidget);

    await tester.tap(find.byTooltip('Close Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);
    expect(find.text('Nested Suggestions'), findsNothing);
512
    expect(nestedSearchResults, <String?>[null]);
513 514
    expect(selectedResults, <String>['Result Foo']);
  });
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
  testWidgets('Custom searchFieldLabel value', (WidgetTester tester) async {
    const String searchHint = 'custom search hint';
    final String defaultSearchHint = const DefaultMaterialLocalizations().searchFieldLabel;

    final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHint);

    await tester.pumpWidget(TestHomePage(
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text(searchHint), findsOneWidget);
    expect(find.text(defaultSearchHint), findsNothing);
  });

  testWidgets('Default searchFieldLabel is used when it is set to null', (WidgetTester tester) async {
    final String searchHint = const DefaultMaterialLocalizations().searchFieldLabel;

    final _TestSearchDelegate delegate = _TestSearchDelegate();

    await tester.pumpWidget(TestHomePage(
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text(searchHint), findsOneWidget);
  });

546
  testWidgets('Custom searchFieldStyle value', (WidgetTester tester) async {
547 548
    const String searchHintText = 'Enter search terms';
    const TextStyle searchFieldStyle = TextStyle(color: Colors.red, fontSize: 3);
549

550
    final _TestSearchDelegate delegate = _TestSearchDelegate(searchHint: searchHintText, searchFieldStyle: searchFieldStyle);
551

552
    await tester.pumpWidget(TestHomePage(delegate: delegate));
553 554 555
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

556 557 558
    final Text hintText = tester.widget(find.text(searchHintText));
    expect(hintText.style?.color, delegate.searchFieldStyle?.color);
    expect(hintText.style?.fontSize, delegate.searchFieldStyle?.fontSize);
559 560
  });

561
  testWidgets('keyboard show search button by default', (WidgetTester tester) async {
562
    final _TestSearchDelegate delegate = _TestSearchDelegate();
563

564
    await tester.pumpWidget(TestHomePage(
565 566 567 568 569 570 571
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    await tester.showKeyboard(find.byType(TextField));

572
    expect(tester.testTextInput.setClientArgs!['inputAction'], TextInputAction.search.toString());
573
  });
574

575 576 577 578 579 580 581 582 583
  testWidgets('Custom textInputAction results in keyboard with corresponding button', (WidgetTester tester) async {
    final _TestSearchDelegate delegate = _TestSearchDelegate(textInputAction: TextInputAction.done);

    await tester.pumpWidget(TestHomePage(
      delegate: delegate,
    ));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();
    await tester.showKeyboard(find.byType(TextField));
584
    expect(tester.testTextInput.setClientArgs!['inputAction'], TextInputAction.done.toString());
585 586
  });

587
  group('contributes semantics', () {
588
    TestSemantics buildExpected({ required String routeName }) {
589
      return TestSemantics.root(
590
        children: <TestSemantics>[
591
          TestSemantics(
592 593 594
            id: 1,
            textDirection: TextDirection.ltr,
            children: <TestSemantics>[
595
              TestSemantics(
596
                id: 2,
597
                children: <TestSemantics>[
598
                  TestSemantics(
599 600 601 602 603 604 605
                    id: 7,
                    flags: <SemanticsFlag>[
                      SemanticsFlag.scopesRoute,
                      SemanticsFlag.namesRoute,
                    ],
                    label: routeName,
                    textDirection: TextDirection.ltr,
606
                    children: <TestSemantics>[
607
                      TestSemantics(
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631
                        id: 9,
                        children: <TestSemantics>[
                          TestSemantics(
                            id: 10,
                            flags: <SemanticsFlag>[
                              SemanticsFlag.hasEnabledState,
                              SemanticsFlag.isButton,
                              SemanticsFlag.isEnabled,
                              SemanticsFlag.isFocusable,
                            ],
                            actions: <SemanticsAction>[SemanticsAction.tap],
                            label: 'Back',
                            textDirection: TextDirection.ltr,
                          ),
                          TestSemantics(
                            id: 11,
                            flags: <SemanticsFlag>[
                              SemanticsFlag.isTextField,
                              SemanticsFlag.isFocused,
                              SemanticsFlag.isHeader,
                              if (debugDefaultTargetPlatformOverride != TargetPlatform.iOS &&
                                debugDefaultTargetPlatformOverride != TargetPlatform.macOS) SemanticsFlag.namesRoute,
                            ],
                            actions: <SemanticsAction>[
632 633
                              if (debugDefaultTargetPlatformOverride == TargetPlatform.macOS ||
                                debugDefaultTargetPlatformOverride == TargetPlatform.windows)
634
                                SemanticsAction.didGainAccessibilityFocus,
635 636
                              SemanticsAction.tap,
                              SemanticsAction.setSelection,
637
                              SemanticsAction.setText,
638 639 640 641 642 643
                              SemanticsAction.paste,
                            ],
                            label: 'Search',
                            textDirection: TextDirection.ltr,
                            textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
                          ),
644 645 646 647 648
                          TestSemantics(
                            id: 14,
                            label: 'Bottom',
                            textDirection: TextDirection.ltr,
                          ),
649 650 651 652
                        ],
                      ),
                      TestSemantics(
                        id: 8,
653 654
                        flags: <SemanticsFlag>[
                          SemanticsFlag.hasEnabledState,
655
                          SemanticsFlag.isButton,
656
                          SemanticsFlag.isEnabled,
657
                          SemanticsFlag.isFocusable,
658 659
                        ],
                        actions: <SemanticsAction>[SemanticsAction.tap],
660
                        label: 'Suggestions',
661 662 663 664 665 666 667 668 669 670 671 672 673
                        textDirection: TextDirection.ltr,
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ],
      );
    }

    testWidgets('includes routeName on Android', (WidgetTester tester) async {
674 675 676
      final SemanticsTester semantics = SemanticsTester(tester);
      final _TestSearchDelegate delegate = _TestSearchDelegate();
      await tester.pumpWidget(TestHomePage(
677 678 679 680 681 682
        delegate: delegate,
      ));

      await tester.tap(find.byTooltip('Search'));
      await tester.pumpAndSettle();

683 684 685 686 687 688
      expect(semantics, hasSemantics(
        buildExpected(routeName: 'Search'),
        ignoreId: true,
        ignoreRect: true,
        ignoreTransform: true,
      ));
689 690 691 692

      semantics.dispose();
    });

Dan Field's avatar
Dan Field committed
693
    testWidgets('does not include routeName', (WidgetTester tester) async {
694 695 696
      final SemanticsTester semantics = SemanticsTester(tester);
      final _TestSearchDelegate delegate = _TestSearchDelegate();
      await tester.pumpWidget(TestHomePage(
697 698 699 700 701 702
        delegate: delegate,
      ));

      await tester.tap(find.byTooltip('Search'));
      await tester.pumpAndSettle();

703 704 705 706 707 708
      expect(semantics, hasSemantics(
        buildExpected(routeName: ''),
        ignoreId: true,
        ignoreRect: true,
        ignoreTransform: true,
      ));
709 710

      semantics.dispose();
Dan Field's avatar
Dan Field committed
711
    }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
712
  });
713

714
  testWidgets('Custom searchFieldDecorationTheme value', (WidgetTester tester) async {
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729
    const InputDecorationTheme searchFieldDecorationTheme = InputDecorationTheme(
      hintStyle: TextStyle(color: _TestSearchDelegate.hintTextColor),
    );
    final _TestSearchDelegate delegate = _TestSearchDelegate(
      searchFieldDecorationTheme: searchFieldDecorationTheme,
    );

    await tester.pumpWidget(TestHomePage(delegate: delegate));
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    final ThemeData textFieldTheme = Theme.of(tester.element(find.byType(TextField)));
    expect(textFieldTheme.inputDecorationTheme, searchFieldDecorationTheme);
  });

730 731
  // Regression test for: https://github.com/flutter/flutter/issues/66781
  testWidgets('text in search bar contrasts background (light mode)', (WidgetTester tester) async {
732 733 734 735 736 737 738 739 740 741 742
    final ThemeData themeData = ThemeData.light();
    final _TestSearchDelegate delegate = _TestSearchDelegate(
      defaultAppBarTheme: true,
    );
    const String query = 'search query';
    await tester.pumpWidget(TestHomePage(
      delegate: delegate,
      passInInitialQuery: true,
      initialQuery: query,
      themeData: themeData,
    ));
743

744 745
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();
746

747 748 749 750 751
    final Material appBarBackground = tester.widget<Material>(find.descendant(
      of: find.byType(AppBar),
      matching: find.byType(Material),
    ));
    expect(appBarBackground.color, Colors.white);
752

753 754 755
    final TextField textField = tester.widget<TextField>(find.byType(TextField));
    expect(textField.style!.color, themeData.textTheme.bodyText1!.color);
    expect(textField.style!.color, isNot(equals(Colors.white)));
756 757 758 759
  });

  // Regression test for: https://github.com/flutter/flutter/issues/66781
  testWidgets('text in search bar contrasts background (dark mode)', (WidgetTester tester) async {
760 761 762 763 764 765 766 767 768 769 770
    final ThemeData themeData = ThemeData.dark();
    final _TestSearchDelegate delegate = _TestSearchDelegate(
      defaultAppBarTheme: true,
    );
    const String query = 'search query';
    await tester.pumpWidget(TestHomePage(
      delegate: delegate,
      passInInitialQuery: true,
      initialQuery: query,
      themeData: themeData,
    ));
771

772 773
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();
774

775 776 777 778 779
    final Material appBarBackground = tester.widget<Material>(find.descendant(
      of: find.byType(AppBar),
      matching: find.byType(Material),
    ));
    expect(appBarBackground.color, themeData.primaryColor);
780

781 782 783
    final TextField textField = tester.widget<TextField>(find.byType(TextField));
    expect(textField.style!.color, themeData.textTheme.bodyText1!.color);
    expect(textField.style!.color, isNot(equals(themeData.primaryColor)));
784
  });
785 786 787 788 789

  // Regression test for: https://github.com/flutter/flutter/issues/78144
  testWidgets('`Leading` and `Actions` nullable test', (WidgetTester tester) async {
    // The search delegate page is displayed with no issues
    // even with a null return values for [buildLeading] and [buildActions].
790
    final _TestEmptySearchDelegate delegate = _TestEmptySearchDelegate();
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823
    final List<String> selectedResults = <String>[];

    await tester.pumpWidget(TestHomePage(
      delegate: delegate,
      results: selectedResults,
    ));

    // We are on the homepage.
    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('HomeTitle'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);

    // Open the search page.
    await tester.tap(find.byTooltip('Search'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsNothing);
    expect(find.text('HomeTitle'), findsNothing);
    expect(find.text('Suggestions'), findsOneWidget);
    expect(selectedResults, hasLength(0));

    final TextField textField = tester.widget(find.byType(TextField));
    expect(textField.focusNode!.hasFocus, isTrue);

    // Close the search page.
    await tester.tap(find.byTooltip('Close'));
    await tester.pumpAndSettle();

    expect(find.text('HomeBody'), findsOneWidget);
    expect(find.text('HomeTitle'), findsOneWidget);
    expect(find.text('Suggestions'), findsNothing);
    expect(selectedResults, <String>['Result']);
  });
824 825 826 827 828

  testWidgets('showSearch with useRootNavigator', (WidgetTester tester) async {
    final _MyNavigatorObserver rootObserver = _MyNavigatorObserver();
    final _MyNavigatorObserver localObserver = _MyNavigatorObserver();

829
    final _TestEmptySearchDelegate delegate = _TestEmptySearchDelegate();
830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880

    await tester.pumpWidget(MaterialApp(
      navigatorObservers: <NavigatorObserver>[rootObserver],
      home: Navigator(
        observers: <NavigatorObserver>[localObserver],
        onGenerateRoute: (RouteSettings settings) {
          if (settings.name == 'nested') {
            return MaterialPageRoute<dynamic>(
              builder: (BuildContext context) => Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  TextButton(
                      onPressed: () async {
                        await showSearch(context: context, delegate: delegate, useRootNavigator: true);
                      },
                      child: const Text('showSearchRootNavigator')),
                  TextButton(
                      onPressed: () async {
                        await showSearch(context: context, delegate: delegate);
                      },
                      child: const Text('showSearchLocalNavigator')),
                ],
              ),
              settings: settings,
            );
          }
          throw UnimplementedError();
        },
        initialRoute: 'nested',
      ),
    ));

    expect(rootObserver.pushCount, 0);
    expect(localObserver.pushCount, 0);

    // showSearch normal and back
    await tester.tap(find.text('showSearchLocalNavigator'));
    await tester.pumpAndSettle();
    await tester.tap(find.byTooltip('Close'));
    await tester.pumpAndSettle();
    expect(rootObserver.pushCount, 0);
    expect(localObserver.pushCount, 1);

    // showSearch with rootNavigator
    await tester.tap(find.text('showSearchRootNavigator'));
    await tester.pumpAndSettle();
    await tester.tap(find.byTooltip('Close'));
    await tester.pumpAndSettle();
    expect(rootObserver.pushCount, 1);
    expect(localObserver.pushCount, 1);
  });
881 882 883 884
}

class TestHomePage extends StatelessWidget {
  const TestHomePage({
885
    Key? key,
886
    this.results,
887
    required this.delegate,
888
    this.passInInitialQuery = false,
889
    this.initialQuery,
890
    this.themeData,
891
  }) : super(key: key);
892

893
  final List<String?>? results;
894 895
  final SearchDelegate<String> delegate;
  final bool passInInitialQuery;
896
  final ThemeData? themeData;
897
  final String? initialQuery;
898 899 900

  @override
  Widget build(BuildContext context) {
901
    return MaterialApp(
902
      theme: themeData,
903
      home: Builder(builder: (BuildContext context) {
904 905
        return Scaffold(
          appBar: AppBar(
906 907
            title: const Text('HomeTitle'),
            actions: <Widget>[
908
              IconButton(
909 910 911
                tooltip: 'Search',
                icon: const Icon(Icons.search),
                onPressed: () async {
912
                  String? selectedResult;
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938
                  if (passInInitialQuery) {
                    selectedResult = await showSearch<String>(
                      context: context,
                      delegate: delegate,
                      query: initialQuery,
                    );
                  } else {
                    selectedResult = await showSearch<String>(
                      context: context,
                      delegate: delegate,
                    );
                  }
                  results?.add(selectedResult);
                },
              ),
            ],
          ),
          body: const Text('HomeBody'),
        );
      }),
    );
  }
}

class _TestSearchDelegate extends SearchDelegate<String> {
  _TestSearchDelegate({
939 940 941
    this.suggestions = 'Suggestions',
    this.result = 'Result',
    this.actions = const <Widget>[],
942
    this.defaultAppBarTheme = false,
943
    InputDecorationTheme? searchFieldDecorationTheme,
944 945
    TextStyle? searchFieldStyle,
    String? searchHint,
946
    TextInputAction textInputAction = TextInputAction.search,
947 948 949 950 951 952
  }) : super(
          searchFieldLabel: searchHint,
          textInputAction: textInputAction,
          searchFieldStyle: searchFieldStyle,
          searchFieldDecorationTheme: searchFieldDecorationTheme,
        );
953

954
  final bool defaultAppBarTheme;
955 956 957
  final String suggestions;
  final String result;
  final List<Widget> actions;
958
  static const Color hintTextColor = Colors.green;
959 960 961

  @override
  ThemeData appBarTheme(BuildContext context) {
962 963 964
    if (defaultAppBarTheme) {
      return super.appBarTheme(context);
    }
965
    final ThemeData theme = Theme.of(context);
966
    return theme.copyWith(
967 968 969 970 971 972 973
      inputDecorationTheme: searchFieldDecorationTheme ??
          InputDecorationTheme(
            hintStyle: searchFieldStyle ??
                const TextStyle(
                  color: hintTextColor,
                ),
          ),
974 975
    );
  }
976 977 978

  @override
  Widget buildLeading(BuildContext context) {
979
    return IconButton(
980 981 982 983 984 985 986 987
      tooltip: 'Back',
      icon: const Icon(Icons.arrow_back),
      onPressed: () {
        close(context, result);
      },
    );
  }

988 989
  final List<String> queriesForSuggestions = <String>[];
  final List<String> queriesForResults = <String>[];
990 991 992

  @override
  Widget buildSuggestions(BuildContext context) {
993
    queriesForSuggestions.add(query);
994
    return MaterialButton(
995 996 997
      onPressed: () {
        showResults(context);
      },
998
      child: Text(suggestions),
999 1000 1001 1002 1003
    );
  }

  @override
  Widget buildResults(BuildContext context) {
1004
    queriesForResults.add(query);
1005 1006 1007 1008 1009 1010 1011
    return const Text('Results');
  }

  @override
  List<Widget> buildActions(BuildContext context) {
    return actions;
  }
1012 1013 1014 1015 1016 1017 1018 1019

  @override
  PreferredSizeWidget buildBottom(BuildContext context) {
    return const PreferredSize(
      preferredSize: Size.fromHeight(56.0),
      child: Text('Bottom'),
    );
  }
1020
}
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065

class _TestEmptySearchDelegate extends SearchDelegate<String> {
  _TestEmptySearchDelegate({
    this.suggestions = 'Suggestions',
    this.result = 'Result',
  }) : super();

  final String suggestions;
  final String result;

  @override
  Widget? buildLeading(BuildContext context) => null;

  @override
  List<Widget>? buildActions(BuildContext context) => null;

  @override
  Widget buildSuggestions(BuildContext context) {
    return MaterialButton(
      onPressed: () {
        showResults(context);
      },
      child: Text(suggestions),
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    return const Text('Results');
  }

  @override
  PreferredSizeWidget buildBottom(BuildContext context) {
    return PreferredSize(
      preferredSize: const Size.fromHeight(56.0),
      child: IconButton(
        tooltip: 'Close',
        icon: const Icon(Icons.arrow_back),
        onPressed: () {
          close(context, result);
        },
      ),
    );
  }
}
1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078

class _MyNavigatorObserver extends NavigatorObserver {
  int pushCount = 0;

  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    // don't count the root route
    if (<String>['nested', '/'].contains(route.settings.name)) {
      return;
    }
    pushCount++;
  }
}