modal_barrier_test.dart 15.7 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 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
7
import 'package:flutter/rendering.dart';
8
import 'package:flutter/services.dart';
9
import 'package:flutter/gestures.dart' show kSecondaryButton, PointerDeviceKind;
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,
58
      reason: 'because the tap is not prevented by ModalBarrier');
yjbanov's avatar
yjbanov committed
59 60
  });

61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
  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);

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

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

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

  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);
    expect(dragged, isTrue,
      reason: 'because the drag is prevented by ModalBarrier');
yjbanov's avatar
yjbanov committed
132 133
  });

134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
  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);
    expect(hovered, isTrue,
      reason: 'because the hover is prevented by ModalBarrier');
    hovered = false;

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

165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
  testWidgets('ModalBarrier plays system alert sound when user tries to dismiss it', (WidgetTester tester) async {
    final List<String> playedSystemSounds = <String>[];
    try {
      SystemChannels.platform.setMockMethodCallHandler((MethodCall methodCall) async {
        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);
182
      await tester.tap(find.text('target'), warnIfMissed: false);
183 184 185 186 187 188 189 190
      await tester.pumpWidget(subject);
    } finally {
      SystemChannels.platform.setMockMethodCallHandler(null);
    }
    expect(playedSystemSounds, hasLength(1));
    expect(playedSystemSounds[0], SystemSoundType.alert.toString());
  });

191
  testWidgets('ModalBarrier pops the Navigator when dismissed by primary tap', (WidgetTester tester) async {
192
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
193 194
      '/': (BuildContext context) => const FirstWidget(),
      '/modal': (BuildContext context) => const SecondWidget(),
195
    };
yjbanov's avatar
yjbanov committed
196

197
    await tester.pumpWidget(MaterialApp(routes: routes));
yjbanov's avatar
yjbanov committed
198

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

202
    // Tapping on X routes to the barrier
203
    await tester.tap(find.text('X'));
204 205
    await tester.pump(); // begin transition
    await tester.pump(const Duration(seconds: 1)); // end transition
yjbanov's avatar
yjbanov committed
206

207 208 209 210 211 212
    // 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);
213

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

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

    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

237 238 239 240 241 242 243
    // 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);
244

245 246 247
    // Release the pointer; the barrier should be dismissed
    await gesture.up();
    await tester.pumpAndSettle(const Duration(seconds: 1)); // end transition
248
    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
249
      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 273 274 275 276
    };

    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

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

277 278 279
  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>{
280
      '/': (BuildContext context) => const FirstWidget(),
281 282
      '/modal': (BuildContext context) => Stack(
        children: <Widget>[
283
          const SecondWidget(),
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
          WillPopScope(
            child: const SizedBox(),
            onWillPop: () async {
              willPopCalled = true;
              return false;
            },
          ),
      ],),
    };

    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

    expect(find.byKey(const ValueKey<String>('barrier')), findsOneWidget,
      reason: 'The route should still be present if the pop is vetoed.');

    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>{
320
      '/': (BuildContext context) => const FirstWidget(),
321 322
      '/modal': (BuildContext context) => Stack(
        children: <Widget>[
323
          const SecondWidget(),
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 351 352 353 354 355 356
          WillPopScope(
            child: const SizedBox(),
            onWillPop: () async {
              willPopCalled = true;
              return true;
            },
          ),
        ],),
    };

    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

    expect(find.byKey(const ValueKey<String>('barrier')), findsNothing,
      reason: 'The route should not be present if the pop is permitted.');

    expect(willPopCalled, isTrue);
  });

357
  testWidgets('Undismissible ModalBarrier hidden in semantic tree', (WidgetTester tester) async {
358
    final SemanticsTester semantics = SemanticsTester(tester);
359 360
    await tester.pumpWidget(const ModalBarrier(dismissible: false));

361
    final TestSemantics expectedSemantics = TestSemantics.root();
362 363 364 365 366
    expect(semantics, hasSemantics(expectedSemantics));

    semantics.dispose();
  });

367
  testWidgets('Dismissible ModalBarrier includes button in semantic tree on iOS', (WidgetTester tester) async {
368
    final SemanticsTester semantics = SemanticsTester(tester);
369 370
    await tester.pumpWidget(const Directionality(
      textDirection: TextDirection.ltr,
371
      child: ModalBarrier(
372 373 374 375
        dismissible: true,
        semanticsLabel: 'Dismiss',
      ),
    ));
376

377
    final TestSemantics expectedSemantics = TestSemantics.root(
378
      children: <TestSemantics>[
379
        TestSemantics.rootChild(
380
          rect: TestSemantics.fullScreen,
381 382 383
          actions: SemanticsAction.tap.index,
          label: 'Dismiss',
          textDirection: TextDirection.ltr,
384
        ),
385
      ],
386
    );
387
    expect(semantics, hasSemantics(expectedSemantics, ignoreId: true));
388

389
    semantics.dispose();
Dan Field's avatar
Dan Field committed
390
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));
391 392

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

396
    final TestSemantics expectedSemantics = TestSemantics.root();
397 398
    expect(semantics, hasSemantics(expectedSemantics));

399 400
    semantics.dispose();
  });
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416

  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();

417
    expect(RendererBinding.instance!.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic);
418
  });
yjbanov's avatar
yjbanov committed
419 420
}

421
class FirstWidget extends StatelessWidget {
422
  const FirstWidget({ Key? key }) : super(key: key);
423
  @override
yjbanov's avatar
yjbanov committed
424
  Widget build(BuildContext context) {
425 426 427 428
    return GestureDetector(
      onTap: () {
        Navigator.pushNamed(context, '/modal');
      },
429
      child: const Text('X'),
430
    );
yjbanov's avatar
yjbanov committed
431 432 433
  }
}

434
class SecondWidget extends StatelessWidget {
435
  const SecondWidget({ Key? key }) : super(key: key);
436
  @override
yjbanov's avatar
yjbanov committed
437
  Widget build(BuildContext context) {
438 439 440 441
    return const ModalBarrier(
      key: ValueKey<String>('barrier'),
      dismissible: true,
    );
yjbanov's avatar
yjbanov committed
442 443
  }
}
444 445

class SecondWidgetWithCompetence extends StatelessWidget {
446
  const SecondWidgetWithCompetence({ Key? key }) : super(key: key);
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        const ModalBarrier(
          key: ValueKey<String>('barrier'),
          dismissible: true,
        ),
        GestureDetector(
          onVerticalDragStart: (_) {},
          behavior: HitTestBehavior.translucent,
          child: Container(),
        )
      ],
    );
  }
}