route_notification_messages_test.dart 10.7 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 60 61 62 63 64 65
      log.add(methodCall);
    });

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

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

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

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

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

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

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

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

    expect(log, hasLength(0));

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

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

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

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

166
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
167 168 169 170 171 172 173
      log.add(methodCall);
    });

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

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

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

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

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

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

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

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

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

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

262
    expect(log, isEmpty);
263
  });
264 265 266

  testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
267
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
268 269 270 271 272 273 274 275 276 277 278
      log.add(methodCall);
    });

    final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
      initialRouteInformation: const RouteInformation(
        location: 'initial',
      ),
    );
    final SimpleRouterDelegate delegate = SimpleRouterDelegate(
      reportConfiguration: true,
      builder: (BuildContext context, RouteInformation information) {
279
        return Text(information.location!);
280
      },
281 282 283 284 285 286 287 288
    );

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

    // 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);

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

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({
338
    required this.builder,
339 340 341 342 343
    this.onPopRoute,
    this.reportConfiguration = false,
  });

  RouteInformation get routeInformation => _routeInformation;
344
  late RouteInformation _routeInformation;
345 346 347 348 349 350
  set routeInformation(RouteInformation newValue) {
    _routeInformation = newValue;
    notifyListeners();
  }

  SimpleRouterDelegateBuilder builder;
351
  SimpleRouterDelegatePopRoute? onPopRoute;
352 353 354
  final bool reportConfiguration;

  @override
355
  RouteInformation? get currentConfiguration {
356 357 358 359 360 361 362 363 364 365 366 367 368 369
    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)
370
      return onPopRoute!();
371 372 373 374 375
    return SynchronousFuture<bool>(true);
  }

  @override
  Widget build(BuildContext context) => builder(context, routeInformation);
376
}
377 378

class TestPage extends Page<void> {
379
  const TestPage({LocalKey? key, String? name}) : super(key: key, name: name);
380 381 382 383 384 385 386 387 388

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