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 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/gestures.dart'; |
7 | import 'package:flutter/material.dart'; |
8 | import 'package:flutter/rendering.dart'; |
9 | import 'package:flutter/src/physics/utils.dart' show nearEqual; |
10 | import 'package:flutter_test/flutter_test.dart'; |
11 | |
12 | import '../widgets/semantics_tester.dart'; |
13 | |
14 | void main() { |
15 | // Regression test for https://github.com/flutter/flutter/issues/105833 |
16 | testWidgets('Drag gesture uses provided gesture settings' , (WidgetTester tester) async { |
17 | RangeValues values = const RangeValues(0.1, 0.5); |
18 | bool dragStarted = false; |
19 | final Key sliderKey = UniqueKey(); |
20 | |
21 | await tester.pumpWidget( |
22 | MaterialApp( |
23 | home: Directionality( |
24 | textDirection: TextDirection.ltr, |
25 | child: StatefulBuilder( |
26 | builder: (BuildContext context, StateSetter setState) { |
27 | return Material( |
28 | child: Center( |
29 | child: GestureDetector( |
30 | behavior: HitTestBehavior.deferToChild, |
31 | onHorizontalDragStart: (DragStartDetails details) { |
32 | dragStarted = true; |
33 | }, |
34 | child: MediaQuery( |
35 | data: MediaQuery.of( |
36 | context, |
37 | ).copyWith(gestureSettings: const DeviceGestureSettings(touchSlop: 20)), |
38 | child: RangeSlider( |
39 | key: sliderKey, |
40 | values: values, |
41 | onChanged: (RangeValues newValues) { |
42 | setState(() { |
43 | values = newValues; |
44 | }); |
45 | }, |
46 | ), |
47 | ), |
48 | ), |
49 | ), |
50 | ); |
51 | }, |
52 | ), |
53 | ), |
54 | ), |
55 | ); |
56 | |
57 | TestGesture drag = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); |
58 | await tester.pump(kPressTimeout); |
59 | |
60 | // Less than configured touch slop, more than default touch slop |
61 | await drag.moveBy(const Offset(19.0, 0)); |
62 | await tester.pump(); |
63 | |
64 | expect(values, const RangeValues(0.1, 0.5)); |
65 | expect(dragStarted, true); |
66 | |
67 | dragStarted = false; |
68 | |
69 | await drag.up(); |
70 | await tester.pumpAndSettle(); |
71 | |
72 | drag = await tester.startGesture(tester.getCenter(find.byKey(sliderKey))); |
73 | await tester.pump(kPressTimeout); |
74 | |
75 | bool sliderEnd = false; |
76 | |
77 | await tester.pumpWidget( |
78 | MaterialApp( |
79 | home: Directionality( |
80 | textDirection: TextDirection.ltr, |
81 | child: StatefulBuilder( |
82 | builder: (BuildContext context, StateSetter setState) { |
83 | return Material( |
84 | child: Center( |
85 | child: GestureDetector( |
86 | behavior: HitTestBehavior.deferToChild, |
87 | onHorizontalDragStart: (DragStartDetails details) { |
88 | dragStarted = true; |
89 | }, |
90 | child: MediaQuery( |
91 | data: MediaQuery.of( |
92 | context, |
93 | ).copyWith(gestureSettings: const DeviceGestureSettings(touchSlop: 10)), |
94 | child: RangeSlider( |
95 | key: sliderKey, |
96 | values: values, |
97 | onChanged: (RangeValues newValues) { |
98 | setState(() { |
99 | values = newValues; |
100 | }); |
101 | }, |
102 | onChangeEnd: (RangeValues newValues) { |
103 | sliderEnd = true; |
104 | }, |
105 | ), |
106 | ), |
107 | ), |
108 | ), |
109 | ); |
110 | }, |
111 | ), |
112 | ), |
113 | ), |
114 | ); |
115 | |
116 | // More than touch slop. |
117 | await drag.moveBy(const Offset(12.0, 0)); |
118 | |
119 | await drag.up(); |
120 | await tester.pumpAndSettle(); |
121 | |
122 | expect(sliderEnd, true); |
123 | expect(dragStarted, false); |
124 | }); |
125 | |
126 | testWidgets('Range Slider can move when tapped (continuous LTR)' , (WidgetTester tester) async { |
127 | RangeValues values = const RangeValues(0.3, 0.7); |
128 | |
129 | await tester.pumpWidget( |
130 | MaterialApp( |
131 | home: Directionality( |
132 | textDirection: TextDirection.ltr, |
133 | child: StatefulBuilder( |
134 | builder: (BuildContext context, StateSetter setState) { |
135 | return Material( |
136 | child: Center( |
137 | child: RangeSlider( |
138 | values: values, |
139 | onChanged: (RangeValues newValues) { |
140 | setState(() { |
141 | values = newValues; |
142 | }); |
143 | }, |
144 | ), |
145 | ), |
146 | ); |
147 | }, |
148 | ), |
149 | ), |
150 | ), |
151 | ); |
152 | |
153 | // No thumbs get select when tapping between the thumbs outside the touch |
154 | // boundaries |
155 | expect(values, equals(const RangeValues(0.3, 0.7))); |
156 | // taps at 0.5 |
157 | await tester.tap(find.byType(RangeSlider)); |
158 | await tester.pump(); |
159 | expect(values, equals(const RangeValues(0.3, 0.7))); |
160 | |
161 | // Get the bounds of the track by finding the slider edges and translating |
162 | // inwards by the overlay radius. |
163 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
164 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
165 | |
166 | // The start thumb is selected when tapping the left inactive track. |
167 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; |
168 | await tester.tapAt(leftTarget); |
169 | expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01)); |
170 | expect(values.end, equals(0.7)); |
171 | |
172 | // The end thumb is selected when tapping the right inactive track. |
173 | await tester.pump(); |
174 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9; |
175 | await tester.tapAt(rightTarget); |
176 | expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01)); |
177 | expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01)); |
178 | }); |
179 | |
180 | testWidgets('Range Slider can move when tapped (continuous RTL)' , (WidgetTester tester) async { |
181 | RangeValues values = const RangeValues(0.3, 0.7); |
182 | |
183 | await tester.pumpWidget( |
184 | MaterialApp( |
185 | home: Directionality( |
186 | textDirection: TextDirection.rtl, |
187 | child: StatefulBuilder( |
188 | builder: (BuildContext context, StateSetter setState) { |
189 | return Material( |
190 | child: Center( |
191 | child: RangeSlider( |
192 | values: values, |
193 | onChanged: (RangeValues newValues) { |
194 | setState(() { |
195 | values = newValues; |
196 | }); |
197 | }, |
198 | ), |
199 | ), |
200 | ); |
201 | }, |
202 | ), |
203 | ), |
204 | ), |
205 | ); |
206 | |
207 | // No thumbs get select when tapping between the thumbs outside the touch |
208 | // boundaries |
209 | expect(values, equals(const RangeValues(0.3, 0.7))); |
210 | // taps at 0.5 |
211 | await tester.tap(find.byType(RangeSlider)); |
212 | await tester.pump(); |
213 | expect(values, equals(const RangeValues(0.3, 0.7))); |
214 | |
215 | // Get the bounds of the track by finding the slider edges and translating |
216 | // inwards by the overlay radius. |
217 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
218 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
219 | |
220 | // The end thumb is selected when tapping the left inactive track. |
221 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; |
222 | await tester.tapAt(leftTarget); |
223 | expect(values.start, 0.3); |
224 | expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01)); |
225 | |
226 | // The start thumb is selected when tapping the right inactive track. |
227 | await tester.pump(); |
228 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9; |
229 | await tester.tapAt(rightTarget); |
230 | expect(values.start, moreOrLessEquals(0.1, epsilon: 0.01)); |
231 | expect(values.end, moreOrLessEquals(0.9, epsilon: 0.01)); |
232 | }); |
233 | |
234 | testWidgets('Range Slider can move when tapped (discrete LTR)' , (WidgetTester tester) async { |
235 | RangeValues values = const RangeValues(30, 70); |
236 | |
237 | await tester.pumpWidget( |
238 | MaterialApp( |
239 | home: Directionality( |
240 | textDirection: TextDirection.ltr, |
241 | child: StatefulBuilder( |
242 | builder: (BuildContext context, StateSetter setState) { |
243 | return Material( |
244 | child: Center( |
245 | child: RangeSlider( |
246 | values: values, |
247 | max: 100.0, |
248 | divisions: 10, |
249 | onChanged: (RangeValues newValues) { |
250 | setState(() { |
251 | values = newValues; |
252 | }); |
253 | }, |
254 | ), |
255 | ), |
256 | ); |
257 | }, |
258 | ), |
259 | ), |
260 | ), |
261 | ); |
262 | |
263 | // No thumbs get select when tapping between the thumbs outside the touch |
264 | // boundaries |
265 | expect(values, equals(const RangeValues(30, 70))); |
266 | // taps at 0.5 |
267 | await tester.tap(find.byType(RangeSlider)); |
268 | await tester.pumpAndSettle(); |
269 | expect(values, equals(const RangeValues(30, 70))); |
270 | |
271 | // Get the bounds of the track by finding the slider edges and translating |
272 | // inwards by the overlay radius. |
273 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
274 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
275 | |
276 | // The start thumb is selected when tapping the left inactive track. |
277 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; |
278 | await tester.tapAt(leftTarget); |
279 | await tester.pumpAndSettle(); |
280 | expect(values.start.round(), equals(10)); |
281 | expect(values.end.round(), equals(70)); |
282 | |
283 | // The end thumb is selected when tapping the right inactive track. |
284 | await tester.pump(); |
285 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9; |
286 | await tester.tapAt(rightTarget); |
287 | await tester.pumpAndSettle(); |
288 | expect(values.start.round(), equals(10)); |
289 | expect(values.end.round(), equals(90)); |
290 | }); |
291 | |
292 | testWidgets('Range Slider can move when tapped (discrete RTL)' , (WidgetTester tester) async { |
293 | RangeValues values = const RangeValues(30, 70); |
294 | |
295 | await tester.pumpWidget( |
296 | MaterialApp( |
297 | home: Directionality( |
298 | textDirection: TextDirection.rtl, |
299 | child: StatefulBuilder( |
300 | builder: (BuildContext context, StateSetter setState) { |
301 | return Material( |
302 | child: Center( |
303 | child: RangeSlider( |
304 | values: values, |
305 | max: 100, |
306 | divisions: 10, |
307 | onChanged: (RangeValues newValues) { |
308 | setState(() { |
309 | values = newValues; |
310 | }); |
311 | }, |
312 | ), |
313 | ), |
314 | ); |
315 | }, |
316 | ), |
317 | ), |
318 | ), |
319 | ); |
320 | |
321 | // No thumbs get select when tapping between the thumbs outside the touch |
322 | // boundaries |
323 | expect(values, equals(const RangeValues(30, 70))); |
324 | // taps at 0.5 |
325 | await tester.tap(find.byType(RangeSlider)); |
326 | await tester.pumpAndSettle(); |
327 | expect(values, equals(const RangeValues(30, 70))); |
328 | |
329 | // Get the bounds of the track by finding the slider edges and translating |
330 | // inwards by the overlay radius. |
331 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
332 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
333 | |
334 | // The start thumb is selected when tapping the left inactive track. |
335 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.1; |
336 | await tester.tapAt(leftTarget); |
337 | await tester.pumpAndSettle(); |
338 | expect(values.start.round(), equals(30)); |
339 | expect(values.end.round(), equals(90)); |
340 | |
341 | // The end thumb is selected when tapping the right inactive track. |
342 | await tester.pump(); |
343 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.9; |
344 | await tester.tapAt(rightTarget); |
345 | await tester.pumpAndSettle(); |
346 | expect(values.start.round(), equals(10)); |
347 | expect(values.end.round(), equals(90)); |
348 | }); |
349 | |
350 | testWidgets('Range Slider thumbs can be dragged to the min and max (continuous LTR)' , ( |
351 | WidgetTester tester, |
352 | ) async { |
353 | RangeValues values = const RangeValues(0.3, 0.7); |
354 | |
355 | await tester.pumpWidget( |
356 | MaterialApp( |
357 | home: Directionality( |
358 | textDirection: TextDirection.ltr, |
359 | child: StatefulBuilder( |
360 | builder: (BuildContext context, StateSetter setState) { |
361 | return Material( |
362 | child: Center( |
363 | child: RangeSlider( |
364 | values: values, |
365 | onChanged: (RangeValues newValues) { |
366 | setState(() { |
367 | values = newValues; |
368 | }); |
369 | }, |
370 | ), |
371 | ), |
372 | ); |
373 | }, |
374 | ), |
375 | ), |
376 | ), |
377 | ); |
378 | |
379 | // Get the bounds of the track by finding the slider edges and translating |
380 | // inwards by the overlay radius. |
381 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
382 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
383 | |
384 | // Drag the start thumb to the min. |
385 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
386 | await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4); |
387 | expect(values.start, equals(0)); |
388 | |
389 | // Drag the end thumb to the max. |
390 | await tester.pumpAndSettle(); |
391 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
392 | await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4); |
393 | expect(values.end, equals(1)); |
394 | }); |
395 | |
396 | testWidgets('Range Slider thumbs can be dragged to the min and max (continuous RTL)' , ( |
397 | WidgetTester tester, |
398 | ) async { |
399 | RangeValues values = const RangeValues(0.3, 0.7); |
400 | |
401 | await tester.pumpWidget( |
402 | MaterialApp( |
403 | home: Directionality( |
404 | textDirection: TextDirection.rtl, |
405 | child: StatefulBuilder( |
406 | builder: (BuildContext context, StateSetter setState) { |
407 | return Material( |
408 | child: Center( |
409 | child: RangeSlider( |
410 | values: values, |
411 | onChanged: (RangeValues newValues) { |
412 | setState(() { |
413 | values = newValues; |
414 | }); |
415 | }, |
416 | ), |
417 | ), |
418 | ); |
419 | }, |
420 | ), |
421 | ), |
422 | ), |
423 | ); |
424 | |
425 | // Get the bounds of the track by finding the slider edges and translating |
426 | // inwards by the overlay radius. |
427 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
428 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
429 | |
430 | // Drag the end thumb to the max. |
431 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
432 | await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4); |
433 | expect(values.end, equals(1)); |
434 | |
435 | // Drag the start thumb to the min. |
436 | await tester.pumpAndSettle(); |
437 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
438 | await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4); |
439 | expect(values.start, equals(0)); |
440 | }); |
441 | |
442 | testWidgets('Range Slider thumbs can be dragged to the min and max (discrete LTR)' , ( |
443 | WidgetTester tester, |
444 | ) async { |
445 | RangeValues values = const RangeValues(30, 70); |
446 | |
447 | await tester.pumpWidget( |
448 | MaterialApp( |
449 | home: Directionality( |
450 | textDirection: TextDirection.ltr, |
451 | child: StatefulBuilder( |
452 | builder: (BuildContext context, StateSetter setState) { |
453 | return Material( |
454 | child: Center( |
455 | child: RangeSlider( |
456 | values: values, |
457 | max: 100, |
458 | divisions: 10, |
459 | onChanged: (RangeValues newValues) { |
460 | setState(() { |
461 | values = newValues; |
462 | }); |
463 | }, |
464 | ), |
465 | ), |
466 | ); |
467 | }, |
468 | ), |
469 | ), |
470 | ), |
471 | ); |
472 | |
473 | // Get the bounds of the track by finding the slider edges and translating |
474 | // inwards by the overlay radius. |
475 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
476 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
477 | |
478 | // Drag the start thumb to the min. |
479 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
480 | await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4); |
481 | expect(values.start, equals(0)); |
482 | |
483 | // Drag the end thumb to the max. |
484 | await tester.pumpAndSettle(); |
485 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
486 | await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4); |
487 | expect(values.end, equals(100)); |
488 | }); |
489 | |
490 | testWidgets('Range Slider thumbs can be dragged to the min and max (discrete RTL)' , ( |
491 | WidgetTester tester, |
492 | ) async { |
493 | RangeValues values = const RangeValues(30, 70); |
494 | |
495 | await tester.pumpWidget( |
496 | MaterialApp( |
497 | home: Directionality( |
498 | textDirection: TextDirection.rtl, |
499 | child: StatefulBuilder( |
500 | builder: (BuildContext context, StateSetter setState) { |
501 | return Material( |
502 | child: Center( |
503 | child: RangeSlider( |
504 | values: values, |
505 | max: 100, |
506 | divisions: 10, |
507 | onChanged: (RangeValues newValues) { |
508 | setState(() { |
509 | values = newValues; |
510 | }); |
511 | }, |
512 | ), |
513 | ), |
514 | ); |
515 | }, |
516 | ), |
517 | ), |
518 | ), |
519 | ); |
520 | |
521 | // Get the bounds of the track by finding the slider edges and translating |
522 | // inwards by the overlay radius. |
523 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
524 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
525 | |
526 | // Drag the end thumb to the max. |
527 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
528 | await tester.dragFrom(leftTarget, topLeft + (bottomRight - topLeft) * -0.4); |
529 | expect(values.end, equals(100)); |
530 | |
531 | // Drag the start thumb to the min. |
532 | await tester.pumpAndSettle(); |
533 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
534 | await tester.dragFrom(rightTarget, topLeft + (bottomRight - topLeft) * 0.4); |
535 | expect(values.start, equals(0)); |
536 | }); |
537 | |
538 | testWidgets( |
539 | 'Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous LTR)' , |
540 | (WidgetTester tester) async { |
541 | RangeValues values = const RangeValues(0.3, 0.7); |
542 | |
543 | await tester.pumpWidget( |
544 | MaterialApp( |
545 | home: Directionality( |
546 | textDirection: TextDirection.ltr, |
547 | child: StatefulBuilder( |
548 | builder: (BuildContext context, StateSetter setState) { |
549 | return Material( |
550 | child: Center( |
551 | child: RangeSlider( |
552 | values: values, |
553 | onChanged: (RangeValues newValues) { |
554 | setState(() { |
555 | values = newValues; |
556 | }); |
557 | }, |
558 | ), |
559 | ), |
560 | ); |
561 | }, |
562 | ), |
563 | ), |
564 | ), |
565 | ); |
566 | |
567 | // Get the bounds of the track by finding the slider edges and translating |
568 | // inwards by the overlay radius. |
569 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
570 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
571 | final Offset middle = topLeft + bottomRight / 2; |
572 | |
573 | // Drag the start thumb towards the center. |
574 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
575 | await tester.dragFrom(leftTarget, middle - leftTarget); |
576 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05)); |
577 | |
578 | // Drag the end thumb towards the center. |
579 | await tester.pumpAndSettle(); |
580 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
581 | await tester.dragFrom(rightTarget, middle - rightTarget); |
582 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05)); |
583 | |
584 | // Drag the start thumb apart. |
585 | await tester.pumpAndSettle(); |
586 | await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3); |
587 | expect(values.start, moreOrLessEquals(0.2, epsilon: 0.05)); |
588 | }, |
589 | ); |
590 | |
591 | testWidgets( |
592 | 'Range Slider thumbs can be dragged together and the start thumb can be dragged apart (continuous RTL)' , |
593 | (WidgetTester tester) async { |
594 | RangeValues values = const RangeValues(0.3, 0.7); |
595 | |
596 | await tester.pumpWidget( |
597 | MaterialApp( |
598 | home: Directionality( |
599 | textDirection: TextDirection.rtl, |
600 | child: StatefulBuilder( |
601 | builder: (BuildContext context, StateSetter setState) { |
602 | return Material( |
603 | child: Center( |
604 | child: RangeSlider( |
605 | values: values, |
606 | onChanged: (RangeValues newValues) { |
607 | setState(() { |
608 | values = newValues; |
609 | }); |
610 | }, |
611 | ), |
612 | ), |
613 | ); |
614 | }, |
615 | ), |
616 | ), |
617 | ), |
618 | ); |
619 | |
620 | // Get the bounds of the track by finding the slider edges and translating |
621 | // inwards by the overlay radius. |
622 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
623 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
624 | final Offset middle = topLeft + bottomRight / 2; |
625 | |
626 | // Drag the end thumb towards the center. |
627 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
628 | await tester.dragFrom(leftTarget, middle - leftTarget); |
629 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05)); |
630 | |
631 | // Drag the start thumb towards the center. |
632 | await tester.pumpAndSettle(); |
633 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
634 | await tester.dragFrom(rightTarget, middle - rightTarget); |
635 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05)); |
636 | |
637 | // Drag the start thumb apart. |
638 | await tester.pumpAndSettle(); |
639 | await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3); |
640 | expect(values.start, moreOrLessEquals(0.2, epsilon: 0.05)); |
641 | }, |
642 | ); |
643 | |
644 | testWidgets( |
645 | 'Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete LTR)' , |
646 | (WidgetTester tester) async { |
647 | RangeValues values = const RangeValues(30, 70); |
648 | |
649 | await tester.pumpWidget( |
650 | MaterialApp( |
651 | home: Directionality( |
652 | textDirection: TextDirection.ltr, |
653 | child: StatefulBuilder( |
654 | builder: (BuildContext context, StateSetter setState) { |
655 | return Material( |
656 | child: Center( |
657 | child: RangeSlider( |
658 | values: values, |
659 | max: 100, |
660 | divisions: 10, |
661 | onChanged: (RangeValues newValues) { |
662 | setState(() { |
663 | values = newValues; |
664 | }); |
665 | }, |
666 | ), |
667 | ), |
668 | ); |
669 | }, |
670 | ), |
671 | ), |
672 | ), |
673 | ); |
674 | |
675 | // Get the bounds of the track by finding the slider edges and translating |
676 | // inwards by the overlay radius. |
677 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
678 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
679 | final Offset middle = topLeft + bottomRight / 2; |
680 | |
681 | // Drag the start thumb towards the center. |
682 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
683 | await tester.dragFrom(leftTarget, middle - leftTarget); |
684 | expect(values.start, moreOrLessEquals(50, epsilon: 0.01)); |
685 | |
686 | // Drag the end thumb towards the center. |
687 | await tester.pumpAndSettle(); |
688 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
689 | await tester.dragFrom(rightTarget, middle - rightTarget); |
690 | expect(values.end, moreOrLessEquals(50, epsilon: 0.01)); |
691 | |
692 | // Drag the start thumb apart. |
693 | await tester.pumpAndSettle(); |
694 | await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3); |
695 | expect(values.start, moreOrLessEquals(20, epsilon: 0.01)); |
696 | }, |
697 | ); |
698 | |
699 | testWidgets( |
700 | 'Range Slider thumbs can be dragged together and the start thumb can be dragged apart (discrete RTL)' , |
701 | (WidgetTester tester) async { |
702 | RangeValues values = const RangeValues(30, 70); |
703 | |
704 | await tester.pumpWidget( |
705 | MaterialApp( |
706 | home: Directionality( |
707 | textDirection: TextDirection.rtl, |
708 | child: StatefulBuilder( |
709 | builder: (BuildContext context, StateSetter setState) { |
710 | return Material( |
711 | child: Center( |
712 | child: RangeSlider( |
713 | values: values, |
714 | max: 100, |
715 | divisions: 10, |
716 | onChanged: (RangeValues newValues) { |
717 | setState(() { |
718 | values = newValues; |
719 | }); |
720 | }, |
721 | ), |
722 | ), |
723 | ); |
724 | }, |
725 | ), |
726 | ), |
727 | ), |
728 | ); |
729 | |
730 | // Get the bounds of the track by finding the slider edges and translating |
731 | // inwards by the overlay radius. |
732 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
733 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
734 | final Offset middle = topLeft + bottomRight / 2; |
735 | |
736 | // Drag the end thumb towards the center. |
737 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
738 | await tester.dragFrom(leftTarget, middle - leftTarget); |
739 | expect(values.end, moreOrLessEquals(50, epsilon: 0.01)); |
740 | |
741 | // Drag the start thumb towards the center. |
742 | await tester.pumpAndSettle(); |
743 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
744 | await tester.dragFrom(rightTarget, middle - rightTarget); |
745 | expect(values.start, moreOrLessEquals(50, epsilon: 0.01)); |
746 | |
747 | // Drag the start thumb apart. |
748 | await tester.pumpAndSettle(); |
749 | await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3); |
750 | expect(values.start, moreOrLessEquals(20, epsilon: 0.01)); |
751 | }, |
752 | ); |
753 | |
754 | testWidgets( |
755 | 'Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous LTR)' , |
756 | (WidgetTester tester) async { |
757 | RangeValues values = const RangeValues(0.3, 0.7); |
758 | |
759 | await tester.pumpWidget( |
760 | MaterialApp( |
761 | home: Directionality( |
762 | textDirection: TextDirection.ltr, |
763 | child: StatefulBuilder( |
764 | builder: (BuildContext context, StateSetter setState) { |
765 | return Material( |
766 | child: Center( |
767 | child: RangeSlider( |
768 | values: values, |
769 | onChanged: (RangeValues newValues) { |
770 | setState(() { |
771 | values = newValues; |
772 | }); |
773 | }, |
774 | ), |
775 | ), |
776 | ); |
777 | }, |
778 | ), |
779 | ), |
780 | ), |
781 | ); |
782 | |
783 | // Get the bounds of the track by finding the slider edges and translating |
784 | // inwards by the overlay radius. |
785 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
786 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
787 | final Offset middle = topLeft + bottomRight / 2; |
788 | |
789 | // Drag the start thumb towards the center. |
790 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
791 | await tester.dragFrom(leftTarget, middle - leftTarget); |
792 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05)); |
793 | |
794 | // Drag the end thumb towards the center. |
795 | await tester.pumpAndSettle(); |
796 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
797 | await tester.dragFrom(rightTarget, middle - rightTarget); |
798 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05)); |
799 | |
800 | // Drag the end thumb apart. |
801 | await tester.pumpAndSettle(); |
802 | await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3); |
803 | expect(values.end, moreOrLessEquals(0.8, epsilon: 0.05)); |
804 | }, |
805 | ); |
806 | |
807 | testWidgets( |
808 | 'Range Slider thumbs can be dragged together and the end thumb can be dragged apart (continuous RTL)' , |
809 | (WidgetTester tester) async { |
810 | RangeValues values = const RangeValues(0.3, 0.7); |
811 | |
812 | await tester.pumpWidget( |
813 | MaterialApp( |
814 | home: Directionality( |
815 | textDirection: TextDirection.rtl, |
816 | child: StatefulBuilder( |
817 | builder: (BuildContext context, StateSetter setState) { |
818 | return Material( |
819 | child: Center( |
820 | child: RangeSlider( |
821 | values: values, |
822 | onChanged: (RangeValues newValues) { |
823 | setState(() { |
824 | values = newValues; |
825 | }); |
826 | }, |
827 | ), |
828 | ), |
829 | ); |
830 | }, |
831 | ), |
832 | ), |
833 | ), |
834 | ); |
835 | |
836 | // Get the bounds of the track by finding the slider edges and translating |
837 | // inwards by the overlay radius. |
838 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
839 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
840 | final Offset middle = topLeft + bottomRight / 2; |
841 | |
842 | // Drag the end thumb towards the center. |
843 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
844 | await tester.dragFrom(leftTarget, middle - leftTarget); |
845 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.05)); |
846 | |
847 | // Drag the start thumb towards the center. |
848 | await tester.pumpAndSettle(); |
849 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
850 | await tester.dragFrom(rightTarget, middle - rightTarget); |
851 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.05)); |
852 | |
853 | // Drag the end thumb apart. |
854 | await tester.pumpAndSettle(); |
855 | await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3); |
856 | expect(values.end, moreOrLessEquals(0.8, epsilon: 0.05)); |
857 | }, |
858 | ); |
859 | |
860 | testWidgets( |
861 | 'Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete LTR)' , |
862 | (WidgetTester tester) async { |
863 | RangeValues values = const RangeValues(30, 70); |
864 | |
865 | await tester.pumpWidget( |
866 | MaterialApp( |
867 | home: Directionality( |
868 | textDirection: TextDirection.ltr, |
869 | child: StatefulBuilder( |
870 | builder: (BuildContext context, StateSetter setState) { |
871 | return Material( |
872 | child: Center( |
873 | child: RangeSlider( |
874 | values: values, |
875 | max: 100, |
876 | divisions: 10, |
877 | onChanged: (RangeValues newValues) { |
878 | setState(() { |
879 | values = newValues; |
880 | }); |
881 | }, |
882 | ), |
883 | ), |
884 | ); |
885 | }, |
886 | ), |
887 | ), |
888 | ), |
889 | ); |
890 | |
891 | // Get the bounds of the track by finding the slider edges and translating |
892 | // inwards by the overlay radius. |
893 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
894 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
895 | final Offset middle = topLeft + bottomRight / 2; |
896 | |
897 | // Drag the start thumb towards the center. |
898 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
899 | await tester.dragFrom(leftTarget, middle - leftTarget); |
900 | expect(values.start, moreOrLessEquals(50, epsilon: 0.01)); |
901 | |
902 | // Drag the end thumb towards the center. |
903 | await tester.pumpAndSettle(); |
904 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
905 | await tester.dragFrom(rightTarget, middle - rightTarget); |
906 | expect(values.end, moreOrLessEquals(50, epsilon: 0.01)); |
907 | |
908 | // Drag the end thumb apart. |
909 | await tester.pumpAndSettle(); |
910 | await tester.dragFrom(middle, (bottomRight - topLeft) * 0.3); |
911 | expect(values.end, moreOrLessEquals(80, epsilon: 0.01)); |
912 | }, |
913 | ); |
914 | |
915 | testWidgets( |
916 | 'Range Slider thumbs can be dragged together and the end thumb can be dragged apart (discrete RTL)' , |
917 | (WidgetTester tester) async { |
918 | RangeValues values = const RangeValues(30, 70); |
919 | |
920 | await tester.pumpWidget( |
921 | MaterialApp( |
922 | home: Directionality( |
923 | textDirection: TextDirection.rtl, |
924 | child: StatefulBuilder( |
925 | builder: (BuildContext context, StateSetter setState) { |
926 | return Material( |
927 | child: Center( |
928 | child: RangeSlider( |
929 | values: values, |
930 | max: 100, |
931 | divisions: 10, |
932 | onChanged: (RangeValues newValues) { |
933 | setState(() { |
934 | values = newValues; |
935 | }); |
936 | }, |
937 | ), |
938 | ), |
939 | ); |
940 | }, |
941 | ), |
942 | ), |
943 | ), |
944 | ); |
945 | |
946 | // Get the bounds of the track by finding the slider edges and translating |
947 | // inwards by the overlay radius. |
948 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
949 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
950 | final Offset middle = topLeft + bottomRight / 2; |
951 | |
952 | // Drag the end thumb towards the center. |
953 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
954 | await tester.dragFrom(leftTarget, middle - leftTarget); |
955 | expect(values.end, moreOrLessEquals(50, epsilon: 0.01)); |
956 | |
957 | // Drag the start thumb towards the center. |
958 | await tester.pumpAndSettle(); |
959 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
960 | await tester.dragFrom(rightTarget, middle - rightTarget); |
961 | expect(values.start, moreOrLessEquals(50, epsilon: 0.01)); |
962 | |
963 | // Drag the end thumb apart. |
964 | await tester.pumpAndSettle(); |
965 | await tester.dragFrom(middle, -(bottomRight - topLeft) * 0.3); |
966 | expect(values.end, moreOrLessEquals(80, epsilon: 0.01)); |
967 | }, |
968 | ); |
969 | |
970 | testWidgets( |
971 | 'Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by tap' , |
972 | (WidgetTester tester) async { |
973 | RangeValues values = const RangeValues(30, 70); |
974 | RangeValues? startValues; |
975 | RangeValues? endValues; |
976 | |
977 | await tester.pumpWidget( |
978 | MaterialApp( |
979 | home: Directionality( |
980 | textDirection: TextDirection.ltr, |
981 | child: StatefulBuilder( |
982 | builder: (BuildContext context, StateSetter setState) { |
983 | return Material( |
984 | child: Center( |
985 | child: RangeSlider( |
986 | values: values, |
987 | max: 100, |
988 | onChanged: (RangeValues newValues) { |
989 | setState(() { |
990 | values = newValues; |
991 | }); |
992 | }, |
993 | onChangeStart: (RangeValues newValues) { |
994 | startValues = newValues; |
995 | }, |
996 | onChangeEnd: (RangeValues newValues) { |
997 | endValues = newValues; |
998 | }, |
999 | ), |
1000 | ), |
1001 | ); |
1002 | }, |
1003 | ), |
1004 | ), |
1005 | ), |
1006 | ); |
1007 | |
1008 | // Get the bounds of the track by finding the slider edges and translating |
1009 | // inwards by the overlay radius. |
1010 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
1011 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
1012 | |
1013 | // Drag the start thumb towards the center. |
1014 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
1015 | expect(startValues, null); |
1016 | expect(endValues, null); |
1017 | await tester.dragFrom(leftTarget, (bottomRight - topLeft) * 0.2); |
1018 | expect(startValues!.start, moreOrLessEquals(30, epsilon: 1)); |
1019 | expect(startValues!.end, moreOrLessEquals(70, epsilon: 1)); |
1020 | expect(values.start, moreOrLessEquals(50, epsilon: 1)); |
1021 | expect(values.end, moreOrLessEquals(70, epsilon: 1)); |
1022 | expect(endValues!.start, moreOrLessEquals(50, epsilon: 1)); |
1023 | expect(endValues!.end, moreOrLessEquals(70, epsilon: 1)); |
1024 | }, |
1025 | ); |
1026 | |
1027 | testWidgets( |
1028 | 'Range Slider onChangeEnd and onChangeStart are called on an interaction initiated by drag' , |
1029 | (WidgetTester tester) async { |
1030 | RangeValues values = const RangeValues(30, 70); |
1031 | late RangeValues startValues; |
1032 | late RangeValues endValues; |
1033 | |
1034 | await tester.pumpWidget( |
1035 | MaterialApp( |
1036 | home: Directionality( |
1037 | textDirection: TextDirection.ltr, |
1038 | child: StatefulBuilder( |
1039 | builder: (BuildContext context, StateSetter setState) { |
1040 | return Material( |
1041 | child: Center( |
1042 | child: RangeSlider( |
1043 | values: values, |
1044 | max: 100, |
1045 | onChanged: (RangeValues newValues) { |
1046 | setState(() { |
1047 | values = newValues; |
1048 | }); |
1049 | }, |
1050 | onChangeStart: (RangeValues newValues) { |
1051 | startValues = newValues; |
1052 | }, |
1053 | onChangeEnd: (RangeValues newValues) { |
1054 | endValues = newValues; |
1055 | }, |
1056 | ), |
1057 | ), |
1058 | ); |
1059 | }, |
1060 | ), |
1061 | ), |
1062 | ), |
1063 | ); |
1064 | |
1065 | // Get the bounds of the track by finding the slider edges and translating |
1066 | // inwards by the overlay radius |
1067 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
1068 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
1069 | |
1070 | // Drag the thumbs together. |
1071 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
1072 | await tester.dragFrom(leftTarget, (bottomRight - topLeft) * 0.2); |
1073 | await tester.pumpAndSettle(); |
1074 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
1075 | await tester.dragFrom(rightTarget, (bottomRight - topLeft) * -0.2); |
1076 | await tester.pumpAndSettle(); |
1077 | expect(values.start, moreOrLessEquals(50, epsilon: 1)); |
1078 | expect(values.end, moreOrLessEquals(51, epsilon: 1)); |
1079 | |
1080 | // Drag the end thumb to the right. |
1081 | final Offset middleTarget = topLeft + (bottomRight - topLeft) * 0.5; |
1082 | await tester.dragFrom(middleTarget, (bottomRight - topLeft) * 0.4); |
1083 | await tester.pumpAndSettle(); |
1084 | expect(startValues.start, moreOrLessEquals(50, epsilon: 1)); |
1085 | expect(startValues.end, moreOrLessEquals(51, epsilon: 1)); |
1086 | expect(endValues.start, moreOrLessEquals(50, epsilon: 1)); |
1087 | expect(endValues.end, moreOrLessEquals(90, epsilon: 1)); |
1088 | }, |
1089 | ); |
1090 | |
1091 | ThemeData buildTheme() { |
1092 | return ThemeData( |
1093 | platform: TargetPlatform.android, |
1094 | primarySwatch: Colors.blue, |
1095 | sliderTheme: const SliderThemeData( |
1096 | disabledThumbColor: Color(0xff000001), |
1097 | disabledActiveTickMarkColor: Color(0xff000002), |
1098 | disabledActiveTrackColor: Color(0xff000003), |
1099 | disabledInactiveTickMarkColor: Color(0xff000004), |
1100 | disabledInactiveTrackColor: Color(0xff000005), |
1101 | activeTrackColor: Color(0xff000006), |
1102 | activeTickMarkColor: Color(0xff000007), |
1103 | inactiveTrackColor: Color(0xff000008), |
1104 | inactiveTickMarkColor: Color(0xff000009), |
1105 | overlayColor: Color(0xff000010), |
1106 | thumbColor: Color(0xff000011), |
1107 | valueIndicatorColor: Color(0xff000012), |
1108 | ), |
1109 | ); |
1110 | } |
1111 | |
1112 | Widget buildThemedApp({ |
1113 | required ThemeData theme, |
1114 | Color? activeColor, |
1115 | Color? inactiveColor, |
1116 | int? divisions, |
1117 | bool enabled = true, |
1118 | }) { |
1119 | RangeValues values = const RangeValues(0.5, 0.75); |
1120 | final ValueChanged<RangeValues>? onChanged = |
1121 | !enabled |
1122 | ? null |
1123 | : (RangeValues newValues) { |
1124 | values = newValues; |
1125 | }; |
1126 | return MaterialApp( |
1127 | home: Directionality( |
1128 | textDirection: TextDirection.ltr, |
1129 | child: Material( |
1130 | child: Center( |
1131 | child: Theme( |
1132 | data: theme, |
1133 | child: RangeSlider( |
1134 | values: values, |
1135 | labels: RangeLabels(values.start.toStringAsFixed(2), values.end.toStringAsFixed(2)), |
1136 | divisions: divisions, |
1137 | activeColor: activeColor, |
1138 | inactiveColor: inactiveColor, |
1139 | onChanged: onChanged, |
1140 | ), |
1141 | ), |
1142 | ), |
1143 | ), |
1144 | ), |
1145 | ); |
1146 | } |
1147 | |
1148 | testWidgets( |
1149 | 'Range Slider uses the right theme colors for the right shapes for a default enabled slider' , |
1150 | (WidgetTester tester) async { |
1151 | final ThemeData theme = buildTheme(); |
1152 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1153 | |
1154 | await tester.pumpWidget(buildThemedApp(theme: theme)); |
1155 | |
1156 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1157 | |
1158 | // Check default theme for enabled widget. |
1159 | expect( |
1160 | sliderBox, |
1161 | paints |
1162 | ..rrect(color: sliderTheme.inactiveTrackColor) |
1163 | ..rrect(color: sliderTheme.inactiveTrackColor) |
1164 | ..rrect(color: sliderTheme.activeTrackColor), |
1165 | ); |
1166 | expect( |
1167 | sliderBox, |
1168 | paints |
1169 | ..circle(color: sliderTheme.thumbColor) |
1170 | ..circle(color: sliderTheme.thumbColor), |
1171 | ); |
1172 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); |
1173 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); |
1174 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); |
1175 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor))); |
1176 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); |
1177 | }, |
1178 | ); |
1179 | |
1180 | testWidgets( |
1181 | 'Range Slider uses the right theme colors for the right shapes when setting the active color' , |
1182 | (WidgetTester tester) async { |
1183 | const Color activeColor = Color(0xcafefeed); |
1184 | final ThemeData theme = buildTheme(); |
1185 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1186 | |
1187 | await tester.pumpWidget(buildThemedApp(theme: theme, activeColor: activeColor)); |
1188 | |
1189 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1190 | |
1191 | expect( |
1192 | sliderBox, |
1193 | paints |
1194 | ..rrect(color: sliderTheme.inactiveTrackColor) |
1195 | ..rrect(color: sliderTheme.inactiveTrackColor) |
1196 | ..rrect(color: activeColor), |
1197 | ); |
1198 | expect( |
1199 | sliderBox, |
1200 | paints |
1201 | ..circle(color: activeColor) |
1202 | ..circle(color: activeColor), |
1203 | ); |
1204 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); |
1205 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); |
1206 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); |
1207 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); |
1208 | }, |
1209 | ); |
1210 | |
1211 | testWidgets( |
1212 | 'Range Slider uses the right theme colors for the right shapes when setting the inactive color' , |
1213 | (WidgetTester tester) async { |
1214 | const Color inactiveColor = Color(0xdeadbeef); |
1215 | final ThemeData theme = buildTheme(); |
1216 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1217 | |
1218 | await tester.pumpWidget(buildThemedApp(theme: theme, inactiveColor: inactiveColor)); |
1219 | |
1220 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1221 | |
1222 | expect( |
1223 | sliderBox, |
1224 | paints |
1225 | ..rrect(color: inactiveColor) |
1226 | ..rrect(color: inactiveColor) |
1227 | ..rrect(color: sliderTheme.activeTrackColor), |
1228 | ); |
1229 | expect( |
1230 | sliderBox, |
1231 | paints |
1232 | ..circle(color: sliderTheme.thumbColor) |
1233 | ..circle(color: sliderTheme.thumbColor), |
1234 | ); |
1235 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); |
1236 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); |
1237 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); |
1238 | }, |
1239 | ); |
1240 | |
1241 | testWidgets( |
1242 | 'Range Slider uses the right theme colors for the right shapes with active and inactive colors' , |
1243 | (WidgetTester tester) async { |
1244 | const Color activeColor = Color(0xcafefeed); |
1245 | const Color inactiveColor = Color(0xdeadbeef); |
1246 | final ThemeData theme = buildTheme(); |
1247 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1248 | |
1249 | await tester.pumpWidget( |
1250 | buildThemedApp(theme: theme, activeColor: activeColor, inactiveColor: inactiveColor), |
1251 | ); |
1252 | |
1253 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1254 | |
1255 | expect( |
1256 | sliderBox, |
1257 | paints |
1258 | ..rrect(color: inactiveColor) |
1259 | ..rrect(color: inactiveColor) |
1260 | ..rrect(color: activeColor), |
1261 | ); |
1262 | expect( |
1263 | sliderBox, |
1264 | paints |
1265 | ..circle(color: activeColor) |
1266 | ..circle(color: activeColor), |
1267 | ); |
1268 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); |
1269 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); |
1270 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); |
1271 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); |
1272 | }, |
1273 | ); |
1274 | |
1275 | testWidgets( |
1276 | 'Range Slider uses the right theme colors for the right shapes for a discrete slider' , |
1277 | (WidgetTester tester) async { |
1278 | final ThemeData theme = buildTheme(); |
1279 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1280 | |
1281 | await tester.pumpWidget(buildThemedApp(theme: theme, divisions: 3)); |
1282 | |
1283 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1284 | |
1285 | expect( |
1286 | sliderBox, |
1287 | paints |
1288 | ..rrect(color: sliderTheme.inactiveTrackColor) |
1289 | ..rrect(color: sliderTheme.inactiveTrackColor) |
1290 | ..rrect(color: sliderTheme.activeTrackColor), |
1291 | ); |
1292 | expect( |
1293 | sliderBox, |
1294 | paints |
1295 | ..circle(color: sliderTheme.inactiveTickMarkColor) |
1296 | ..circle(color: sliderTheme.inactiveTickMarkColor) |
1297 | ..circle(color: sliderTheme.activeTickMarkColor) |
1298 | ..circle(color: sliderTheme.inactiveTickMarkColor) |
1299 | ..circle(color: sliderTheme.thumbColor) |
1300 | ..circle(color: sliderTheme.thumbColor), |
1301 | ); |
1302 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); |
1303 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); |
1304 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); |
1305 | }, |
1306 | ); |
1307 | |
1308 | testWidgets( |
1309 | 'Range Slider uses the right theme colors for the right shapes for a discrete slider with active and inactive colors' , |
1310 | (WidgetTester tester) async { |
1311 | const Color activeColor = Color(0xcafefeed); |
1312 | const Color inactiveColor = Color(0xdeadbeef); |
1313 | final ThemeData theme = buildTheme(); |
1314 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1315 | |
1316 | await tester.pumpWidget( |
1317 | buildThemedApp( |
1318 | theme: theme, |
1319 | activeColor: activeColor, |
1320 | inactiveColor: inactiveColor, |
1321 | divisions: 3, |
1322 | ), |
1323 | ); |
1324 | |
1325 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1326 | |
1327 | expect( |
1328 | sliderBox, |
1329 | paints |
1330 | ..rrect(color: inactiveColor) |
1331 | ..rrect(color: inactiveColor) |
1332 | ..rrect(color: activeColor), |
1333 | ); |
1334 | expect( |
1335 | sliderBox, |
1336 | paints |
1337 | ..circle(color: activeColor) |
1338 | ..circle(color: activeColor) |
1339 | ..circle(color: inactiveColor) |
1340 | ..circle(color: activeColor) |
1341 | ..circle(color: activeColor) |
1342 | ..circle(color: activeColor), |
1343 | ); |
1344 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); |
1345 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.disabledThumbColor))); |
1346 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledActiveTrackColor))); |
1347 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.disabledInactiveTrackColor))); |
1348 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.activeTickMarkColor))); |
1349 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.inactiveTickMarkColor))); |
1350 | }, |
1351 | ); |
1352 | |
1353 | testWidgets( |
1354 | 'Range Slider uses the right theme colors for the right shapes for a default disabled slider' , |
1355 | (WidgetTester tester) async { |
1356 | final ThemeData theme = buildTheme(); |
1357 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1358 | |
1359 | await tester.pumpWidget(buildThemedApp(theme: theme, enabled: false)); |
1360 | |
1361 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1362 | |
1363 | expect( |
1364 | sliderBox, |
1365 | paints |
1366 | ..rrect(color: sliderTheme.disabledInactiveTrackColor) |
1367 | ..rrect(color: sliderTheme.disabledInactiveTrackColor) |
1368 | ..rrect(color: sliderTheme.disabledActiveTrackColor), |
1369 | ); |
1370 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); |
1371 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor))); |
1372 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); |
1373 | }, |
1374 | ); |
1375 | |
1376 | testWidgets( |
1377 | 'Range Slider uses the right theme colors for the right shapes for a disabled slider with active and inactive colors' , |
1378 | (WidgetTester tester) async { |
1379 | const Color activeColor = Color(0xcafefeed); |
1380 | const Color inactiveColor = Color(0xdeadbeef); |
1381 | final ThemeData theme = buildTheme(); |
1382 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1383 | |
1384 | await tester.pumpWidget( |
1385 | buildThemedApp( |
1386 | theme: theme, |
1387 | activeColor: activeColor, |
1388 | inactiveColor: inactiveColor, |
1389 | enabled: false, |
1390 | ), |
1391 | ); |
1392 | |
1393 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1394 | |
1395 | expect( |
1396 | sliderBox, |
1397 | paints |
1398 | ..rrect(color: sliderTheme.disabledInactiveTrackColor) |
1399 | ..rrect(color: sliderTheme.disabledInactiveTrackColor) |
1400 | ..rrect(color: sliderTheme.disabledActiveTrackColor), |
1401 | ); |
1402 | expect(sliderBox, isNot(paints..circle(color: sliderTheme.thumbColor))); |
1403 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.activeTrackColor))); |
1404 | expect(sliderBox, isNot(paints..rect(color: sliderTheme.inactiveTrackColor))); |
1405 | }, |
1406 | ); |
1407 | |
1408 | testWidgets( |
1409 | 'Range Slider uses the right theme colors for the right shapes when the value indicators are showing' , |
1410 | (WidgetTester tester) async { |
1411 | final ThemeData theme = buildTheme(); |
1412 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1413 | RangeValues values = const RangeValues(0.5, 0.75); |
1414 | |
1415 | Widget buildApp({ |
1416 | Color? activeColor, |
1417 | Color? inactiveColor, |
1418 | int? divisions, |
1419 | bool enabled = true, |
1420 | }) { |
1421 | final ValueChanged<RangeValues>? onChanged = |
1422 | !enabled |
1423 | ? null |
1424 | : (RangeValues newValues) { |
1425 | values = newValues; |
1426 | }; |
1427 | return MaterialApp( |
1428 | home: Directionality( |
1429 | textDirection: TextDirection.ltr, |
1430 | child: Material( |
1431 | child: Center( |
1432 | child: Theme( |
1433 | data: theme, |
1434 | child: RangeSlider( |
1435 | values: values, |
1436 | labels: RangeLabels( |
1437 | values.start.toStringAsFixed(2), |
1438 | values.end.toStringAsFixed(2), |
1439 | ), |
1440 | divisions: divisions, |
1441 | activeColor: activeColor, |
1442 | inactiveColor: inactiveColor, |
1443 | onChanged: onChanged, |
1444 | ), |
1445 | ), |
1446 | ), |
1447 | ), |
1448 | ), |
1449 | ); |
1450 | } |
1451 | |
1452 | await tester.pumpWidget(buildApp(divisions: 3)); |
1453 | |
1454 | final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
1455 | |
1456 | final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0); |
1457 | final TestGesture gesture = await tester.startGesture(topRight); |
1458 | // Wait for value indicator animation to finish. |
1459 | await tester.pumpAndSettle(); |
1460 | expect(values.end, equals(1)); |
1461 | expect( |
1462 | valueIndicatorBox, |
1463 | paints |
1464 | ..path(color: Colors.black) // shadow |
1465 | ..path(color: Colors.black) // shadow |
1466 | ..path(color: sliderTheme.valueIndicatorColor) |
1467 | ..paragraph(), |
1468 | ); |
1469 | await gesture.up(); |
1470 | // Wait for value indicator animation to finish. |
1471 | await tester.pumpAndSettle(); |
1472 | }, |
1473 | ); |
1474 | |
1475 | testWidgets( |
1476 | 'Range Slider removes value indicator from overlay if Slider gets disposed without value indicator animation completing.' , |
1477 | (WidgetTester tester) async { |
1478 | RangeValues values = const RangeValues(0.5, 0.75); |
1479 | const Color fillColor = Color(0xf55f5f5f); |
1480 | |
1481 | Widget buildApp({ |
1482 | Color? activeColor, |
1483 | Color? inactiveColor, |
1484 | int? divisions, |
1485 | bool enabled = true, |
1486 | }) { |
1487 | void onChanged(RangeValues newValues) { |
1488 | values = newValues; |
1489 | } |
1490 | |
1491 | return MaterialApp( |
1492 | theme: ThemeData(useMaterial3: false), |
1493 | home: Scaffold( |
1494 | // The builder is used to pass the context from the MaterialApp widget |
1495 | // to the [Navigator]. This context is required in order for the |
1496 | // Navigator to work. |
1497 | body: Builder( |
1498 | builder: (BuildContext context) { |
1499 | return Column( |
1500 | children: <Widget>[ |
1501 | RangeSlider( |
1502 | values: values, |
1503 | labels: RangeLabels( |
1504 | values.start.toStringAsFixed(2), |
1505 | values.end.toStringAsFixed(2), |
1506 | ), |
1507 | divisions: divisions, |
1508 | onChanged: onChanged, |
1509 | ), |
1510 | ElevatedButton( |
1511 | child: const Text('Next' ), |
1512 | onPressed: () { |
1513 | Navigator.of(context).pushReplacement( |
1514 | MaterialPageRoute<void>( |
1515 | builder: (BuildContext context) { |
1516 | return ElevatedButton( |
1517 | child: const Text('Inner page' ), |
1518 | onPressed: () { |
1519 | Navigator.of(context).pop(); |
1520 | }, |
1521 | ); |
1522 | }, |
1523 | ), |
1524 | ); |
1525 | }, |
1526 | ), |
1527 | ], |
1528 | ); |
1529 | }, |
1530 | ), |
1531 | ), |
1532 | ); |
1533 | } |
1534 | |
1535 | await tester.pumpWidget(buildApp(divisions: 3)); |
1536 | |
1537 | final RenderObject valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
1538 | final Offset topRight = tester.getTopRight(find.byType(RangeSlider)).translate(-24, 0); |
1539 | final TestGesture gesture = await tester.startGesture(topRight); |
1540 | // Wait for value indicator animation to finish. |
1541 | await tester.pumpAndSettle(); |
1542 | |
1543 | expect( |
1544 | valueIndicatorBox, |
1545 | paints |
1546 | // Represents the raised button wth next text. |
1547 | ..path(color: Colors.black) |
1548 | ..paragraph() |
1549 | // Represents the range slider. |
1550 | ..path(color: fillColor) |
1551 | ..paragraph() |
1552 | ..path(color: fillColor) |
1553 | ..paragraph(), |
1554 | ); |
1555 | |
1556 | // Represents the Raised Button and Range Slider. |
1557 | expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 6)); |
1558 | expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 3)); |
1559 | |
1560 | await tester.tap(find.text('Next' )); |
1561 | await tester.pumpAndSettle(); |
1562 | |
1563 | expect(find.byType(RangeSlider), findsNothing); |
1564 | expect( |
1565 | valueIndicatorBox, |
1566 | isNot( |
1567 | paints |
1568 | ..path(color: fillColor) |
1569 | ..paragraph() |
1570 | ..path(color: fillColor) |
1571 | ..paragraph(), |
1572 | ), |
1573 | ); |
1574 | |
1575 | // Represents the raised button with inner page text. |
1576 | expect(valueIndicatorBox, paintsExactlyCountTimes(#drawPath, 2)); |
1577 | expect(valueIndicatorBox, paintsExactlyCountTimes(#drawParagraph, 1)); |
1578 | |
1579 | // Don't stop holding the value indicator. |
1580 | await gesture.up(); |
1581 | await tester.pumpAndSettle(); |
1582 | }, |
1583 | ); |
1584 | |
1585 | testWidgets('Range Slider top thumb gets stroked when overlapping' , (WidgetTester tester) async { |
1586 | RangeValues values = const RangeValues(0.3, 0.7); |
1587 | |
1588 | final ThemeData theme = ThemeData( |
1589 | platform: TargetPlatform.android, |
1590 | primarySwatch: Colors.blue, |
1591 | sliderTheme: const SliderThemeData( |
1592 | thumbColor: Color(0xff000001), |
1593 | overlappingShapeStrokeColor: Color(0xff000002), |
1594 | ), |
1595 | ); |
1596 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1597 | |
1598 | await tester.pumpWidget( |
1599 | MaterialApp( |
1600 | home: Directionality( |
1601 | textDirection: TextDirection.ltr, |
1602 | child: StatefulBuilder( |
1603 | builder: (BuildContext context, StateSetter setState) { |
1604 | return Material( |
1605 | child: Center( |
1606 | child: Theme( |
1607 | data: theme, |
1608 | child: RangeSlider( |
1609 | values: values, |
1610 | onChanged: (RangeValues newValues) { |
1611 | setState(() { |
1612 | values = newValues; |
1613 | }); |
1614 | }, |
1615 | ), |
1616 | ), |
1617 | ), |
1618 | ); |
1619 | }, |
1620 | ), |
1621 | ), |
1622 | ), |
1623 | ); |
1624 | |
1625 | final RenderBox sliderBox = tester.firstRenderObject<RenderBox>(find.byType(RangeSlider)); |
1626 | |
1627 | // Get the bounds of the track by finding the slider edges and translating |
1628 | // inwards by the overlay radius. |
1629 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
1630 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
1631 | final Offset middle = topLeft + bottomRight / 2; |
1632 | |
1633 | // Drag the thumbs towards the center. |
1634 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
1635 | await tester.dragFrom(leftTarget, middle - leftTarget); |
1636 | await tester.pumpAndSettle(); |
1637 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
1638 | await tester.dragFrom(rightTarget, middle - rightTarget); |
1639 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03)); |
1640 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03)); |
1641 | await tester.pumpAndSettle(); |
1642 | |
1643 | expect( |
1644 | sliderBox, |
1645 | paints |
1646 | ..circle(color: sliderTheme.thumbColor) |
1647 | ..circle(color: sliderTheme.overlappingShapeStrokeColor) |
1648 | ..circle(color: sliderTheme.thumbColor), |
1649 | ); |
1650 | }); |
1651 | |
1652 | testWidgets('Range Slider top value indicator gets stroked when overlapping' , ( |
1653 | WidgetTester tester, |
1654 | ) async { |
1655 | RangeValues values = const RangeValues(0.3, 0.7); |
1656 | |
1657 | final ThemeData theme = ThemeData( |
1658 | platform: TargetPlatform.android, |
1659 | primarySwatch: Colors.blue, |
1660 | sliderTheme: const SliderThemeData( |
1661 | valueIndicatorColor: Color(0xff000001), |
1662 | overlappingShapeStrokeColor: Color(0xff000002), |
1663 | showValueIndicator: ShowValueIndicator.always, |
1664 | ), |
1665 | ); |
1666 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1667 | |
1668 | await tester.pumpWidget( |
1669 | MaterialApp( |
1670 | home: Directionality( |
1671 | textDirection: TextDirection.ltr, |
1672 | child: StatefulBuilder( |
1673 | builder: (BuildContext context, StateSetter setState) { |
1674 | return Material( |
1675 | child: Center( |
1676 | child: Theme( |
1677 | data: theme, |
1678 | child: RangeSlider( |
1679 | values: values, |
1680 | labels: RangeLabels( |
1681 | values.start.toStringAsFixed(2), |
1682 | values.end.toStringAsFixed(2), |
1683 | ), |
1684 | onChanged: (RangeValues newValues) { |
1685 | setState(() { |
1686 | values = newValues; |
1687 | }); |
1688 | }, |
1689 | ), |
1690 | ), |
1691 | ), |
1692 | ); |
1693 | }, |
1694 | ), |
1695 | ), |
1696 | ), |
1697 | ); |
1698 | |
1699 | final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
1700 | |
1701 | // Get the bounds of the track by finding the slider edges and translating |
1702 | // inwards by the overlay radius. |
1703 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
1704 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
1705 | final Offset middle = topLeft + bottomRight / 2; |
1706 | |
1707 | // Drag the thumbs towards the center. |
1708 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
1709 | await tester.dragFrom(leftTarget, middle - leftTarget); |
1710 | await tester.pumpAndSettle(); |
1711 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
1712 | await tester.dragFrom(rightTarget, middle - rightTarget); |
1713 | await tester.pumpAndSettle(); |
1714 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03)); |
1715 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03)); |
1716 | final TestGesture gesture = await tester.startGesture(middle); |
1717 | await tester.pumpAndSettle(); |
1718 | |
1719 | expect( |
1720 | valueIndicatorBox, |
1721 | paints |
1722 | ..path(color: Colors.black) // shadow |
1723 | ..path(color: Colors.black) // shadow |
1724 | ..path(color: sliderTheme.valueIndicatorColor) |
1725 | ..paragraph(), |
1726 | ); |
1727 | |
1728 | await gesture.up(); |
1729 | }); |
1730 | |
1731 | testWidgets( |
1732 | 'Range Slider top value indicator gets stroked when overlapping with large text scale' , |
1733 | (WidgetTester tester) async { |
1734 | RangeValues values = const RangeValues(0.3, 0.7); |
1735 | |
1736 | final ThemeData theme = ThemeData( |
1737 | platform: TargetPlatform.android, |
1738 | primarySwatch: Colors.blue, |
1739 | sliderTheme: const SliderThemeData( |
1740 | valueIndicatorColor: Color(0xff000001), |
1741 | overlappingShapeStrokeColor: Color(0xff000002), |
1742 | showValueIndicator: ShowValueIndicator.always, |
1743 | ), |
1744 | ); |
1745 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1746 | |
1747 | await tester.pumpWidget( |
1748 | MaterialApp( |
1749 | home: Directionality( |
1750 | textDirection: TextDirection.ltr, |
1751 | child: StatefulBuilder( |
1752 | builder: (BuildContext context, StateSetter setState) { |
1753 | return MediaQuery( |
1754 | data: const MediaQueryData(textScaler: TextScaler.linear(2)), |
1755 | child: Material( |
1756 | child: Center( |
1757 | child: Theme( |
1758 | data: theme, |
1759 | child: RangeSlider( |
1760 | values: values, |
1761 | labels: RangeLabels( |
1762 | values.start.toStringAsFixed(2), |
1763 | values.end.toStringAsFixed(2), |
1764 | ), |
1765 | onChanged: (RangeValues newValues) { |
1766 | setState(() { |
1767 | values = newValues; |
1768 | }); |
1769 | }, |
1770 | ), |
1771 | ), |
1772 | ), |
1773 | ), |
1774 | ); |
1775 | }, |
1776 | ), |
1777 | ), |
1778 | ), |
1779 | ); |
1780 | |
1781 | final RenderBox valueIndicatorBox = tester.renderObject(find.byType(Overlay)); |
1782 | |
1783 | // Get the bounds of the track by finding the slider edges and translating |
1784 | // inwards by the overlay radius. |
1785 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
1786 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
1787 | final Offset middle = topLeft + bottomRight / 2; |
1788 | |
1789 | // Drag the thumbs towards the center. |
1790 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
1791 | await tester.dragFrom(leftTarget, middle - leftTarget); |
1792 | await tester.pumpAndSettle(); |
1793 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
1794 | await tester.dragFrom(rightTarget, middle - rightTarget); |
1795 | await tester.pumpAndSettle(); |
1796 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03)); |
1797 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03)); |
1798 | final TestGesture gesture = await tester.startGesture(middle); |
1799 | await tester.pumpAndSettle(); |
1800 | |
1801 | expect( |
1802 | valueIndicatorBox, |
1803 | paints |
1804 | ..path(color: Colors.black) // shadow |
1805 | ..path(color: Colors.black) // shadow |
1806 | ..path(color: sliderTheme.valueIndicatorColor) |
1807 | ..paragraph(), |
1808 | ); |
1809 | |
1810 | await gesture.up(); |
1811 | }, |
1812 | ); |
1813 | |
1814 | testWidgets('Range Slider thumb gets stroked when overlapping' , (WidgetTester tester) async { |
1815 | RangeValues values = const RangeValues(0.3, 0.7); |
1816 | |
1817 | final ThemeData theme = ThemeData( |
1818 | platform: TargetPlatform.android, |
1819 | primarySwatch: Colors.blue, |
1820 | sliderTheme: const SliderThemeData( |
1821 | valueIndicatorColor: Color(0xff000001), |
1822 | showValueIndicator: ShowValueIndicator.onlyForContinuous, |
1823 | ), |
1824 | ); |
1825 | final SliderThemeData sliderTheme = theme.sliderTheme; |
1826 | |
1827 | await tester.pumpWidget( |
1828 | MaterialApp( |
1829 | home: Directionality( |
1830 | textDirection: TextDirection.ltr, |
1831 | child: StatefulBuilder( |
1832 | builder: (BuildContext context, StateSetter setState) { |
1833 | return Material( |
1834 | child: Center( |
1835 | child: Theme( |
1836 | data: theme, |
1837 | child: RangeSlider( |
1838 | values: values, |
1839 | labels: RangeLabels( |
1840 | values.start.toStringAsFixed(2), |
1841 | values.end.toStringAsFixed(2), |
1842 | ), |
1843 | onChanged: (RangeValues newValues) { |
1844 | setState(() { |
1845 | values = newValues; |
1846 | }); |
1847 | }, |
1848 | ), |
1849 | ), |
1850 | ), |
1851 | ); |
1852 | }, |
1853 | ), |
1854 | ), |
1855 | ), |
1856 | ); |
1857 | |
1858 | // Get the bounds of the track by finding the slider edges and translating |
1859 | // inwards by the overlay radius. |
1860 | final Offset topLeft = tester.getTopLeft(find.byType(RangeSlider)).translate(24, 0); |
1861 | final Offset bottomRight = tester.getBottomRight(find.byType(RangeSlider)).translate(-24, 0); |
1862 | final Offset middle = topLeft + bottomRight / 2; |
1863 | |
1864 | // Drag the thumbs towards the center. |
1865 | final Offset leftTarget = topLeft + (bottomRight - topLeft) * 0.3; |
1866 | await tester.dragFrom(leftTarget, middle - leftTarget); |
1867 | await tester.pumpAndSettle(); |
1868 | final Offset rightTarget = topLeft + (bottomRight - topLeft) * 0.7; |
1869 | await tester.dragFrom(rightTarget, middle - rightTarget); |
1870 | await tester.pumpAndSettle(); |
1871 | expect(values.start, moreOrLessEquals(0.5, epsilon: 0.03)); |
1872 | expect(values.end, moreOrLessEquals(0.5, epsilon: 0.03)); |
1873 | final TestGesture gesture = await tester.startGesture(middle); |
1874 | await tester.pumpAndSettle(); |
1875 | |
1876 | /// The first circle is the thumb, the second one is the overlapping shape |
1877 | /// circle, and the last one is the second thumb. |
1878 | expect( |
1879 | find.byType(RangeSlider), |
1880 | paints |
1881 | ..circle() |
1882 | ..circle(color: sliderTheme.overlappingShapeStrokeColor) |
1883 | ..circle(), |
1884 | ); |
1885 | |
1886 | await gesture.up(); |
1887 | |
1888 | expect( |
1889 | find.byType(RangeSlider), |
1890 | paints |
1891 | ..circle() |
1892 | ..circle(color: sliderTheme.overlappingShapeStrokeColor) |
1893 | ..circle(), |
1894 | ); |
1895 | }); |
1896 | |
1897 | // Regression test for https://github.com/flutter/flutter/issues/101868 |
1898 | testWidgets('RangeSlider.label info should not write to semantic node' , ( |
1899 | WidgetTester tester, |
1900 | ) async { |
1901 | await tester.pumpWidget( |
1902 | MaterialApp( |
1903 | home: Theme( |
1904 | data: ThemeData(), |
1905 | child: Directionality( |
1906 | textDirection: TextDirection.ltr, |
1907 | child: Material( |
1908 | child: RangeSlider( |
1909 | values: const RangeValues(10.0, 12.0), |
1910 | max: 100.0, |
1911 | onChanged: (RangeValues v) {}, |
1912 | labels: const RangeLabels('Begin' , 'End' ), |
1913 | ), |
1914 | ), |
1915 | ), |
1916 | ), |
1917 | ), |
1918 | ); |
1919 | |
1920 | await tester.pumpAndSettle(); |
1921 | |
1922 | expect( |
1923 | tester.getSemantics(find.byType(RangeSlider)), |
1924 | matchesSemantics( |
1925 | scopesRoute: true, |
1926 | children: <Matcher>[ |
1927 | matchesSemantics( |
1928 | children: <Matcher>[ |
1929 | matchesSemantics( |
1930 | isEnabled: true, |
1931 | isSlider: true, |
1932 | hasEnabledState: true, |
1933 | hasIncreaseAction: true, |
1934 | hasDecreaseAction: true, |
1935 | value: '10%' , |
1936 | increasedValue: '10%' , |
1937 | decreasedValue: '5%' , |
1938 | label: '' , |
1939 | ), |
1940 | matchesSemantics( |
1941 | isEnabled: true, |
1942 | isSlider: true, |
1943 | hasEnabledState: true, |
1944 | hasIncreaseAction: true, |
1945 | hasDecreaseAction: true, |
1946 | value: '12%' , |
1947 | increasedValue: '17%' , |
1948 | decreasedValue: '12%' , |
1949 | label: '' , |
1950 | ), |
1951 | ], |
1952 | ), |
1953 | ], |
1954 | ), |
1955 | ); |
1956 | }); |
1957 | |
1958 | testWidgets('Range Slider Semantics - ltr' , (WidgetTester tester) async { |
1959 | await tester.pumpWidget( |
1960 | MaterialApp( |
1961 | home: Theme( |
1962 | data: ThemeData(), |
1963 | child: Directionality( |
1964 | textDirection: TextDirection.ltr, |
1965 | child: Material( |
1966 | child: RangeSlider( |
1967 | values: const RangeValues(10.0, 30.0), |
1968 | max: 100.0, |
1969 | onChanged: (RangeValues v) {}, |
1970 | ), |
1971 | ), |
1972 | ), |
1973 | ), |
1974 | ), |
1975 | ); |
1976 | |
1977 | await tester.pumpAndSettle(); |
1978 | |
1979 | final SemanticsNode semanticsNode = tester.getSemantics(find.byType(RangeSlider)); |
1980 | expect( |
1981 | semanticsNode, |
1982 | matchesSemantics( |
1983 | scopesRoute: true, |
1984 | children: <Matcher>[ |
1985 | matchesSemantics( |
1986 | children: <Matcher>[ |
1987 | matchesSemantics( |
1988 | isEnabled: true, |
1989 | isSlider: true, |
1990 | hasEnabledState: true, |
1991 | hasIncreaseAction: true, |
1992 | hasDecreaseAction: true, |
1993 | value: '10%' , |
1994 | increasedValue: '15%' , |
1995 | decreasedValue: '5%' , |
1996 | rect: const Rect.fromLTRB(75.2, 276.0, 123.2, 324.0), |
1997 | ), |
1998 | matchesSemantics( |
1999 | isEnabled: true, |
2000 | isSlider: true, |
2001 | hasEnabledState: true, |
2002 | hasIncreaseAction: true, |
2003 | hasDecreaseAction: true, |
2004 | value: '30%' , |
2005 | increasedValue: '35%' , |
2006 | decreasedValue: '25%' , |
2007 | rect: const Rect.fromLTRB(225.6, 276.0, 273.6, 324.0), |
2008 | ), |
2009 | ], |
2010 | ), |
2011 | ], |
2012 | ), |
2013 | ); |
2014 | |
2015 | // TODO(tahatesser): This is a workaround for matching |
2016 | // the semantics node rects by avoiding floating point errors. |
2017 | // https://github.com/flutter/flutter/issues/115079 |
2018 | // Get semantics node rects. |
2019 | final List<Rect> rects = <Rect>[]; |
2020 | semanticsNode.visitChildren((SemanticsNode node) { |
2021 | node.visitChildren((SemanticsNode node) { |
2022 | // Round rect values to avoid floating point errors. |
2023 | rects.add( |
2024 | Rect.fromLTRB( |
2025 | node.rect.left.roundToDouble(), |
2026 | node.rect.top.roundToDouble(), |
2027 | node.rect.right.roundToDouble(), |
2028 | node.rect.bottom.roundToDouble(), |
2029 | ), |
2030 | ); |
2031 | return true; |
2032 | }); |
2033 | return true; |
2034 | }); |
2035 | // Test that the semantics node rect sizes are correct. |
2036 | expect(rects, <Rect>[ |
2037 | const Rect.fromLTRB(75.0, 276.0, 123.0, 324.0), |
2038 | const Rect.fromLTRB(226.0, 276.0, 274.0, 324.0), |
2039 | ]); |
2040 | }); |
2041 | |
2042 | testWidgets('Range Slider Semantics - rtl' , (WidgetTester tester) async { |
2043 | await tester.pumpWidget( |
2044 | MaterialApp( |
2045 | home: Theme( |
2046 | data: ThemeData(), |
2047 | child: Directionality( |
2048 | textDirection: TextDirection.rtl, |
2049 | child: Material( |
2050 | child: RangeSlider( |
2051 | values: const RangeValues(10.0, 30.0), |
2052 | max: 100.0, |
2053 | onChanged: (RangeValues v) {}, |
2054 | ), |
2055 | ), |
2056 | ), |
2057 | ), |
2058 | ), |
2059 | ); |
2060 | |
2061 | await tester.pumpAndSettle(); |
2062 | |
2063 | final SemanticsNode semanticsNode = tester.getSemantics(find.byType(RangeSlider)); |
2064 | expect( |
2065 | semanticsNode, |
2066 | matchesSemantics( |
2067 | scopesRoute: true, |
2068 | children: <Matcher>[ |
2069 | matchesSemantics( |
2070 | children: <Matcher>[ |
2071 | matchesSemantics( |
2072 | isEnabled: true, |
2073 | isSlider: true, |
2074 | hasEnabledState: true, |
2075 | hasIncreaseAction: true, |
2076 | hasDecreaseAction: true, |
2077 | value: '10%' , |
2078 | increasedValue: '15%' , |
2079 | decreasedValue: '5%' , |
2080 | ), |
2081 | matchesSemantics( |
2082 | isEnabled: true, |
2083 | isSlider: true, |
2084 | hasEnabledState: true, |
2085 | hasIncreaseAction: true, |
2086 | hasDecreaseAction: true, |
2087 | value: '30%' , |
2088 | increasedValue: '35%' , |
2089 | decreasedValue: '25%' , |
2090 | ), |
2091 | ], |
2092 | ), |
2093 | ], |
2094 | ), |
2095 | ); |
2096 | |
2097 | // TODO(tahatesser): This is a workaround for matching |
2098 | // the semantics node rects by avoiding floating point errors. |
2099 | // https://github.com/flutter/flutter/issues/115079 |
2100 | // Get semantics node rects. |
2101 | final List<Rect> rects = <Rect>[]; |
2102 | semanticsNode.visitChildren((SemanticsNode node) { |
2103 | node.visitChildren((SemanticsNode node) { |
2104 | // Round rect values to avoid floating point errors. |
2105 | rects.add( |
2106 | Rect.fromLTRB( |
2107 | node.rect.left.roundToDouble(), |
2108 | node.rect.top.roundToDouble(), |
2109 | node.rect.right.roundToDouble(), |
2110 | node.rect.bottom.roundToDouble(), |
2111 | ), |
2112 | ); |
2113 | return true; |
2114 | }); |
2115 | return true; |
2116 | }); |
2117 | // Test that the semantics node rect sizes are correct. |
2118 | expect(rects, <Rect>[ |
2119 | const Rect.fromLTRB(526.0, 276.0, 574.0, 324.0), |
2120 | const Rect.fromLTRB(677.0, 276.0, 725.0, 324.0), |
2121 | ]); |
2122 | }); |
2123 | |
2124 | testWidgets('Range Slider implements debugFillProperties' , (WidgetTester tester) async { |
2125 | final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); |
2126 | |
2127 | RangeSlider( |
2128 | activeColor: Colors.blue, |
2129 | divisions: 4, |
2130 | inactiveColor: Colors.grey, |
2131 | labels: const RangeLabels('lowerValue' , 'upperValue' ), |
2132 | max: 100.0, |
2133 | onChanged: null, |
2134 | values: const RangeValues(25.0, 75.0), |
2135 | ).debugFillProperties(builder); |
2136 | |
2137 | final List<String> description = |
2138 | builder.properties |
2139 | .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) |
2140 | .map((DiagnosticsNode node) => node.toString()) |
2141 | .toList(); |
2142 | |
2143 | expect(description, <String>[ |
2144 | 'valueStart: 25.0' , |
2145 | 'valueEnd: 75.0' , |
2146 | 'disabled' , |
2147 | 'min: 0.0' , |
2148 | 'max: 100.0' , |
2149 | 'divisions: 4' , |
2150 | 'labelStart: "lowerValue"' , |
2151 | 'labelEnd: "upperValue"' , |
2152 | 'activeColor: MaterialColor(primary value: ${const Color(0xff2196f3)})' , |
2153 | 'inactiveColor: MaterialColor(primary value: ${const Color(0xff9e9e9e)})' , |
2154 | ]); |
2155 | }); |
2156 | |
2157 | testWidgets( |
2158 | 'Range Slider can be painted in a narrower constraint when track shape is RoundedRectRange' , |
2159 | (WidgetTester tester) async { |
2160 | await tester.pumpWidget( |
2161 | MaterialApp( |
2162 | home: Directionality( |
2163 | textDirection: TextDirection.ltr, |
2164 | child: Material( |
2165 | child: Center( |
2166 | child: SizedBox( |
2167 | height: 10.0, |
2168 | width: 0.0, |
2169 | child: RangeSlider(values: const RangeValues(0.25, 0.5), onChanged: null), |
2170 | ), |
2171 | ), |
2172 | ), |
2173 | ), |
2174 | ), |
2175 | ); |
2176 | |
2177 | // _RenderRangeSlider is the last render object in the tree. |
2178 | final RenderObject renderObject = tester.allRenderObjects.last; |
2179 | |
2180 | expect( |
2181 | renderObject, |
2182 | paints |
2183 | // left inactive track RRect |
2184 | ..rrect( |
2185 | rrect: RRect.fromLTRBAndCorners( |
2186 | -24.0, |
2187 | 3.0, |
2188 | -12.0, |
2189 | 7.0, |
2190 | topLeft: const Radius.circular(2.0), |
2191 | bottomLeft: const Radius.circular(2.0), |
2192 | ), |
2193 | ) |
2194 | // right inactive track RRect |
2195 | ..rrect( |
2196 | rrect: RRect.fromLTRBAndCorners( |
2197 | 0.0, |
2198 | 3.0, |
2199 | 24.0, |
2200 | 7.0, |
2201 | topRight: const Radius.circular(2.0), |
2202 | bottomRight: const Radius.circular(2.0), |
2203 | ), |
2204 | ) |
2205 | // active track RRect |
2206 | ..rrect(rrect: RRect.fromLTRBR(-14.0, 2.0, 2.0, 8.0, const Radius.circular(2.0))) |
2207 | // thumbs |
2208 | ..circle(x: -12.0, y: 5.0, radius: 10.0) |
2209 | ..circle(x: 0.0, y: 5.0, radius: 10.0), |
2210 | ); |
2211 | }, |
2212 | ); |
2213 | |
2214 | testWidgets( |
2215 | 'Range Slider can be painted in a narrower constraint when track shape is Rectangular' , |
2216 | (WidgetTester tester) async { |
2217 | await tester.pumpWidget( |
2218 | MaterialApp( |
2219 | theme: ThemeData( |
2220 | sliderTheme: const SliderThemeData(rangeTrackShape: RectangularRangeSliderTrackShape()), |
2221 | ), |
2222 | home: Directionality( |
2223 | textDirection: TextDirection.ltr, |
2224 | child: Material( |
2225 | child: Center( |
2226 | child: SizedBox( |
2227 | height: 10.0, |
2228 | width: 0.0, |
2229 | child: RangeSlider(values: const RangeValues(0.25, 0.5), onChanged: null), |
2230 | ), |
2231 | ), |
2232 | ), |
2233 | ), |
2234 | ), |
2235 | ); |
2236 | |
2237 | // _RenderRangeSlider is the last render object in the tree. |
2238 | final RenderObject renderObject = tester.allRenderObjects.last; |
2239 | |
2240 | //There should no gap between the inactive track and active track. |
2241 | expect( |
2242 | renderObject, |
2243 | paints |
2244 | // left inactive track RRect |
2245 | ..rect(rect: const Rect.fromLTRB(-24.0, 3.0, -12.0, 7.0)) |
2246 | // active track RRect |
2247 | ..rect(rect: const Rect.fromLTRB(-12.0, 3.0, 0.0, 7.0)) |
2248 | // right inactive track RRect |
2249 | ..rect(rect: const Rect.fromLTRB(0.0, 3.0, 24.0, 7.0)) |
2250 | // thumbs |
2251 | ..circle(x: -12.0, y: 5.0, radius: 10.0) |
2252 | ..circle(x: 0.0, y: 5.0, radius: 10.0), |
2253 | ); |
2254 | }, |
2255 | ); |
2256 | |
2257 | testWidgets('Update the divisions and values at the same time for RangeSlider' , ( |
2258 | WidgetTester tester, |
2259 | ) async { |
2260 | // Regress test for https://github.com/flutter/flutter/issues/65943 |
2261 | Widget buildFrame(double maxValue) { |
2262 | return MaterialApp( |
2263 | home: Material( |
2264 | child: Center( |
2265 | child: RangeSlider( |
2266 | values: const RangeValues(5, 8), |
2267 | max: maxValue, |
2268 | divisions: maxValue.toInt(), |
2269 | onChanged: (RangeValues newValue) {}, |
2270 | ), |
2271 | ), |
2272 | ), |
2273 | ); |
2274 | } |
2275 | |
2276 | await tester.pumpWidget(buildFrame(10)); |
2277 | |
2278 | // _RenderRangeSlider is the last render object in the tree. |
2279 | final RenderObject renderObject = tester.allRenderObjects.last; |
2280 | |
2281 | // Update the divisions from 10 to 15, the thumbs should be paint at the correct position. |
2282 | await tester.pumpWidget(buildFrame(15)); |
2283 | await tester.pumpAndSettle(); // Finish the animation. |
2284 | |
2285 | late RRect activeTrackRRect; |
2286 | expect( |
2287 | renderObject, |
2288 | paints |
2289 | ..rrect() |
2290 | ..rrect() |
2291 | ..something((Symbol method, List<dynamic> arguments) { |
2292 | if (method != #drawRRect) { |
2293 | return false; |
2294 | } |
2295 | activeTrackRRect = arguments[0] as RRect; |
2296 | return true; |
2297 | }), |
2298 | ); |
2299 | |
2300 | const double padding = 4.0; |
2301 | // The 1st thumb should at one-third(5 / 15) of the Slider. |
2302 | // The 2nd thumb should at (8 / 15) of the Slider. |
2303 | // The left of the active track shape is the position of the 1st thumb. |
2304 | // The right of the active track shape is the position of the 2nd thumb. |
2305 | // 24.0 is the default margin, (800.0 - 24.0 - 24.0 - padding) is the slider's width. |
2306 | // Where the padding value equals to the track height. |
2307 | expect( |
2308 | nearEqual(activeTrackRRect.left, (800.0 - 24.0 - 24.0 - padding) * (5 / 15) + 24.0, 0.01), |
2309 | true, |
2310 | ); |
2311 | expect( |
2312 | nearEqual( |
2313 | activeTrackRRect.right, |
2314 | (800.0 - 24.0 - 24.0 - padding) * (8 / 15) + 24.0 + padding, |
2315 | 0.01, |
2316 | ), |
2317 | true, |
2318 | ); |
2319 | }); |
2320 | |
2321 | testWidgets('RangeSlider changes mouse cursor when hovered' , (WidgetTester tester) async { |
2322 | const RangeValues values = RangeValues(50, 70); |
2323 | |
2324 | // Test default cursor. |
2325 | await tester.pumpWidget( |
2326 | MaterialApp( |
2327 | home: Directionality( |
2328 | textDirection: TextDirection.ltr, |
2329 | child: Material( |
2330 | child: Center( |
2331 | child: MouseRegion( |
2332 | cursor: SystemMouseCursors.forbidden, |
2333 | child: RangeSlider(values: values, max: 100.0, onChanged: (RangeValues values) {}), |
2334 | ), |
2335 | ), |
2336 | ), |
2337 | ), |
2338 | ), |
2339 | ); |
2340 | |
2341 | final TestGesture gesture = await tester.createGesture( |
2342 | kind: PointerDeviceKind.mouse, |
2343 | pointer: 1, |
2344 | ); |
2345 | await gesture.addPointer(location: tester.getCenter(find.byType(RangeSlider))); |
2346 | |
2347 | expect( |
2348 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
2349 | SystemMouseCursors.click, |
2350 | ); |
2351 | |
2352 | // Test custom cursor. |
2353 | await tester.pumpWidget( |
2354 | MaterialApp( |
2355 | home: Directionality( |
2356 | textDirection: TextDirection.ltr, |
2357 | child: Material( |
2358 | child: Center( |
2359 | child: MouseRegion( |
2360 | cursor: SystemMouseCursors.forbidden, |
2361 | child: RangeSlider( |
2362 | values: values, |
2363 | max: 100.0, |
2364 | mouseCursor: const MaterialStatePropertyAll<MouseCursor?>( |
2365 | SystemMouseCursors.text, |
2366 | ), |
2367 | onChanged: (RangeValues values) {}, |
2368 | ), |
2369 | ), |
2370 | ), |
2371 | ), |
2372 | ), |
2373 | ), |
2374 | ); |
2375 | |
2376 | await tester.pump(); |
2377 | expect( |
2378 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
2379 | SystemMouseCursors.text, |
2380 | ); |
2381 | }); |
2382 | |
2383 | testWidgets('RangeSlider MaterialStateMouseCursor resolves correctly' , ( |
2384 | WidgetTester tester, |
2385 | ) async { |
2386 | RangeValues values = const RangeValues(50, 70); |
2387 | const MouseCursor disabledCursor = SystemMouseCursors.basic; |
2388 | const MouseCursor hoveredCursor = SystemMouseCursors.grab; |
2389 | const MouseCursor draggedCursor = SystemMouseCursors.move; |
2390 | |
2391 | Widget buildFrame({required bool enabled}) { |
2392 | return MaterialApp( |
2393 | home: Directionality( |
2394 | textDirection: TextDirection.ltr, |
2395 | child: Material( |
2396 | child: StatefulBuilder( |
2397 | builder: (BuildContext context, StateSetter setState) { |
2398 | return Center( |
2399 | child: MouseRegion( |
2400 | cursor: SystemMouseCursors.forbidden, |
2401 | child: RangeSlider( |
2402 | mouseCursor: MaterialStateProperty.resolveWith<MouseCursor?>(( |
2403 | Set<MaterialState> states, |
2404 | ) { |
2405 | if (states.contains(MaterialState.disabled)) { |
2406 | return disabledCursor; |
2407 | } |
2408 | if (states.contains(MaterialState.dragged)) { |
2409 | return draggedCursor; |
2410 | } |
2411 | if (states.contains(MaterialState.hovered)) { |
2412 | return hoveredCursor; |
2413 | } |
2414 | |
2415 | return SystemMouseCursors.none; |
2416 | }), |
2417 | values: values, |
2418 | max: 100.0, |
2419 | onChanged: |
2420 | enabled |
2421 | ? (RangeValues newValues) { |
2422 | setState(() { |
2423 | values = newValues; |
2424 | }); |
2425 | } |
2426 | : null, |
2427 | onChangeStart: enabled ? (RangeValues newValues) {} : null, |
2428 | onChangeEnd: enabled ? (RangeValues newValues) {} : null, |
2429 | ), |
2430 | ), |
2431 | ); |
2432 | }, |
2433 | ), |
2434 | ), |
2435 | ), |
2436 | ); |
2437 | } |
2438 | |
2439 | final TestGesture gesture = await tester.createGesture( |
2440 | kind: PointerDeviceKind.mouse, |
2441 | pointer: 1, |
2442 | ); |
2443 | await gesture.addPointer(location: Offset.zero); |
2444 | |
2445 | await tester.pumpWidget(buildFrame(enabled: false)); |
2446 | expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), disabledCursor); |
2447 | |
2448 | await tester.pumpWidget(buildFrame(enabled: true)); |
2449 | expect( |
2450 | RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
2451 | SystemMouseCursors.none, |
2452 | ); |
2453 | |
2454 | await gesture.moveTo(tester.getCenter(find.byType(RangeSlider))); // start hover |
2455 | await tester.pumpAndSettle(); |
2456 | expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), hoveredCursor); |
2457 | |
2458 | await tester.timedDrag( |
2459 | find.byType(RangeSlider), |
2460 | const Offset(20.0, 0.0), |
2461 | const Duration(milliseconds: 100), |
2462 | ); |
2463 | expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), draggedCursor); |
2464 | }); |
2465 | |
2466 | testWidgets('RangeSlider can be hovered and has correct hover color' , ( |
2467 | WidgetTester tester, |
2468 | ) async { |
2469 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
2470 | RangeValues values = const RangeValues(50, 70); |
2471 | final ThemeData theme = ThemeData(); |
2472 | |
2473 | Widget buildApp({bool enabled = true}) { |
2474 | return MaterialApp( |
2475 | home: Directionality( |
2476 | textDirection: TextDirection.ltr, |
2477 | child: StatefulBuilder( |
2478 | builder: (BuildContext context, StateSetter setState) { |
2479 | return Material( |
2480 | child: Center( |
2481 | child: RangeSlider( |
2482 | values: values, |
2483 | max: 100.0, |
2484 | onChanged: |
2485 | enabled |
2486 | ? (RangeValues newValues) { |
2487 | setState(() { |
2488 | values = newValues; |
2489 | }); |
2490 | } |
2491 | : null, |
2492 | ), |
2493 | ), |
2494 | ); |
2495 | }, |
2496 | ), |
2497 | ), |
2498 | ); |
2499 | } |
2500 | |
2501 | await tester.pumpWidget(buildApp()); |
2502 | |
2503 | // RangeSlider does not have overlay when enabled and not hovered. |
2504 | await tester.pumpAndSettle(); |
2505 | expect( |
2506 | Material.of(tester.element(find.byType(RangeSlider))), |
2507 | isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))), |
2508 | ); |
2509 | |
2510 | // Start hovering. |
2511 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
2512 | await gesture.addPointer(); |
2513 | await gesture.moveTo(tester.getCenter(find.byType(RangeSlider))); |
2514 | |
2515 | // RangeSlider has overlay when enabled and hovered. |
2516 | await tester.pumpWidget(buildApp()); |
2517 | await tester.pumpAndSettle(); |
2518 | expect( |
2519 | Material.of(tester.element(find.byType(RangeSlider))), |
2520 | paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)), |
2521 | ); |
2522 | |
2523 | // RangeSlider does not have an overlay when disabled and hovered. |
2524 | await tester.pumpWidget(buildApp(enabled: false)); |
2525 | await tester.pumpAndSettle(); |
2526 | expect( |
2527 | Material.of(tester.element(find.byType(RangeSlider))), |
2528 | isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))), |
2529 | ); |
2530 | }); |
2531 | |
2532 | testWidgets('RangeSlider is draggable and has correct dragged color' , ( |
2533 | WidgetTester tester, |
2534 | ) async { |
2535 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
2536 | RangeValues values = const RangeValues(50, 70); |
2537 | final ThemeData theme = ThemeData(); |
2538 | |
2539 | Widget buildApp({bool enabled = true}) { |
2540 | return MaterialApp( |
2541 | home: Directionality( |
2542 | textDirection: TextDirection.ltr, |
2543 | child: StatefulBuilder( |
2544 | builder: (BuildContext context, StateSetter setState) { |
2545 | return Material( |
2546 | child: Center( |
2547 | child: RangeSlider( |
2548 | values: values, |
2549 | max: 100.0, |
2550 | onChanged: |
2551 | enabled |
2552 | ? (RangeValues newValues) { |
2553 | setState(() { |
2554 | values = newValues; |
2555 | }); |
2556 | } |
2557 | : null, |
2558 | ), |
2559 | ), |
2560 | ); |
2561 | }, |
2562 | ), |
2563 | ), |
2564 | ); |
2565 | } |
2566 | |
2567 | await tester.pumpWidget(buildApp()); |
2568 | |
2569 | // RangeSlider does not have overlay when enabled and not dragged. |
2570 | await tester.pumpAndSettle(); |
2571 | expect( |
2572 | Material.of(tester.element(find.byType(RangeSlider))), |
2573 | isNot(paints..circle(color: theme.colorScheme.primary.withOpacity(0.12))), |
2574 | ); |
2575 | |
2576 | // Start dragging. |
2577 | final TestGesture drag = await tester.startGesture(tester.getCenter(find.byType(RangeSlider))); |
2578 | await tester.pump(kPressTimeout); |
2579 | |
2580 | // Less than configured touch slop, more than default touch slop |
2581 | await drag.moveBy(const Offset(19.0, 0)); |
2582 | await tester.pump(); |
2583 | |
2584 | // RangeSlider has overlay when enabled and dragged. |
2585 | expect( |
2586 | Material.of(tester.element(find.byType(RangeSlider))), |
2587 | paints..circle(color: theme.colorScheme.primary.withOpacity(0.12)), |
2588 | ); |
2589 | }); |
2590 | |
2591 | testWidgets('RangeSlider overlayColor supports hovered and dragged states' , ( |
2592 | WidgetTester tester, |
2593 | ) async { |
2594 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
2595 | RangeValues values = const RangeValues(50, 70); |
2596 | const Color hoverColor = Color(0xffff0000); |
2597 | const Color draggedColor = Color(0xff0000ff); |
2598 | |
2599 | Widget buildApp({bool enabled = true}) { |
2600 | return MaterialApp( |
2601 | home: Directionality( |
2602 | textDirection: TextDirection.ltr, |
2603 | child: StatefulBuilder( |
2604 | builder: (BuildContext context, StateSetter setState) { |
2605 | return Material( |
2606 | child: Center( |
2607 | child: RangeSlider( |
2608 | values: values, |
2609 | max: 100.0, |
2610 | overlayColor: MaterialStateProperty.resolveWith<Color?>(( |
2611 | Set<MaterialState> states, |
2612 | ) { |
2613 | if (states.contains(MaterialState.hovered)) { |
2614 | return hoverColor; |
2615 | } |
2616 | if (states.contains(MaterialState.dragged)) { |
2617 | return draggedColor; |
2618 | } |
2619 | |
2620 | return null; |
2621 | }), |
2622 | onChanged: |
2623 | enabled |
2624 | ? (RangeValues newValues) { |
2625 | setState(() { |
2626 | values = newValues; |
2627 | }); |
2628 | } |
2629 | : null, |
2630 | onChangeStart: enabled ? (RangeValues newValues) {} : null, |
2631 | onChangeEnd: enabled ? (RangeValues newValues) {} : null, |
2632 | ), |
2633 | ), |
2634 | ); |
2635 | }, |
2636 | ), |
2637 | ), |
2638 | ); |
2639 | } |
2640 | |
2641 | await tester.pumpWidget(buildApp()); |
2642 | |
2643 | // RangeSlider does not have overlay when enabled and not hovered. |
2644 | await tester.pumpAndSettle(); |
2645 | expect( |
2646 | Material.of(tester.element(find.byType(RangeSlider))), |
2647 | isNot(paints..circle(color: hoverColor)), |
2648 | ); |
2649 | |
2650 | // Hover on the range slider but outside the thumb. |
2651 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
2652 | await gesture.addPointer(); |
2653 | await gesture.moveTo(tester.getTopLeft(find.byType(RangeSlider))); |
2654 | |
2655 | await tester.pumpWidget(buildApp()); |
2656 | await tester.pumpAndSettle(); |
2657 | expect( |
2658 | Material.of(tester.element(find.byType(RangeSlider))), |
2659 | isNot(paints..circle(color: hoverColor)), |
2660 | ); |
2661 | |
2662 | // Hover on the thumb. |
2663 | await gesture.moveTo(tester.getCenter(find.byType(RangeSlider))); |
2664 | await tester.pumpAndSettle(); |
2665 | expect( |
2666 | Material.of(tester.element(find.byType(RangeSlider))), |
2667 | paints..circle(color: hoverColor), |
2668 | ); |
2669 | |
2670 | // Hover on the slider but outside the thumb. |
2671 | await gesture.moveTo(tester.getBottomRight(find.byType(RangeSlider))); |
2672 | await tester.pumpAndSettle(); |
2673 | expect( |
2674 | Material.of(tester.element(find.byType(RangeSlider))), |
2675 | isNot(paints..circle(color: hoverColor)), |
2676 | ); |
2677 | |
2678 | // Reset range slider values. |
2679 | values = const RangeValues(50, 70); |
2680 | |
2681 | // RangeSlider does not have overlay when enabled and not dragged. |
2682 | await tester.pumpWidget(buildApp()); |
2683 | await tester.pumpAndSettle(); |
2684 | expect( |
2685 | Material.of(tester.element(find.byType(RangeSlider))), |
2686 | isNot(paints..circle(color: draggedColor)), |
2687 | ); |
2688 | |
2689 | // Start dragging. |
2690 | final TestGesture drag = await tester.startGesture(tester.getCenter(find.byType(RangeSlider))); |
2691 | await tester.pump(kPressTimeout); |
2692 | |
2693 | // Less than configured touch slop, more than default touch slop. |
2694 | await drag.moveBy(const Offset(19.0, 0)); |
2695 | await tester.pump(); |
2696 | |
2697 | // RangeSlider has overlay when enabled and dragged. |
2698 | expect( |
2699 | Material.of(tester.element(find.byType(RangeSlider))), |
2700 | paints..circle(color: draggedColor), |
2701 | ); |
2702 | |
2703 | // Stop dragging. |
2704 | await drag.up(); |
2705 | await tester.pumpAndSettle(); |
2706 | expect( |
2707 | Material.of(tester.element(find.byType(RangeSlider))), |
2708 | isNot(paints..circle(color: draggedColor)), |
2709 | ); |
2710 | }); |
2711 | |
2712 | testWidgets('RangeSlider onChangeStart and onChangeEnd fire once' , (WidgetTester tester) async { |
2713 | // Regression test for https://github.com/flutter/flutter/issues/128433 |
2714 | |
2715 | int startFired = 0; |
2716 | int endFired = 0; |
2717 | await tester.pumpWidget( |
2718 | MaterialApp( |
2719 | home: Directionality( |
2720 | textDirection: TextDirection.ltr, |
2721 | child: Material( |
2722 | child: Center( |
2723 | child: GestureDetector( |
2724 | onHorizontalDragUpdate: (_) {}, |
2725 | child: RangeSlider( |
2726 | values: const RangeValues(40, 80), |
2727 | max: 100, |
2728 | onChanged: (RangeValues newValue) {}, |
2729 | onChangeStart: (RangeValues value) { |
2730 | startFired += 1; |
2731 | }, |
2732 | onChangeEnd: (RangeValues value) { |
2733 | endFired += 1; |
2734 | }, |
2735 | ), |
2736 | ), |
2737 | ), |
2738 | ), |
2739 | ), |
2740 | ), |
2741 | ); |
2742 | |
2743 | await tester.timedDragFrom( |
2744 | tester.getTopLeft(find.byType(RangeSlider)), |
2745 | const Offset(100.0, 0.0), |
2746 | const Duration(milliseconds: 500), |
2747 | ); |
2748 | |
2749 | expect(startFired, equals(1)); |
2750 | expect(endFired, equals(1)); |
2751 | }); |
2752 | |
2753 | testWidgets('RangeSlider in a ListView does not throw an exception' , (WidgetTester tester) async { |
2754 | // Regression test for https://github.com/flutter/flutter/issues/126648 |
2755 | |
2756 | await tester.pumpWidget( |
2757 | MaterialApp( |
2758 | home: Directionality( |
2759 | textDirection: TextDirection.ltr, |
2760 | child: Material( |
2761 | child: ListView( |
2762 | children: <Widget>[ |
2763 | const SizedBox(height: 600, child: Placeholder()), |
2764 | RangeSlider( |
2765 | values: const RangeValues(40, 80), |
2766 | max: 100, |
2767 | onChanged: (RangeValues newValue) {}, |
2768 | ), |
2769 | ], |
2770 | ), |
2771 | ), |
2772 | ), |
2773 | ), |
2774 | ); |
2775 | |
2776 | // No exception should be thrown. |
2777 | expect(tester.takeException(), null); |
2778 | }); |
2779 | |
2780 | // This is a regression test for https://github.com/flutter/flutter/issues/141953. |
2781 | testWidgets('Semantic nodes do not throw an error after clearSemantics' , ( |
2782 | WidgetTester tester, |
2783 | ) async { |
2784 | SemanticsTester semantics = SemanticsTester(tester); |
2785 | |
2786 | await tester.pumpWidget( |
2787 | Directionality( |
2788 | textDirection: TextDirection.ltr, |
2789 | child: Material( |
2790 | child: RangeSlider( |
2791 | values: const RangeValues(40, 80), |
2792 | max: 100, |
2793 | onChanged: (RangeValues newValue) {}, |
2794 | ), |
2795 | ), |
2796 | ), |
2797 | ); |
2798 | |
2799 | // Dispose the semantics to trigger clearSemantics. |
2800 | semantics.dispose(); |
2801 | await tester.pumpAndSettle(); |
2802 | |
2803 | expect(tester.takeException(), isNull); |
2804 | |
2805 | // Initialize the semantics again. |
2806 | semantics = SemanticsTester(tester); |
2807 | await tester.pumpAndSettle(); |
2808 | |
2809 | expect(tester.takeException(), isNull); |
2810 | |
2811 | semantics.dispose(); |
2812 | }, semanticsEnabled: false); |
2813 | |
2814 | testWidgets('RangeSlider overlay appears correctly for specific thumb interactions' , ( |
2815 | WidgetTester tester, |
2816 | ) async { |
2817 | tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; |
2818 | RangeValues values = const RangeValues(50, 70); |
2819 | const Color hoverColor = Color(0xffff0000); |
2820 | const Color dragColor = Color(0xff0000ff); |
2821 | |
2822 | Widget buildApp() { |
2823 | return MaterialApp( |
2824 | home: Directionality( |
2825 | textDirection: TextDirection.ltr, |
2826 | child: StatefulBuilder( |
2827 | builder: (BuildContext context, StateSetter setState) { |
2828 | return Material( |
2829 | child: Center( |
2830 | child: RangeSlider( |
2831 | values: values, |
2832 | max: 100.0, |
2833 | overlayColor: WidgetStateProperty.resolveWith<Color?>(( |
2834 | Set<WidgetState> states, |
2835 | ) { |
2836 | if (states.contains(WidgetState.hovered)) { |
2837 | return hoverColor; |
2838 | } |
2839 | if (states.contains(WidgetState.dragged)) { |
2840 | return dragColor; |
2841 | } |
2842 | |
2843 | return null; |
2844 | }), |
2845 | onChanged: (RangeValues newValues) { |
2846 | setState(() { |
2847 | values = newValues; |
2848 | }); |
2849 | }, |
2850 | onChangeStart: (RangeValues newValues) {}, |
2851 | onChangeEnd: (RangeValues newValues) {}, |
2852 | ), |
2853 | ), |
2854 | ); |
2855 | }, |
2856 | ), |
2857 | ), |
2858 | ); |
2859 | } |
2860 | |
2861 | await tester.pumpWidget(buildApp()); |
2862 | await tester.pumpAndSettle(); |
2863 | |
2864 | // Initial state - no overlay. |
2865 | expect( |
2866 | Material.of(tester.element(find.byType(RangeSlider))), |
2867 | isNot(paints..circle(color: dragColor)), |
2868 | ); |
2869 | |
2870 | // Drag start thumb to left. |
2871 | final Offset topThumbLocation = tester.getCenter(find.byType(RangeSlider)); |
2872 | final TestGesture dragStartThumb = await tester.startGesture(topThumbLocation); |
2873 | await tester.pump(kPressTimeout); |
2874 | await dragStartThumb.moveBy(const Offset(-20.0, 0)); |
2875 | await tester.pumpAndSettle(); |
2876 | |
2877 | // Verify overlay is visible and shadow is visible on single thumb. |
2878 | expect( |
2879 | Material.of(tester.element(find.byType(RangeSlider))), |
2880 | paints |
2881 | ..circle(color: dragColor) |
2882 | ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0) |
2883 | ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 12.0), |
2884 | ); |
2885 | |
2886 | // Move back and release. |
2887 | await dragStartThumb.moveBy(const Offset(20.0, 0)); |
2888 | await dragStartThumb.up(); |
2889 | await tester.pumpAndSettle(); |
2890 | |
2891 | // Verify overlay and shadow disappears |
2892 | expect( |
2893 | Material.of(tester.element(find.byType(RangeSlider))), |
2894 | isNot( |
2895 | paints |
2896 | ..circle(color: dragColor) |
2897 | ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0) |
2898 | ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0), |
2899 | ), |
2900 | ); |
2901 | |
2902 | // Drag end thumb and return to original position. |
2903 | final Offset bottomThumbLocation = tester |
2904 | .getCenter(find.byType(RangeSlider)) |
2905 | .translate(220.0, 0.0); |
2906 | final TestGesture dragEndThumb = await tester.startGesture(bottomThumbLocation); |
2907 | await tester.pump(kPressTimeout); |
2908 | await dragEndThumb.moveBy(const Offset(20.0, 0)); |
2909 | await tester.pump(kPressTimeout); |
2910 | await dragEndThumb.moveBy(const Offset(-20.0, 0)); |
2911 | await dragEndThumb.up(); |
2912 | await tester.pumpAndSettle(); |
2913 | |
2914 | // Verify overlay disappears. |
2915 | expect( |
2916 | Material.of(tester.element(find.byType(RangeSlider))), |
2917 | isNot(paints..circle(color: dragColor)), |
2918 | ); |
2919 | |
2920 | // Hover on start thumb. |
2921 | final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); |
2922 | await gesture.addPointer(); |
2923 | await gesture.moveTo(topThumbLocation); |
2924 | await tester.pumpAndSettle(); |
2925 | |
2926 | // Verify overlay appears only for start thumb and no shadow is visible. |
2927 | expect( |
2928 | Material.of(tester.element(find.byType(RangeSlider))), |
2929 | paints |
2930 | ..circle(color: hoverColor) |
2931 | ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0) |
2932 | ..path(color: Colors.black, style: PaintingStyle.stroke, strokeWidth: 2.0), |
2933 | ); |
2934 | |
2935 | final RenderObject renderObject = tester.renderObject(find.byType(RangeSlider)); |
2936 | // 2 thumbs and 1 overlay. |
2937 | expect(renderObject, paintsExactlyCountTimes(#drawCircle, 3)); |
2938 | |
2939 | // Move away from thumb |
2940 | await gesture.moveTo(tester.getTopRight(find.byType(RangeSlider))); |
2941 | await tester.pumpAndSettle(); |
2942 | |
2943 | // Verify overlay disappears |
2944 | expect( |
2945 | Material.of(tester.element(find.byType(RangeSlider))), |
2946 | isNot(paints..circle(color: hoverColor)), |
2947 | ); |
2948 | }); |
2949 | } |
2950 | |