route_notification_messages_test.dart 10.2 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 73
        },
      ),
74 75
    ]);
    log.clear();
76 77 78 79 80

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

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

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

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

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

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

    expect(log, hasLength(0));

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

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

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

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

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

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

171 172 173
    expect(log, <Object>[
      isMethodCall('selectSingleEntryHistory', arguments: null),
      isMethodCall('routeInformationUpdated',
174
        arguments: <String, dynamic>{
175 176
          'location': '/',
          'state': null,
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 195 196
        },
      ),
    );
197
    log.clear();
198 199 200 201 202

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

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

  testWidgets('Nameless routes should send platform messages', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
218
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239
      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);
            },
          );
        },
      },
    ));

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

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

255
    expect(log, isEmpty);
256
  });
257 258 259

  testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async {
    final List<MethodCall> log = <MethodCall>[];
260
    tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, (MethodCall methodCall) async {
261 262 263 264 265 266 267 268 269 270 271
      log.add(methodCall);
    });

    final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider(
      initialRouteInformation: const RouteInformation(
        location: 'initial',
      ),
    );
    final SimpleRouterDelegate delegate = SimpleRouterDelegate(
      reportConfiguration: true,
      builder: (BuildContext context, RouteInformation information) {
272
        return Text(information.location!);
273
      },
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
    );

    await tester.pumpWidget(MaterialApp.router(
      routeInformationProvider: provider,
      routeInformationParser: SimpleRouteInformationParser(),
      routerDelegate: delegate,
    ));
    expect(find.text('initial'), findsOneWidget);

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

292 293
    expect(log, <Object>[
      isMethodCall('selectMultiEntryHistory', arguments: null),
294 295 296 297
      isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{
        'location': 'update',
        'state': 'state',
      }),
298
    ]);
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
  });
}

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({
321
    required this.builder,
322 323 324 325 326
    this.onPopRoute,
    this.reportConfiguration = false,
  });

  RouteInformation get routeInformation => _routeInformation;
327
  late RouteInformation _routeInformation;
328 329 330 331 332 333
  set routeInformation(RouteInformation newValue) {
    _routeInformation = newValue;
    notifyListeners();
  }

  SimpleRouterDelegateBuilder builder;
334
  SimpleRouterDelegatePopRoute? onPopRoute;
335 336 337
  final bool reportConfiguration;

  @override
338
  RouteInformation? get currentConfiguration {
339 340 341 342 343 344 345 346 347 348 349 350 351 352
    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)
353
      return onPopRoute!();
354 355 356 357 358
    return SynchronousFuture<bool>(true);
  }

  @override
  Widget build(BuildContext context) => builder(context, routeInformation);
359
}
360 361

class TestPage extends Page<void> {
362
  const TestPage({LocalKey? key, String? name}) : super(key: key, name: name);
363 364 365 366 367 368 369 370 371

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