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

import 'package:flutter/material.dart';
6
import 'package:flutter_test/flutter_test.dart';
7 8 9 10

bool willPopValue = false;

class SamplePage extends StatefulWidget {
11
  const SamplePage({ super.key });
12
  @override
13
  SamplePageState createState() => SamplePageState();
14 15 16
}

class SamplePageState extends State<SamplePage> {
17
  ModalRoute<void>? _route;
18 19 20

  Future<bool> _callback() async => willPopValue;

21
  @override
22 23
  void didChangeDependencies() {
    super.didChangeDependencies();
24 25 26 27 28 29 30 31 32
    _route?.removeScopedWillPopCallback(_callback);
    _route = ModalRoute.of(context);
    _route?.addScopedWillPopCallback(_callback);
  }

  @override
  void dispose() {
    super.dispose();
    _route?.removeScopedWillPopCallback(_callback);
33 34 35 36
  }

  @override
  Widget build(BuildContext context) {
37 38
    return Scaffold(
      appBar: AppBar(title: const Text('Sample Page')),
39 40 41 42 43 44 45
    );
  }
}

int willPopCount = 0;

class SampleForm extends StatelessWidget {
46
  const SampleForm({ super.key, required this.callback });
47 48 49 50 51

  final WillPopCallback callback;

  @override
  Widget build(BuildContext context) {
52 53 54 55
    return Scaffold(
      appBar: AppBar(title: const Text('Sample Form')),
      body: SizedBox.expand(
        child: Form(
56 57 58 59
          onWillPop: () {
            willPopCount += 1;
            return callback();
          },
60
          child: const TextField(),
61 62 63 64 65 66
        ),
      ),
    );
  }
}

67
// Expose the protected hasScopedWillPopCallback getter
68 69
class _TestPageRoute<T> extends MaterialPageRoute<T> {
  _TestPageRoute({
70 71 72
    super.settings,
    required super.builder,
  }) : super(maintainState: true);
73 74 75 76

  bool get hasCallback => super.hasScopedWillPopCallback;
}

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
class _TestPage extends Page<dynamic> {
  _TestPage({
    required this.builder,
    required LocalKey key,
  })  : _key = GlobalKey(),
        super(key: key);

  final WidgetBuilder builder;
  final GlobalKey<dynamic> _key;

  @override
  Route<dynamic> createRoute(BuildContext context) {
    return _TestPageRoute<dynamic>(
        settings: this,
        builder: (BuildContext context) {
          // keep state during move to another location in tree
          return KeyedSubtree(key: _key, child: builder.call(context));
        });
  }
}
97

98 99 100
void main() {
  testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async {
    await tester.pumpWidget(
101 102 103 104
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
105
            builder: (BuildContext context) {
106
              return Center(
107
                child: TextButton(
108
                  child: const Text('X'),
109
                  onPressed: () {
110
                    showDialog<void>(
111
                      context: context,
112
                      builder: (BuildContext context) => const SamplePage(),
113 114 115 116 117 118 119 120 121 122
                    );
                  },
                ),
              );
            },
          ),
        ),
      ),
    );

123 124 125
    expect(find.byTooltip('Back'), findsNothing);
    expect(find.text('Sample Page'), findsNothing);

126 127 128 129 130 131 132 133 134 135 136 137 138
    await tester.tap(find.text('X'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(find.text('Sample Page'), findsOneWidget);

    willPopValue = false;
    await tester.tap(find.byTooltip('Back'));
    await tester.pump();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    expect(find.text('Sample Page'), findsOneWidget);

139
    // Use didPopRoute() to simulate the system back button. Check that
140
    // didPopRoute() indicates that the notification was handled.
141
    final dynamic widgetsAppState = tester.state(find.byType(WidgetsApp));
142
    // ignore: avoid_dynamic_calls
143 144 145
    expect(await widgetsAppState.didPopRoute(), isTrue);
    expect(find.text('Sample Page'), findsOneWidget);

146 147 148 149 150 151 152 153
    willPopValue = true;
    await tester.tap(find.byTooltip('Back'));
    await tester.pump();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    expect(find.text('Sample Page'), findsNothing);
  });

154 155 156 157 158 159 160 161
  testWidgets('willPop will only pop if the callback returns true', (WidgetTester tester) async {
    Widget buildFrame() {
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
            builder: (BuildContext context) {
              return Center(
162
                child: TextButton(
163 164
                  child: const Text('X'),
                  onPressed: () {
165
                    Navigator.of(context).push(MaterialPageRoute<void>(
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
                      builder: (BuildContext context) {
                        return SampleForm(
                          callback: () => Future<bool>.value(willPopValue),
                        );
                      },
                    ));
                  },
                ),
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());
    await tester.tap(find.text('X'));
    await tester.pumpAndSettle();
    expect(find.text('Sample Form'), findsOneWidget);

    // Should pop if callback returns true
    willPopValue = true;
    await tester.tap(find.byTooltip('Back'));
    await tester.pumpAndSettle();
    expect(find.text('Sample Form'), findsNothing);
  });

193 194
  testWidgets('Form.willPop can inhibit back button', (WidgetTester tester) async {
    Widget buildFrame() {
195 196 197 198
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
199
            builder: (BuildContext context) {
200
              return Center(
201
                child: TextButton(
202
                  child: const Text('X'),
203
                  onPressed: () {
204
                    Navigator.of(context).push(MaterialPageRoute<void>(
205
                      builder: (BuildContext context) {
206 207
                        return SampleForm(
                          callback: () => Future<bool>.value(willPopValue),
208
                        );
209
                      },
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
                    ));
                  },
                ),
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());

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

    expect(find.text('Sample Form'), findsOneWidget);

    willPopValue = false;
    willPopCount = 0;
    await tester.tap(find.byTooltip('Back'));
    await tester.pump(); // Start the pop "back" operation.
    await tester.pump(); // Complete the willPop() Future.
    await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
    expect(find.text('Sample Form'), findsOneWidget);
    expect(willPopCount, 1);

    willPopValue = true;
    willPopCount = 0;
    await tester.tap(find.byTooltip('Back'));
    await tester.pump(); // Start the pop "back" operation.
    await tester.pump(); // Complete the willPop() Future.
    await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
    expect(find.text('Sample Form'), findsNothing);
    expect(willPopCount, 1);
  });

  testWidgets('Form.willPop callbacks do not accumulate', (WidgetTester tester) async {
248 249
    Future<bool> showYesNoAlert(BuildContext context) async {
      return (await showDialog<bool>(
250
        context: context,
251
        builder: (BuildContext context) {
252
          return AlertDialog(
253
            actions: <Widget> [
254
              TextButton(
255
                child: const Text('YES'),
256
                onPressed: () { Navigator.of(context).pop(true); },
257
              ),
258
              TextButton(
259
                child: const Text('NO'),
260
                onPressed: () { Navigator.of(context).pop(false); },
261 262 263 264
              ),
            ],
          );
        },
265
      ))!;
266 267 268
    }

    Widget buildFrame() {
269 270 271 272
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
273
            builder: (BuildContext context) {
274
              return Center(
275
                child: TextButton(
276
                  child: const Text('X'),
277
                  onPressed: () {
278
                    Navigator.of(context).push(MaterialPageRoute<void>(
279
                      builder: (BuildContext context) {
280
                        return SampleForm(
281 282
                          callback: () => showYesNoAlert(context),
                        );
283
                      },
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
                    ));
                  },
                ),
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());

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

    expect(find.text('Sample Form'), findsOneWidget);

    // Press the Scaffold's back button. This causes the willPop callback
    // to run, which shows the YES/NO Alert Dialog. Veto the back operation
    // by pressing the Alert's NO button.
    await tester.tap(find.byTooltip('Back'));
    await tester.pump(); // Start the pop "back" operation.
    await tester.pump(); // Call willPop which will show an Alert.
    await tester.tap(find.text('NO'));
    await tester.pump(); // Start the dismiss animation.
    await tester.pump(); // Resolve the willPop callback.
    await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
    expect(find.text('Sample Form'), findsOneWidget);

Ian Hickson's avatar
Ian Hickson committed
314 315 316 317
    // Do it again.
    // Each time the Alert is shown and dismissed the FormState's
    // didChangeDependencies() method runs. We're making sure that the
    // didChangeDependencies() method doesn't add an extra willPop callback.
318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
    await tester.tap(find.byTooltip('Back'));
    await tester.pump(); // Start the pop "back" operation.
    await tester.pump(); // Call willPop which will show an Alert.
    await tester.tap(find.text('NO'));
    await tester.pump(); // Start the dismiss animation.
    await tester.pump(); // Resolve the willPop callback.
    await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
    expect(find.text('Sample Form'), findsOneWidget);

    // This time really dismiss the SampleForm by pressing the Alert's
    // YES button.
    await tester.tap(find.byTooltip('Back'));
    await tester.pump(); // Start the pop "back" operation.
    await tester.pump(); // Call willPop which will show an Alert.
    await tester.tap(find.text('YES'));
    await tester.pump(); // Start the dismiss animation.
    await tester.pump(); // Resolve the willPop callback.
    await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
    expect(find.text('Sample Form'), findsNothing);
  });

339
  testWidgets('Route.scopedWillPop callbacks do not accumulate', (WidgetTester tester) async {
340
    late StateSetter contentsSetState; // call this to rebuild the route's SampleForm contents
341 342
    bool contentsEmpty = false; // when true, don't include the SampleForm in the route

343
    final _TestPageRoute<void> route = _TestPageRoute<void>(
344
      builder: (BuildContext context) {
345
        return StatefulBuilder(
346 347
          builder: (BuildContext context, StateSetter setState) {
            contentsSetState = setState;
348
            return contentsEmpty ? Container() : SampleForm(key: UniqueKey(), callback: () async => false);
349
          },
350 351 352 353 354
        );
      },
    );

    Widget buildFrame() {
355 356 357 358
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
359
            builder: (BuildContext context) {
360
              return Center(
361
                child: TextButton(
362
                  child: const Text('X'),
363
                  onPressed: () {
364
                    Navigator.of(context).push(route);
365 366 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
                  },
                ),
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());

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

    expect(find.text('Sample Form'), findsOneWidget);
    expect(route.hasCallback, isTrue);

    // Rebuild the route's SampleForm child an additional 3x for good measure.
    contentsSetState(() { });
    await tester.pump();
    contentsSetState(() { });
    await tester.pump();
    contentsSetState(() { });
    await tester.pump();

    // Now build the route's contents without the sample form.
    contentsEmpty = true;
    contentsSetState(() { });
    await tester.pump();

    expect(route.hasCallback, isFalse);
  });
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 454

  testWidgets('should handle new route if page moved from one navigator to another', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/89133
    late StateSetter contentsSetState;
    bool moveToAnotherNavigator = false;

    final List<Page<dynamic>> pages = <Page<dynamic>>[
      _TestPage(
        key: UniqueKey(),
        builder: (BuildContext context) {
          return WillPopScope(
            onWillPop: () async => true,
            child: const Text('anchor'),
          );
        },
      )
    ];

    Widget _buildNavigator(Key? key, List<Page<dynamic>> pages) {
      return Navigator(
        key: key,
        pages: pages,
        onPopPage: (Route<dynamic> route, dynamic result) {
          return route.didPop(result);
        },
      );
    }

    Widget buildFrame() {
      return MaterialApp(
        home: Scaffold(
          body: StatefulBuilder(
            builder: (BuildContext context, StateSetter setState) {
              contentsSetState = setState;
              if (moveToAnotherNavigator) {
                return _buildNavigator(const ValueKey<int>(1), pages);
              }
              return _buildNavigator(const ValueKey<int>(2), pages);
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(buildFrame());
    await tester.pump();
    final _TestPageRoute<dynamic> route1 = ModalRoute.of(tester.element(find.text('anchor')))! as _TestPageRoute<dynamic>;
    expect(route1.hasCallback, isTrue);
    moveToAnotherNavigator = true;
    contentsSetState(() {});

    await tester.pump();
    final _TestPageRoute<dynamic> route2 = ModalRoute.of(tester.element(find.text('anchor')))! as _TestPageRoute<dynamic>;

    expect(route1.hasCallback, isFalse);
    expect(route2.hasCallback, isTrue);
  });
455
}