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
5import 'dart:typed_data';
6
7import 'package:flutter/cupertino.dart';
8import 'package:flutter/material.dart';
9import 'package:flutter_test/flutter_test.dart';
10
11import '../../image_data.dart';
12
13late List<int> selectedTabs;
14
15void main() {
16 setUp(() {
17 selectedTabs = <int>[];
18 });
19
20 testWidgets('Last tab gets focus', (WidgetTester tester) async {
21 // 2 nodes for 2 tabs
22 final List<FocusNode> focusNodes = <FocusNode>[];
23 for (int i = 0; i < 2; i++) {
24 final FocusNode focusNode = FocusNode();
25 focusNodes.add(focusNode);
26 addTearDown(focusNode.dispose);
27 }
28
29 await tester.pumpWidget(
30 MaterialApp(
31 home: Material(
32 child: CupertinoTabScaffold(
33 tabBar: _buildTabBar(),
34 tabBuilder: (BuildContext context, int index) {
35 return TextField(focusNode: focusNodes[index], autofocus: true);
36 },
37 ),
38 ),
39 ),
40 );
41
42 expect(focusNodes[0].hasFocus, isTrue);
43
44 await tester.tap(find.text('Tab 2'));
45 await tester.pump();
46
47 expect(focusNodes[0].hasFocus, isFalse);
48 expect(focusNodes[1].hasFocus, isTrue);
49
50 await tester.tap(find.text('Tab 1'));
51 await tester.pump();
52
53 expect(focusNodes[0].hasFocus, isTrue);
54 expect(focusNodes[1].hasFocus, isFalse);
55 });
56
57 testWidgets('Do not affect focus order in the route', (WidgetTester tester) async {
58 final List<FocusNode> focusNodes = <FocusNode>[];
59 for (int i = 0; i < 4; i++) {
60 final FocusNode focusNode = FocusNode();
61 focusNodes.add(focusNode);
62 addTearDown(focusNode.dispose);
63 }
64
65 await tester.pumpWidget(
66 MaterialApp(
67 home: Material(
68 child: CupertinoTabScaffold(
69 tabBar: _buildTabBar(),
70 tabBuilder: (BuildContext context, int index) {
71 return Column(
72 children: <Widget>[
73 TextField(
74 focusNode: focusNodes[index * 2],
75 decoration: const InputDecoration(hintText: 'TextField 1'),
76 ),
77 TextField(
78 focusNode: focusNodes[index * 2 + 1],
79 decoration: const InputDecoration(hintText: 'TextField 2'),
80 ),
81 ],
82 );
83 },
84 ),
85 ),
86 ),
87 );
88
89 expect(focusNodes.any((FocusNode node) => node.hasFocus), isFalse);
90
91 await tester.tap(find.widgetWithText(TextField, 'TextField 2'));
92
93 expect(focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), 1);
94
95 await tester.tap(find.text('Tab 2'));
96 await tester.pump();
97
98 await tester.tap(find.widgetWithText(TextField, 'TextField 1'));
99
100 expect(focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), 2);
101
102 await tester.tap(find.text('Tab 1'));
103 await tester.pump();
104
105 // Upon going back to tab 1, the item it tab 1 that previously had the focus
106 // (TextField 2) gets it back.
107 expect(focusNodes.indexOf(focusNodes.singleWhere((FocusNode node) => node.hasFocus)), 1);
108 });
109
110 testWidgets('Tab bar respects themes', (WidgetTester tester) async {
111 await tester.pumpWidget(
112 CupertinoApp(
113 home: CupertinoTabScaffold(
114 tabBar: _buildTabBar(),
115 tabBuilder: (BuildContext context, int index) {
116 return const Placeholder();
117 },
118 ),
119 ),
120 );
121
122 BoxDecoration tabDecoration =
123 tester
124 .widget<DecoratedBox>(
125 find.descendant(
126 of: find.byType(CupertinoTabBar),
127 matching: find.byType(DecoratedBox),
128 ),
129 )
130 .decoration
131 as BoxDecoration;
132
133 expect(tabDecoration.color, isSameColorAs(const Color(0xF0F9F9F9))); // Inherited from theme.
134
135 await tester.tap(find.text('Tab 2'));
136 await tester.pump();
137
138 // Pump again but with dark theme.
139 await tester.pumpWidget(
140 CupertinoApp(
141 theme: const CupertinoThemeData(
142 brightness: Brightness.dark,
143 primaryColor: CupertinoColors.destructiveRed,
144 ),
145 home: CupertinoTabScaffold(
146 tabBar: _buildTabBar(),
147 tabBuilder: (BuildContext context, int index) {
148 return const Placeholder();
149 },
150 ),
151 ),
152 );
153
154 tabDecoration =
155 tester
156 .widget<DecoratedBox>(
157 find.descendant(
158 of: find.byType(CupertinoTabBar),
159 matching: find.byType(DecoratedBox),
160 ),
161 )
162 .decoration
163 as BoxDecoration;
164
165 expect(tabDecoration.color, isSameColorAs(const Color(0xF01D1D1D)));
166
167 final RichText tab1 = tester.widget(
168 find.descendant(of: find.text('Tab 1'), matching: find.byType(RichText)),
169 );
170 // Tab 2 should still be selected after changing theme.
171 expect(tab1.text.style!.color!.value, 0xFF757575);
172 final RichText tab2 = tester.widget(
173 find.descendant(of: find.text('Tab 2'), matching: find.byType(RichText)),
174 );
175 expect(tab2.text.style!.color!.value, CupertinoColors.systemRed.darkColor.value);
176 });
177
178 testWidgets('dark mode background color', (WidgetTester tester) async {
179 const CupertinoDynamicColor backgroundColor = CupertinoDynamicColor.withBrightness(
180 color: Color(0xFF123456),
181 darkColor: Color(0xFF654321),
182 );
183 await tester.pumpWidget(
184 CupertinoApp(
185 theme: const CupertinoThemeData(brightness: Brightness.light),
186 home: CupertinoTabScaffold(
187 backgroundColor: backgroundColor,
188 tabBar: _buildTabBar(),
189 tabBuilder: (BuildContext context, int index) {
190 return const Placeholder();
191 },
192 ),
193 ),
194 );
195
196 // The DecoratedBox with the smallest depth is the DecoratedBox of the
197 // CupertinoTabScaffold.
198 BoxDecoration tabDecoration =
199 tester
200 .firstWidget<DecoratedBox>(
201 find.descendant(
202 of: find.byType(CupertinoTabScaffold),
203 matching: find.byType(DecoratedBox),
204 ),
205 )
206 .decoration
207 as BoxDecoration;
208
209 expect(tabDecoration.color!.value, backgroundColor.color.value);
210
211 // Dark mode
212 await tester.pumpWidget(
213 CupertinoApp(
214 theme: const CupertinoThemeData(brightness: Brightness.dark),
215 home: CupertinoTabScaffold(
216 backgroundColor: backgroundColor,
217 tabBar: _buildTabBar(),
218 tabBuilder: (BuildContext context, int index) {
219 return const Placeholder();
220 },
221 ),
222 ),
223 );
224
225 tabDecoration =
226 tester
227 .firstWidget<DecoratedBox>(
228 find.descendant(
229 of: find.byType(CupertinoTabScaffold),
230 matching: find.byType(DecoratedBox),
231 ),
232 )
233 .decoration
234 as BoxDecoration;
235
236 expect(tabDecoration.color!.value, backgroundColor.darkColor.value);
237 });
238
239 testWidgets('Does not lose state when focusing on text input', (WidgetTester tester) async {
240 // Regression testing for https://github.com/flutter/flutter/issues/28457.
241
242 await tester.pumpWidget(
243 MediaQuery(
244 data: const MediaQueryData(),
245 child: MaterialApp(
246 home: Material(
247 child: CupertinoTabScaffold(
248 tabBar: _buildTabBar(),
249 tabBuilder: (BuildContext context, int index) {
250 return const TextField();
251 },
252 ),
253 ),
254 ),
255 ),
256 );
257
258 final EditableTextState editableState = tester.state<EditableTextState>(
259 find.byType(EditableText),
260 );
261
262 await tester.enterText(find.byType(TextField), "don't lose me");
263
264 await tester.pumpWidget(
265 MediaQuery(
266 data: const MediaQueryData(viewInsets: EdgeInsets.only(bottom: 100)),
267 child: MaterialApp(
268 home: Material(
269 child: CupertinoTabScaffold(
270 tabBar: _buildTabBar(),
271 tabBuilder: (BuildContext context, int index) {
272 return const TextField();
273 },
274 ),
275 ),
276 ),
277 ),
278 );
279
280 // The exact same state instance is still there.
281 expect(tester.state<EditableTextState>(find.byType(EditableText)), editableState);
282 expect(find.text("don't lose me"), findsOneWidget);
283 });
284
285 testWidgets('textScaleFactor is set to 1.0', (WidgetTester tester) async {
286 await tester.pumpWidget(
287 MaterialApp(
288 home: Builder(
289 builder: (BuildContext context) {
290 return MediaQuery.withClampedTextScaling(
291 minScaleFactor: 99,
292 maxScaleFactor: 99,
293 child: CupertinoTabScaffold(
294 tabBar: CupertinoTabBar(
295 items: List<BottomNavigationBarItem>.generate(
296 10,
297 (int i) => BottomNavigationBarItem(
298 icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
299 label: '$i',
300 ),
301 ),
302 ),
303 tabBuilder: (BuildContext context, int index) => const Text('content'),
304 ),
305 );
306 },
307 ),
308 ),
309 );
310
311 final Iterable<RichText> barItems = tester.widgetList<RichText>(
312 find.descendant(of: find.byType(CupertinoTabBar), matching: find.byType(RichText)),
313 );
314
315 final Iterable<RichText> contents = tester.widgetList<RichText>(
316 find.descendant(
317 of: find.text('content'),
318 matching: find.byType(RichText),
319 skipOffstage: false,
320 ),
321 );
322
323 expect(barItems.length, greaterThan(0));
324 expect(
325 barItems,
326 isNot(contains(predicate((RichText t) => t.textScaler != TextScaler.noScaling))),
327 );
328
329 expect(contents.length, greaterThan(0));
330 expect(
331 contents,
332 isNot(contains(predicate((RichText t) => t.textScaler != const TextScaler.linear(99.0)))),
333 );
334 imageCache.clear();
335 });
336}
337
338CupertinoTabBar _buildTabBar({int selectedTab = 0}) {
339 return CupertinoTabBar(
340 items: <BottomNavigationBarItem>[
341 BottomNavigationBarItem(
342 icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
343 label: 'Tab 1',
344 ),
345 BottomNavigationBarItem(
346 icon: ImageIcon(MemoryImage(Uint8List.fromList(kTransparentImage))),
347 label: 'Tab 2',
348 ),
349 ],
350 currentIndex: selectedTab,
351 onTap: (int newTab) => selectedTabs.add(newTab),
352 );
353}
354