route_notification_messages_test.dart 10.8 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
@TestOn('!chrome')
6 7 8
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
9
import 'package:flutter_test/flutter_test.dart';
10 11

class OnTapPage extends StatelessWidget {
12
  const OnTapPage({Key? key, required this.id, required this.onTap}) : super(key: key);
13 14 15 16 17 18 19 20 21 22 23

  final String id;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Page $id')),
      body: GestureDetector(
        onTap: onTap,
        behavior: HitTestBehavior.opaque,
24 25
        child: Center(
          child: Text(id, style: Theme.of(context).textTheme.headline3),
26 27 28 29 30 31
        ),
      ),
    );
  }
}

32 33 34 35 36 37 38
Map<String, dynamic> convertRouteInformationToMap(RouteInformation routeInformation) {
  return <String, dynamic>{
    'location': routeInformation.location,
    'state': routeInformation.state,
  };
}

39
void main() {
40
  testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async {
41 42 43 44 45
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/': (BuildContext context) => OnTapPage(
          id: '/',
          onTap: () {
            Navigator.pushNamed(context, '/A');
46 47
          },
        ),
48 49 50 51
      '/A': (BuildContext context) => OnTapPage(
          id: 'A',
          onTap: () {
            Navigator.pop(context);
52 53
          },
        ),
54 55 56 57
    };

    final List<MethodCall> log = <MethodCall>[];

58
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
59
      log.add(methodCall);
60
      return null;
61 62 63 64 65 66
    });

    await tester.pumpWidget(MaterialApp(
      routes: routes,
    ));

67 68 69
    expect(log, <Object>[
      isMethodCall('selectSingleEntryHistory', arguments: null),
      isMethodCall('routeInformationUpdated',
70
        arguments: <String, dynamic>{
71 72
          'location': '/',
          'state': null,
73
          'replace': false,
74 75
        },
      ),
76 77
    ]);
    log.clear();
78 79 80 81 82

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

83
    expect(log, hasLength(1));
84
    expect(
85 86
      log.last,
      isMethodCall(
87
        'routeInformationUpdated',
88
        arguments: <String, dynamic>{
89 90
          'location': '/A',
          'state': null,
91
          'replace': false,
92 93 94
        },
      ),
    );
95
    log.clear();
96 97 98 99 100

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

101
    expect(log, hasLength(1));
102
    expect(
103 104
      log.last,
      isMethodCall(
105
        'routeInformationUpdated',
106
        arguments: <String, dynamic>{
107 108
          'location': '/',
          'state': null,
109
          'replace': false,
110 111 112
        },
      ),
    );
113 114
  });

115 116
  testWidgets('Navigator does not report route name by default', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
117
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
118
      log.add(methodCall);
119
      return null;
120 121 122 123 124
    });

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Navigator(
125 126
        pages: const <Page<void>>[
          TestPage(name: '/'),
127 128
        ],
        onPopPage: (Route<void> route, void result) => false,
129
      ),
130 131 132 133 134 135 136
    ));

    expect(log, hasLength(0));

    await tester.pumpWidget(Directionality(
      textDirection: TextDirection.ltr,
      child: Navigator(
137 138
        pages: const <Page<void>>[
          TestPage(name: '/'),
139
          TestPage(name: '/abc'),
140 141
        ],
        onPopPage: (Route<void> route, void result) => false,
142
      ),
143 144 145 146 147 148
    ));

    await tester.pumpAndSettle();
    expect(log, hasLength(0));
  });

149
  testWidgets('Replace should send platform messages', (WidgetTester tester) async {
150 151 152 153 154
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/': (BuildContext context) => OnTapPage(
          id: '/',
          onTap: () {
            Navigator.pushNamed(context, '/A');
155 156
          },
        ),
157 158 159 160
      '/A': (BuildContext context) => OnTapPage(
          id: 'A',
          onTap: () {
            Navigator.pushReplacementNamed(context, '/B');
161 162
          },
        ),
163 164 165 166 167
      '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () {}),
    };

    final List<MethodCall> log = <MethodCall>[];

168
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
169
      log.add(methodCall);
170
      return null;
171 172 173 174 175 176
    });

    await tester.pumpWidget(MaterialApp(
      routes: routes,
    ));

177 178 179
    expect(log, <Object>[
      isMethodCall('selectSingleEntryHistory', arguments: null),
      isMethodCall('routeInformationUpdated',
180
        arguments: <String, dynamic>{
181 182
          'location': '/',
          'state': null,
183
          'replace': false,
184 185
        },
      ),
186 187
    ]);
    log.clear();
188 189 190 191 192

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

193
    expect(log, hasLength(1));
194
    expect(
195 196
      log.last,
      isMethodCall(
197
        'routeInformationUpdated',
198
        arguments: <String, dynamic>{
199 200
          'location': '/A',
          'state': null,
201
          'replace': false,
202 203 204
        },
      ),
    );
205
    log.clear();
206 207 208 209 210

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

211
    expect(log, hasLength(1));
212
    expect(
213 214
      log.last,
      isMethodCall(
215
        'routeInformationUpdated',
216
        arguments: <String, dynamic>{
217 218
          'location': '/B',
          'state': null,
219
          'replace': false,
220 221 222
        },
      ),
    );
223
  });
224 225 226

  testWidgets('Nameless routes should send platform messages', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
227
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
228
      log.add(methodCall);
229
      return null;
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
    });

    await tester.pumpWidget(MaterialApp(
      initialRoute: '/home',
      routes: <String, WidgetBuilder>{
        '/home': (BuildContext context) {
          return OnTapPage(
            id: 'Home',
            onTap: () {
              // Create a route with no name.
              final Route<void> route = MaterialPageRoute<void>(
                builder: (BuildContext context) => const Text('Nameless Route'),
              );
              Navigator.push<void>(context, route);
            },
          );
        },
      },
    ));

250 251 252 253 254 255
    expect(log, <Object>[
      isMethodCall('selectSingleEntryHistory', arguments: null),
      isMethodCall('routeInformationUpdated',
        arguments: <String, dynamic>{
          'location': '/home',
          'state': null,
256
          'replace': false,
257 258 259 260
        },
      ),
    ]);
    log.clear();
261 262 263 264 265

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

266
    expect(log, isEmpty);
267
  });
268 269 270

  testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
271
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
272
      log.add(methodCall);
273
      return null;
274 275 276 277 278 279 280 281 282 283
    });

    final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
      initialRouteInformation: const RouteInformation(
        location: 'initial',
      ),
    );
    final SimpleRouterDelegate delegate = SimpleRouterDelegate(
      reportConfiguration: true,
      builder: (BuildContext context, RouteInformation information) {
284
        return Text(information.location!);
285
      },
286 287 288 289 290 291 292 293
    );

    await tester.pumpWidget(MaterialApp.router(
      routeInformationProvider: provider,
      routeInformationParser: SimpleRouteInformationParser(),
      routerDelegate: delegate,
    ));
    expect(find.text('initial'), findsOneWidget);
294 295 296 297 298 299 300 301 302
    expect(log, <Object>[
      isMethodCall('selectMultiEntryHistory', arguments: null),
      isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
        'location': 'initial',
        'state': null,
        'replace': false,
      }),
    ]);
    log.clear();
303 304 305 306 307 308 309 310 311 312

    // Triggers a router rebuild and verify the route information is reported
    // to the web engine.
    delegate.routeInformation = const RouteInformation(
      location: 'update',
      state: 'state',
    );
    await tester.pump();
    expect(find.text('update'), findsOneWidget);

313 314
    expect(log, <Object>[
      isMethodCall('selectMultiEntryHistory', arguments: null),
315 316 317
      isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
        'location': 'update',
        'state': 'state',
318
        'replace': false,
319
      }),
320
    ]);
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
  });
}

typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext, RouteInformation);
typedef SimpleRouterDelegatePopRoute = Future<bool> Function();

class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> {
  SimpleRouteInformationParser();

  @override
  Future<RouteInformation> parseRouteInformation(RouteInformation information) {
    return SynchronousFuture<RouteInformation>(information);
  }

  @override
  RouteInformation restoreRouteInformation(RouteInformation configuration) {
    return configuration;
  }
}

class SimpleRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier {
  SimpleRouterDelegate({
343
    required this.builder,
344 345 346 347 348
    this.onPopRoute,
    this.reportConfiguration = false,
  });

  RouteInformation get routeInformation => _routeInformation;
349
  late RouteInformation _routeInformation;
350 351 352 353 354 355
  set routeInformation(RouteInformation newValue) {
    _routeInformation = newValue;
    notifyListeners();
  }

  SimpleRouterDelegateBuilder builder;
356
  SimpleRouterDelegatePopRoute? onPopRoute;
357 358 359
  final bool reportConfiguration;

  @override
360
  RouteInformation? get currentConfiguration {
361 362 363 364 365 366 367 368 369 370 371 372 373 374
    if (reportConfiguration)
      return routeInformation;
    return null;
  }

  @override
  Future<void> setNewRoutePath(RouteInformation configuration) {
    _routeInformation = configuration;
    return SynchronousFuture<void>(null);
  }

  @override
  Future<bool> popRoute() {
    if (onPopRoute != null)
375
      return onPopRoute!();
376 377 378 379 380
    return SynchronousFuture<bool>(true);
  }

  @override
  Widget build(BuildContext context) => builder(context, routeInformation);
381
}
382 383

class TestPage extends Page<void> {
384
  const TestPage({LocalKey? key, String? name}) : super(key: key, name: name);
385 386 387 388 389 390 391 392 393

  @override
  Route<void> createRoute(BuildContext context) {
    return PageRouteBuilder<void>(
      settings: this,
      pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => const Placeholder(),
    );
  }
}