// 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/gestures.dart'; import 'package:flutter/material.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: [ const Hero(tag: 'a', child: Text('foo')), Builder( builder: (BuildContext context) { return CupertinoButton( child: const Text('next'), onPressed: () { Navigator.push( context, CupertinoPageRoute( 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: [ 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 dynamicColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF000000), darkColor: Color(0xFF000001), ); await tester.pumpWidget( const CupertinoApp( theme: CupertinoThemeData(brightness: Brightness.light), title: '', color: dynamicColor, home: Placeholder(), ), ); expect(tester.widget(find.byType(Title)).color.value, 0xFF000000); await tester.pumpWidget( const CupertinoApp( theme: CupertinoThemeData(brightness: Brightness.dark), color: dynamicColor, title: '', home: Placeholder(), ), ); expect(tester.widget<Title>(find.byType(Title)).color.value, 0xFF000001); }); testWidgets('Can customize initial routes', (WidgetTester tester) async { final 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 key1 = GlobalKey<NavigatorState>(); await tester.pumpWidget(CupertinoApp(navigatorKey: key1, home: const Placeholder())); expect(key1.currentState, isA<NavigatorState>()); final 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 provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); final 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 provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); final 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); var 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 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 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 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 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 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 = 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 provider = PlatformRouteInformationProvider( initialRouteInformation: RouteInformation(uri: Uri.parse('initial')), ); addTearDown(provider.dispose); final 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('CupertinoApp has correct default multitouchDragStrategy', ( WidgetTester tester, ) async { late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); final ScrollBehavior scrollBehavior = ScrollConfiguration.of(capturedContext); expect(scrollBehavior.runtimeType, CupertinoScrollBehavior); expect( scrollBehavior.getMultitouchDragStrategy(capturedContext), MultitouchDragStrategy.averageBoundaryPointers, ); }); testWidgets('CupertinoApp has correct default KeyboardDismissBehavior', ( WidgetTester tester, ) async { late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); expect( ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), ScrollViewKeyboardDismissBehavior.manual, ); }); testWidgets('CupertinoApp can override default KeyboardDismissBehavior', ( WidgetTester tester, ) async { late BuildContext capturedContext; await tester.pumpWidget( CupertinoApp( scrollBehavior: const CupertinoScrollBehavior().copyWith( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, ), home: Builder( builder: (BuildContext context) { capturedContext = context; return const Placeholder(); }, ), ), ); expect( ScrollConfiguration.of(capturedContext).getKeyboardDismissBehavior(capturedContext), ScrollViewKeyboardDismissBehavior.onDrag, ); }); 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(); 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('CupertinoApp uses the dark SystemUIOverlayStyle when the background is light', ( WidgetTester tester, ) async { await tester.pumpWidget( const CupertinoApp( theme: CupertinoThemeData(brightness: Brightness.light), home: CupertinoPageScaffold(child: Text('Hello')), ), ); expect(SystemChrome.latestStyle, SystemUiOverlayStyle.dark); }); testWidgets('CupertinoApp uses the light SystemUIOverlayStyle when the background is dark', ( WidgetTester tester, ) async { await tester.pumpWidget( const CupertinoApp( theme: CupertinoThemeData(brightness: Brightness.dark), home: CupertinoPageScaffold(child: Text('Hello')), ), ); expect(SystemChrome.latestStyle, SystemUiOverlayStyle.light); }); testWidgets( 'CupertinoApp uses the dark SystemUIOverlayStyle when theme brightness is null and the system is in light mode', (WidgetTester tester) async { // The theme brightness is null by default. // The system is in light mode by default. await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), child: CupertinoApp( builder: (BuildContext context, Widget? child) { return const Placeholder(); }, ), ), ); expect(SystemChrome.latestStyle, SystemUiOverlayStyle.dark); }, ); testWidgets( 'CupertinoApp uses the light SystemUIOverlayStyle when theme brightness is null and the system is in dark mode', (WidgetTester tester) async { // The theme brightness is null by default. // Simulates setting the system to dark mode. await tester.pumpWidget( MediaQuery( data: const MediaQueryData(platformBrightness: Brightness.dark), child: CupertinoApp( builder: (BuildContext context, Widget? child) { return const Placeholder(); }, ), ), ); expect(SystemChrome.latestStyle, SystemUiOverlayStyle.light); }, ); 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 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; }); // TODO(justinmc): Cupertino tests should not import Material. // https://github.com/flutter/flutter/issues/177028 testWidgets('CupertinoApp creates a Material theme with colors based off of Cupertino theme', ( WidgetTester tester, ) async { late ThemeData appliedTheme; await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(primaryColor: CupertinoColors.activeGreen), home: Builder( builder: (BuildContext context) { appliedTheme = Theme.of(context); return const SizedBox(); }, ), ), ); expect(appliedTheme.colorScheme.primary, CupertinoColors.activeGreen); }); 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(); addTearDown(focusNode.dispose); final 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 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(), ); testWidgets('CupertinoApp does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const Center( child: SizedBox.shrink(child: CupertinoApp(home: Text('X'))), ), ); expect(tester.getSize(find.byType(CupertinoApp)), Size.zero); }); } 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), ), ], ); } }