page_test.dart 17.3 KB
Newer Older
1 2 3 4 5
// Copyright 2017 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.

import 'package:flutter/material.dart';
6
import 'package:flutter/rendering.dart';
7 8 9
import 'package:flutter_test/flutter_test.dart' hide TypeMatcher;

import '../rendering/mock_canvas.dart';
10 11 12 13 14 15

void main() {
  testWidgets('test Android page transition', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.android),
16
        home: const Material(child: const Text('Page 1')),
17 18
        routes: <String, WidgetBuilder>{
          '/next': (BuildContext context) {
19
            return const Material(child: const Text('Page 2'));
20
          },
21
        },
22 23 24
      )
    );

25
    final Offset widget1TopLeft = tester.getTopLeft(find.text('Page 1'));
26 27 28 29 30

    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 1));

31 32
    FadeTransition widget2Opacity =
        tester.element(find.text('Page 2')).ancestorWidgetOfExactType(FadeTransition);
33
    Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
34 35
    final Size widget2Size = tester.getSize(find.text('Page 2'));

36
    // Android transition is vertical only.
37
    expect(widget1TopLeft.dx == widget2TopLeft.dx, true);
38
    // Page 1 is above page 2 mid-transition.
39
    expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
40 41 42
    // Animation begins 3/4 of the way up the page.
    expect(widget2TopLeft.dy < widget2Size.height / 4.0, true);
    // Animation starts with page 2 being near transparent.
43
    expect(widget2Opacity.opacity.value < 0.01, MaterialPageRoute.debugEnableFadingRoutes); // ignore: deprecated_member_use
44

45
    await tester.pump(const Duration(milliseconds: 300));
46 47 48 49 50 51 52 53 54

    // Page 2 covers page 1.
    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 1));

55
    widget2Opacity =
56
        tester.element(find.text('Page 2')).ancestorWidgetOfExactType(FadeTransition);
57 58 59
    widget2TopLeft = tester.getTopLeft(find.text('Page 2'));

    // Page 2 starts to move down.
60
    expect(widget1TopLeft.dy < widget2TopLeft.dy, true);
61
    // Page 2 starts to lose opacity.
62
    expect(widget2Opacity.opacity.value < 1.0, MaterialPageRoute.debugEnableFadingRoutes); // ignore: deprecated_member_use
63

64
    await tester.pump(const Duration(milliseconds: 300));
65 66 67

    expect(find.text('Page 1'), isOnstage);
    expect(find.text('Page 2'), findsNothing);
68
  });
69 70

  testWidgets('test iOS page transition', (WidgetTester tester) async {
71
    final Key page2Key = new UniqueKey();
72 73 74
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.iOS),
75
        home: const Material(child: const Text('Page 1')),
76 77
        routes: <String, WidgetBuilder>{
          '/next': (BuildContext context) {
78 79 80 81
            return new Material(
              key: page2Key,
              child: const Text('Page 2'),
            );
82
          },
83
        },
84 85 86
      )
    );

87
    final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
88 89

    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
90

91
    await tester.pump();
92
    await tester.pump(const Duration(milliseconds: 150));
93

94 95
    Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
    Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
96 97
    final RenderDecoratedBox box = tester.element(find.byKey(page2Key))
        .ancestorRenderObjectOfType(const TypeMatcher<RenderDecoratedBox>());
98

99
    // Page 1 is moving to the left.
100
    expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true);
101
    // Page 1 isn't moving vertically.
102
    expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true);
103
    // iOS transition is horizontal only.
104
    expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
105
    // Page 2 is coming in from the right.
106
    expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true);
107
    // The shadow should be drawn to one screen width to the left of where
108 109
    // the page 2 box is. `paints` tests relative to the painter's given canvas
    // rather than relative to the screen so assert that it's one screen
110
    // width to the left of 0 offset box rect and nothing is drawn inside the
111 112 113 114
    // box's rect.
    expect(box, paints..rect(
      rect: new Rect.fromLTWH(-800.0, 0.0, 800.0, 600.0)
    ));
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129

    await tester.pumpAndSettle();

    // Page 2 covers page 1.
    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 100));

    widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
    widget2TopLeft = tester.getTopLeft(find.text('Page 2'));

    // Page 1 is coming back from the left.
130
    expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true);
131
    // Page 1 isn't moving vertically.
132
    expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true);
133
    // iOS transition is horizontal only.
134
    expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
135
    // Page 2 is leaving towards the right.
136
    expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true);
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152

    await tester.pumpAndSettle();

    expect(find.text('Page 1'), isOnstage);
    expect(find.text('Page 2'), findsNothing);

    widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));

    // Page 1 is back where it started.
    expect(widget1InitialTopLeft == widget1TransientTopLeft, true);
  });

  testWidgets('test iOS fullscreen dialog transition', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.iOS),
153
        home: const Material(child: const Text('Page 1')),
154 155 156
      )
    );

157
    final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));
158 159 160

    tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>(
      builder: (BuildContext context) {
161
        return const Material(child: const Text('Page 2'));
162 163 164 165 166 167 168
      },
      fullscreenDialog: true,
    ));

    await tester.pump();
    await tester.pump(const Duration(milliseconds: 100));

169 170
    Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
    Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
171 172 173 174

    // Page 1 doesn't move.
    expect(widget1TransientTopLeft == widget1InitialTopLeft, true);
    // Fullscreen dialogs transitions vertically only.
175
    expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true);
176
    // Page 2 is coming in from the bottom.
177
    expect(widget2TopLeft.dy > widget1InitialTopLeft.dy, true);
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194

    await tester.pumpAndSettle();

    // Page 2 covers page 1.
    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 100));

    widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
    widget2TopLeft = tester.getTopLeft(find.text('Page 2'));

    // Page 1 doesn't move.
    expect(widget1TransientTopLeft == widget1InitialTopLeft, true);
    // Fullscreen dialogs transitions vertically only.
195
    expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true);
196
    // Page 2 is leaving towards the bottom.
197
    expect(widget2TopLeft.dy > widget1InitialTopLeft.dy, true);
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213

    await tester.pumpAndSettle();

    expect(find.text('Page 1'), isOnstage);
    expect(find.text('Page 2'), findsNothing);

    widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));

    // Page 1 is back where it started.
    expect(widget1InitialTopLeft == widget1TransientTopLeft, true);
  });

  testWidgets('test no back gesture on Android', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.android),
214
        home: const Scaffold(body: const Text('Page 1')),
215 216
        routes: <String, WidgetBuilder>{
          '/next': (BuildContext context) {
217
            return const Scaffold(body: const Text('Page 2'));
218 219 220 221 222 223 224 225 226 227 228 229
          },
        },
      )
    );

    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
    await tester.pumpAndSettle();

    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    // Drag from left edge to invoke the gesture.
230
    final TestGesture gesture = await tester.startGesture(const Offset(5.0, 100.0));
231
    await gesture.moveBy(const Offset(400.0, 0.0));
232
    await tester.pump();
233 234 235 236 237

    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    // Page 2 didn't move
238
    expect(tester.getTopLeft(find.text('Page 2')), Offset.zero);
239 240 241 242 243 244
  });

  testWidgets('test back gesture on iOS', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.iOS),
245
        home: const Scaffold(body: const Text('Page 1')),
246 247
        routes: <String, WidgetBuilder>{
          '/next': (BuildContext context) {
248
            return const Scaffold(body: const Text('Page 2'));
249 250 251 252 253 254 255 256 257 258 259 260
          },
        },
      )
    );

    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
    await tester.pumpAndSettle();

    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    // Drag from left edge to invoke the gesture.
261
    final TestGesture gesture = await tester.startGesture(const Offset(5.0, 100.0));
262
    await gesture.moveBy(const Offset(400.0, 0.0));
263
    await tester.pump();
264 265 266 267

    // Page 1 is now visible.
    expect(find.text('Page 1'), isOnstage);
    expect(find.text('Page 2'), isOnstage);
268 269

    // The route widget position needs to track the finger position very exactly.
270
    expect(tester.getTopLeft(find.text('Page 2')), const Offset(400.0, 0.0));
271 272 273 274

    await gesture.moveBy(const Offset(-200.0, 0.0));
    await tester.pump();

275
    expect(tester.getTopLeft(find.text('Page 2')), const Offset(200.0, 0.0));
276 277 278 279

    await gesture.moveBy(const Offset(-100.0, 200.0));
    await tester.pump();

280
    expect(tester.getTopLeft(find.text('Page 2')), const Offset(100.0, 0.0));
281 282
  });

283 284 285 286
  testWidgets('back gesture while OS changes', (WidgetTester tester) async {
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/': (BuildContext context) => new Material(
        child: new FlatButton(
287
          child: const Text('PUSH'),
288 289 290
          onPressed: () { Navigator.of(context).pushNamed('/b'); },
        ),
      ),
291
      '/b': (BuildContext context) => new Container(child: const Text('HELLO')),
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 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
    };
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.iOS),
        routes: routes,
      ),
    );
    await tester.tap(find.text('PUSH'));
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
    expect(find.text('PUSH'), findsNothing);
    expect(find.text('HELLO'), findsOneWidget);
    final Offset helloPosition1 = tester.getCenter(find.text('HELLO'));
    final TestGesture gesture = await tester.startGesture(const Offset(2.5, 300.0));
    await tester.pump(const Duration(milliseconds: 20));
    await gesture.moveBy(const Offset(100.0, 0.0));
    expect(find.text('PUSH'), findsNothing);
    expect(find.text('HELLO'), findsOneWidget);
    await tester.pump(const Duration(milliseconds: 20));
    expect(find.text('PUSH'), findsOneWidget);
    expect(find.text('HELLO'), findsOneWidget);
    final Offset helloPosition2 = tester.getCenter(find.text('HELLO'));
    expect(helloPosition1.dx, lessThan(helloPosition2.dx));
    expect(helloPosition1.dy, helloPosition2.dy);
    expect(Theme.of(tester.element(find.text('HELLO'))).platform, TargetPlatform.iOS);
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.android),
        routes: routes,
      ),
    );
    // Now we have to let the theme animation run through.
    // This takes three frames (including the first one above):
    //  1. Start the Theme animation. It's at t=0 so everything else is identical.
    //  2. Start any animations that are informed by the Theme, for example, the
    //     DefaultTextStyle, on the first frame that the theme is not at t=0. In
    //     this case, it's at t=1.0 of the theme animation, so this is also the
    //     frame in which the theme animation ends.
    //  3. End all the other animations.
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
    expect(Theme.of(tester.element(find.text('HELLO'))).platform, TargetPlatform.android);
    final Offset helloPosition3 = tester.getCenter(find.text('HELLO'));
    expect(helloPosition3, helloPosition2);
    expect(find.text('PUSH'), findsOneWidget);
    expect(find.text('HELLO'), findsOneWidget);
    await gesture.moveBy(const Offset(100.0, 0.0));
    await tester.pump(const Duration(milliseconds: 20));
    expect(find.text('PUSH'), findsOneWidget);
    expect(find.text('HELLO'), findsOneWidget);
    final Offset helloPosition4 = tester.getCenter(find.text('HELLO'));
    expect(helloPosition3.dx, lessThan(helloPosition4.dx));
    expect(helloPosition3.dy, helloPosition4.dy);
    await gesture.moveBy(const Offset(500.0, 0.0));
    await gesture.up();
    expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2);
    expect(find.text('PUSH'), findsOneWidget);
    expect(find.text('HELLO'), findsNothing);
  });

350 351 352 353
  testWidgets('test no back gesture on iOS fullscreen dialogs', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.iOS),
354
        home: const Scaffold(body: const Text('Page 1')),
355 356 357 358 359
      )
    );

    tester.state<NavigatorState>(find.byType(Navigator)).push(new MaterialPageRoute<Null>(
      builder: (BuildContext context) {
360
        return const Scaffold(body: const Text('Page 2'));
361 362 363 364 365 366 367 368 369
      },
      fullscreenDialog: true,
    ));
    await tester.pumpAndSettle();

    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    // Drag from left edge to invoke the gesture.
370
    final TestGesture gesture = await tester.startGesture(const Offset(5.0, 100.0));
371
    await gesture.moveBy(const Offset(400.0, 0.0));
372
    await tester.pump();
373 374 375 376 377

    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    // Page 2 didn't move
378
    expect(tester.getTopLeft(find.text('Page 2')), Offset.zero);
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 454

  testWidgets('test adaptable transitions switch during execution', (WidgetTester tester) async {
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.android),
        home: const Material(child: const Text('Page 1')),
        routes: <String, WidgetBuilder>{
          '/next': (BuildContext context) {
            return const Material(child: const Text('Page 2'));
          },
        },
      )
    );

    final Offset widget1InitialTopLeft = tester.getTopLeft(find.text('Page 1'));

    tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('/next');
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 100));

    Offset widget2TopLeft = tester.getTopLeft(find.text('Page 2'));
    final Size widget2Size = tester.getSize(find.text('Page 2'));

    // Android transition is vertical only.
    expect(widget1InitialTopLeft.dx == widget2TopLeft.dx, true);
    // Page 1 is above page 2 mid-transition.
    expect(widget1InitialTopLeft.dy < widget2TopLeft.dy, true);
    // Animation begins from the top of the page.
    expect(widget2TopLeft.dy < widget2Size.height, true);

    await tester.pump(const Duration(milliseconds: 300));

    // Page 2 covers page 1.
    expect(find.text('Page 1'), findsNothing);
    expect(find.text('Page 2'), isOnstage);

    // Re-pump the same app but with iOS instead of Android.
    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.iOS),
        home: const Material(child: const Text('Page 1')),
        routes: <String, WidgetBuilder>{
          '/next': (BuildContext context) {
            return const Material(child: const Text('Page 2'));
          },
        },
      )
    );

    tester.state<NavigatorState>(find.byType(Navigator)).pop();
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 100));

    Offset widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));
    widget2TopLeft = tester.getTopLeft(find.text('Page 2'));

    // Page 1 is coming back from the left.
    expect(widget1TransientTopLeft.dx < widget1InitialTopLeft.dx, true);
    // Page 1 isn't moving vertically.
    expect(widget1TransientTopLeft.dy == widget1InitialTopLeft.dy, true);
    // iOS transition is horizontal only.
    expect(widget1InitialTopLeft.dy == widget2TopLeft.dy, true);
    // Page 2 is leaving towards the right.
    expect(widget2TopLeft.dx > widget1InitialTopLeft.dx, true);

    await tester.pump(const Duration(milliseconds: 300));

    expect(find.text('Page 1'), isOnstage);
    expect(find.text('Page 2'), findsNothing);

    widget1TransientTopLeft = tester.getTopLeft(find.text('Page 1'));

    // Page 1 is back where it started.
    expect(widget1InitialTopLeft == widget1TransientTopLeft, true);
  });
455
}