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 'package:flutter/cupertino.dart';
6import 'package:flutter/material.dart';
7import 'package:flutter_test/flutter_test.dart';
8
9const String text = 'Hello World! How are you? Life is good!';
10const String alternativeText = 'Everything is awesome!!';
11
12void main() {
13 testWidgets('CupertinoTextFormFieldRow restoration', (WidgetTester tester) async {
14 await tester.pumpWidget(
15 const CupertinoApp(restorationScopeId: 'app', home: RestorableTestWidget()),
16 );
17
18 await restoreAndVerify(tester);
19 });
20
21 testWidgets('CupertinoTextFormFieldRow restoration with external controller', (
22 WidgetTester tester,
23 ) async {
24 await tester.pumpWidget(
25 const CupertinoApp(
26 restorationScopeId: 'root',
27 home: RestorableTestWidget(useExternalController: true),
28 ),
29 );
30
31 await restoreAndVerify(tester);
32 });
33
34 testWidgets('State restoration (No Form ancestor) - onUserInteraction error text validation', (
35 WidgetTester tester,
36 ) async {
37 String? errorText(String? value) => '$value/error';
38 late GlobalKey<FormFieldState<String>> formState;
39
40 Widget builder() {
41 return CupertinoApp(
42 restorationScopeId: 'app',
43 home: MediaQuery(
44 data: const MediaQueryData(),
45 child: Directionality(
46 textDirection: TextDirection.ltr,
47 child: Center(
48 child: StatefulBuilder(
49 builder: (BuildContext context, StateSetter state) {
50 formState = GlobalKey<FormFieldState<String>>();
51 return Material(
52 child: CupertinoTextFormFieldRow(
53 key: formState,
54 autovalidateMode: AutovalidateMode.onUserInteraction,
55 restorationId: 'text_form_field',
56 initialValue: 'foo',
57 validator: errorText,
58 ),
59 );
60 },
61 ),
62 ),
63 ),
64 ),
65 );
66 }
67
68 await tester.pumpWidget(builder());
69
70 // No error text is visible yet.
71 expect(find.text(errorText('foo')!), findsNothing);
72
73 await tester.enterText(find.byType(CupertinoTextFormFieldRow), 'bar');
74 await tester.pumpAndSettle();
75 expect(find.text(errorText('bar')!), findsOneWidget);
76
77 final TestRestorationData data = await tester.getRestorationData();
78 await tester.restartAndRestore();
79 // Error text should be present after restart and restore.
80 expect(find.text(errorText('bar')!), findsOneWidget);
81
82 // Resetting the form state should remove the error text.
83 formState.currentState!.reset();
84 await tester.pumpAndSettle();
85 expect(find.text(errorText('bar')!), findsNothing);
86 await tester.restartAndRestore();
87 // Error text should still be removed after restart and restore.
88 expect(find.text(errorText('bar')!), findsNothing);
89
90 await tester.restoreFrom(data);
91 expect(find.text(errorText('bar')!), findsOneWidget);
92 });
93
94 testWidgets(
95 'State Restoration (No Form ancestor) - validator sets the error text only when validate is called',
96 (WidgetTester tester) async {
97 String? errorText(String? value) => '$value/error';
98 late GlobalKey<FormFieldState<String>> formState;
99
100 Widget builder(AutovalidateMode mode) {
101 return CupertinoApp(
102 restorationScopeId: 'app',
103 home: MediaQuery(
104 data: const MediaQueryData(),
105 child: Directionality(
106 textDirection: TextDirection.ltr,
107 child: Center(
108 child: StatefulBuilder(
109 builder: (BuildContext context, StateSetter state) {
110 formState = GlobalKey<FormFieldState<String>>();
111 return Material(
112 child: CupertinoTextFormFieldRow(
113 key: formState,
114 restorationId: 'form_field',
115 autovalidateMode: mode,
116 initialValue: 'foo',
117 validator: errorText,
118 ),
119 );
120 },
121 ),
122 ),
123 ),
124 ),
125 );
126 }
127
128 // Start off not autovalidating.
129 await tester.pumpWidget(builder(AutovalidateMode.disabled));
130
131 Future<void> checkErrorText(String testValue) async {
132 formState.currentState!.reset();
133 await tester.pumpWidget(builder(AutovalidateMode.disabled));
134 await tester.enterText(find.byType(CupertinoTextFormFieldRow), testValue);
135 await tester.pump();
136
137 // We have to manually validate if we're not autovalidating.
138 expect(find.text(errorText(testValue)!), findsNothing);
139 formState.currentState!.validate();
140 await tester.pump();
141 expect(find.text(errorText(testValue)!), findsOneWidget);
142 final TestRestorationData data = await tester.getRestorationData();
143 await tester.restartAndRestore();
144 // Error text should be present after restart and restore.
145 expect(find.text(errorText(testValue)!), findsOneWidget);
146
147 formState.currentState!.reset();
148 await tester.pumpAndSettle();
149 expect(find.text(errorText(testValue)!), findsNothing);
150
151 await tester.restoreFrom(data);
152 expect(find.text(errorText(testValue)!), findsOneWidget);
153
154 // Try again with autovalidation. Should validate immediately.
155 formState.currentState!.reset();
156 await tester.pumpWidget(builder(AutovalidateMode.always));
157 await tester.enterText(find.byType(CupertinoTextFormFieldRow), testValue);
158 await tester.pump();
159
160 expect(find.text(errorText(testValue)!), findsOneWidget);
161 await tester.restartAndRestore();
162 // Error text should be present after restart and restore.
163 expect(find.text(errorText(testValue)!), findsOneWidget);
164 }
165
166 await checkErrorText('Test');
167 await checkErrorText('');
168 },
169 );
170}
171
172Future<void> restoreAndVerify(WidgetTester tester) async {
173 expect(find.text(text), findsNothing);
174 expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
175
176 await tester.enterText(find.byType(CupertinoTextFormFieldRow), text);
177 await skipPastScrollingAnimation(tester);
178 expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 0);
179
180 await tester.drag(find.byType(Scrollable), const Offset(0, -80));
181 await skipPastScrollingAnimation(tester);
182
183 expect(find.text(text), findsOneWidget);
184 expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
185
186 await tester.restartAndRestore();
187
188 expect(find.text(text), findsOneWidget);
189 expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
190
191 final TestRestorationData data = await tester.getRestorationData();
192
193 await tester.enterText(find.byType(CupertinoTextFormFieldRow), alternativeText);
194 await skipPastScrollingAnimation(tester);
195 await tester.drag(find.byType(Scrollable), const Offset(0, 80));
196 await skipPastScrollingAnimation(tester);
197
198 expect(find.text(text), findsNothing);
199 expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, isNot(60));
200
201 await tester.restoreFrom(data);
202
203 expect(find.text(text), findsOneWidget);
204 expect(tester.state<ScrollableState>(find.byType(Scrollable)).position.pixels, 60);
205}
206
207class RestorableTestWidget extends StatefulWidget {
208 const RestorableTestWidget({super.key, this.useExternalController = false});
209
210 final bool useExternalController;
211
212 @override
213 RestorableTestWidgetState createState() => RestorableTestWidgetState();
214}
215
216class RestorableTestWidgetState extends State<RestorableTestWidget> with RestorationMixin {
217 final RestorableTextEditingController controller = RestorableTextEditingController();
218
219 @override
220 String get restorationId => 'widget';
221
222 @override
223 void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
224 registerForRestoration(controller, 'controller');
225 }
226
227 @override
228 void dispose() {
229 controller.dispose();
230 super.dispose();
231 }
232
233 @override
234 Widget build(BuildContext context) {
235 return Material(
236 child: Align(
237 child: SizedBox(
238 width: 50,
239 child: CupertinoTextFormFieldRow(
240 restorationId: 'text',
241 maxLines: 3,
242 controller: widget.useExternalController ? controller.value : null,
243 ),
244 ),
245 ),
246 );
247 }
248}
249
250Future<void> skipPastScrollingAnimation(WidgetTester tester) async {
251 await tester.pump();
252 await tester.pump(const Duration(milliseconds: 200));
253}
254