route_notification_messages_test.dart 10.6 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({super.key, required this.id, required this.onTap});
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
        child: Center(
25
          child: Text(id, style: Theme.of(context).textTheme.displaySmall),
26 27 28 29 30 31 32
        ),
      ),
    );
  }
}

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

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

51
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
52
      log.add(methodCall);
53
      return null;
54 55 56 57 58 59
    });

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

60 61 62
    expect(log, <Object>[
      isMethodCall('selectSingleEntryHistory', arguments: null),
      isMethodCall('routeInformationUpdated',
63
        arguments: <String, dynamic>{
64 65
          'location': '/',
          'state': null,
66
          'replace': false,
67 68
        },
      ),
69 70
    ]);
    log.clear();
71 72 73 74 75

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

76
    expect(log, hasLength(1));
77
    expect(
78 79
      log.last,
      isMethodCall(
80
        'routeInformationUpdated',
81
        arguments: <String, dynamic>{
82 83
          'location': '/A',
          'state': null,
84
          'replace': false,
85 86 87
        },
      ),
    );
88
    log.clear();
89 90 91 92 93

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

94
    expect(log, hasLength(1));
95
    expect(
96 97
      log.last,
      isMethodCall(
98
        'routeInformationUpdated',
99
        arguments: <String, dynamic>{
100 101
          'location': '/',
          'state': null,
102
          'replace': false,
103 104 105
        },
      ),
    );
106 107
  });

108 109
  testWidgets('Navigator does not report route name by default', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
110
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
111
      log.add(methodCall);
112
      return null;
113 114 115 116 117
    });

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

    expect(log, hasLength(0));

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

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

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

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

161
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
162
      log.add(methodCall);
163
      return null;
164 165 166 167 168 169
    });

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

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

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

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

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

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

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

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

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

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

259
    expect(log, isEmpty);
260
  });
261 262 263

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

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

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

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

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

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

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

  SimpleRouterDelegateBuilder builder;
349
  SimpleRouterDelegatePopRoute? onPopRoute;
350 351 352
  final bool reportConfiguration;

  @override
353
  RouteInformation? get currentConfiguration {
354
    if (reportConfiguration) {
355
      return routeInformation;
356
    }
357 358 359 360 361 362 363 364 365 366 367
    return null;
  }

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

  @override
  Future<bool> popRoute() {
368
    if (onPopRoute != null) {
369
      return onPopRoute!();
370
    }
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({super.key, super.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(),
    );
  }
}