will_pop_test.dart 12 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 6
// @dart = 2.8

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

bool willPopValue = false;

class SamplePage extends StatefulWidget {
13
  const SamplePage({ Key key }) : super(key: key);
14
  @override
15
  SamplePageState createState() => SamplePageState();
16 17 18
}

class SamplePageState extends State<SamplePage> {
19
  ModalRoute<void> _route;
20 21 22

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

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

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

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

int willPopCount = 0;

class SampleForm extends StatelessWidget {
48
  const SampleForm({ Key key, this.callback }) : super(key: key);
49 50 51 52 53

  final WillPopCallback callback;

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

69 70 71
// Expose the protected hasScopedWillPopCallback getter
class TestPageRoute<T> extends MaterialPageRoute<T> {
  TestPageRoute({ WidgetBuilder builder })
72
    : super(builder: builder, maintainState: true);
73 74 75 76 77

  bool get hasCallback => super.hasScopedWillPopCallback;
}


78 79 80
void main() {
  testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async {
    await tester.pumpWidget(
81 82 83 84
      MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
85
            builder: (BuildContext context) {
86 87
              return Center(
                child: FlatButton(
88
                  child: const Text('X'),
89
                  onPressed: () {
90
                    showDialog<void>(
91
                      context: context,
92
                      builder: (BuildContext context) => const SamplePage(),
93 94 95 96 97 98 99 100 101 102
                    );
                  },
                ),
              );
            },
          ),
        ),
      ),
    );

103 104 105
    expect(find.byTooltip('Back'), findsNothing);
    expect(find.text('Sample Page'), findsNothing);

106 107 108 109 110 111 112 113 114 115 116 117 118
    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);

119
    // Use didPopRoute() to simulate the system back button. Check that
120 121 122 123 124
    // didPopRoute() indicates that the notification was handled.
    final dynamic widgetsAppState = tester.state(find.byType(WidgetsApp));
    expect(await widgetsAppState.didPopRoute(), isTrue);
    expect(find.text('Sample Page'), findsOneWidget);

125 126 127 128 129 130 131 132
    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);
  });

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 165 166 167 168 169 170 171 172 173 174 175 176 177
  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(
                child: FlatButton(
                  child: const Text('X'),
                  onPressed: () {
                    Navigator.of(context).push(MaterialPageRoute<void>(
                      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 not pop if callback returns null
    willPopValue = null;
    await tester.tap(find.byTooltip('Back'));
    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);
  });

178 179
  testWidgets('Form.willPop can inhibit back button', (WidgetTester tester) async {
    Widget buildFrame() {
180 181 182 183
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
184
            builder: (BuildContext context) {
185 186
              return Center(
                child: FlatButton(
187
                  child: const Text('X'),
188
                  onPressed: () {
189
                    Navigator.of(context).push(MaterialPageRoute<void>(
190
                      builder: (BuildContext context) {
191 192
                        return SampleForm(
                          callback: () => Future<bool>.value(willPopValue),
193
                        );
194
                      },
195 196 197 198 199 200 201 202 203 204 205 206 207 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
                    ));
                  },
                ),
              );
            },
          ),
        ),
      );
    }

    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 {
    Future<bool> showYesNoAlert(BuildContext context) {
234
      return showDialog<bool>(
235
        context: context,
236
        builder: (BuildContext context) {
237
          return AlertDialog(
238
            actions: <Widget> [
239
              FlatButton(
240 241 242
                child: const Text('YES'),
                onPressed: () { Navigator.of(context).pop(true); },
              ),
243
              FlatButton(
244 245 246 247 248 249
                child: const Text('NO'),
                onPressed: () { Navigator.of(context).pop(false); },
              ),
            ],
          );
        },
250 251 252 253
      );
    }

    Widget buildFrame() {
254 255 256 257
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
258
            builder: (BuildContext context) {
259 260
              return Center(
                child: FlatButton(
261
                  child: const Text('X'),
262
                  onPressed: () {
263
                    Navigator.of(context).push(MaterialPageRoute<void>(
264
                      builder: (BuildContext context) {
265
                        return SampleForm(
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
                          callback: () => showYesNoAlert(context),
                        );
                      }
                    ));
                  },
                ),
              );
            },
          ),
        ),
      );
    }

    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
299 300 301 302
    // 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.
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
    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);
  });

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

328
    final TestPageRoute<void> route = TestPageRoute<void>(
329
      builder: (BuildContext context) {
330
        return StatefulBuilder(
331 332
          builder: (BuildContext context, StateSetter setState) {
            contentsSetState = setState;
333
            return contentsEmpty ? Container() : SampleForm(key: UniqueKey());
334 335 336 337 338 339
          }
        );
      },
    );

    Widget buildFrame() {
340 341 342 343
      return MaterialApp(
        home: Scaffold(
          appBar: AppBar(title: const Text('Home')),
          body: Builder(
344
            builder: (BuildContext context) {
345 346
              return Center(
                child: FlatButton(
347
                  child: const Text('X'),
348 349 350 351 352 353 354 355 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 381 382
                  onPressed: () {
                    Navigator.of(context).push(route);
                  },
                ),
              );
            },
          ),
        ),
      );
    }

    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);
  });
383
}