// Copyright 2015 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/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';

import '../widgets/semantics_tester.dart';

void main() {
  testWidgets('Scaffold control test', (WidgetTester tester) async {
    final Key bodyKey = new UniqueKey();
    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new Scaffold(
        appBar: new AppBar(title: const Text('Title')),
        body: new Container(key: bodyKey),
      ),
    ));
    expect(tester.takeException(), isFlutterError);

    await tester.pumpWidget(new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(title: const Text('Title')),
        body: new Container(key: bodyKey),
      ),
    ));
    RenderBox bodyBox = tester.renderObject(find.byKey(bodyKey));
    expect(bodyBox.size, equals(const Size(800.0, 544.0)));

    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new MediaQuery(
        data: const MediaQueryData(viewInsets: const EdgeInsets.only(bottom: 100.0)),
        child: new Scaffold(
          appBar: new AppBar(title: const Text('Title')),
          body: new Container(key: bodyKey),
        ),
      ),
    ));

    bodyBox = tester.renderObject(find.byKey(bodyKey));
    expect(bodyBox.size, equals(const Size(800.0, 444.0)));

    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new MediaQuery(
        data: const MediaQueryData(viewInsets: const EdgeInsets.only(bottom: 100.0)),
        child: new Scaffold(
          appBar: new AppBar(title: const Text('Title')),
          body: new Container(key: bodyKey),
          resizeToAvoidBottomPadding: false,
        ),
      ),
    ));

    bodyBox = tester.renderObject(find.byKey(bodyKey));
    expect(bodyBox.size, equals(const Size(800.0, 544.0)));
  });

  testWidgets('Scaffold large bottom padding test', (WidgetTester tester) async {
    final Key bodyKey = new UniqueKey();
    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new MediaQuery(
        data: const MediaQueryData(
          viewInsets: const EdgeInsets.only(bottom: 700.0),
        ),
        child: new Scaffold(
          body: new Container(key: bodyKey),
        ),
      ),
    ));

    final RenderBox bodyBox = tester.renderObject(find.byKey(bodyKey));
    expect(bodyBox.size, equals(const Size(800.0, 0.0)));

    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new MediaQuery(
        data: const MediaQueryData(
          viewInsets: const EdgeInsets.only(bottom: 500.0),
        ),
        child: new Scaffold(
          body: new Container(key: bodyKey),
        ),
      ),
    ));

    expect(bodyBox.size, equals(const Size(800.0, 100.0)));

    await tester.pumpWidget(new Directionality(
      textDirection: TextDirection.ltr,
      child: new MediaQuery(
        data: const MediaQueryData(
          viewInsets: const EdgeInsets.only(bottom: 580.0),
        ),
        child: new Scaffold(
          appBar: new AppBar(
            title: const Text('Title'),
          ),
          body: new Container(key: bodyKey),
        ),
      ),
    ));

    expect(bodyBox.size, equals(const Size(800.0, 0.0)));
  });

  testWidgets('Floating action entrance/exit animation', (WidgetTester tester) async {
    await tester.pumpWidget(new MaterialApp(home: const Scaffold(
      floatingActionButton: const FloatingActionButton(
        key: const Key('one'),
        onPressed: null,
        child: const Text('1'),
      ),
    )));

    expect(tester.binding.transientCallbackCount, 0);

    await tester.pumpWidget(new MaterialApp(home: const Scaffold(
      floatingActionButton: const FloatingActionButton(
        key: const Key('two'),
        onPressed: null,
        child: const Text('2'),
      ),
    )));

    expect(tester.binding.transientCallbackCount, greaterThan(0));
    await tester.pumpWidget(new Container());
    expect(tester.binding.transientCallbackCount, 0);

    await tester.pumpWidget(new MaterialApp(home: const Scaffold()));

    expect(tester.binding.transientCallbackCount, 0);

    await tester.pumpWidget(new MaterialApp(home: const Scaffold(
      floatingActionButton: const FloatingActionButton(
        key: const Key('one'),
        onPressed: null,
        child: const Text('1'),
      ),
    )));

    expect(tester.binding.transientCallbackCount, greaterThan(0));
  });

  testWidgets('Floating action button directionality', (WidgetTester tester) async {
    Widget build(TextDirection textDirection) {
      return new Directionality(
        textDirection: textDirection,
        child: const MediaQuery(
          data: const MediaQueryData(
            viewInsets: const EdgeInsets.only(bottom: 200.0),
          ),
          child: const Scaffold(
            floatingActionButton: const FloatingActionButton(
              onPressed: null,
              child: const Text('1'),
            ),
          ),
        ),
      );
    }

    await tester.pumpWidget(build(TextDirection.ltr));

    expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 356.0));

    await tester.pumpWidget(build(TextDirection.rtl));
    expect(tester.binding.transientCallbackCount, 0);

    expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(44.0, 356.0));
  });

  testWidgets('Drawer scrolling', (WidgetTester tester) async {
    final Key drawerKey = new UniqueKey();
    const double appBarHeight = 256.0;

    final ScrollController scrollOffset = new ScrollController();

    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          drawer: new Drawer(
            key: drawerKey,
            child: new ListView(
              controller: scrollOffset,
              children: new List<Widget>.generate(10,
                (int index) => new SizedBox(height: 100.0, child: new Text('D$index'))
              )
            )
          ),
          body: new CustomScrollView(
            slivers: <Widget>[
              const SliverAppBar(
                pinned: true,
                expandedHeight: appBarHeight,
                title: const Text('Title'),
                flexibleSpace: const FlexibleSpaceBar(title: const Text('Title')),
              ),
              new SliverPadding(
                padding: const EdgeInsets.only(top: appBarHeight),
                sliver: new SliverList(
                  delegate: new SliverChildListDelegate(new List<Widget>.generate(
                    10, (int index) => new SizedBox(height: 100.0, child: new Text('B$index')),
                  )),
                ),
              ),
            ],
          ),
        )
      )
    );

    final ScaffoldState state = tester.firstState(find.byType(Scaffold));
    state.openDrawer();

    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(scrollOffset.offset, 0.0);

    const double scrollDelta = 80.0;
    await tester.drag(find.byKey(drawerKey), const Offset(0.0, -scrollDelta));
    await tester.pump();

    expect(scrollOffset.offset, scrollDelta);

    final RenderBox renderBox = tester.renderObject(find.byType(AppBar));
    expect(renderBox.size.height, equals(appBarHeight));
  });

  Widget _buildStatusBarTestApp(TargetPlatform platform) {
    return new MaterialApp(
      theme: new ThemeData(platform: platform),
      home: new MediaQuery(
        data: const MediaQueryData(padding: const EdgeInsets.only(top: 25.0)), // status bar
        child: new Scaffold(
          body: new CustomScrollView(
            primary: true,
            slivers: <Widget>[
              const SliverAppBar(
                title: const Text('Title')
              ),
              new SliverList(
                delegate: new SliverChildListDelegate(new List<Widget>.generate(
                  20, (int index) => new SizedBox(height: 100.0, child: new Text('$index')),
                )),
              ),
            ],
          ),
        ),
      ),
    );
  }

  testWidgets('Tapping the status bar scrolls to top on iOS', (WidgetTester tester) async {
    await tester.pumpWidget(_buildStatusBarTestApp(TargetPlatform.iOS));
    final ScrollableState scrollable = tester.state(find.byType(Scrollable));
    scrollable.position.jumpTo(500.0);
    expect(scrollable.position.pixels, equals(500.0));
    await tester.tapAt(const Offset(100.0, 10.0));
    await tester.pumpAndSettle();
    expect(scrollable.position.pixels, equals(0.0));
  });

  testWidgets('Tapping the status bar does not scroll to top on Android', (WidgetTester tester) async {
    await tester.pumpWidget(_buildStatusBarTestApp(TargetPlatform.android));
    final ScrollableState scrollable = tester.state(find.byType(Scrollable));
    scrollable.position.jumpTo(500.0);
    expect(scrollable.position.pixels, equals(500.0));
    await tester.tapAt(const Offset(100.0, 10.0));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));
    expect(scrollable.position.pixels, equals(500.0));
  });

  testWidgets('Bottom sheet cannot overlap app bar', (WidgetTester tester) async {
    final Key sheetKey = new UniqueKey();

    await tester.pumpWidget(
      new MaterialApp(
        theme: new ThemeData(platform: TargetPlatform.android),
        home: new Scaffold(
          appBar: new AppBar(
            title: const Text('Title'),
          ),
          body: new Builder(
            builder: (BuildContext context) {
              return new GestureDetector(
                onTap: () {
                  Scaffold.of(context).showBottomSheet<Null>((BuildContext context) {
                    return new Container(
                      key: sheetKey,
                      color: Colors.blue[500],
                    );
                  });
                },
                child: const Text('X'),
              );
            },
          ),
        ),
      ),
    );

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

    final RenderBox appBarBox = tester.renderObject(find.byType(AppBar));
    final RenderBox sheetBox = tester.renderObject(find.byKey(sheetKey));

    final Offset appBarBottomRight = appBarBox.localToGlobal(appBarBox.size.bottomRight(Offset.zero));
    final Offset sheetTopRight = sheetBox.localToGlobal(sheetBox.size.topRight(Offset.zero));

    expect(appBarBottomRight, equals(sheetTopRight));
  });

  testWidgets('Persistent bottom buttons are persistent', (WidgetTester tester) async {
    bool didPressButton = false;
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new SingleChildScrollView(
            child: new Container(
              color: Colors.amber[500],
              height: 5000.0,
              child: const Text('body'),
            ),
          ),
          persistentFooterButtons: <Widget>[
            new FlatButton(
              onPressed: () {
                didPressButton = true;
              },
              child: const Text('X'),
            )
          ],
        ),
      ),
    );

    await tester.drag(find.text('body'), const Offset(0.0, -1000.0));
    expect(didPressButton, isFalse);
    await tester.tap(find.text('X'));
    expect(didPressButton, isTrue);
  });

  testWidgets('Persistent bottom buttons apply media padding', (WidgetTester tester) async {
    await tester.pumpWidget(
      new Directionality(
        textDirection: TextDirection.ltr,
        child: new MediaQuery(
          data: const MediaQueryData(
            padding: const EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
          ),
          child: new Scaffold(
            body: new SingleChildScrollView(
              child: new Container(
                color: Colors.amber[500],
                height: 5000.0,
                child: const Text('body'),
              ),
            ),
            persistentFooterButtons: const <Widget>[const Placeholder()],
          ),
        ),
      ),
    );
    expect(tester.getBottomLeft(find.byType(ButtonBar)), const Offset(10.0, 560.0));
    expect(tester.getBottomRight(find.byType(ButtonBar)), const Offset(770.0, 560.0));
  });

  group('back arrow', () {
    Future<Null> expectBackIcon(WidgetTester tester, TargetPlatform platform, IconData expectedIcon) async {
      final GlobalKey rootKey = new GlobalKey();
      final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
        '/': (_) => new Container(key: rootKey, child: const Text('Home')),
        '/scaffold': (_) => new Scaffold(
            appBar: new AppBar(),
            body: const Text('Scaffold'),
        )
      };
      await tester.pumpWidget(
        new MaterialApp(theme: new ThemeData(platform: platform), routes: routes)
      );

      Navigator.pushNamed(rootKey.currentContext, '/scaffold');
      await tester.pump();
      await tester.pump(const Duration(seconds: 1));

      final Icon icon = tester.widget(find.byType(Icon));
      expect(icon.icon, expectedIcon);
    }

    testWidgets('Back arrow uses correct default on Android', (WidgetTester tester) async {
      await expectBackIcon(tester, TargetPlatform.android, Icons.arrow_back);
    });

    testWidgets('Back arrow uses correct default on Fuchsia', (WidgetTester tester) async {
      await expectBackIcon(tester, TargetPlatform.fuchsia, Icons.arrow_back);
    });

    testWidgets('Back arrow uses correct default on iOS', (WidgetTester tester) async {
      await expectBackIcon(tester, TargetPlatform.iOS, Icons.arrow_back_ios);
    });
  });

  group('close button', () {
    Future<Null> expectCloseIcon(WidgetTester tester, TargetPlatform platform, IconData expectedIcon, PageRoute<void> routeBuilder()) async {
      await tester.pumpWidget(
        new MaterialApp(
          theme: new ThemeData(platform: platform),
          home: new Scaffold(appBar: new AppBar(), body: const Text('Page 1')),
        )
      );

      tester.state<NavigatorState>(find.byType(Navigator)).push(routeBuilder());

      await tester.pump();
      await tester.pump(const Duration(seconds: 1));

      final Icon icon = tester.widget(find.byType(Icon));
      expect(icon.icon, expectedIcon);
      expect(find.byType(CloseButton), findsOneWidget);
    }

    PageRoute<void> materialRouteBuilder() {
      return new MaterialPageRoute<void>(
        builder: (BuildContext context) {
          return new Scaffold(appBar: new AppBar(), body: const Text('Page 2'));
        },
        fullscreenDialog: true,
      );
    }

    PageRoute<void> customPageRouteBuilder() {
      return new _CustomPageRoute<void>(
        builder: (BuildContext context) {
          return new Scaffold(appBar: new AppBar(), body: const Text('Page 2'));
        },
        fullscreenDialog: true,
      );
    }

    testWidgets('Close button shows correctly on Android', (WidgetTester tester) async {
      await expectCloseIcon(tester, TargetPlatform.android, Icons.close, materialRouteBuilder);
    });

    testWidgets('Close button shows correctly on Fuchsia', (WidgetTester tester) async {
      await expectCloseIcon(tester, TargetPlatform.fuchsia, Icons.close, materialRouteBuilder);
    });

    testWidgets('Close button shows correctly on iOS', (WidgetTester tester) async {
      await expectCloseIcon(tester, TargetPlatform.iOS, Icons.close, materialRouteBuilder);
    });

    testWidgets('Close button shows correctly with custom page route on Android', (WidgetTester tester) async {
      await expectCloseIcon(tester, TargetPlatform.android, Icons.close, customPageRouteBuilder);
    });

    testWidgets('Close button shows correctly with custom page route on Fuchsia', (WidgetTester tester) async {
      await expectCloseIcon(tester, TargetPlatform.fuchsia, Icons.close, customPageRouteBuilder);
    });

    testWidgets('Close button shows correctly with custom page route on iOS', (WidgetTester tester) async {
      await expectCloseIcon(tester, TargetPlatform.iOS, Icons.close, customPageRouteBuilder);
    });
  });

  group('body size', () {
    testWidgets('body size with container', (WidgetTester tester) async {
      final Key testKey = new UniqueKey();
      await tester.pumpWidget(new Directionality(
        textDirection: TextDirection.ltr,
        child: new MediaQuery(
          data: const MediaQueryData(),
          child: new Scaffold(
            body: new Container(
              key: testKey,
            ),
          ),
        ),
      ));
      expect(tester.element(find.byKey(testKey)).size, const Size(800.0, 600.0));
      expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
    });

    testWidgets('body size with sized container', (WidgetTester tester) async {
      final Key testKey = new UniqueKey();
      await tester.pumpWidget(new Directionality(
        textDirection: TextDirection.ltr,
        child: new MediaQuery(
          data: const MediaQueryData(),
          child: new Scaffold(
            body: new Container(
              key: testKey,
              height: 100.0,
            ),
          ),
        ),
      ));
      expect(tester.element(find.byKey(testKey)).size, const Size(800.0, 100.0));
      expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
    });

    testWidgets('body size with centered container', (WidgetTester tester) async {
      final Key testKey = new UniqueKey();
      await tester.pumpWidget(new Directionality(
        textDirection: TextDirection.ltr,
        child: new MediaQuery(
          data: const MediaQueryData(),
          child: new Scaffold(
            body: new Center(
              child: new Container(
                key: testKey,
              ),
            ),
          ),
        ),
      ));
      expect(tester.element(find.byKey(testKey)).size, const Size(800.0, 600.0));
      expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
    });

    testWidgets('body size with button', (WidgetTester tester) async {
      final Key testKey = new UniqueKey();
      await tester.pumpWidget(new Directionality(
        textDirection: TextDirection.ltr,
        child: new MediaQuery(
          data: const MediaQueryData(),
          child: new Scaffold(
            body: new FlatButton(
              key: testKey,
              onPressed: () { },
              child: const Text(''),
            ),
          ),
        ),
      ));
      expect(tester.element(find.byKey(testKey)).size, const Size(88.0, 48.0));
      expect(tester.renderObject<RenderBox>(find.byKey(testKey)).localToGlobal(Offset.zero), const Offset(0.0, 0.0));
    });
  });

  testWidgets('Open drawer hides underlying semantics tree', (WidgetTester tester) async {
    const String bodyLabel = 'I am the body';
    const String persistentFooterButtonLabel = 'a button on the bottom';
    const String bottomNavigationBarLabel = 'a bar in an app';
    const String floatingActionButtonLabel = 'I float in space';
    const String drawerLabel = 'I am the reason for this test';

    final SemanticsTester semantics = new SemanticsTester(tester);
    await tester.pumpWidget(new MaterialApp(home: const Scaffold(
      body: const Text(bodyLabel),
      persistentFooterButtons: const <Widget>[const Text(persistentFooterButtonLabel)],
      bottomNavigationBar: const Text(bottomNavigationBarLabel),
      floatingActionButton: const Text(floatingActionButtonLabel),
      drawer: const Drawer(child: const Text(drawerLabel)),
    )));

    expect(semantics, includesNodeWith(label: bodyLabel));
    expect(semantics, includesNodeWith(label: persistentFooterButtonLabel));
    expect(semantics, includesNodeWith(label: bottomNavigationBarLabel));
    expect(semantics, includesNodeWith(label: floatingActionButtonLabel));
    expect(semantics, isNot(includesNodeWith(label: drawerLabel)));

    final ScaffoldState state = tester.firstState(find.byType(Scaffold));
    state.openDrawer();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(semantics, isNot(includesNodeWith(label: bodyLabel)));
    expect(semantics, isNot(includesNodeWith(label: persistentFooterButtonLabel)));
    expect(semantics, isNot(includesNodeWith(label: bottomNavigationBarLabel)));
    expect(semantics, isNot(includesNodeWith(label: floatingActionButtonLabel)));
    expect(semantics, includesNodeWith(label: drawerLabel));

    semantics.dispose();
  });

  testWidgets('Scaffold and extreme window padding', (WidgetTester tester) async {
    final Key appBar = new UniqueKey();
    final Key body = new UniqueKey();
    final Key floatingActionButton = new UniqueKey();
    final Key persistentFooterButton = new UniqueKey();
    final Key drawer = new UniqueKey();
    final Key bottomNavigationBar = new UniqueKey();
    final Key insideAppBar = new UniqueKey();
    final Key insideBody = new UniqueKey();
    final Key insideFloatingActionButton = new UniqueKey();
    final Key insidePersistentFooterButton = new UniqueKey();
    final Key insideDrawer = new UniqueKey();
    final Key insideBottomNavigationBar = new UniqueKey();
    await tester.pumpWidget(
      new Directionality(
        textDirection: TextDirection.rtl,
        child: new MediaQuery(
          data: const MediaQueryData(
            padding: const EdgeInsets.only(
              left: 20.0,
              top: 30.0,
              right: 50.0,
              bottom: 60.0,
            ),
            viewInsets: const EdgeInsets.only(bottom: 200.0),
          ),
          child: new Scaffold(
            appBar: new PreferredSize(
              preferredSize: const Size(11.0, 13.0),
              child: new Container(
                key: appBar,
                child: new SafeArea(
                  child: new Placeholder(key: insideAppBar),
                ),
              ),
            ),
            body: new Container(
              key: body,
              child: new SafeArea(
                child: new Placeholder(key: insideBody),
              ),
            ),
            floatingActionButton: new SizedBox(
              key: floatingActionButton,
              width: 77.0,
              height: 77.0,
              child: new SafeArea(
                child: new Placeholder(key: insideFloatingActionButton),
              ),
            ),
            persistentFooterButtons: <Widget>[
              new SizedBox(
                key: persistentFooterButton,
                width: 100.0,
                height: 90.0,
                child: new SafeArea(
                  child: new Placeholder(key: insidePersistentFooterButton),
                ),
              ),
            ],
            drawer: new Container(
              key: drawer,
              width: 204.0,
              child: new SafeArea(
                child: new Placeholder(key: insideDrawer),
              ),
            ),
            bottomNavigationBar: new SizedBox(
              key: bottomNavigationBar,
              height: 85.0,
              child: new SafeArea(
                child: new Placeholder(key: insideBottomNavigationBar),
              ),
            ),
          ),
        ),
      ),
    );
    // open drawer
    await tester.flingFrom(const Offset(795.0, 5.0), const Offset(-200.0, 0.0), 10.0);
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(tester.getRect(find.byKey(appBar)), new Rect.fromLTRB(0.0, 0.0, 800.0, 43.0));
    expect(tester.getRect(find.byKey(body)), new Rect.fromLTRB(0.0, 43.0, 800.0, 348.0));
    expect(tester.getRect(find.byKey(floatingActionButton)), new Rect.fromLTRB(36.0, 255.0, 113.0, 332.0));
    expect(tester.getRect(find.byKey(persistentFooterButton)), new Rect.fromLTRB(28.0, 357.0, 128.0, 447.0)); // Note: has 8px each top/bottom padding.
    expect(tester.getRect(find.byKey(drawer)), new Rect.fromLTRB(596.0, 0.0, 800.0, 600.0));
    expect(tester.getRect(find.byKey(bottomNavigationBar)), new Rect.fromLTRB(0.0, 515.0, 800.0, 600.0));
    expect(tester.getRect(find.byKey(insideAppBar)), new Rect.fromLTRB(20.0, 30.0, 750.0, 43.0));
    expect(tester.getRect(find.byKey(insideBody)), new Rect.fromLTRB(20.0, 43.0, 750.0, 348.0));
    expect(tester.getRect(find.byKey(insideFloatingActionButton)), new Rect.fromLTRB(36.0, 255.0, 113.0, 332.0));
    expect(tester.getRect(find.byKey(insidePersistentFooterButton)), new Rect.fromLTRB(28.0, 357.0, 128.0, 447.0));
    expect(tester.getRect(find.byKey(insideDrawer)), new Rect.fromLTRB(596.0, 30.0, 750.0, 540.0));
    expect(tester.getRect(find.byKey(insideBottomNavigationBar)), new Rect.fromLTRB(20.0, 515.0, 750.0, 540.0));
  });

  testWidgets('Scaffold and extreme window padding - persistent footer buttons only', (WidgetTester tester) async {
    final Key appBar = new UniqueKey();
    final Key body = new UniqueKey();
    final Key floatingActionButton = new UniqueKey();
    final Key persistentFooterButton = new UniqueKey();
    final Key drawer = new UniqueKey();
    final Key insideAppBar = new UniqueKey();
    final Key insideBody = new UniqueKey();
    final Key insideFloatingActionButton = new UniqueKey();
    final Key insidePersistentFooterButton = new UniqueKey();
    final Key insideDrawer = new UniqueKey();
    await tester.pumpWidget(
      new Directionality(
        textDirection: TextDirection.rtl,
        child: new MediaQuery(
          data: const MediaQueryData(
            padding: const EdgeInsets.only(
              left: 20.0,
              top: 30.0,
              right: 50.0,
              bottom: 60.0,
            ),
            viewInsets: const EdgeInsets.only(bottom: 200.0),
          ),
          child: new Scaffold(
            appBar: new PreferredSize(
              preferredSize: const Size(11.0, 13.0),
              child: new Container(
                key: appBar,
                child: new SafeArea(
                  child: new Placeholder(key: insideAppBar),
                ),
              ),
            ),
            body: new Container(
              key: body,
              child: new SafeArea(
                child: new Placeholder(key: insideBody),
              ),
            ),
            floatingActionButton: new SizedBox(
              key: floatingActionButton,
              width: 77.0,
              height: 77.0,
              child: new SafeArea(
                child: new Placeholder(key: insideFloatingActionButton),
              ),
            ),
            persistentFooterButtons: <Widget>[
              new SizedBox(
                key: persistentFooterButton,
                width: 100.0,
                height: 90.0,
                child: new SafeArea(
                  child: new Placeholder(key: insidePersistentFooterButton),
                ),
              ),
            ],
            drawer: new Container(
              key: drawer,
              width: 204.0,
              child: new SafeArea(
                child: new Placeholder(key: insideDrawer),
              ),
            ),
          ),
        ),
      ),
    );
    // open drawer
    await tester.flingFrom(const Offset(795.0, 5.0), const Offset(-200.0, 0.0), 10.0);
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(tester.getRect(find.byKey(appBar)), new Rect.fromLTRB(0.0, 0.0, 800.0, 43.0));
    expect(tester.getRect(find.byKey(body)), new Rect.fromLTRB(0.0, 43.0, 800.0, 400.0));
    expect(tester.getRect(find.byKey(floatingActionButton)), new Rect.fromLTRB(36.0, 307.0, 113.0, 384.0));
    expect(tester.getRect(find.byKey(persistentFooterButton)), new Rect.fromLTRB(28.0, 442.0, 128.0, 532.0)); // Note: has 8px each top/bottom padding.
    expect(tester.getRect(find.byKey(drawer)), new Rect.fromLTRB(596.0, 0.0, 800.0, 600.0));
    expect(tester.getRect(find.byKey(insideAppBar)), new Rect.fromLTRB(20.0, 30.0, 750.0, 43.0));
    expect(tester.getRect(find.byKey(insideBody)), new Rect.fromLTRB(20.0, 43.0, 750.0, 400.0));
    expect(tester.getRect(find.byKey(insideFloatingActionButton)), new Rect.fromLTRB(36.0, 307.0, 113.0, 384.0));
    expect(tester.getRect(find.byKey(insidePersistentFooterButton)), new Rect.fromLTRB(28.0, 442.0, 128.0, 532.0));
    expect(tester.getRect(find.byKey(insideDrawer)), new Rect.fromLTRB(596.0, 30.0, 750.0, 540.0));
  });

  testWidgets('Simultaneous drawers on either side', (WidgetTester tester) async {
    const String bodyLabel = 'I am the body';
    const String drawerLabel = 'I am the label on start side';
    const String endDrawerLabel = 'I am the label on end side';

    final SemanticsTester semantics = new SemanticsTester(tester);
    await tester.pumpWidget(new MaterialApp(home: const Scaffold(
      body: const Text(bodyLabel),
      drawer: const Drawer(child: const Text(drawerLabel)),
      endDrawer: const Drawer(child: const Text(endDrawerLabel)),
    )));

    expect(semantics, includesNodeWith(label: bodyLabel));
    expect(semantics, isNot(includesNodeWith(label: drawerLabel)));
    expect(semantics, isNot(includesNodeWith(label: endDrawerLabel)));

    final ScaffoldState state = tester.firstState(find.byType(Scaffold));
    state.openDrawer();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(semantics, isNot(includesNodeWith(label: bodyLabel)));
    expect(semantics, includesNodeWith(label: drawerLabel));

    state.openEndDrawer();
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    expect(semantics, isNot(includesNodeWith(label: bodyLabel)));
    expect(semantics, includesNodeWith(label: endDrawerLabel));

    semantics.dispose();
  });

  group('ScaffoldGeometry', () {
    testWidgets('bottomNavigationBar', (WidgetTester tester) async {
      final GlobalKey key = new GlobalKey();
      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new Container(),
            bottomNavigationBar: new ConstrainedBox(
              key: key,
              constraints: const BoxConstraints.expand(height: 80.0),
              child: new _GeometryListener(),
            ),
      )));

      final RenderBox navigationBox = tester.renderObject(find.byKey(key));
      final RenderBox appBox = tester.renderObject(find.byType(MaterialApp));
      final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
      final ScaffoldGeometry geometry = listenerState.cache.value;

      expect(
        geometry.bottomNavigationBarTop,
        appBox.size.height - navigationBox.size.height
      );
    });

    testWidgets('no bottomNavigationBar', (WidgetTester tester) async {
      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new ConstrainedBox(
              constraints: const BoxConstraints.expand(height: 80.0),
              child: new _GeometryListener(),
            ),
      )));

      final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
      final ScaffoldGeometry geometry = listenerState.cache.value;

      expect(
        geometry.bottomNavigationBarTop,
        null
      );
    });

    testWidgets('floatingActionButton', (WidgetTester tester) async {
      final GlobalKey key = new GlobalKey();
      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new Container(),
            floatingActionButton: new FloatingActionButton(
              key: key,
              child: new _GeometryListener(),
              onPressed: () {},
            ),
      )));

      final RenderBox floatingActionButtonBox = tester.renderObject(find.byKey(key));
      final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
      final ScaffoldGeometry geometry = listenerState.cache.value;

      final Rect fabRect = floatingActionButtonBox.localToGlobal(Offset.zero) & floatingActionButtonBox.size;

      expect(
        geometry.floatingActionButtonArea,
        fabRect
      );
    });

    testWidgets('no floatingActionButton', (WidgetTester tester) async {
      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new ConstrainedBox(
              constraints: const BoxConstraints.expand(height: 80.0),
              child: new _GeometryListener(),
            ),
      )));

      final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
      final ScaffoldGeometry geometry = listenerState.cache.value;

      expect(
          geometry.floatingActionButtonArea,
          null
      );
    });

    testWidgets('floatingActionButton entrance/exit animation', (WidgetTester tester) async {
      final GlobalKey key = new GlobalKey();
      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new ConstrainedBox(
              constraints: const BoxConstraints.expand(height: 80.0),
              child: new _GeometryListener(),
            ),
      )));

      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new Container(),
            floatingActionButton: new FloatingActionButton(
              key: key,
              child: new _GeometryListener(),
              onPressed: () {},
            ),
      )));

      final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));
      await tester.pump(const Duration(milliseconds: 50));

      ScaffoldGeometry geometry = listenerState.cache.value;

      final Rect transitioningFabRect = geometry.floatingActionButtonArea;

      await tester.pump(const Duration(seconds: 3));
      geometry = listenerState.cache.value;
      final RenderBox floatingActionButtonBox = tester.renderObject(find.byKey(key));
      final Rect fabRect = floatingActionButtonBox.localToGlobal(Offset.zero) & floatingActionButtonBox.size;

      expect(
        geometry.floatingActionButtonArea,
        fabRect
      );

      expect(
        geometry.floatingActionButtonArea.center,
        transitioningFabRect.center
      );

      expect(
        geometry.floatingActionButtonArea.width,
        greaterThan(transitioningFabRect.width)
      );

      expect(
        geometry.floatingActionButtonArea.height,
        greaterThan(transitioningFabRect.height)
      );
    });

    testWidgets('change notifications', (WidgetTester tester) async {
      final GlobalKey key = new GlobalKey();
      int numNotificationsAtLastFrame = 0;
      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new ConstrainedBox(
              constraints: const BoxConstraints.expand(height: 80.0),
              child: new _GeometryListener(),
            ),
      )));

      final _GeometryListenerState listenerState = tester.state(find.byType(_GeometryListener));

      expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
      numNotificationsAtLastFrame = listenerState.numNotifications;

      await tester.pumpWidget(new MaterialApp(home: new Scaffold(
            body: new Container(),
            floatingActionButton: new FloatingActionButton(
              key: key,
              child: new _GeometryListener(),
              onPressed: () {},
            ),
      )));

      expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
      numNotificationsAtLastFrame = listenerState.numNotifications;

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

      expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
      numNotificationsAtLastFrame = listenerState.numNotifications;

      await tester.pump(const Duration(seconds: 3));

      expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
      numNotificationsAtLastFrame = listenerState.numNotifications;
    });

  });
}

class _GeometryListener extends StatefulWidget {
  @override
  _GeometryListenerState createState() => new _GeometryListenerState();
}

class _GeometryListenerState extends State<_GeometryListener> {
  @override
  Widget build(BuildContext context) {
    return new CustomPaint(
      painter: cache
    );
  }

  int numNotifications = 0;
  ValueListenable<ScaffoldGeometry> geometryListenable;
  _GeometryCachePainter cache;

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
    if (geometryListenable == newListenable)
      return;

    if (geometryListenable != null)
      geometryListenable.removeListener(onGeometryChanged);

    geometryListenable = newListenable;
    geometryListenable.addListener(onGeometryChanged);
    cache = new _GeometryCachePainter(geometryListenable);
  }

  void onGeometryChanged() {
    numNotifications += 1;
  }
}

// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class _GeometryCachePainter extends CustomPainter {
  _GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);

  final ValueListenable<ScaffoldGeometry> geometryListenable;

  ScaffoldGeometry value;
  @override
  void paint(Canvas canvas, Size size) {
    value = geometryListenable.value;
  }

  @override
  bool shouldRepaint(_GeometryCachePainter oldDelegate) {
    return true;
  }
}

class _CustomPageRoute<T> extends PageRoute<T> {
  _CustomPageRoute({
    @required this.builder,
    RouteSettings settings = const RouteSettings(),
    this.maintainState = true,
    bool fullscreenDialog = false,
  }) : assert(builder != null),
       super(settings: settings, fullscreenDialog: fullscreenDialog);

  final WidgetBuilder builder;

  @override
  Duration get transitionDuration => const Duration(milliseconds: 300);

  @override
  Color get barrierColor => null;

  @override
  String get barrierLabel => null;

  @override
  final bool maintainState;

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
    return builder(context);
  }

  @override
  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
    return child;
  }
}
