// 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 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('Heroes work', (WidgetTester tester) async { await tester.pumpWidget(CupertinoApp( home: ListView(children: <Widget>[ const Hero(tag: 'a', child: Text('foo')), Builder(builder: (BuildContext context) { return CupertinoButton( child: const Text('next'), onPressed: () { Navigator.push( context, CupertinoPageRoute<void>(builder: (BuildContext context) { return const Hero(tag: 'a', child: Text('foo')); }), ); }, ); }), ]), )); await tester.tap(find.text('next')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); // During the hero transition, the hero widget is lifted off of both // page routes and exists as its own overlay on top of both routes. expect(find.widgetWithText(CupertinoPageRoute, 'foo'), findsNothing); expect(find.widgetWithText(Navigator, 'foo'), findsOneWidget); }); testWidgets('Has default cupertino localizations', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Builder( builder: (BuildContext context) { return Column( children: <Widget>[ Text(CupertinoLocalizations.of(context).selectAllButtonLabel), Text(CupertinoLocalizations.of(context).datePickerMediumDate( DateTime(2018, 10, 4), )), ], ); }, ), ), ); expect(find.text('Select All'), findsOneWidget); expect(find.text('Thu Oct 4 '), findsOneWidget); }); testWidgets('Can use dynamic color', (WidgetTester tester) async { const CupertinoDynamicColor dynamicColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF000000), darkColor: Color(0xFF000001), ); await tester.pumpWidget(const CupertinoApp( theme: CupertinoThemeData(brightness: Brightness.light), color: dynamicColor, home: Placeholder(), )); expect(tester.widget<Title>(find.byType(Title)).color.value, 0xFF000000); await tester.pumpWidget(const CupertinoApp( theme: CupertinoThemeData(brightness: Brightness.dark), color: dynamicColor, home: Placeholder(), )); expect(tester.widget<Title>(find.byType(Title)).color.value, 0xFF000001); }); testWidgets('Can customize initial routes', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( CupertinoApp( navigatorKey: navigatorKey, onGenerateInitialRoutes: (String initialRoute) { expect(initialRoute, '/abc'); return <Route<void>>[ PageRouteBuilder<void>( pageBuilder: ( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, ) { return const Text('non-regular page one'); }, ), PageRouteBuilder<void>( pageBuilder: ( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, ) { return const Text('non-regular page two'); }, ), ]; }, initialRoute: '/abc', routes: <String, WidgetBuilder>{ '/': (BuildContext context) => const Text('regular page one'), '/abc': (BuildContext context) => const Text('regular page two'), }, ), ); expect(find.text('non-regular page two'), findsOneWidget); expect(find.text('non-regular page one'), findsNothing); expect(find.text('regular page one'), findsNothing); expect(find.text('regular page two'), findsNothing); navigatorKey.currentState!.pop(); await tester.pumpAndSettle(); expect(find.text('non-regular page two'), findsNothing); expect(find.text('non-regular page one'), findsOneWidget); expect(find.text('regular page one'), findsNothing); expect(find.text('regular page two'), findsNothing); }); testWidgets('CupertinoApp.navigatorKey can be updated', (WidgetTester tester) async { final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>(); await tester.pumpWidget(CupertinoApp( navigatorKey: key1, home: const Placeholder(), )); expect(key1.currentState, isA<NavigatorState>()); final GlobalKey<NavigatorState> key2 = GlobalKey<NavigatorState>(); await tester.pumpWidget(CupertinoApp( navigatorKey: key2, home: const Placeholder(), )); expect(key2.currentState, isA<NavigatorState>()); expect(key1.currentState, isNull); }); testWidgets('CupertinoApp.router works', (WidgetTester tester) async { final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); addTearDown(provider.dispose); final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation( uri: Uri.parse('popped'), ); return route.didPop(result); }, ); addTearDown(delegate.dispose); await tester.pumpWidget(CupertinoApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, )); expect(find.text('initial'), findsOneWidget); // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); testWidgets('CupertinoApp.router works with onNavigationNotification', (WidgetTester tester) async { // This is a regression test for https://github.com/flutter/flutter/issues/139903. final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); addTearDown(provider.dispose); final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation( uri: Uri.parse('popped'), ); return route.didPop(result); }, ); addTearDown(delegate.dispose); int navigationCount = 0; await tester.pumpWidget(CupertinoApp.router( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate, onNavigationNotification: (NavigationNotification? notification) { navigationCount += 1; return true; }, )); expect(find.text('initial'), findsOneWidget); expect(navigationCount, greaterThan(0)); final int navigationCountAfterBuild = navigationCount; // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); expect(navigationCount, greaterThan(navigationCountAfterBuild)); }); testWidgets('CupertinoApp.router route information parser is optional', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation( uri: Uri.parse('popped'), ); return route.didPop(result); }, ); addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); await tester.pumpWidget(CupertinoApp.router( routerDelegate: delegate, )); expect(find.text('initial'), findsOneWidget); // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); testWidgets('CupertinoApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation( uri: Uri.parse('popped'), ); return route.didPop(result); }, ); addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); addTearDown(provider.dispose); await tester.pumpWidget(CupertinoApp.router( routeInformationProvider: provider, routerDelegate: delegate, )); expect(tester.takeException(), isAssertionError); }); testWidgets('CupertinoApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation( uri: Uri.parse('popped'), ); return route.didPop(result); }, ); addTearDown(delegate.dispose); delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>(routerDelegate: delegate); await tester.pumpWidget(CupertinoApp.router( routerDelegate: delegate, routerConfig: routerConfig, )); expect(tester.takeException(), isAssertionError); }); testWidgets('CupertinoApp.router router config works', (WidgetTester tester) async { late SimpleNavigatorRouterDelegate delegate; addTearDown(() => delegate.dispose()); final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation( uri: Uri.parse('initial'), ), ); addTearDown(provider.dispose); final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>( routeInformationProvider: provider, routeInformationParser: SimpleRouteInformationParser(), routerDelegate: delegate = SimpleNavigatorRouterDelegate( builder: (BuildContext context, RouteInformation information) { return Text(information.uri.toString()); }, onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { delegate.routeInformation = RouteInformation( uri: Uri.parse('popped'), ); return route.didPop(result); }, ), backButtonDispatcher: RootBackButtonDispatcher() ); await tester.pumpWidget(CupertinoApp.router( routerConfig: routerConfig, )); expect(find.text('initial'), findsOneWidget); // Simulate android back button intent. final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); await tester.pumpAndSettle(); expect(find.text('popped'), findsOneWidget); }); testWidgets('CupertinoApp has correct default ScrollBehavior', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); expect(ScrollConfiguration.of(capturedContext).runtimeType, CupertinoScrollBehavior); }); testWidgets('A ScrollBehavior can be set for CupertinoApp', (WidgetTester tester) async { late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( scrollBehavior: const MockScrollBehavior(), home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); expect(scrollBehavior.runtimeType, MockScrollBehavior); expect(scrollBehavior.getScrollPhysics(capturedContext).runtimeType, NeverScrollableScrollPhysics); }); testWidgets('When `useInheritedMediaQuery` is true an existing MediaQuery is used if one is available', (WidgetTester tester) async { late BuildContext capturedContext; final UniqueKey uniqueKey = UniqueKey(); await tester.pumpWidget( MediaQuery( key: uniqueKey, data: const MediaQueryData(), child: CupertinoApp( useInheritedMediaQuery: true, builder: (BuildContext context, Widget? child) { capturedContext = context; return const Placeholder(); }, color: const Color(0xFF123456), ), ), ); expect(capturedContext.dependOnInheritedWidgetOfExactType<MediaQuery>()?.key, uniqueKey); }); testWidgets('Text color is correctly resolved when CupertinoThemeData.brightness is null', (WidgetTester tester) async { debugBrightnessOverride = Brightness.dark; await tester.pumpWidget( const CupertinoApp( home: CupertinoPageScaffold( child: Text('Hello'), ), ), ); final RenderParagraph paragraph = tester.renderObject(find.text('Hello')); final CupertinoDynamicColor textColor = paragraph.text.style!.color! as CupertinoDynamicColor; // App with non-null brightness, so resolving color // doesn't depend on the MediaQuery.platformBrightness. late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData( brightness: Brightness.dark, ), home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); // We expect the string representations of the colors to have darkColor indicated (*) as effective color. // (color = Color(0xff000000), *darkColor = Color(0xffffffff)*, resolved by: Builder) expect(textColor.toString(), CupertinoColors.label.resolveFrom(capturedContext).toString()); debugBrightnessOverride = null; }); testWidgets('Cursor color is resolved when CupertinoThemeData.brightness is null', (WidgetTester tester) async { debugBrightnessOverride = Brightness.dark; RenderEditable findRenderEditable(WidgetTester tester) { final RenderObject root = tester.renderObject(find.byType(EditableText)); expect(root, isNotNull); RenderEditable? renderEditable; void recursiveFinder(RenderObject child) { if (child is RenderEditable) { renderEditable = child; return; } child.visitChildren(recursiveFinder); } root.visitChildren(recursiveFinder); expect(renderEditable, isNotNull); return renderEditable!; } final FocusNode focusNode = FocusNode(); addTearDown(focusNode.dispose); final TextEditingController controller = TextEditingController(); addTearDown(controller.dispose); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData( primaryColor: CupertinoColors.activeOrange, ), home: CupertinoPageScaffold( child: Builder( builder: (BuildContext context) { return EditableText( backgroundCursorColor: DefaultSelectionStyle.of(context).selectionColor!, cursorColor: DefaultSelectionStyle.of(context).cursorColor!, controller: controller, focusNode: focusNode, style: const TextStyle(), ); }, ), ), ), ); final RenderEditable editableText = findRenderEditable(tester); final Color cursorColor = editableText.cursorColor!; // Cursor color should be equal to the dark variant of the primary color. // Alpha value needs to be 0, because cursor is not visible by default. expect(cursorColor, CupertinoColors.activeOrange.darkColor.withAlpha(0)); debugBrightnessOverride = null; }); testWidgets('Assert in buildScrollbar that controller != null when using it', (WidgetTester tester) async { const ScrollBehavior defaultBehavior = CupertinoScrollBehavior(); late BuildContext capturedContext; await tester.pumpWidget(ScrollConfiguration( // Avoid the default ones here. behavior: const CupertinoScrollBehavior().copyWith(scrollbars: false), child: SingleChildScrollView( child: Builder( builder: (BuildContext context) { capturedContext = context; return Container(height: 1000.0); }, ), ), )); const ScrollableDetails details = ScrollableDetails(direction: AxisDirection.down); final Widget child = Container(); switch (defaultTargetPlatform) { case TargetPlatform.android: case TargetPlatform.fuchsia: case TargetPlatform.iOS: // Does not throw if we aren't using it. defaultBehavior.buildScrollbar(capturedContext, child, details); case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: expect( () { defaultBehavior.buildScrollbar(capturedContext, child, details); }, throwsA( isA<AssertionError>().having((AssertionError error) => error.toString(), 'description', contains('details.controller != null')), ), ); } }, variant: TargetPlatformVariant.all()); } class MockScrollBehavior extends ScrollBehavior { const MockScrollBehavior(); @override ScrollPhysics getScrollPhysics(BuildContext context) => const NeverScrollableScrollPhysics(); } typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext context, RouteInformation information); typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate); class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> { SimpleRouteInformationParser(); @override Future<RouteInformation> parseRouteInformation(RouteInformation information) { return SynchronousFuture<RouteInformation>(information); } @override RouteInformation restoreRouteInformation(RouteInformation configuration) { return configuration; } } class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier { SimpleNavigatorRouterDelegate({ required this.builder, this.onPopPage, }); @override GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); RouteInformation get routeInformation => _routeInformation; late RouteInformation _routeInformation; set routeInformation(RouteInformation newValue) { _routeInformation = newValue; notifyListeners(); } SimpleRouterDelegateBuilder builder; SimpleNavigatorRouterDelegatePopPage<void>? onPopPage; @override Future<void> setNewRoutePath(RouteInformation configuration) { _routeInformation = configuration; return SynchronousFuture<void>(null); } bool _handlePopPage(Route<void> route, void data) { return onPopPage!(route, data, this); } @override Widget build(BuildContext context) { return Navigator( key: navigatorKey, onPopPage: _handlePopPage, pages: <Page<void>>[ // We need at least two pages for the pop to propagate through. // Otherwise, the navigator will bubble the pop to the system navigator. const CupertinoPage<void>( child: Text('base'), ), CupertinoPage<void>( key: ValueKey<String?>(routeInformation.uri.toString()), child: builder(context, routeInformation), ), ], ); } }