// 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:typed_data'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import '../../image_data.dart'; late List<int> selectedTabs; void main() { setUp(() { selectedTabs = <int>[]; }); testWidgets('Last tab gets focus', (WidgetTester tester) async { // 2 nodes for 2 tabs final List<FocusNode> focusNodes = <FocusNode>[FocusNode(), FocusNode()]; await tester.pumpWidget( MaterialApp( home: Material( child: CupertinoTabScaffold( tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return TextField( focusNode: focusNodes[index], autofocus: true, ); }, ), ), ), ); expect(focusNodes[0].hasFocus, isTrue); await tester.tap(find.text('Tab 2')); await tester.pump(); expect(focusNodes[0].hasFocus, isFalse); expect(focusNodes[1].hasFocus, isTrue); await tester.tap(find.text('Tab 1')); await tester.pump(); expect(focusNodes[0].hasFocus, isTrue); expect(focusNodes[1].hasFocus, isFalse); }); testWidgets('Do not affect focus order in the route', (WidgetTester tester) async { final List<FocusNode> focusNodes = <FocusNode>[ FocusNode(), FocusNode(), FocusNode(), FocusNode(), ]; await tester.pumpWidget( MaterialApp( home: Material( child: CupertinoTabScaffold( tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return Column( children: <Widget>[ TextField( focusNode: focusNodes[index * 2], decoration: const InputDecoration( hintText: 'TextField 1', ), ), TextField( focusNode: focusNodes[index * 2 + 1], decoration: const InputDecoration( hintText: 'TextField 2', ), ), ], ); }, ), ), ), ); expect( focusNodes.any((FocusNode node) => node.hasFocus), isFalse, ); await tester.tap(find.widgetWithText(TextField, 'TextField 2')); expect( focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), 1, ); await tester.tap(find.text('Tab 2')); await tester.pump(); await tester.tap(find.widgetWithText(TextField, 'TextField 1')); expect( focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), 2, ); await tester.tap(find.text('Tab 1')); await tester.pump(); // Upon going back to tab 1, the item it tab 1 that previously had the focus // (TextField 2) gets it back. expect( focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), 1, ); }); testWidgets('Tab bar respects themes', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: CupertinoTabScaffold( tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return const Placeholder(); }, ), ), ); BoxDecoration tabDecoration = tester.widget<DecoratedBox>(find.descendant( of: find.byType(CupertinoTabBar), matching: find.byType(DecoratedBox), )).decoration as BoxDecoration; expect(tabDecoration.color, isSameColorAs(const Color(0xF0F9F9F9))); // Inherited from theme. await tester.tap(find.text('Tab 2')); await tester.pump(); // Pump again but with dark theme. await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData( brightness: Brightness.dark, primaryColor: CupertinoColors.destructiveRed, ), home: CupertinoTabScaffold( tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return const Placeholder(); }, ), ), ); tabDecoration = tester.widget<DecoratedBox>(find.descendant( of: find.byType(CupertinoTabBar), matching: find.byType(DecoratedBox), )).decoration as BoxDecoration; expect(tabDecoration.color, isSameColorAs(const Color(0xF01D1D1D))); final RichText tab1 = tester.widget(find.descendant( of: find.text('Tab 1'), matching: find.byType(RichText), )); // Tab 2 should still be selected after changing theme. expect(tab1.text.style!.color!.value, 0xFF757575); final RichText tab2 = tester.widget(find.descendant( of: find.text('Tab 2'), matching: find.byType(RichText), )); expect(tab2.text.style!.color!.value, CupertinoColors.systemRed.darkColor.value); }); testWidgets('dark mode background color', (WidgetTester tester) async { const CupertinoDynamicColor backgroundColor = CupertinoDynamicColor.withBrightness( color: Color(0xFF123456), darkColor: Color(0xFF654321), ); await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.light), home: CupertinoTabScaffold( backgroundColor: backgroundColor, tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return const Placeholder(); }, ), ), ); // The DecoratedBox with the smallest depth is the DecoratedBox of the // CupertinoTabScaffold. BoxDecoration tabDecoration = tester.firstWidget<DecoratedBox>( find.descendant( of: find.byType(CupertinoTabScaffold), matching: find.byType(DecoratedBox), ), ).decoration as BoxDecoration; expect(tabDecoration.color!.value, backgroundColor.color.value); // Dark mode await tester.pumpWidget( CupertinoApp( theme: const CupertinoThemeData(brightness: Brightness.dark), home: CupertinoTabScaffold( backgroundColor: backgroundColor, tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return const Placeholder(); }, ), ), ); tabDecoration = tester.firstWidget<DecoratedBox>( find.descendant( of: find.byType(CupertinoTabScaffold), matching: find.byType(DecoratedBox), ), ).decoration as BoxDecoration; expect(tabDecoration.color!.value, backgroundColor.darkColor.value); }); testWidgets('Does not lose state when focusing on text input', (WidgetTester tester) async { // Regression testing for https://github.com/flutter/flutter/issues/28457. await tester.pumpWidget( MediaQuery( data: const MediaQueryData(), child: MaterialApp( home: Material( child: CupertinoTabScaffold( tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return const TextField(); }, ), ), ), ), ); final EditableTextState editableState = tester.state<EditableTextState>(find.byType(EditableText)); await tester.enterText(find.byType(TextField), "don't lose me"); await tester.pumpWidget( MediaQuery( data: const MediaQueryData( viewInsets: EdgeInsets.only(bottom: 100), ), child: MaterialApp( home: Material( child: CupertinoTabScaffold( tabBar: _buildTabBar(), tabBuilder: (BuildContext context, int index) { return const TextField(); }, ), ), ), ), ); // The exact same state instance is still there. expect(tester.state<EditableTextState>(find.byType(EditableText)), editableState); expect(find.text("don't lose me"), findsOneWidget); }); testWidgets('textScaleFactor is set to 1.0', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Builder(builder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 99), child: CupertinoTabScaffold( tabBar: CupertinoTabBar( items: List<BottomNavigationBarItem>.generate( 10, (int i) => BottomNavigationBarItem(icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: '$i'), ), ), tabBuilder: (BuildContext context, int index) => const Text('content'), ), ); }), ), ); final Iterable<RichText> barItems = tester.widgetList<RichText>( find.descendant( of: find.byType(CupertinoTabBar), matching: find.byType(RichText), ), ); final Iterable<RichText> contents = tester.widgetList<RichText>( find.descendant( of: find.text('content'), matching: find.byType(RichText), skipOffstage: false, ), ); expect(barItems.length, greaterThan(0)); expect(barItems.any((RichText t) => t.textScaleFactor != 1), isFalse); expect(contents.length, greaterThan(0)); expect(contents.any((RichText t) => t.textScaleFactor != 99), isFalse); }); } CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) { return CupertinoTabBar( items: <BottomNavigationBarItem>[ BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 1', ), BottomNavigationBarItem( icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))), label: 'Tab 2', ), ], currentIndex: selectedTab, onTap: (int newTab) => selectedTabs.add(newTab), ); }