bottom_sheet_test.dart 14.7 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
@TestOn('!chrome') // flaky on CI
Adam Barth's avatar
Adam Barth committed
6
import 'package:flutter_test/flutter_test.dart';
7 8
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
9
import 'package:flutter/gestures.dart';
10

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

13
void main() {
14
  testWidgets('Tapping on a modal BottomSheet should not dismiss it', (WidgetTester tester) async {
15 16
    BuildContext savedContext;

17 18
    await tester.pumpWidget(MaterialApp(
      home: Builder(
19 20
        builder: (BuildContext context) {
          savedContext = context;
21
          return Container();
22
        }
23
      ),
24 25
    ));

26
    await tester.pump();
27 28
    expect(find.text('BottomSheet'), findsNothing);

29
    bool showBottomSheetThenCalled = false;
30
    showModalBottomSheet<void>(
31
      context: savedContext,
32
      builder: (BuildContext context) => const Text('BottomSheet'),
33
    ).then<void>((void value) {
34
      showBottomSheetThenCalled = true;
35
    });
36

37
    await tester.pumpAndSettle();
38 39 40
    expect(find.text('BottomSheet'), findsOneWidget);
    expect(showBottomSheetThenCalled, isFalse);

41
    // Tap on the bottom sheet itself, it should not be dismissed
42
    await tester.tap(find.text('BottomSheet'));
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
    await tester.pumpAndSettle();
    expect(find.text('BottomSheet'), findsOneWidget);
    expect(showBottomSheetThenCalled, isFalse);
  });

  testWidgets('Tapping outside a modal BottomSheet should dismiss it', (WidgetTester tester) async {
    BuildContext savedContext;

    await tester.pumpWidget(MaterialApp(
      home: Builder(
          builder: (BuildContext context) {
            savedContext = context;
            return Container();
          }
      ),
    ));

    await tester.pump();
61 62
    expect(find.text('BottomSheet'), findsNothing);

63
    bool showBottomSheetThenCalled = false;
64
    showModalBottomSheet<void>(
65
      context: savedContext,
66
      builder: (BuildContext context) => const Text('BottomSheet'),
67
    ).then<void>((void value) {
68 69
      showBottomSheetThenCalled = true;
    });
70 71

    await tester.pumpAndSettle();
72
    expect(find.text('BottomSheet'), findsOneWidget);
73
    expect(showBottomSheetThenCalled, isFalse);
74

75
    // Tap above the bottom sheet to dismiss it
76
    await tester.tapAt(const Offset(20.0, 20.0));
77
    await tester.pump(); // bottom sheet dismiss animation starts
78
    expect(showBottomSheetThenCalled, isTrue);
79 80
    await tester.pump(const Duration(seconds: 1)); // animation done
    await tester.pump(const Duration(seconds: 1)); // rebuild frame
81 82
    expect(find.text('BottomSheet'), findsNothing);
  });
83

84
  testWidgets('Verify that a downwards fling dismisses a persistent BottomSheet', (WidgetTester tester) async {
85
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
86 87
    bool showBottomSheetThenCalled = false;

88 89
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
90
        key: scaffoldKey,
91 92
        body: const Center(child: Text('body')),
      ),
93 94 95 96 97
    ));

    expect(showBottomSheetThenCalled, isFalse);
    expect(find.text('BottomSheet'), findsNothing);

98
    scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) {
99
      return Container(
100
        margin: const EdgeInsets.all(40.0),
101
        child: const Text('BottomSheet'),
102
      );
103
    }).closed.whenComplete(() {
104 105
      showBottomSheetThenCalled = true;
    });
106

107 108
    expect(showBottomSheetThenCalled, isFalse);
    expect(find.text('BottomSheet'), findsNothing);
109

110
    await tester.pump(); // bottom sheet show animation starts
111

112 113
    expect(showBottomSheetThenCalled, isFalse);
    expect(find.text('BottomSheet'), findsOneWidget);
114

115
    await tester.pump(const Duration(seconds: 1)); // animation done
116

117 118
    expect(showBottomSheetThenCalled, isFalse);
    expect(find.text('BottomSheet'), findsOneWidget);
119

120 121 122 123 124
    // The fling below must be such that the velocity estimation examines an
    // offset greater than the kTouchSlop. Too slow or too short a distance, and
    // it won't trigger. Also, it musn't be so much that it drags the bottom
    // sheet off the screen, or we won't see it after we pump!
    await tester.fling(find.text('BottomSheet'), const Offset(0.0, 50.0), 2000.0);
125
    await tester.pump(); // drain the microtask queue (Future completion callback)
126

127 128
    expect(showBottomSheetThenCalled, isTrue);
    expect(find.text('BottomSheet'), findsOneWidget);
129

130
    await tester.pump(); // bottom sheet dismiss animation starts
131

132 133
    expect(showBottomSheetThenCalled, isTrue);
    expect(find.text('BottomSheet'), findsOneWidget);
134

135
    await tester.pump(const Duration(seconds: 1)); // animation done
136

137 138
    expect(showBottomSheetThenCalled, isTrue);
    expect(find.text('BottomSheet'), findsNothing);
139 140
  });

141 142
  testWidgets('Verify that dragging past the bottom dismisses a persistent BottomSheet', (WidgetTester tester) async {
    // This is a regression test for https://github.com/flutter/flutter/issues/5528
143
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
144

145 146
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
147
        key: scaffoldKey,
148 149
        body: const Center(child: Text('body')),
      ),
150 151
    ));

152
    scaffoldKey.currentState.showBottomSheet<void>((BuildContext context) {
153
      return Container(
154
        margin: const EdgeInsets.all(40.0),
155
        child: const Text('BottomSheet'),
156 157 158 159
      );
    });

    await tester.pump(); // bottom sheet show animation starts
160
    await tester.pump(const Duration(seconds: 1)); // animation done
161 162 163 164 165
    expect(find.text('BottomSheet'), findsOneWidget);

    await tester.fling(find.text('BottomSheet'), const Offset(0.0, 400.0), 1000.0);
    await tester.pump(); // drain the microtask queue (Future completion callback)
    await tester.pump(); // bottom sheet dismiss animation starts
166
    await tester.pump(const Duration(seconds: 1)); // animation done
167 168 169

    expect(find.text('BottomSheet'), findsNothing);
  });
170 171 172 173 174

  testWidgets('modal BottomSheet has no top MediaQuery', (WidgetTester tester) async {
    BuildContext outerContext;
    BuildContext innerContext;

175
    await tester.pumpWidget(Localizations(
176
      locale: const Locale('en', 'US'),
177
      delegates: const <LocalizationsDelegate<dynamic>>[
178 179 180
        DefaultWidgetsLocalizations.delegate,
        DefaultMaterialLocalizations.delegate,
      ],
181
      child: Directionality(
182
        textDirection: TextDirection.ltr,
183
        child: MediaQuery(
184
          data: const MediaQueryData(
185
            padding: EdgeInsets.all(50.0),
186
            size: Size(400.0, 600.0),
187
          ),
188
          child: Navigator(
189
            onGenerateRoute: (_) {
190
              return PageRouteBuilder<void>(
191 192
                pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
                  outerContext = context;
193
                  return Container();
194 195 196 197
                },
              );
            },
          ),
198 199 200 201
        ),
      ),
    ));

202
    showModalBottomSheet<void>(
203 204 205
      context: outerContext,
      builder: (BuildContext context) {
        innerContext = context;
206
        return Container();
207 208 209 210 211 212 213 214 215 216 217 218 219 220
      },
    );
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(
      MediaQuery.of(outerContext).padding,
      const EdgeInsets.all(50.0),
    );
    expect(
      MediaQuery.of(innerContext).padding,
      const EdgeInsets.only(left: 50.0, right: 50.0, bottom: 50.0),
    );
  });
221 222

  testWidgets('modal BottomSheet has semantics', (WidgetTester tester) async {
223 224
    final SemanticsTester semantics = SemanticsTester(tester);
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
225

226 227
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
228
        key: scaffoldKey,
229 230
        body: const Center(child: Text('body')),
      ),
231 232 233 234
    ));


    showModalBottomSheet<void>(context: scaffoldKey.currentContext, builder: (BuildContext context) {
235
      return Container(
236
        child: const Text('BottomSheet'),
237 238 239 240 241 242
      );
    });

    await tester.pump(); // bottom sheet show animation starts
    await tester.pump(const Duration(seconds: 1)); // animation done

243
    expect(semantics, hasSemantics(TestSemantics.root(
244
      children: <TestSemantics>[
245
        TestSemantics.rootChild(
246
          children: <TestSemantics>[
247
            TestSemantics(
248 249 250 251 252 253 254
              label: 'Dialog',
              textDirection: TextDirection.ltr,
              flags: <SemanticsFlag>[
                SemanticsFlag.scopesRoute,
                SemanticsFlag.namesRoute,
              ],
              children: <TestSemantics>[
255
                TestSemantics(
256 257 258 259 260 261 262 263 264 265 266
                  label: 'BottomSheet',
                  textDirection: TextDirection.ltr,
                ),
              ],
            ),
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });
267

268 269 270 271 272
  testWidgets('Verify that visual properties are passed through', (WidgetTester tester) async {
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
    const Color color = Colors.pink;
    const double elevation = 9.0;
    final ShapeBorder shape = BeveledRectangleBorder(borderRadius: BorderRadius.circular(12));
273
    const Clip clipBehavior = Clip.antiAlias;
274 275 276 277 278 279 280 281 282 283 284 285 286

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body')),
      ),
    ));

    showModalBottomSheet<void>(
      context: scaffoldKey.currentContext,
      backgroundColor: color,
      elevation: elevation,
      shape: shape,
287
      clipBehavior: clipBehavior,
288 289 290 291 292 293 294 295 296 297 298 299 300 301
      builder: (BuildContext context) {
        return Container(
          child: const Text('BottomSheet'),
        );
      },
    );

    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    final BottomSheet bottomSheet = tester.widget(find.byType(BottomSheet));
    expect(bottomSheet.backgroundColor, color);
    expect(bottomSheet.elevation, elevation);
    expect(bottomSheet.shape, shape);
302
    expect(bottomSheet.clipBehavior, clipBehavior);
303 304
  });

305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
  testWidgets('modal BottomSheet with scrollController has semantics', (WidgetTester tester) async {
    final SemanticsTester semantics = SemanticsTester(tester);
    final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();

    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        key: scaffoldKey,
        body: const Center(child: Text('body'))
      )
    ));


    showModalBottomSheet<void>(
      context: scaffoldKey.currentContext,
      builder: (BuildContext context) {
        return DraggableScrollableSheet(
          expand: false,
          builder: (_, ScrollController controller) {
            return SingleChildScrollView(
              controller: controller,
              child: Container(
                child: const Text('BottomSheet'),
              ),
            );
          },
        );
      },
    );

    await tester.pump(); // bottom sheet show animation starts
    await tester.pump(const Duration(seconds: 1)); // animation done

    expect(semantics, hasSemantics(TestSemantics.root(
      children: <TestSemantics>[
        TestSemantics.rootChild(
          children: <TestSemantics>[
            TestSemantics(
              label: 'Dialog',
              textDirection: TextDirection.ltr,
              flags: <SemanticsFlag>[
                SemanticsFlag.scopesRoute,
                SemanticsFlag.namesRoute,
              ],
              children: <TestSemantics>[
                TestSemantics(
                  flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
351
                  actions: <SemanticsAction>[SemanticsAction.scrollDown, SemanticsAction.scrollUp],
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
                  children: <TestSemantics>[
                    TestSemantics(
                      label: 'BottomSheet',
                      textDirection: TextDirection.ltr,
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ],
    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));
    semantics.dispose();
  });
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 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 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 445 446 447 448 449 450 451 452 453

  testWidgets('showModalBottomSheet does not use root Navigator by default', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(builder: (_) {
          return const _TestPage();
        })),
        bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.ac_unit),
              title: Text('Item 1'),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.style),
              title: Text('Item 2'),
            )
          ],
        ),
      ),
    ));

    await tester.tap(find.text('Show bottom sheet'));
    await tester.pumpAndSettle();

    // Bottom sheet is displayed in correct position within the inner navigator
    // and above the BottomNavigationBar.
    expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 544.0);
  });

  testWidgets('showModalBottomSheet uses root Navigator when specified', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: Scaffold(
        body: Navigator(onGenerateRoute: (RouteSettings settings) => MaterialPageRoute<void>(builder: (_) {
          return const _TestPage(useRootNavigator: true);
        })),
        bottomNavigationBar: BottomNavigationBar(
          items: const <BottomNavigationBarItem>[
            BottomNavigationBarItem(
              icon: Icon(Icons.ac_unit),
              title: Text('Item 1'),
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.style),
              title: Text('Item 2'),
            )
          ],
        ),
      ),
    ));

    await tester.tap(find.text('Show bottom sheet'));
    await tester.pumpAndSettle();

    // Bottom sheet is displayed in correct position above all content including
    // the BottomNavigationBar.
    expect(tester.getBottomLeft(find.byType(BottomSheet)).dy, 600.0);
  });
}

class _TestPage extends StatelessWidget {
  const _TestPage({Key key, this.useRootNavigator}) : super(key: key);

  final bool useRootNavigator;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: FlatButton(
        child: const Text('Show bottom sheet'),
        onPressed: () {
          if (useRootNavigator != null) {
            showModalBottomSheet<void>(
              useRootNavigator: useRootNavigator,
              context: context,
              builder: (_) => const Text('Modal bottom sheet'),
            );
          } else {
            showModalBottomSheet<void>(
              context: context,
              builder: (_) => const Text('Modal bottom sheet'),
            );
          }
        }
      ),
    );
  }
454
}