// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @TestOn('chrome') import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class OnTapPage extends StatelessWidget { const OnTapPage({Key? key, required this.id, required this.onTap}) : super(key: key); 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, child: Container( child: Center( child: Text(id, style: Theme.of(context).textTheme.headline3), ), ), ), ); } } Map<String, dynamic> convertRouteInformationToMap(RouteInformation routeInformation) { return <String, dynamic>{ 'location': routeInformation.location, 'state': routeInformation.state, }; } void main() { testWidgets('Push and Pop should send platform messages', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => OnTapPage( id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage( id: 'A', onTap: () { Navigator.pop(context); }), }; final List<MethodCall> log = <MethodCall>[]; SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); }); await tester.pumpWidget(MaterialApp( routes: routes, )); expect(log, hasLength(1)); expect( log.last, isMethodCall( 'routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': null, 'routeName': '/', }, )); await tester.tap(find.text('/')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, hasLength(2)); expect( log.last, isMethodCall( 'routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': '/', 'routeName': '/A', }, )); await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, hasLength(3)); expect( log.last, isMethodCall( 'routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': '/A', 'routeName': '/', }, )); }); testWidgets('Navigator does not report route name by default', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); }); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Navigator( pages: const <Page<void>>[ TestPage(name: '/'), ], onPopPage: (Route<void> route, void result) => false, ) )); expect(log, hasLength(0)); await tester.pumpWidget(Directionality( textDirection: TextDirection.ltr, child: Navigator( pages: const <Page<void>>[ TestPage(name: '/'), TestPage(name: '/abc',), ], onPopPage: (Route<void> route, void result) => false, ) )); await tester.pumpAndSettle(); expect(log, hasLength(0)); }); testWidgets('Replace should send platform messages', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => OnTapPage( id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage( id: 'A', onTap: () { Navigator.pushReplacementNamed(context, '/B'); }), '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () {}), }; final List<MethodCall> log = <MethodCall>[]; SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); }); await tester.pumpWidget(MaterialApp( routes: routes, )); expect(log, hasLength(1)); expect( log.last, isMethodCall( 'routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': null, 'routeName': '/', }, )); await tester.tap(find.text('/')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, hasLength(2)); expect( log.last, isMethodCall( 'routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': '/', 'routeName': '/A', }, )); await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, hasLength(3)); expect( log.last, isMethodCall( 'routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': '/A', 'routeName': '/B', }, )); }); testWidgets('Nameless routes should send platform messages', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async { 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); }, ); }, }, )); expect(log, hasLength(1)); expect( log.last, isMethodCall('routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': null, 'routeName': '/home', }), ); await tester.tap(find.text('Home')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, hasLength(2)); expect( log.last, isMethodCall('routeUpdated', arguments: <String, dynamic>{ 'previousRouteName': '/home', 'routeName': null, }), ); }); testWidgets('PlatformRouteInformationProvider reports URL', (WidgetTester tester) async { final List<MethodCall> log = <MethodCall>[]; SystemChannels.navigation.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); }); final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: const RouteInformation( location: 'initial', ), ); final SimpleRouterDelegate delegate = SimpleRouterDelegate( reportConfiguration: true, builder: (BuildContext context, RouteInformation information) { return Text(information.location!); } ); 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); expect(log, hasLength(1)); // TODO(chunhtai): check routeInformationUpdated instead once the engine // side is done. expect( log.last, isMethodCall('routeInformationUpdated', arguments: <String, dynamic>{ 'location': 'update', 'state': 'state', }), ); }); } 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({ required this.builder, this.onPopRoute, this.reportConfiguration = false, }); RouteInformation get routeInformation => _routeInformation; late RouteInformation _routeInformation; set routeInformation(RouteInformation newValue) { _routeInformation = newValue; notifyListeners(); } SimpleRouterDelegateBuilder builder; SimpleRouterDelegatePopRoute? onPopRoute; final bool reportConfiguration; @override RouteInformation? get currentConfiguration { 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) return onPopRoute!(); return SynchronousFuture<bool>(true); } @override Widget build(BuildContext context) => builder(context, routeInformation); } class TestPage extends Page<void> { const TestPage({LocalKey? key, String? name}) : super(key: key, name: name); @override Route<void> createRoute(BuildContext context) { return PageRouteBuilder<void>( settings: this, pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => const Placeholder(), ); } }