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

5
import 'package:flutter/gestures.dart' show kSecondaryButton, PointerDeviceKind;
yjbanov's avatar
yjbanov committed
6
import 'package:flutter/material.dart';
7
import 'package:flutter/rendering.dart';
8
import 'package:flutter/services.dart';
9
import 'package:flutter_test/flutter_test.dart';
yjbanov's avatar
yjbanov committed
10

11 12
import 'semantics_tester.dart';

yjbanov's avatar
yjbanov committed
13
void main() {
14 15 16 17
  late bool tapped;
  late bool hovered;
  late Widget tapTarget;
  late Widget hoverTarget;
yjbanov's avatar
yjbanov committed
18 19 20

  setUp(() {
    tapped = false;
21
    tapTarget = GestureDetector(
yjbanov's avatar
yjbanov committed
22 23 24
      onTap: () {
        tapped = true;
      },
25
      child: const SizedBox(
yjbanov's avatar
yjbanov committed
26 27
        width: 10.0,
        height: 10.0,
28 29
        child: Text('target', textDirection: TextDirection.ltr),
      ),
yjbanov's avatar
yjbanov committed
30
    );
31 32 33 34 35 36 37 38 39 40 41 42

    hovered = false;
    hoverTarget = MouseRegion(
      onHover: (_) { hovered = true; },
      onEnter: (_) { hovered = true; },
      onExit: (_) { hovered = true; },
      child: const SizedBox(
        width: 10.0,
        height: 10.0,
        child: Text('target', textDirection: TextDirection.ltr),
      ),
    );
yjbanov's avatar
yjbanov committed
43 44
  });

45
  testWidgets('ModalBarrier prevents interactions with widgets behind it', (WidgetTester tester) async {
46
    final Widget subject = Stack(
47
      textDirection: TextDirection.ltr,
48 49
      children: <Widget>[
        tapTarget,
50
        const ModalBarrier(dismissible: false),
51
      ],
52
    );
yjbanov's avatar
yjbanov committed
53

54
    await tester.pumpWidget(subject);
55
    await tester.tap(find.text('target'), warnIfMissed: false);
56
    await tester.pumpWidget(subject);
57
    expect(tapped, isFalse, reason: 'because the tap is not prevented by ModalBarrier');
yjbanov's avatar
yjbanov committed
58 59
  });

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
  testWidgets('ModalBarrier prevents hover interactions with widgets behind it', (WidgetTester tester) async {
    final Widget subject = Stack(
      textDirection: TextDirection.ltr,
      children: <Widget>[
        hoverTarget,
        const ModalBarrier(dismissible: false),
      ],
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(gesture.removePointer);
    // Start out of hoverTarget
    await gesture.moveTo(const Offset(100, 100));

    await tester.pumpWidget(subject);
    // Move into hoverTarget and tap
    await gesture.down(const Offset(5, 5));
    await tester.pumpWidget(subject);
    await gesture.up();
    await tester.pumpWidget(subject);

    // Move out
    await gesture.moveTo(const Offset(100, 100));
    await tester.pumpWidget(subject);

85
    expect(hovered, isFalse, reason: 'because the hover is not prevented by ModalBarrier');
86 87
  });

88
  testWidgets('ModalBarrier does not prevent interactions with widgets in front of it', (WidgetTester tester) async {
89
    final Widget subject = Stack(
90
      textDirection: TextDirection.ltr,
91
      children: <Widget>[
92
        const ModalBarrier(dismissible: false),
93
        tapTarget,
94
      ],
95
    );
yjbanov's avatar
yjbanov committed
96

97 98 99
    await tester.pumpWidget(subject);
    await tester.tap(find.text('target'));
    await tester.pumpWidget(subject);
100
    expect(tapped, isTrue, reason: 'because the tap is prevented by ModalBarrier');
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
  });

  testWidgets('ModalBarrier does not prevent interactions with translucent widgets in front of it', (WidgetTester tester) async {
    bool dragged = false;
    final Widget subject = Stack(
      textDirection: TextDirection.ltr,
      children: <Widget>[
        const ModalBarrier(dismissible: false),
        GestureDetector(
          behavior: HitTestBehavior.translucent,
          onHorizontalDragStart: (_) {
            dragged = true;
          },
          child: const Center(
            child: Text('target', textDirection: TextDirection.ltr),
          ),
        ),
      ],
    );

    await tester.pumpWidget(subject);
    await tester.dragFrom(
      tester.getBottomRight(find.byType(GestureDetector)) - const Offset(10, 10),
      const Offset(-20, 0),
    );
    await tester.pumpWidget(subject);
127
    expect(dragged, isTrue, reason: 'because the drag is prevented by ModalBarrier');
yjbanov's avatar
yjbanov committed
128 129
  });

130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
  testWidgets('ModalBarrier does not prevent hover interactions with widgets in front of it', (WidgetTester tester) async {
    final Widget subject = Stack(
      textDirection: TextDirection.ltr,
      children: <Widget>[
        const ModalBarrier(dismissible: false),
        hoverTarget,
      ],
    );

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
    addTearDown(gesture.removePointer);
    // Start out of hoverTarget
    await gesture.moveTo(const Offset(100, 100));
    await tester.pumpWidget(subject);
    expect(hovered, isFalse);

    // Move into hoverTarget
    await gesture.moveTo(const Offset(5, 5));
    await tester.pumpWidget(subject);
149
    expect(hovered, isTrue, reason: 'because the hover is prevented by ModalBarrier');
150 151 152 153 154
    hovered = false;

    // Move out
    await gesture.moveTo(const Offset(100, 100));
    await tester.pumpWidget(subject);
155
    expect(hovered, isTrue, reason: 'because the hover is prevented by ModalBarrier');
156 157 158
    hovered = false;
  });

159 160 161
  testWidgets('ModalBarrier plays system alert sound when user tries to dismiss it', (WidgetTester tester) async {
    final List<String> playedSystemSounds = <String>[];
    try {
162
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
163 164 165 166 167 168 169 170 171 172 173 174 175
        if (methodCall.method == 'SystemSound.play')
          playedSystemSounds.add(methodCall.arguments as String);
      });

      final Widget subject = Stack(
        textDirection: TextDirection.ltr,
        children: <Widget>[
          tapTarget,
          const ModalBarrier(dismissible: false),
        ],
      );

      await tester.pumpWidget(subject);
176
      await tester.tap(find.text('target'), warnIfMissed: false);
177 178
      await tester.pumpWidget(subject);
    } finally {
179
      tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, null);
180 181 182 183 184
    }
    expect(playedSystemSounds, hasLength(1));
    expect(playedSystemSounds[0], SystemSoundType.alert.toString());
  });

185
  testWidgets('ModalBarrier pops the Navigator when dismissed by primary tap', (WidgetTester tester) async {
186
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
187 188
      '/': (BuildContext context) => const FirstWidget(),
      '/modal': (BuildContext context) => const SecondWidget(),
189
    };
yjbanov's avatar
yjbanov committed
190

191
    await tester.pumpWidget(MaterialApp(routes: routes));
yjbanov's avatar
yjbanov committed
192

193 194
    // Initially the barrier is not visible
    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);
yjbanov's avatar
yjbanov committed
195

196
    // Tapping on X routes to the barrier
197
    await tester.tap(find.text('X'));
198 199
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition
yjbanov's avatar
yjbanov committed
200

201 202 203 204 205 206
    // Press the barrier; it shouldn't dismiss yet
    final TestGesture gesture = await tester.press(
      find.byKey(const ValueKey<String>('barrier')),
    );
    await tester.pumpAndSettle(); // begin transition
    expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget);
207

208 209 210
    // Release the pointer; the barrier should be dismissed
    await gesture.up();
    await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition
211 212 213 214 215
    expect(
      find.byKey(const ValueKey<String>('barrier')),
      findsNothing,
      reason: 'The route should have been dismissed by tapping the barrier.',
    );
216 217
  });

218
  testWidgets('ModalBarrier pops the Navigator when dismissed by non-primary tap', (WidgetTester tester) async {
219
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
220 221
      '/': (BuildContext context) => const FirstWidget(),
      '/modal': (BuildContext context) => const SecondWidget(),
222 223 224 225 226 227 228 229 230 231 232 233
    };

    await tester.pumpWidget(MaterialApp(routes: routes));

    // Initially the barrier is not visible
    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);

    // Tapping on X routes to the barrier
    await tester.tap(find.text('X'));
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition

234 235 236 237 238 239 240
    // Press the barrier; it shouldn't dismiss yet
    final TestGesture gesture = await tester.press(
      find.byKey(const ValueKey<String>('barrier')),
      buttons: kSecondaryButton,
    );
    await tester.pumpAndSettle(); // begin transition
    expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget);
241

242 243 244
    // Release the pointer; the barrier should be dismissed
    await gesture.up();
    await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition
245 246 247 248 249
    expect(
      find.byKey(const ValueKey<String>('barrier')),
      findsNothing,
      reason: 'The route should have been dismissed by tapping the barrier.',
    );
yjbanov's avatar
yjbanov committed
250
  });
251

252 253
  testWidgets('ModalBarrier may pop the Navigator when competing with other gestures', (WidgetTester tester) async {
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
254 255
      '/': (BuildContext context) => const FirstWidget(),
      '/modal': (BuildContext context) => const SecondWidgetWithCompetence(),
256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
    };

    await tester.pumpWidget(MaterialApp(routes: routes));

    // Initially the barrier is not visible
    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);

    // Tapping on X routes to the barrier
    await tester.tap(find.text('X'));
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition

    // Tap on the barrier to dismiss it
    await tester.tap(find.byKey(const ValueKey<String>('barrier')));
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition

273 274 275 276 277
    expect(
      find.byKey(const ValueKey<String>('barrier')),
      findsNothing,
      reason: 'The route should have been dismissed by tapping the barrier.',
    );
278 279
  });

280 281 282
  testWidgets('ModalBarrier does not pop the Navigator with a WillPopScope that returns false', (WidgetTester tester) async {
    bool willPopCalled = false;
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
283
      '/': (BuildContext context) => const FirstWidget(),
284 285
      '/modal': (BuildContext context) => Stack(
        children: <Widget>[
286
          const SecondWidget(),
287 288 289 290 291 292 293
          WillPopScope(
            child: const SizedBox(),
            onWillPop: () async {
              willPopCalled = true;
              return false;
            },
          ),
294 295
        ],
      ),
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
    };

    await tester.pumpWidget(MaterialApp(routes: routes));

    // Initially the barrier is not visible
    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);

    // Tapping on X routes to the barrier
    await tester.tap(find.text('X'));
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition

    expect(willPopCalled, isFalse);

    // Tap on the barrier to attempt to dismiss it
    await tester.tap(find.byKey(const ValueKey<String>('barrier')));
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition

315 316 317 318 319
    expect(
      find.byKey(const ValueKey<String>('barrier')),
      findsOneWidget,
      reason: 'The route should still be present if the pop is vetoed.',
    );
320 321 322 323 324 325 326

    expect(willPopCalled, isTrue);
  });

  testWidgets('ModalBarrier pops the Navigator with a WillPopScope that returns true', (WidgetTester tester) async {
    bool willPopCalled = false;
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
327
      '/': (BuildContext context) => const FirstWidget(),
328 329
      '/modal': (BuildContext context) => Stack(
        children: <Widget>[
330
          const SecondWidget(),
331 332 333 334 335 336 337
          WillPopScope(
            child: const SizedBox(),
            onWillPop: () async {
              willPopCalled = true;
              return true;
            },
          ),
338 339
        ],
      ),
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358
    };

    await tester.pumpWidget(MaterialApp(routes: routes));

    // Initially the barrier is not visible
    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing);

    // Tapping on X routes to the barrier
    await tester.tap(find.text('X'));
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition

    expect(willPopCalled, isFalse);

    // Tap on the barrier to attempt to dismiss it
    await tester.tap(find.byKey(const ValueKey<String>('barrier')));
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition

359 360 361 362 363
    expect(
      find.byKey(const ValueKey<String>('barrier')),
      findsNothing,
      reason: 'The route should not be present if the pop is permitted.',
    );
364 365 366 367

    expect(willPopCalled, isTrue);
  });

368
  testWidgets('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async {
369
    final SemanticsTester semantics = SemanticsTester(tester);
370 371
    await tester.pumpWidget(const ModalBarrier(dismissible: false));

372
    final TestSemantics expectedSemantics = TestSemantics.root();
373 374 375 376 377
    expect(semantics, hasSemantics(expectedSemantics));

    semantics.dispose();
  });

378
  testWidgets('Dismissible ModalBarrier includes button in semantic tree on iOS', (WidgetTester tester) async {
379
    final SemanticsTester semantics = SemanticsTester(tester);
380 381
    await tester.pumpWidget(const Directionality(
      textDirection: TextDirection.ltr,
382
      child: ModalBarrier(
383 384 385 386
        dismissible: true,
        semanticsLabel: 'Dismiss',
      ),
    ));
387

388
    final TestSemantics expectedSemantics = TestSemantics.root(
389
      children: <TestSemantics>[
390
        TestSemantics.rootChild(
391
          rect: TestSemantics.fullScreen,
392
          actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.dismiss],
393 394
          label: 'Dismiss',
          textDirection: TextDirection.ltr,
395
        ),
396
      ],
397
    );
398
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
399

400
    semantics.dispose();
Dan Field's avatar
Dan Field committed
401
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
402 403

  testWidgets('Dismissible ModalBarrier is hidden on Android (back button is used to dismiss)', (WidgetTester tester) async {
404
    final SemanticsTester semantics = SemanticsTester(tester);
405 406
    await tester.pumpWidget(const ModalBarrier(dismissible: true));

407
    final TestSemantics expectedSemantics = TestSemantics.root();
408 409
    expect(semantics, hasSemantics(expectedSemantics));

410 411
    semantics.dispose();
  });
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427

  testWidgets('ModalBarrier uses default mouse cursor', (WidgetTester tester) async {
    await tester.pumpWidget(Stack(
      textDirection: TextDirection.ltr,
      children: const <Widget>[
        MouseRegion(cursor: SystemMouseCursors.click),
        ModalBarrier(dismissible: false),
      ],
    ));

    final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1);
    await gesture.addPointer(location: tester.getCenter(find.byType(ModalBarrier)));
    addTearDown(gesture.removePointer);

    await tester.pump();

428
    expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
429
  });
yjbanov's avatar
yjbanov committed
430 431
}

432
class FirstWidget extends StatelessWidget {
433
  const FirstWidget({ Key? key }) : super(key: key);
434
  @override
yjbanov's avatar
yjbanov committed
435
  Widget build(BuildContext context) {
436 437 438 439
    return GestureDetector(
      onTap: () {
        Navigator.pushNamed(context, '/modal');
      },
440
      child: const Text('X'),
441
    );
yjbanov's avatar
yjbanov committed
442 443 444
  }
}

445
class SecondWidget extends StatelessWidget {
446
  const SecondWidget({ Key? key }) : super(key: key);
447
  @override
yjbanov's avatar
yjbanov committed
448
  Widget build(BuildContext context) {
449 450 451 452
    return const ModalBarrier(
      key: ValueKey<String>('barrier'),
      dismissible: true,
    );
yjbanov's avatar
yjbanov committed
453 454
  }
}
455 456

class SecondWidgetWithCompetence extends StatelessWidget {
457
  const SecondWidgetWithCompetence({ Key? key }) : super(key: key);
458 459 460 461 462 463 464 465 466 467 468 469
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        const ModalBarrier(
          key: ValueKey<String>('barrier'),
          dismissible: true,
        ),
        GestureDetector(
          onVerticalDragStart: (_) {},
          behavior: HitTestBehavior.translucent,
          child: Container(),
470
        ),
471 472 473 474
      ],
    );
  }
}