// 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. import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; void main() { testWidgets('Simple router basic functionality - synchronized', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, ); addTearDown(delegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); provider.value = RouteInformation(uri: Uri.parse('update')); await tester.pump(); expect(find.text('initial'), findsNothing); expect(find.text('update'), findsOneWidget); }); testWidgets('Router respects update order', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final delegate = MutableRouterDelegate(); addTearDown(delegate.dispose); final notifier = ValueNotifier(0); addTearDown(notifier.dispose); await tester.pumpWidget( buildBoilerPlate( IntInheritedNotifier( notifier: notifier, child: Router( routeInformationProvider: provider, routeInformationParser: CustomRouteInformationParser(( RouteInformation information, BuildContext context, ) { IntInheritedNotifier.of(context); // create dependency return information; }), routerDelegate: delegate, ), ), ), ); expect(find.text('initial'), findsOneWidget); expect(delegate.currentConfiguration!.uri.toString(), 'initial'); delegate.updateConfiguration(RouteInformation(uri: Uri.parse('update'))); notifier.value = 1; // The delegate should still retain the update. await tester.pumpAndSettle(); expect(find.text('update'), findsOneWidget); expect(delegate.currentConfiguration!.uri.toString(), 'update'); }); testWidgets('Simple router basic functionality - asynchronized', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final parser = SimpleAsyncRouteInformationParser(); final delegate = SimpleAsyncRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(information?.uri.toString() ?? 'waiting'); }, ); addTearDown(delegate.dispose); await tester.runAsync(() async { await tester.pumpWidget( buildBoilerPlate( Router( routeInformationProvider: provider, routeInformationParser: parser, routerDelegate: delegate, ), ), ); // Future has not yet completed. expect(find.text('waiting'), findsOneWidget); await parser.parsingFuture; await delegate.setNewRouteFuture; await tester.pump(); expect(find.text('initial'), findsOneWidget); provider.value = RouteInformation(uri: Uri.parse('update')); await tester.pump(); // Future has not yet completed. expect(find.text('initial'), findsOneWidget); await parser.parsingFuture; await delegate.setNewRouteFuture; await tester.pump(); expect(find.text('update'), findsOneWidget); }); }); testWidgets('Interrupts route parsing should not crash', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final parser = CompleterRouteInformationParser(); final delegate = SimpleAsyncRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(information?.uri.toString() ?? 'waiting'); }, ); addTearDown(delegate.dispose); await tester.runAsync(() async { await tester.pumpWidget( buildBoilerPlate( Router( routeInformationProvider: provider, routeInformationParser: parser, routerDelegate: delegate, ), ), ); // Future has not yet completed. expect(find.text('waiting'), findsOneWidget); final Completer firstTransactionCompleter = parser.completer; // Start a new parsing transaction before the previous one complete. provider.value = RouteInformation(uri: Uri.parse('update')); await tester.pump(); expect(find.text('waiting'), findsOneWidget); // Completing the previous transaction does not cause an update. firstTransactionCompleter.complete(); await firstTransactionCompleter.future; await tester.pump(); expect(find.text('waiting'), findsOneWidget); expect(tester.takeException(), isNull); // Make sure the new transaction can complete and update correctly. parser.completer.complete(); await parser.completer.future; await delegate.setNewRouteFuture; await tester.pump(); expect(find.text('update'), findsOneWidget); }); }); testWidgets('Router.maybeOf can be null', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); await tester.pumpWidget(buildBoilerPlate(Text('dummy', key: key))); final BuildContext textContext = key.currentContext!; // This should not throw error. final Router? router = Router.maybeOf(textContext); expect(router, isNull); expect( () => Router.of(textContext), throwsA( isFlutterError.having((FlutterError e) => e.message, 'message', startsWith('Router')), ), ); }); testWidgets('Simple router can handle pop route', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped')); return SynchronousFuture(true); }, ); addTearDown(delegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, backButtonDispatcher: dispatcher, ), ), ); expect(find.text('initial'), findsOneWidget); var result = false; // SynchronousFuture should complete immediately. dispatcher.invokeCallback(SynchronousFuture(false)).then((bool data) { result = data; }); expect(result, isTrue); await tester.pump(); expect(find.text('popped'), findsOneWidget); }); testWidgets('Router throw when passing routeInformationProvider without routeInformationParser', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, ); addTearDown(delegate.dispose); expect( () { Router(routeInformationProvider: provider, routerDelegate: delegate); }, throwsA( isAssertionError.having( (AssertionError e) => e.message, 'message', 'A routeInformationParser must be provided when a routeInformationProvider is specified.', ), ), ); }); testWidgets('PopNavigatorRouterDelegateMixin works', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); final delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, onPopPage: (Route route, void result) { provider.value = RouteInformation(uri: Uri.parse('popped')); return route.didPop(result); }, ); addTearDown(delegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, backButtonDispatcher: dispatcher, ), ), ); expect(find.text('initial'), findsOneWidget); // Pushes a nameless route. showDialog( useRootNavigator: false, context: delegate.navigatorKey.currentContext!, builder: (BuildContext context) => const Text('dialog'), ); await tester.pumpAndSettle(); expect(find.text('dialog'), findsOneWidget); // Pops the nameless route and makes sure the initial page is shown. var result = false; result = await dispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pumpAndSettle(); expect(find.text('initial'), findsOneWidget); expect(find.text('dialog'), findsNothing); // Pops one more time. result = false; result = await dispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); testWidgets('Nested routers back button dispatcher works', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final outerDelegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { final BackButtonDispatcher innerDispatcher = ChildBackButtonDispatcher(outerDispatcher); innerDispatcher.takePriority(); final innerDelegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? innerInformation) { return Text(Uri.decodeComponent(information!.uri.toString())); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped inner')); return SynchronousFuture(true); }, ); addTearDown(innerDelegate.dispose); // Creates the sub-router. return Router( backButtonDispatcher: innerDispatcher, routerDelegate: innerDelegate, ); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }, ); addTearDown(outerDelegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: outerDelegate, ), ), ); expect(find.text('initial'), findsOneWidget); // The outer dispatcher should trigger the pop on the inner router. var result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped inner'), findsOneWidget); }); testWidgets('Nested router back button dispatcher works for multiple children', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final BackButtonDispatcher innerDispatcher1 = ChildBackButtonDispatcher(outerDispatcher); final BackButtonDispatcher innerDispatcher2 = ChildBackButtonDispatcher(outerDispatcher); final outerDelegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { late final SimpleRouterDelegate innerDelegate1; addTearDown(() => innerDelegate1.dispose()); late final SimpleRouterDelegate innerDelegate2; addTearDown(() => innerDelegate2.dispose()); // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), Router( backButtonDispatcher: innerDispatcher1, routerDelegate: innerDelegate1 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? innerInformation) { return Container(); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped inner1')); return SynchronousFuture(true); }, ), ), Router( backButtonDispatcher: innerDispatcher2, routerDelegate: innerDelegate2 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? innerInformation) { return Container(); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped inner2')); return SynchronousFuture(true); }, ), ), ], ); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }, ); addTearDown(outerDelegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: outerDelegate, ), ), ); expect(find.text('initial'), findsOneWidget); // If none of the children have taken the priority, the root router handles // the pop. var result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped outer'), findsOneWidget); innerDispatcher1.takePriority(); result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped inner1'), findsOneWidget); // The last child dispatcher that took priority handles the pop. innerDispatcher2.takePriority(); result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped inner2'), findsOneWidget); }); testWidgets('ChildBackButtonDispatcher can be replaced without calling the takePriority', ( WidgetTester tester, ) async { final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); BackButtonDispatcher innerDispatcher = ChildBackButtonDispatcher(outerDispatcher); final outerDelegate1 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { final innerDelegate1 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? innerInformation) { return Container(); }, ); addTearDown(innerDelegate1.dispose); // Creates the sub-router. return Column( children: [ const Text('initial'), Router( backButtonDispatcher: innerDispatcher, routerDelegate: innerDelegate1, ), ], ); }, ); addTearDown(outerDelegate1.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routerDelegate: outerDelegate1, ), ), ); // Creates a new child back button dispatcher and rebuild, this will cause // the old one to be replaced and discarded. innerDispatcher = ChildBackButtonDispatcher(outerDispatcher); final outerDelegate2 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { final innerDelegate2 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? innerInformation) { return Container(); }, ); addTearDown(innerDelegate2.dispose); // Creates the sub-router. return Column( children: [ const Text('initial'), Router( backButtonDispatcher: innerDispatcher, routerDelegate: innerDelegate2, ), ], ); }, ); addTearDown(outerDelegate2.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routerDelegate: outerDelegate2, ), ), ); expect(tester.takeException(), isNull); }); testWidgets('ChildBackButtonDispatcher take priority recursively', (WidgetTester tester) async { final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final BackButtonDispatcher innerDispatcher1 = ChildBackButtonDispatcher(outerDispatcher); final BackButtonDispatcher innerDispatcher2 = ChildBackButtonDispatcher(innerDispatcher1); final BackButtonDispatcher innerDispatcher3 = ChildBackButtonDispatcher(innerDispatcher2); late final SimpleRouterDelegate outerDelegate; addTearDown(() => outerDelegate.dispose()); late final SimpleRouterDelegate innerDelegate1; addTearDown(() => innerDelegate1.dispose()); late final SimpleRouterDelegate innerDelegate2; addTearDown(() => innerDelegate2.dispose()); late final SimpleRouterDelegate innerDelegate3; addTearDown(() => innerDelegate3.dispose()); var isPopped = false; await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routerDelegate: outerDelegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Router( backButtonDispatcher: innerDispatcher1, routerDelegate: innerDelegate1 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? innerInformation) { return Router( backButtonDispatcher: innerDispatcher2, routerDelegate: innerDelegate2 = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? innerInformation) { return Router( backButtonDispatcher: innerDispatcher3, routerDelegate: innerDelegate3 = SimpleRouterDelegate( onPopRoute: () { isPopped = true; return SynchronousFuture(true); }, builder: (BuildContext context, RouteInformation? innerInformation) { return Container(); }, ), ); }, ), ); }, ), ); }, ), ), ), ); // This should work without calling the takePriority on the innerDispatcher2 // and the innerDispatcher1. innerDispatcher3.takePriority(); var result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); expect(isPopped, isTrue); }); testWidgets('router does report URL change correctly', (WidgetTester tester) async { RouteInformation? reportedRouteInformation; RouteInformationReportingType? reportedType; final provider = SimpleRouteInformationProvider( onRouterReport: (RouteInformation information, RouteInformationReportingType type) { // Makes sure we only report once after manually cleaning up. expect(reportedRouteInformation, isNull); expect(reportedType, isNull); reportedRouteInformation = information; reportedType = type; }, ); addTearDown(provider.dispose); final delegate = SimpleRouterDelegate( reportConfiguration: true, builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, ); delegate.onPopRoute = () { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return SynchronousFuture(true); }; addTearDown(delegate.dispose); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); provider.value = RouteInformation(uri: Uri.parse('initial')); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); expect(reportedRouteInformation!.uri.toString(), 'initial'); expect(reportedType, RouteInformationReportingType.none); reportedRouteInformation = null; reportedType = null; delegate.routeInformation = RouteInformation(uri: Uri.parse('update')); await tester.pump(); expect(find.text('initial'), findsNothing); expect(find.text('update'), findsOneWidget); expect(reportedRouteInformation!.uri.toString(), 'update'); expect(reportedType, RouteInformationReportingType.none); // The router should report as non navigation event if only state changes. reportedRouteInformation = null; reportedType = null; delegate.routeInformation = RouteInformation(uri: Uri.parse('update'), state: 'another state'); await tester.pump(); expect(find.text('update'), findsOneWidget); expect(reportedRouteInformation!.uri.toString(), 'update'); expect(reportedRouteInformation!.state, 'another state'); expect(reportedType, RouteInformationReportingType.none); reportedRouteInformation = null; reportedType = null; var result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped'), findsOneWidget); expect(reportedRouteInformation!.uri.toString(), 'popped'); expect(reportedType, RouteInformationReportingType.none); }); testWidgets('router can be forced to recognize or ignore navigating events', ( WidgetTester tester, ) async { RouteInformation? reportedRouteInformation; RouteInformationReportingType? reportedType; var isNavigating = false; late RouteInformation nextRouteInformation; final provider = SimpleRouteInformationProvider( onRouterReport: (RouteInformation information, RouteInformationReportingType type) { // Makes sure we only report once after manually cleaning up. expect(reportedRouteInformation, isNull); expect(reportedType, isNull); reportedRouteInformation = information; reportedType = type; }, ); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final delegate = SimpleRouterDelegate(reportConfiguration: true); addTearDown(delegate.dispose); delegate.builder = (BuildContext context, RouteInformation? information) { return ElevatedButton( child: Text(Uri.decodeComponent(information!.uri.toString())), onPressed: () { if (isNavigating) { Router.navigate(context, () { if (delegate.routeInformation != nextRouteInformation) { delegate.routeInformation = nextRouteInformation; } }); } else { Router.neglect(context, () { if (delegate.routeInformation != nextRouteInformation) { delegate.routeInformation = nextRouteInformation; } }); } }, ); }; final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); expect(reportedRouteInformation!.uri.toString(), 'initial'); expect(reportedType, RouteInformationReportingType.none); reportedType = null; reportedRouteInformation = null; nextRouteInformation = RouteInformation(uri: Uri.parse('update')); await tester.tap(find.byType(ElevatedButton)); await tester.pump(); expect(find.text('initial'), findsNothing); expect(find.text('update'), findsOneWidget); expect(reportedType, RouteInformationReportingType.neglect); expect(reportedRouteInformation!.uri.toString(), 'update'); reportedType = null; reportedRouteInformation = null; isNavigating = true; // This should not trigger any real navigating event because the // nextRouteInformation does not change. However, the router should still // report a route information because isNavigating = true. await tester.tap(find.byType(ElevatedButton)); await tester.pump(); expect(reportedType, RouteInformationReportingType.navigate); expect(reportedRouteInformation!.uri.toString(), 'update'); reportedType = null; reportedRouteInformation = null; }); testWidgets('router ignore navigating events updates RouteInformationProvider', ( WidgetTester tester, ) async { RouteInformation? updatedRouteInformation; late RouteInformation nextRouteInformation; RouteInformationReportingType? reportingType; final provider = SimpleRouteInformationProvider( onRouterReport: (RouteInformation information, RouteInformationReportingType type) { expect(reportingType, isNull); expect(updatedRouteInformation, isNull); updatedRouteInformation = information; reportingType = type; }, ); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final delegate = SimpleRouterDelegate(reportConfiguration: true); addTearDown(delegate.dispose); delegate.builder = (BuildContext context, RouteInformation? information) { return ElevatedButton( child: Text(Uri.decodeComponent(information!.uri.toString())), onPressed: () { Router.neglect(context, () { if (delegate.routeInformation != nextRouteInformation) { delegate.routeInformation = nextRouteInformation; } }); }, ); }; final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); expect(updatedRouteInformation!.uri.toString(), 'initial'); expect(reportingType, RouteInformationReportingType.none); updatedRouteInformation = null; reportingType = null; nextRouteInformation = RouteInformation(uri: Uri.parse('update')); await tester.tap(find.byType(ElevatedButton)); await tester.pump(); expect(find.text('initial'), findsNothing); expect(find.text('update'), findsOneWidget); expect(updatedRouteInformation!.uri.toString(), 'update'); expect(reportingType, RouteInformationReportingType.neglect); }); testWidgets('state change without location changes updates RouteInformationProvider', ( WidgetTester tester, ) async { RouteInformation? updatedRouteInformation; late RouteInformation nextRouteInformation; RouteInformationReportingType? reportingType; final provider = SimpleRouteInformationProvider( onRouterReport: (RouteInformation information, RouteInformationReportingType type) { // This should never be a navigation event. expect(reportingType, isNull); expect(updatedRouteInformation, isNull); updatedRouteInformation = information; reportingType = type; }, ); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial'), state: 'state1'); final delegate = SimpleRouterDelegate(reportConfiguration: true); addTearDown(delegate.dispose); delegate.builder = (BuildContext context, RouteInformation? information) { return ElevatedButton( child: Text(Uri.decodeComponent(information!.uri.toString())), onPressed: () { delegate.routeInformation = nextRouteInformation; }, ); }; final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); expect(updatedRouteInformation!.uri.toString(), 'initial'); expect(reportingType, RouteInformationReportingType.none); updatedRouteInformation = null; reportingType = null; nextRouteInformation = RouteInformation(uri: Uri.parse('initial'), state: 'state2'); await tester.tap(find.byType(ElevatedButton)); await tester.pump(); expect(updatedRouteInformation!.uri.toString(), 'initial'); expect(updatedRouteInformation!.state, 'state2'); expect(reportingType, RouteInformationReportingType.none); }); testWidgets('PlatformRouteInformationProvider works', (WidgetTester tester) async { final provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { final children = []; if (information!.uri.toString().isNotEmpty) { children.add(Text(information.uri.toString())); } if (information.state != null) { children.add(Text(information.state.toString())); } return Column(children: children); }, ); addTearDown(delegate.dispose); await tester.pumpWidget( MaterialApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ); expect(find.text('initial'), findsOneWidget); // Pushes through the `pushRouteInformation` in the navigation method channel. const testRouteInformation = {'location': 'testRouteName', 'state': 'state'}; final ByteData routerMessage = const JSONMethodCodec().encodeMethodCall( const MethodCall('pushRouteInformation', testRouteInformation), ); await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/navigation', routerMessage, (_) {}, ); await tester.pump(); expect(find.text('testRouteName'), findsOneWidget); expect(find.text('state'), findsOneWidget); // Pushes through the `pushRoute` in the navigation method channel. const testRouteName = 'newTestRouteName'; final ByteData message = const JSONMethodCodec().encodeMethodCall( const MethodCall('pushRoute', testRouteName), ); await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/navigation', message, (_) {}, ); await tester.pump(); expect(find.text('newTestRouteName'), findsOneWidget); }); testWidgets('PlatformRouteInformationProvider updates route information', ( WidgetTester tester, ) async { final log = []; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( SystemChannels.navigation, (MethodCall methodCall) async { log.add(methodCall); return null; }, ); final provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); log.clear(); provider.routerReportsNewRouteInformation(RouteInformation(uri: Uri.parse('a'), state: true)); // Implicit reporting pushes new history entry if the location changes. expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), isMethodCall( 'routeInformationUpdated', arguments: {'uri': 'a', 'state': true, 'replace': false}, ), ]); log.clear(); provider.routerReportsNewRouteInformation(RouteInformation(uri: Uri.parse('a'), state: false)); // Since the location is the same, the provider sends replaces message. expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), isMethodCall( 'routeInformationUpdated', arguments: {'uri': 'a', 'state': false, 'replace': true}, ), ]); log.clear(); provider.routerReportsNewRouteInformation( RouteInformation(uri: Uri.parse('b'), state: false), type: RouteInformationReportingType.neglect, ); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), isMethodCall( 'routeInformationUpdated', arguments: {'uri': 'b', 'state': false, 'replace': true}, ), ]); log.clear(); provider.routerReportsNewRouteInformation( RouteInformation(uri: Uri.parse('b'), state: false), type: RouteInformationReportingType.navigate, ); expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), isMethodCall( 'routeInformationUpdated', arguments: {'uri': 'b', 'state': false, 'replace': false}, ), ]); }); testWidgets( 'PlatformRouteInformationProvider does not push new entry if query parameters are semantically the same', (WidgetTester tester) async { final log = []; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( SystemChannels.navigation, (MethodCall methodCall) async { log.add(methodCall); return null; }, ); final initial = RouteInformation(uri: Uri.parse('initial?a=ws/abcd')); final provider = PlatformRouteInformationProvider(initialRouteInformation: initial); addTearDown(provider.dispose); // Make sure engine is updated with initial route provider.routerReportsNewRouteInformation(initial); log.clear(); provider.routerReportsNewRouteInformation( RouteInformation( uri: Uri( path: 'initial', queryParameters: {'a': 'ws/abcd'}, // This will be escaped. ), ), ); expect(provider.value.uri.toString(), 'initial?a=ws%2Fabcd'); // should use `replace: true` expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), isMethodCall( 'routeInformationUpdated', arguments: { 'uri': 'initial?a=ws%2Fabcd', 'state': null, 'replace': true, }, ), ]); log.clear(); provider.routerReportsNewRouteInformation( RouteInformation(uri: Uri.parse('initial?a=1&b=2')), ); log.clear(); // Change query parameters order provider.routerReportsNewRouteInformation( RouteInformation(uri: Uri.parse('initial?b=2&a=1')), ); // should use `replace: true` expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), isMethodCall( 'routeInformationUpdated', arguments: {'uri': 'initial?b=2&a=1', 'state': null, 'replace': true}, ), ]); log.clear(); provider.routerReportsNewRouteInformation( RouteInformation(uri: Uri.parse('initial?a=1&a=2')), ); log.clear(); // Change query parameters order for same key provider.routerReportsNewRouteInformation( RouteInformation(uri: Uri.parse('initial?a=2&a=1')), ); // should use `replace: true` expect(log, [ isMethodCall('selectMultiEntryHistory', arguments: null), isMethodCall( 'routeInformationUpdated', arguments: {'uri': 'initial?a=2&a=1', 'state': null, 'replace': true}, ), ]); log.clear(); }, ); testWidgets('RootBackButtonDispatcher works', (WidgetTester tester) async { final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); final delegate = SimpleRouterDelegate( reportConfiguration: true, builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, ); addTearDown(delegate.dispose); delegate.onPopRoute = () { delegate.routeInformation = RouteInformation(uri: Uri.parse('popped')); return SynchronousFuture(true); }; await tester.pumpWidget( MaterialApp.router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ); expect(find.text('initial'), findsOneWidget); // Pop route through the message channel. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage( 'flutter/navigation', message, (_) {}, ); await tester.pump(); expect(find.text('popped'), findsOneWidget); }); testWidgets('BackButtonListener takes priority over root back dispatcher', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), BackButtonListener( child: Container(), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('popped inner1')); return SynchronousFuture(true); }, ), ], ); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }, ); addTearDown(delegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); var result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped inner1'), findsOneWidget); }); testWidgets('BackButtonListener updates callback if it has been changed', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final routerDelegate = SimpleRouterDelegate() ..builder = (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), BackButtonListener( child: Container(), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('first callback')); return SynchronousFuture(true); }, ), ], ); } ..onPopRoute = () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }; addTearDown(routerDelegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: routerDelegate, ), ), ); routerDelegate ..builder = (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), BackButtonListener( child: Container(), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('second callback')); return SynchronousFuture(true); }, ), ], ); } ..onPopRoute = () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }; await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: routerDelegate, ), ), ); await tester.pump(); await outerDispatcher.invokeCallback(SynchronousFuture(false)); await tester.pump(); expect(find.text('second callback'), findsOneWidget); }); testWidgets('BackButtonListener clears callback if it is disposed', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); final routerDelegate = SimpleRouterDelegate() ..builder = (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), BackButtonListener( child: Container(), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('first callback')); return SynchronousFuture(true); }, ), ], ); } ..onPopRoute = () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }; addTearDown(routerDelegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: routerDelegate, ), ), ); routerDelegate ..builder = (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column(children: [Text(Uri.decodeComponent(information!.uri.toString()))]); } ..onPopRoute = () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }; await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: routerDelegate, ), ), ); await tester.pump(); await outerDispatcher.invokeCallback(SynchronousFuture(false)); await tester.pump(); expect(find.text('popped outer'), findsOneWidget); }); testWidgets('Nested backButtonListener should take priority', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), BackButtonListener( child: BackButtonListener( child: Container(), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('popped inner2')); return SynchronousFuture(true); }, ), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('popped inner1')); return SynchronousFuture(true); }, ), ], ); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }, ); addTearDown(delegate.dispose); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); var result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped inner2'), findsOneWidget); }); testWidgets('Nested backButtonListener that returns false should call next on the line', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), BackButtonListener( child: BackButtonListener( child: Container(), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('popped inner2')); return SynchronousFuture(false); }, ), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse('popped inner1')); return SynchronousFuture(true); }, ), ], ); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }, ); addTearDown(delegate.dispose); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, ), ), ); expect(find.text('initial'), findsOneWidget); var result = false; result = await outerDispatcher.invokeCallback(SynchronousFuture(false)); expect(result, isTrue); await tester.pump(); expect(find.text('popped inner1'), findsOneWidget); }); testWidgets('`didUpdateWidget` test', (WidgetTester tester) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher outerDispatcher = RootBackButtonDispatcher(); late StateSetter setState; var location = 'first callback'; final routerDelegate = SimpleRouterDelegate() ..builder = (BuildContext context, RouteInformation? information) { // Creates the sub-router. return Column( children: [ Text(Uri.decodeComponent(information!.uri.toString())), StatefulBuilder( builder: (BuildContext context, StateSetter setter) { setState = setter; return BackButtonListener( child: Container(), onBackButtonPressed: () { provider.value = RouteInformation(uri: Uri.parse(location)); return SynchronousFuture(true); }, ); }, ), ], ); } ..onPopRoute = () { provider.value = RouteInformation(uri: Uri.parse('popped outer')); return SynchronousFuture(true); }; addTearDown(routerDelegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( backButtonDispatcher: outerDispatcher, routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: routerDelegate, ), ), ); // Only update BackButtonListener widget. setState(() { location = 'second callback'; }); await tester.pump(); await outerDispatcher.invokeCallback(SynchronousFuture(false)); await tester.pump(); expect(find.text('second callback'), findsOneWidget); }); testWidgets('Router reports location if it is different from location given by OS', ( WidgetTester tester, ) async { final reportedRouteInformation = []; final provider = SimpleRouteInformationProvider( onRouterReport: (RouteInformation info, RouteInformationReportingType type) => reportedRouteInformation.add(info), )..value = RouteInformation(uri: Uri.parse('/home')); addTearDown(provider.dispose); final delegate = SimpleRouterDelegate( builder: (BuildContext _, RouteInformation? info) => Text('Current route: ${info?.uri}'), reportConfiguration: true, ); addTearDown(delegate.dispose); await tester.pumpWidget( buildBoilerPlate( Router( routeInformationProvider: provider, routeInformationParser: RedirectingInformationParser({ '/doesNotExist': RouteInformation(uri: Uri.parse('/404')), }), routerDelegate: delegate, ), ), ); expect(find.text('Current route: /home'), findsOneWidget); expect(reportedRouteInformation.single.uri.toString(), '/home'); provider.value = RouteInformation(uri: Uri.parse('/doesNotExist')); await tester.pump(); expect(find.text('Current route: /404'), findsOneWidget); expect(reportedRouteInformation[1].uri.toString(), '/404'); }); testWidgets('RouterInformationParser can look up dependencies and reparse', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped')); return SynchronousFuture(true); }, ); addTearDown(delegate.dispose); var expectedMaxLines = 1; var parserCalled = false; final Widget router = Router( routeInformationProvider: provider, routeInformationParser: CustomRouteInformationParser(( RouteInformation information, BuildContext context, ) { parserCalled = true; final DefaultTextStyle style = DefaultTextStyle.of(context); return RouteInformation(uri: Uri.parse('${style.maxLines}')); }), routerDelegate: delegate, backButtonDispatcher: dispatcher, ); await tester.pumpWidget( buildBoilerPlate( DefaultTextStyle(style: const TextStyle(), maxLines: expectedMaxLines, child: router), ), ); expect(find.text('$expectedMaxLines'), findsOneWidget); expect(parserCalled, isTrue); parserCalled = false; expectedMaxLines = 2; await tester.pumpWidget( buildBoilerPlate( DefaultTextStyle(style: const TextStyle(), maxLines: expectedMaxLines, child: router), ), ); await tester.pump(); expect(find.text('$expectedMaxLines'), findsOneWidget); expect(parserCalled, isTrue); }); testWidgets('RouterInformationParser can look up dependencies without reparsing', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { return Text(Uri.decodeComponent(information!.uri.toString())); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped')); return SynchronousFuture(true); }, ); addTearDown(delegate.dispose); const expectedMaxLines = 1; var parserCalled = false; final Widget router = Router( routeInformationProvider: provider, routeInformationParser: CustomRouteInformationParser(( RouteInformation information, BuildContext context, ) { parserCalled = true; final DefaultTextStyle style = context.getInheritedWidgetOfExactType()!; return RouteInformation(uri: Uri.parse('${style.maxLines}')); }), routerDelegate: delegate, backButtonDispatcher: dispatcher, ); await tester.pumpWidget( buildBoilerPlate( DefaultTextStyle(style: const TextStyle(), maxLines: expectedMaxLines, child: router), ), ); expect(find.text('$expectedMaxLines'), findsOneWidget); expect(parserCalled, isTrue); parserCalled = false; const newMaxLines = 2; // This rebuild should not trigger re-parsing. await tester.pumpWidget( buildBoilerPlate( DefaultTextStyle(style: const TextStyle(), maxLines: newMaxLines, child: router), ), ); await tester.pump(); expect(find.text('$newMaxLines'), findsNothing); expect(find.text('$expectedMaxLines'), findsOneWidget); expect(parserCalled, isFalse); }); testWidgets('Looks up dependencies in RouterDelegate does not trigger re-parsing', ( WidgetTester tester, ) async { final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('initial')); final BackButtonDispatcher dispatcher = RootBackButtonDispatcher(); final delegate = SimpleRouterDelegate( builder: (BuildContext context, RouteInformation? information) { final DefaultTextStyle style = DefaultTextStyle.of(context); return Text('${style.maxLines}'); }, onPopRoute: () { provider.value = RouteInformation(uri: Uri.parse('popped')); return SynchronousFuture(true); }, ); addTearDown(delegate.dispose); var expectedMaxLines = 1; var parserCalled = false; final Widget router = Router( routeInformationProvider: provider, routeInformationParser: CustomRouteInformationParser(( RouteInformation information, BuildContext context, ) { parserCalled = true; return information; }), routerDelegate: delegate, backButtonDispatcher: dispatcher, ); await tester.pumpWidget( buildBoilerPlate( DefaultTextStyle(style: const TextStyle(), maxLines: expectedMaxLines, child: router), ), ); expect(find.text('$expectedMaxLines'), findsOneWidget); // Initial route will be parsed regardless. expect(parserCalled, isTrue); parserCalled = false; expectedMaxLines = 2; await tester.pumpWidget( buildBoilerPlate( DefaultTextStyle(style: const TextStyle(), maxLines: expectedMaxLines, child: router), ), ); await tester.pump(); expect(find.text('$expectedMaxLines'), findsOneWidget); expect(parserCalled, isFalse); }); testWidgets('Router can initialize with RouterConfig', (WidgetTester tester) async { const expected = 'text'; final provider = SimpleRouteInformationProvider(); addTearDown(provider.dispose); provider.value = RouteInformation(uri: Uri.parse('/')); final delegate = SimpleRouterDelegate(builder: (_, _) => const Text(expected)); addTearDown(delegate.dispose); final config = RouterConfig( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, backButtonDispatcher: RootBackButtonDispatcher(), ); final router = Router.withConfig(config: config); expect(router.routerDelegate, config.routerDelegate); expect(router.routeInformationParser, config.routeInformationParser); expect(router.routeInformationProvider, config.routeInformationProvider); expect(router.backButtonDispatcher, config.backButtonDispatcher); await tester.pumpWidget(buildBoilerPlate(router)); expect(find.text(expected), findsOneWidget); }); group('RouteInformation uri api', () { test('can produce correct uri from location', () async { final info1 = RouteInformation(uri: Uri.parse('/a?abc=def&abc=jkl#mno')); expect(info1.location, '/a?abc=def&abc=jkl#mno'); final Uri uri1 = info1.uri; expect(uri1.scheme, ''); expect(uri1.host, ''); expect(uri1.path, '/a'); expect(uri1.fragment, 'mno'); expect(uri1.queryParametersAll.length, 1); expect(uri1.queryParametersAll['abc']!.length, 2); expect(uri1.queryParametersAll['abc']![0], 'def'); expect(uri1.queryParametersAll['abc']![1], 'jkl'); final info2 = RouteInformation(uri: Uri.parse('1')); expect(info2.location, '1'); final Uri uri2 = info2.uri; expect(uri2.scheme, ''); expect(uri2.host, ''); expect(uri2.path, '1'); expect(uri2.fragment, ''); expect(uri2.queryParametersAll.length, 0); }); test('can produce correct location from uri', () async { final info1 = RouteInformation(uri: Uri.parse('http://mydomain.com')); expect(info1.uri.toString(), 'http://mydomain.com'); expect(info1.location, '/'); final info2 = RouteInformation(uri: Uri.parse('http://mydomain.com/abc?def=ghi&def=jkl#mno')); expect(info2.uri.toString(), 'http://mydomain.com/abc?def=ghi&def=jkl#mno'); expect(info2.location, '/abc?def=ghi&def=jkl#mno'); }); }); test('$PlatformRouteInformationProvider dispatches object creation in constructor', () async { Future createAndDispose() async { PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('http://google.com')), ).dispose(); } await expectLater( await memoryEvents(createAndDispose, PlatformRouteInformationProvider), areCreateAndDispose, ); }); } Widget buildBoilerPlate(Widget child) { return MaterialApp(home: Scaffold(body: child)); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext context, RouteInformation? information); typedef SimpleRouterDelegatePopRoute = Future Function(); typedef SimpleNavigatorRouterDelegatePopPage = bool Function(Route route, T result); typedef RouterReportRouterInformation = void Function(RouteInformation information, RouteInformationReportingType type); typedef CustomRouteInformationParserCallback = RouteInformation Function(RouteInformation information, BuildContext context); class SimpleRouteInformationParser extends RouteInformationParser { SimpleRouteInformationParser(); @override Future parseRouteInformation(RouteInformation information) { return SynchronousFuture(information); } @override RouteInformation restoreRouteInformation(RouteInformation configuration) { return configuration; } } class CustomRouteInformationParser extends RouteInformationParser { const CustomRouteInformationParser(this.callback); final CustomRouteInformationParserCallback callback; @override Future parseRouteInformationWithDependencies( RouteInformation information, BuildContext context, ) { return SynchronousFuture(callback(information, context)); } @override RouteInformation restoreRouteInformation(RouteInformation configuration) { return configuration; } } class SimpleRouterDelegate extends RouterDelegate with ChangeNotifier { SimpleRouterDelegate({this.builder, this.onPopRoute, this.reportConfiguration = false}) { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } RouteInformation? get routeInformation => _routeInformation; 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 setNewRoutePath(RouteInformation configuration) { _routeInformation = configuration; return SynchronousFuture(null); } @override Future popRoute() { return onPopRoute?.call() ?? SynchronousFuture(true); } @override Widget build(BuildContext context) => builder!(context, routeInformation); } class SimpleNavigatorRouterDelegate extends RouterDelegate with PopNavigatorRouterDelegateMixin, ChangeNotifier { SimpleNavigatorRouterDelegate({required this.builder, required this.onPopPage}); @override GlobalKey navigatorKey = GlobalKey(); RouteInformation get routeInformation => _routeInformation; late RouteInformation _routeInformation; SimpleRouterDelegateBuilder builder; SimpleNavigatorRouterDelegatePopPage onPopPage; @override Future setNewRoutePath(RouteInformation configuration) { _routeInformation = configuration; return SynchronousFuture(null); } bool _handlePopPage(Route route, void data) { return onPopPage(route, data); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, onPopPage: _handlePopPage, pages: >[ // We need at least two pages for the pop to propagate through. // Otherwise, the navigator will bubble the pop to the system navigator. const MaterialPage(child: Text('base')), MaterialPage( key: ValueKey(routeInformation.uri.toString()), child: builder(context, routeInformation), ), ], ); } } class SimpleRouteInformationProvider extends RouteInformationProvider with ChangeNotifier { SimpleRouteInformationProvider({this.onRouterReport}) { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } RouterReportRouterInformation? onRouterReport; @override RouteInformation get value => _value; late RouteInformation _value; set value(RouteInformation newValue) { _value = newValue; notifyListeners(); } @override void routerReportsNewRouteInformation( RouteInformation routeInformation, { RouteInformationReportingType type = RouteInformationReportingType.none, }) { _value = routeInformation; onRouterReport?.call(routeInformation, type); } } class SimpleAsyncRouteInformationParser extends RouteInformationParser { SimpleAsyncRouteInformationParser(); late Future parsingFuture; @override Future parseRouteInformation(RouteInformation information) { return parsingFuture = Future.value(information); } @override RouteInformation restoreRouteInformation(RouteInformation configuration) { return configuration; } } class CompleterRouteInformationParser extends RouteInformationParser { CompleterRouteInformationParser(); late Completer completer; @override Future parseRouteInformation(RouteInformation information) async { completer = Completer(); await completer.future; return SynchronousFuture(information); } @override RouteInformation restoreRouteInformation(RouteInformation configuration) { return configuration; } } class SimpleAsyncRouterDelegate extends RouterDelegate with ChangeNotifier { SimpleAsyncRouterDelegate({required this.builder}) { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } RouteInformation? get routeInformation => _routeInformation; RouteInformation? _routeInformation; SimpleRouterDelegateBuilder builder; late Future setNewRouteFuture; @override Future setNewRoutePath(RouteInformation configuration) { _routeInformation = configuration; return setNewRouteFuture = Future.value(); } @override Future popRoute() { return Future.value(true); } @override Widget build(BuildContext context) => builder(context, routeInformation); } class RedirectingInformationParser extends RouteInformationParser { RedirectingInformationParser(this.redirects); final Map redirects; @override Future parseRouteInformation(RouteInformation information) { return SynchronousFuture( redirects[information.uri.toString()] ?? information, ); } @override RouteInformation restoreRouteInformation(RouteInformation configuration) { return configuration; } } class MutableRouterDelegate extends RouterDelegate with ChangeNotifier { MutableRouterDelegate() { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } @override RouteInformation? currentConfiguration; @override Future setNewRoutePath(RouteInformation configuration) { currentConfiguration = configuration; return SynchronousFuture(null); } void updateConfiguration(RouteInformation newConfig) { currentConfiguration = newConfig; notifyListeners(); } @override Future popRoute() { throw UnimplementedError(); } @override Widget build(BuildContext context) { return Text(currentConfiguration?.uri.toString() ?? ''); } } class IntInheritedNotifier extends InheritedNotifier> { const IntInheritedNotifier({super.key, required super.notifier, required super.child}); static int of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType()!.notifier!.value; } }