1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6import 'package:flutter/gestures.dart';
7import 'package:flutter/material.dart';
8import 'package:flutter/rendering.dart';
9import 'package:flutter/src/physics/utils.dart' show nearEqual;
10import 'package:flutter_test/flutter_test.dart';
11
12import '../widgets/semantics_tester.dart';
13
14void 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com