1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | @TestOn('!chrome' ) |
6 | library; |
7 | |
8 | import 'package:flutter/foundation.dart'; |
9 | import 'package:flutter/material.dart'; |
10 | import 'package:flutter/services.dart'; |
11 | import 'package:flutter_test/flutter_test.dart'; |
12 | |
13 | class OnTapPage extends StatelessWidget { |
14 | const OnTapPage({super.key, required this.id, required this.onTap}); |
15 | |
16 | final String id; |
17 | final VoidCallback onTap; |
18 | |
19 | @override |
20 | Widget build(BuildContext context) { |
21 | return Scaffold( |
22 | appBar: AppBar(title: Text('Page $id' )), |
23 | body: GestureDetector( |
24 | onTap: onTap, |
25 | behavior: HitTestBehavior.opaque, |
26 | child: Center(child: Text(id, style: Theme.of(context).textTheme.displaySmall)), |
27 | ), |
28 | ); |
29 | } |
30 | } |
31 | |
32 | void main() { |
33 | testWidgets('Push and Pop should send platform messages' , (WidgetTester tester) async { |
34 | final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
35 | '/' : |
36 | (BuildContext context) => OnTapPage( |
37 | id: '/' , |
38 | onTap: () { |
39 | Navigator.pushNamed(context, '/A' ); |
40 | }, |
41 | ), |
42 | '/A' : |
43 | (BuildContext context) => OnTapPage( |
44 | id: 'A' , |
45 | onTap: () { |
46 | Navigator.pop(context); |
47 | }, |
48 | ), |
49 | }; |
50 | |
51 | final List<MethodCall> log = <MethodCall>[]; |
52 | |
53 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, ( |
54 | MethodCall methodCall, |
55 | ) async { |
56 | log.add(methodCall); |
57 | return null; |
58 | }); |
59 | |
60 | await tester.pumpWidget(MaterialApp(routes: routes)); |
61 | |
62 | expect(log, <Object>[ |
63 | isMethodCall('selectSingleEntryHistory' , arguments: null), |
64 | isMethodCall( |
65 | 'routeInformationUpdated' , |
66 | arguments: <String, dynamic>{'uri' : '/' , 'state' : null, 'replace' : false}, |
67 | ), |
68 | ]); |
69 | log.clear(); |
70 | |
71 | await tester.tap(find.text('/' )); |
72 | await tester.pump(); |
73 | await tester.pump(const Duration(seconds: 1)); |
74 | |
75 | expect(log, hasLength(1)); |
76 | expect( |
77 | log.last, |
78 | isMethodCall( |
79 | 'routeInformationUpdated' , |
80 | arguments: <String, dynamic>{'uri' : '/A' , 'state' : null, 'replace' : false}, |
81 | ), |
82 | ); |
83 | log.clear(); |
84 | |
85 | await tester.tap(find.text('A' )); |
86 | await tester.pump(); |
87 | await tester.pump(const Duration(seconds: 1)); |
88 | |
89 | expect(log, hasLength(1)); |
90 | expect( |
91 | log.last, |
92 | isMethodCall( |
93 | 'routeInformationUpdated' , |
94 | arguments: <String, dynamic>{'uri' : '/' , 'state' : null, 'replace' : false}, |
95 | ), |
96 | ); |
97 | }); |
98 | |
99 | testWidgets('Navigator does not report route name by default' , (WidgetTester tester) async { |
100 | final List<MethodCall> log = <MethodCall>[]; |
101 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, ( |
102 | MethodCall methodCall, |
103 | ) async { |
104 | log.add(methodCall); |
105 | return null; |
106 | }); |
107 | |
108 | await tester.pumpWidget( |
109 | Directionality( |
110 | textDirection: TextDirection.ltr, |
111 | child: Navigator( |
112 | pages: const <Page<void>>[TestPage(name: '/' )], |
113 | onPopPage: (Route<void> route, void result) => false, |
114 | ), |
115 | ), |
116 | ); |
117 | |
118 | expect(log, hasLength(0)); |
119 | |
120 | await tester.pumpWidget( |
121 | Directionality( |
122 | textDirection: TextDirection.ltr, |
123 | child: Navigator( |
124 | pages: const <Page<void>>[TestPage(name: '/' ), TestPage(name: '/abc' )], |
125 | onPopPage: (Route<void> route, void result) => false, |
126 | ), |
127 | ), |
128 | ); |
129 | |
130 | await tester.pumpAndSettle(); |
131 | expect(log, hasLength(0)); |
132 | }); |
133 | |
134 | testWidgets('Replace should send platform messages' , (WidgetTester tester) async { |
135 | final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
136 | '/' : |
137 | (BuildContext context) => OnTapPage( |
138 | id: '/' , |
139 | onTap: () { |
140 | Navigator.pushNamed(context, '/A' ); |
141 | }, |
142 | ), |
143 | '/A' : |
144 | (BuildContext context) => OnTapPage( |
145 | id: 'A' , |
146 | onTap: () { |
147 | Navigator.pushReplacementNamed(context, '/B' ); |
148 | }, |
149 | ), |
150 | '/B' : (BuildContext context) => OnTapPage(id: 'B' , onTap: () {}), |
151 | }; |
152 | |
153 | final List<MethodCall> log = <MethodCall>[]; |
154 | |
155 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, ( |
156 | MethodCall methodCall, |
157 | ) async { |
158 | log.add(methodCall); |
159 | return null; |
160 | }); |
161 | |
162 | await tester.pumpWidget(MaterialApp(routes: routes)); |
163 | |
164 | expect(log, <Object>[ |
165 | isMethodCall('selectSingleEntryHistory' , arguments: null), |
166 | isMethodCall( |
167 | 'routeInformationUpdated' , |
168 | arguments: <String, dynamic>{'uri' : '/' , 'state' : null, 'replace' : false}, |
169 | ), |
170 | ]); |
171 | log.clear(); |
172 | |
173 | await tester.tap(find.text('/' )); |
174 | await tester.pump(); |
175 | await tester.pump(const Duration(seconds: 1)); |
176 | |
177 | expect(log, hasLength(1)); |
178 | expect( |
179 | log.last, |
180 | isMethodCall( |
181 | 'routeInformationUpdated' , |
182 | arguments: <String, dynamic>{'uri' : '/A' , 'state' : null, 'replace' : false}, |
183 | ), |
184 | ); |
185 | log.clear(); |
186 | |
187 | await tester.tap(find.text('A' )); |
188 | await tester.pump(); |
189 | await tester.pump(const Duration(seconds: 1)); |
190 | |
191 | expect(log, hasLength(1)); |
192 | expect( |
193 | log.last, |
194 | isMethodCall( |
195 | 'routeInformationUpdated' , |
196 | arguments: <String, dynamic>{'uri' : '/B' , 'state' : null, 'replace' : false}, |
197 | ), |
198 | ); |
199 | }); |
200 | |
201 | testWidgets('Nameless routes should send platform messages' , (WidgetTester tester) async { |
202 | final List<MethodCall> log = <MethodCall>[]; |
203 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, ( |
204 | MethodCall methodCall, |
205 | ) async { |
206 | log.add(methodCall); |
207 | return null; |
208 | }); |
209 | |
210 | await tester.pumpWidget( |
211 | MaterialApp( |
212 | initialRoute: '/home' , |
213 | routes: <String, WidgetBuilder>{ |
214 | '/home' : (BuildContext context) { |
215 | return OnTapPage( |
216 | id: 'Home' , |
217 | onTap: () { |
218 | // Create a route with no name. |
219 | final Route<void> route = MaterialPageRoute<void>( |
220 | builder: (BuildContext context) => const Text('Nameless Route' ), |
221 | ); |
222 | Navigator.push<void>(context, route); |
223 | }, |
224 | ); |
225 | }, |
226 | }, |
227 | ), |
228 | ); |
229 | |
230 | expect(log, <Object>[ |
231 | isMethodCall('selectSingleEntryHistory' , arguments: null), |
232 | isMethodCall( |
233 | 'routeInformationUpdated' , |
234 | arguments: <String, dynamic>{'uri' : '/home' , 'state' : null, 'replace' : false}, |
235 | ), |
236 | ]); |
237 | log.clear(); |
238 | |
239 | await tester.tap(find.text('Home' )); |
240 | await tester.pump(); |
241 | await tester.pump(const Duration(seconds: 1)); |
242 | |
243 | expect(log, isEmpty); |
244 | }); |
245 | |
246 | testWidgets('PlatformRouteInformationProvider reports URL' , (WidgetTester tester) async { |
247 | final List<MethodCall> log = <MethodCall>[]; |
248 | tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.navigation, ( |
249 | MethodCall methodCall, |
250 | ) async { |
251 | log.add(methodCall); |
252 | return null; |
253 | }); |
254 | |
255 | final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( |
256 | initialRouteInformation: RouteInformation(uri: Uri.parse('initial' )), |
257 | ); |
258 | addTearDown(provider.dispose); |
259 | final SimpleRouterDelegate delegate = SimpleRouterDelegate( |
260 | reportConfiguration: true, |
261 | builder: (BuildContext context, RouteInformation information) { |
262 | return Text(information.uri.toString()); |
263 | }, |
264 | ); |
265 | addTearDown(delegate.dispose); |
266 | |
267 | await tester.pumpWidget( |
268 | MaterialApp.router( |
269 | routeInformationProvider: provider, |
270 | routeInformationParser: SimpleRouteInformationParser(), |
271 | routerDelegate: delegate, |
272 | ), |
273 | ); |
274 | expect(find.text('initial' ), findsOneWidget); |
275 | expect(log, <Object>[ |
276 | isMethodCall('selectMultiEntryHistory' , arguments: null), |
277 | isMethodCall( |
278 | 'routeInformationUpdated' , |
279 | arguments: <String, dynamic>{'uri' : 'initial' , 'state' : null, 'replace' : false}, |
280 | ), |
281 | ]); |
282 | log.clear(); |
283 | |
284 | // Triggers a router rebuild and verify the route information is reported |
285 | // to the web engine. |
286 | delegate.routeInformation = RouteInformation(uri: Uri.parse('update' ), state: 'state' ); |
287 | await tester.pump(); |
288 | expect(find.text('update' ), findsOneWidget); |
289 | |
290 | expect(log, <Object>[ |
291 | isMethodCall('selectMultiEntryHistory' , arguments: null), |
292 | isMethodCall( |
293 | 'routeInformationUpdated' , |
294 | arguments: <String, dynamic>{'uri' : 'update' , 'state' : 'state' , 'replace' : false}, |
295 | ), |
296 | ]); |
297 | }); |
298 | } |
299 | |
300 | typedef SimpleRouterDelegateBuilder = |
301 | Widget Function(BuildContext context, RouteInformation information); |
302 | typedef SimpleRouterDelegatePopRoute = Future<bool> Function(); |
303 | |
304 | class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> { |
305 | SimpleRouteInformationParser(); |
306 | |
307 | @override |
308 | Future<RouteInformation> parseRouteInformation(RouteInformation information) { |
309 | return SynchronousFuture<RouteInformation>(information); |
310 | } |
311 | |
312 | @override |
313 | RouteInformation restoreRouteInformation(RouteInformation configuration) { |
314 | return configuration; |
315 | } |
316 | } |
317 | |
318 | class SimpleRouterDelegate extends RouterDelegate<RouteInformation> with ChangeNotifier { |
319 | SimpleRouterDelegate({required this.builder, this.onPopRoute, this.reportConfiguration = false}) { |
320 | if (kFlutterMemoryAllocationsEnabled) { |
321 | ChangeNotifier.maybeDispatchObjectCreation(this); |
322 | } |
323 | } |
324 | |
325 | RouteInformation get routeInformation => _routeInformation; |
326 | late RouteInformation _routeInformation; |
327 | set routeInformation(RouteInformation newValue) { |
328 | _routeInformation = newValue; |
329 | notifyListeners(); |
330 | } |
331 | |
332 | SimpleRouterDelegateBuilder builder; |
333 | SimpleRouterDelegatePopRoute? onPopRoute; |
334 | final bool reportConfiguration; |
335 | |
336 | @override |
337 | RouteInformation? get currentConfiguration { |
338 | if (reportConfiguration) { |
339 | return routeInformation; |
340 | } |
341 | return null; |
342 | } |
343 | |
344 | @override |
345 | Future<void> setNewRoutePath(RouteInformation configuration) { |
346 | _routeInformation = configuration; |
347 | return SynchronousFuture<void>(null); |
348 | } |
349 | |
350 | @override |
351 | Future<bool> popRoute() => onPopRoute?.call() ?? SynchronousFuture<bool>(true); |
352 | |
353 | @override |
354 | Widget build(BuildContext context) => builder(context, routeInformation); |
355 | } |
356 | |
357 | class TestPage extends Page<void> { |
358 | const TestPage({super.key, super.name}); |
359 | |
360 | @override |
361 | Route<void> createRoute(BuildContext context) { |
362 | return PageRouteBuilder<void>( |
363 | settings: this, |
364 | pageBuilder: |
365 | ( |
366 | BuildContext context, |
367 | Animation<double> animation, |
368 | Animation<double> secondaryAnimation, |
369 | ) => const Placeholder(), |
370 | ); |
371 | } |
372 | } |
373 | |