blob: 28f9073fb6ad513cf0cf287a3d9a3fa79363636d [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// reduced-test-set:
// This file is run as part of a reduced test set in CI on Mac and Windows
// machines.
@Tags(<String>['reduced-test-set'])
library;
import 'dart:collection';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../widgets/semantics_tester.dart';
RenderBox getRenderSegmentedControl(WidgetTester tester) {
return tester.allRenderObjects.firstWhere((RenderObject currentObject) {
return currentObject.toStringShort().contains('_RenderSegmentedControl');
})
as RenderBox;
}
Rect currentUnscaledThumbRect(WidgetTester tester, {bool useGlobalCoordinate = false}) {
final dynamic renderSegmentedControl = getRenderSegmentedControl(tester);
// Using dynamic to access private class in test.
// ignore: avoid_dynamic_calls
final local = renderSegmentedControl.currentThumbRect as Rect;
if (!useGlobalCoordinate) {
return local;
}
final segmentedControl = renderSegmentedControl as RenderBox;
return local.shift(segmentedControl.localToGlobal(Offset.zero));
}
int? getHighlightedIndex(WidgetTester tester) {
return (getRenderSegmentedControl(tester) as dynamic).highlightedIndex as int?;
}
Color getThumbColor(WidgetTester tester) {
return (getRenderSegmentedControl(tester) as dynamic).thumbColor as Color;
}
double currentThumbScale(WidgetTester tester) {
return (getRenderSegmentedControl(tester) as dynamic).thumbScale as double;
}
Widget setupSimpleSegmentedControl() {
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2')};
return boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
);
}
StateSetter? setState;
int? groupValue = 0;
void defaultCallback(int? newValue) {
setState!(() {
groupValue = newValue;
});
}
Widget boilerplate({required WidgetBuilder builder}) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return builder(context);
},
),
),
);
}
void main() {
setUp(() {
setState = null;
groupValue = 0;
});
testWidgets('Need at least 2 children', (WidgetTester tester) async {
groupValue = null;
await expectLater(
() => tester.pumpWidget(
CupertinoSlidingSegmentedControl<int>(
children: const <int, Widget>{},
groupValue: groupValue,
onValueChanged: defaultCallback,
),
),
throwsA(
isAssertionError.having(
(AssertionError error) => error.toString(),
'.toString()',
contains('children.length'),
),
),
);
await expectLater(
() => tester.pumpWidget(
CupertinoSlidingSegmentedControl<int>(
children: const <int, Widget>{0: Text('Child 1')},
groupValue: groupValue,
onValueChanged: defaultCallback,
),
),
throwsA(
isAssertionError.having(
(AssertionError error) => error.toString(),
'.toString()',
contains('children.length'),
),
),
);
groupValue = -1;
await expectLater(
() => tester.pumpWidget(
CupertinoSlidingSegmentedControl<int>(
children: const <int, Widget>{0: Text('Child 1'), 1: Text('Child 2'), 2: Text('Child 3')},
groupValue: groupValue,
onValueChanged: defaultCallback,
),
),
throwsA(
isAssertionError.having(
(AssertionError error) => error.toString(),
'.toString()',
contains('groupValue must be either null or one of the keys in the children map'),
),
),
);
});
testWidgets('Padding works', (WidgetTester tester) async {
const key = Key('Container');
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2')};
Future<void> verifyPadding({EdgeInsets? padding}) async {
final EdgeInsets effectivePadding =
padding ?? const EdgeInsets.symmetric(vertical: 2, horizontal: 3);
final Rect segmentedControlRect = tester.getRect(find.byKey(key));
expect(
tester.getTopLeft(
find.ancestor(of: find.byWidget(children[0]!), matching: find.byType(MetaData)),
),
segmentedControlRect.topLeft + effectivePadding.topLeft,
);
expect(
tester.getBottomLeft(
find.ancestor(of: find.byWidget(children[0]!), matching: find.byType(MetaData)),
),
segmentedControlRect.bottomLeft + effectivePadding.bottomLeft,
);
expect(
tester.getTopRight(
find.ancestor(of: find.byWidget(children[1]!), matching: find.byType(MetaData)),
),
segmentedControlRect.topRight + effectivePadding.topRight,
);
expect(
tester.getBottomRight(
find.ancestor(of: find.byWidget(children[1]!), matching: find.byType(MetaData)),
),
segmentedControlRect.bottomRight + effectivePadding.bottomRight,
);
}
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: key,
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
// Default padding works.
await verifyPadding();
// Switch to Child 2 padding should remain the same.
await tester.tap(find.text('Child 2'));
await tester.pumpAndSettle();
await verifyPadding();
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: key,
padding: const EdgeInsets.fromLTRB(1, 3, 5, 7),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
// Custom padding works.
await verifyPadding(padding: const EdgeInsets.fromLTRB(1, 3, 5, 7));
// Switch back to Child 1 padding should remain the same.
await tester.tap(find.text('Child 1'));
await tester.pumpAndSettle();
await verifyPadding(padding: const EdgeInsets.fromLTRB(1, 3, 5, 7));
});
testWidgets('Tap changes toggle state', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2'), 2: Text('Child 3')};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(groupValue, 0);
await tester.tap(find.text('Child 2'));
expect(groupValue, 1);
// Tapping the currently selected item should not change groupValue.
await tester.tap(find.text('Child 2'));
expect(groupValue, 1);
});
testWidgets('Segmented controls respect theme', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Icon(IconData(1))};
await tester.pumpWidget(
CupertinoApp(
theme: const CupertinoThemeData(brightness: Brightness.dark),
home: boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
),
);
DefaultTextStyle textStyle = tester.widget(
find.widgetWithText(DefaultTextStyle, 'Child 1').first,
);
expect(textStyle.style.fontWeight, FontWeight.w600);
await tester.tap(find.byIcon(const IconData(1)));
await tester.pump();
await tester.pumpAndSettle();
textStyle = tester.widget(find.widgetWithText(DefaultTextStyle, 'Child 1').first);
expect(groupValue, 1);
expect(textStyle.style.fontWeight, FontWeight.w500);
});
testWidgets('SegmentedControl dark mode', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Icon(IconData(1))};
Brightness brightness = Brightness.light;
late StateSetter setState;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return MediaQuery(
data: MediaQueryData(platformBrightness: brightness),
child: boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
thumbColor: CupertinoColors.systemGreen,
backgroundColor: CupertinoColors.systemRed,
);
},
),
);
},
),
);
final decoration =
tester
.widget<Container>(
find.descendant(
of: find.byType(UnconstrainedBox),
matching: find.byType(Container),
),
)
.decoration!
as ShapeDecoration;
expect(getThumbColor(tester).value, CupertinoColors.systemGreen.color.value);
expect(decoration.color!.value, CupertinoColors.systemRed.color.value);
setState(() {
brightness = Brightness.dark;
});
await tester.pump();
final decorationDark =
tester
.widget<Container>(
find.descendant(
of: find.byType(UnconstrainedBox),
matching: find.byType(Container),
),
)
.decoration!
as ShapeDecoration;
expect(getThumbColor(tester).value, CupertinoColors.systemGreen.darkColor.value);
expect(decorationDark.color!.value, CupertinoColors.systemRed.darkColor.value);
});
testWidgets('Children can be non-Text or Icon widgets (in this case, '
'a Container or Placeholder widget)', (WidgetTester tester) async {
const children = <int, Widget>{
0: Text('Child 1'),
1: SizedBox(width: 50, height: 50),
2: Placeholder(),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
});
testWidgets('Passed in value is child initially selected', (WidgetTester tester) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
expect(getHighlightedIndex(tester), 0);
});
testWidgets('Null input for value results in no child initially selected', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2')};
groupValue = null;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
);
},
),
);
expect(getHighlightedIndex(tester), null);
});
testWidgets('Disabled segment can be selected programmatically', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2'), 2: Text('Child 3')};
groupValue = 0;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
disabledChildren: const <int>{0},
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
);
},
),
);
expect(getHighlightedIndex(tester), 0);
});
testWidgets('Long press not-selected child interactions', (WidgetTester tester) async {
const children = <int, Widget>{
0: Text('Child 1'),
1: Text('Child 2'),
2: Text('Child 3'),
3: Text('Child 4'),
4: Text('Child 5'),
};
// Child 3 is initially selected.
groupValue = 2;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
double getChildOpacityByName(String childName) {
return tester
.renderObject<RenderAnimatedOpacity>(
find.ancestor(matching: find.byType(AnimatedOpacity), of: find.text(childName)),
)
.opacity
.value;
}
// Opacity 1 with no interaction.
expect(getChildOpacityByName('Child 1'), 1);
final Offset center = tester.getCenter(find.text('Child 1'));
final TestGesture gesture = await tester.startGesture(center);
await tester.pumpAndSettle();
// Opacity drops to 0.2.
expect(getChildOpacityByName('Child 1'), 0.2);
// Move down slightly, slightly outside of the segmented control.
await gesture.moveBy(const Offset(0, 50));
await tester.pumpAndSettle();
expect(getChildOpacityByName('Child 1'), 0.2);
// Move further down and far away from the segmented control.
await gesture.moveBy(const Offset(0, 200));
await tester.pumpAndSettle();
expect(getChildOpacityByName('Child 1'), 1);
// Move to child 5.
await gesture.moveTo(tester.getCenter(find.text('Child 5')));
await tester.pumpAndSettle();
expect(getChildOpacityByName('Child 1'), 1);
expect(getChildOpacityByName('Child 5'), 0.2);
// Move to child 2.
await gesture.moveTo(tester.getCenter(find.text('Child 2')));
await tester.pumpAndSettle();
expect(getChildOpacityByName('Child 1'), 1);
expect(getChildOpacityByName('Child 5'), 1);
expect(getChildOpacityByName('Child 2'), 0.2);
});
testWidgets('Long press does not change the opacity of currently-selected child', (
WidgetTester tester,
) async {
double getChildOpacityByName(String childName) {
return tester
.renderObject<RenderAnimatedOpacity>(
find.ancestor(matching: find.byType(AnimatedOpacity), of: find.text(childName)),
)
.opacity
.value;
}
await tester.pumpWidget(setupSimpleSegmentedControl());
final Offset center = tester.getCenter(find.text('Child 1'));
final TestGesture gesture = await tester.startGesture(center);
await tester.pump();
await tester.pumpAndSettle();
expect(getChildOpacityByName('Child 1'), 1);
// Finish gesture to release resources.
await gesture.up();
await tester.pumpAndSettle();
});
testWidgets('Height of segmented control is determined by tallest widget', (
WidgetTester tester,
) async {
final children = <int, Widget>{
0: Container(constraints: const BoxConstraints.tightFor(height: 100.0)),
1: Container(constraints: const BoxConstraints.tightFor(height: 400.0)),
2: Container(constraints: const BoxConstraints.tightFor(height: 200.0)),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
final RenderBox buttonBox = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control')),
);
expect(
buttonBox.size.height,
400.0 + 2 * 2, // 2 px padding on both sides.
);
});
testWidgets('Width of each segmented control segment is determined by widest widget by default', (
WidgetTester tester,
) async {
final children = <int, Widget>{
0: Container(constraints: const BoxConstraints.tightFor(width: 50.0)),
1: Container(constraints: const BoxConstraints.tightFor(width: 100.0)),
2: Container(constraints: const BoxConstraints.tightFor(width: 200.0)),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
final RenderBox segmentedControl = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control')),
);
// Subtract the 8.0px for horizontal padding separator. Remaining width should be allocated
// to each child equally.
final double childWidth = (segmentedControl.size.width - 8) / 3;
expect(childWidth, 200.0 + 10 * 2);
});
testWidgets('If proportionalWidth is true, the width of each segmented '
'control segment is determined by its own content', (WidgetTester tester) async {
final children = <int, Widget>{
0: const SizedBox(width: 50, child: Text('First')),
1: const SizedBox(width: 100, child: Text('Second')),
2: const SizedBox(width: 70, child: Text('Third')),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
proportionalWidth: true,
onValueChanged: defaultCallback,
);
},
),
);
Size getChildSize(int index) {
return tester.getSize(
find.ancestor(of: find.byWidget(children[index]!), matching: find.byType(MetaData)),
);
}
final Size firstChildSize = getChildSize(0);
expect(firstChildSize.width, 50 + 10.0 * 2);
final Size secondChildSize = getChildSize(1);
expect(secondChildSize.width, 100 + 10.0 * 2);
final Size thirdChildSize = getChildSize(2);
expect(thirdChildSize.width, 70 + 10.0 * 2);
// Overall segment control width is the sum of the segment widths + horizontal paddings + 2 separator width.
final RenderBox segmentedControl = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control')),
);
final double childWidthSum =
firstChildSize.width + secondChildSize.width + thirdChildSize.width;
expect(segmentedControl.size.width, childWidthSum + 6.0 + 2.0);
});
testWidgets('proportionalWidth rebuild', (WidgetTester tester) async {
final children = <int, Widget>{
0: const SizedBox(width: 50, child: Text('First')),
1: const SizedBox(width: 200, child: Text('Second')),
2: const SizedBox(width: 70, child: Text('Third')),
};
var proportionalWidth = false;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
proportionalWidth: proportionalWidth,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
Size getChildSize(int index) {
return tester.getSize(
find.ancestor(of: find.byWidget(children[index]!), matching: find.byType(MetaData)),
);
}
Size firstChildSize = getChildSize(0);
expect(firstChildSize.width, 200 + 10.0 * 2);
Size secondChildSize = getChildSize(1);
expect(secondChildSize.width, 200 + 10.0 * 2);
Size thirdChildSize = getChildSize(2);
expect(thirdChildSize.width, 200 + 10.0 * 2);
setState!(() {
proportionalWidth = true;
});
await tester.pump();
firstChildSize = getChildSize(0);
expect(firstChildSize.width, 50 + 10.0 * 2);
secondChildSize = getChildSize(1);
expect(secondChildSize.width, 200 + 10.0 * 2);
thirdChildSize = getChildSize(2);
expect(thirdChildSize.width, 70 + 10.0 * 2);
});
testWidgets('If proportionalWidth is true, the width of each segmented '
'control segment is updated when children change', (WidgetTester tester) async {
var children = <int, Widget>{
0: const SizedBox(width: 50, child: Text('First')),
1: const SizedBox(width: 100, child: Text('Second')),
2: const SizedBox(width: 70, child: Text('Third')),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
proportionalWidth: true,
onValueChanged: defaultCallback,
);
},
),
);
Size getChildSize(int index) {
return tester.getSize(
find.ancestor(of: find.byWidget(children[index]!), matching: find.byType(MetaData)),
);
}
Size firstChildSize = getChildSize(0);
expect(firstChildSize.width, 50 + 10.0 * 2);
Size secondChildSize = getChildSize(1);
expect(secondChildSize.width, 100 + 10.0 * 2);
Size thirdChildSize = getChildSize(2);
expect(thirdChildSize.width, 70 + 10.0 * 2);
setState!(() {
children = <int, Widget>{
0: const SizedBox(),
1: const SizedBox(width: 220, child: Text('Second')),
2: const SizedBox(width: 170, child: Text('Third')),
};
});
await tester.pump();
firstChildSize = getChildSize(0);
expect(firstChildSize.width, 0 + 10.0 * 2);
secondChildSize = getChildSize(1);
expect(secondChildSize.width, 220 + 10.0 * 2);
thirdChildSize = getChildSize(2);
expect(thirdChildSize.width, 170 + 10.0 * 2);
});
testWidgets('If proportionalWidth is true and the overall segment control width '
'is larger than the max width of the parent constraints, each segment scales down', (
WidgetTester tester,
) async {
final children = <int, Widget>{
0: const SizedBox(width: 50, child: Text('First')),
1: const SizedBox(width: 100, child: Text('Second')),
2: const SizedBox(width: 200, child: Text('Third')),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 200),
child: CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
proportionalWidth: true,
onValueChanged: defaultCallback,
),
);
},
),
);
Size getChildSize(int index) {
return tester.getSize(
find.ancestor(of: find.byWidget(children[index]!), matching: find.byType(MetaData)),
);
}
// Without constraints, the overall size should be 410: 50 + 100 + 200
// + 10.0 * 6(horizontal padding). To fit in 194(allowed max width - padding),
// each segment width should scale down to original width * (194 - separator) / 413.5.
final Size firstChildSize = getChildSize(0);
const double maxAllowedTotal = 200 - 6 - 2;
const double originalTotal = 410;
expect(firstChildSize.width, (50 + 10.0 * 2) * maxAllowedTotal / originalTotal);
final Size secondChildSize = getChildSize(1);
expect(secondChildSize.width, (100 + 10.0 * 2) * maxAllowedTotal / originalTotal);
final Size thirdChildSize = getChildSize(2);
expect(thirdChildSize.width, (200 + 10.0 * 2) * maxAllowedTotal / originalTotal);
});
testWidgets('If proportionalWidth is true and the overall segment control width '
'is smaller than the min width of the parent constraints, each segment scales up', (
WidgetTester tester,
) async {
final children = <int, Widget>{
0: const SizedBox(width: 20, child: Text('First')),
1: const SizedBox(width: 30, child: Text('Second')),
2: const SizedBox(width: 50, child: Text('Third')),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(minWidth: 200),
child: CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
proportionalWidth: true,
onValueChanged: defaultCallback,
),
);
},
),
);
Size getChildSize(int index) {
return tester.getSize(
find.ancestor(of: find.byWidget(children[index]!), matching: find.byType(MetaData)),
);
}
// Without constraints, the overall size should be 160.0: 20 + 30 + 50
// + 10.0 * 6(horizontal padding). To fit in 194(allowed max width - padding),
// each segment width should scale up to original width * (194 - separator) / 155.5.
final Size firstChildSize = getChildSize(0);
const double constraintsMinWidth = 200 - 6 - 2;
const originalTotal = 160.0;
expect(
firstChildSize.width,
moreOrLessEquals((20 + 10.0 * 2) * constraintsMinWidth / originalTotal),
);
final Size secondChildSize = getChildSize(1);
expect(
secondChildSize.width,
moreOrLessEquals((30 + 10.0 * 2) * constraintsMinWidth / originalTotal),
);
final Size thirdChildSize = getChildSize(2);
expect(
thirdChildSize.width,
moreOrLessEquals((50 + 10.0 * 2) * constraintsMinWidth / originalTotal),
);
});
testWidgets('Width is finite in unbounded space', (WidgetTester tester) async {
const children = <int, Widget>{0: SizedBox(width: 50), 1: SizedBox(width: 70)};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return Row(
children: <Widget>[
CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
),
],
);
},
),
);
final RenderBox segmentedControl = tester.renderObject(
find.byKey(const ValueKey<String>('Segmented Control')),
);
expect(
segmentedControl.size.width,
70 * 2 + 10.0 * 4 + 3 * 2 + 1, // 2 children + 4 child padding + 2 outer padding + 1 separator
);
});
testWidgets('Directionality test - RTL should reverse order of widgets', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2')};
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
),
),
);
expect(
tester.getTopRight(find.text('Child 1')).dx > tester.getTopRight(find.text('Child 2')).dx,
isTrue,
);
});
testWidgets('Correct initial selection and toggling behavior - RTL', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2')};
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.rtl,
child: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
),
),
);
// highlightedIndex is 1 instead of 0 because of RTL.
expect(getHighlightedIndex(tester), 1);
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(getHighlightedIndex(tester), 0);
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(getHighlightedIndex(tester), 0);
});
testWidgets('Segmented control semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2')};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
role: SemanticsRole.radioGroup,
children: <TestSemantics>[
TestSemantics(
label: 'Child 1',
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.hasSelectedState,
SemanticsFlag.isSelected,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
TestSemantics(
label: 'Child 2',
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isInMutuallyExclusiveGroup,
// Declares that it is selectable, but not currently selected.
SemanticsFlag.hasSelectedState,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
role: SemanticsRole.radioGroup,
children: <TestSemantics>[
TestSemantics(
label: 'Child 1',
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isInMutuallyExclusiveGroup,
// Declares that it is selectable, but not currently selected.
SemanticsFlag.hasSelectedState,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
TestSemantics(
label: 'Child 2',
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isInMutuallyExclusiveGroup,
SemanticsFlag.hasSelectedState,
SemanticsFlag.isSelected,
SemanticsFlag.isFocusable,
SemanticsFlag.isFocused,
],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
),
],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets('Non-centered taps work on smaller widgets', (WidgetTester tester) async {
final children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const SizedBox();
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(groupValue, 0);
final Offset centerOfTwo = tester.getCenter(find.byWidget(children[1]!));
// Tap within the bounds of children[1], but not at the center.
// children[1] is a SizedBox thus not hittable by itself.
await tester.tapAt(centerOfTwo + const Offset(10, 0));
expect(groupValue, 1);
});
testWidgets('Non-centered taps work on proportional segments', (WidgetTester tester) async {
final children = <int, Widget>{};
children[0] = const SizedBox(width: 50, height: 30);
children[1] = const SizedBox();
children[2] = const SizedBox(width: 100, height: 30);
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
proportionalWidth: true,
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(groupValue, 0);
final Rect firstChild = tester.getRect(
find.ancestor(of: find.byWidget(children[0]!), matching: find.byType(MetaData)),
);
expect(firstChild.width, 50.0 + 10.0 * 2);
final Rect secondChild = tester.getRect(
find.ancestor(of: find.byWidget(children[1]!), matching: find.byType(MetaData)),
);
expect(secondChild.width, 0.0 + 10.0 * 2);
final Rect thirdChild = tester.getRect(
find.ancestor(of: find.byWidget(children[2]!), matching: find.byType(MetaData)),
);
expect(thirdChild.width, 100.0 + 10.0 * 2);
final Finder child0 = find.ancestor(
of: find.byWidget(children[0]!),
matching: find.byType(MetaData),
);
final Offset centerOfChild0 = tester.getCenter(child0);
await tester.tapAt(centerOfChild0 + Offset(firstChild.width / 2 + 1, 0));
expect(groupValue, 1);
await tester.tapAt(
centerOfChild0 + Offset(firstChild.width / 2 + 1 + secondChild.width + 1, 0),
);
expect(groupValue, 2);
});
testWidgets('Hit-tests report accurate local position in segments', (WidgetTester tester) async {
final children = <int, Widget>{};
late TapDownDetails tapDownDetails;
children[0] = GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (TapDownDetails details) {
tapDownDetails = details;
},
child: const SizedBox(width: 200, height: 200),
);
children[1] = const Text('Child 2');
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(groupValue, 0);
final Offset segment0GlobalOffset = tester.getTopLeft(find.byWidget(children[0]!));
await tester.tapAt(segment0GlobalOffset + const Offset(7, 11));
expect(tapDownDetails.localPosition, const Offset(7, 11));
expect(tapDownDetails.globalPosition, segment0GlobalOffset + const Offset(7, 11));
});
testWidgets('Hit-tests report accurate local position in proportional segments', (
WidgetTester tester,
) async {
final children = <int, Widget>{};
late TapDownDetails tapDownDetails;
children[0] = GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (TapDownDetails details) {
tapDownDetails = details;
},
child: const SizedBox(width: 200, height: 200),
);
children[1] = const Text('Child 2');
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
proportionalWidth: true,
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(groupValue, 0);
final Offset segment0GlobalOffset = tester.getTopLeft(find.byWidget(children[0]!));
await tester.tapAt(segment0GlobalOffset + const Offset(7, 11));
expect(tapDownDetails.localPosition, const Offset(7, 11));
expect(tapDownDetails.globalPosition, segment0GlobalOffset + const Offset(7, 11));
});
testWidgets('Thumb animation is correct when the selected segment changes', (
WidgetTester tester,
) async {
await tester.pumpWidget(setupSimpleSegmentedControl());
final Rect initialRect = currentUnscaledThumbRect(tester, useGlobalCoordinate: true);
expect(currentThumbScale(tester), 1);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Child 2')));
await tester.pump();
// Does not move until tapUp.
expect(currentThumbScale(tester), 1);
expect(currentUnscaledThumbRect(tester, useGlobalCoordinate: true), initialRect);
// Tap up and the sliding animation should play.
await gesture.up();
await tester.pump();
// 10 ms isn't long enough for this gesture to be recognized as a longpress.
await tester.pump(const Duration(milliseconds: 10));
expect(currentThumbScale(tester), 1);
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
greaterThan(initialRect.center.dx),
);
await tester.pumpAndSettle();
expect(currentThumbScale(tester), 1);
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center,
// We're using a critically damped spring so expect the value of the
// animation controller to not be 1.
offsetMoreOrLessEquals(tester.getCenter(find.text('Child 2')), epsilon: 0.01),
);
// Press the currently selected widget.
await gesture.down(tester.getCenter(find.text('Child 2')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
// The thumb shrinks but does not moves towards left; the shrink alignment
// is Alignment.centerRight, with ltr text direction because "Child 2" is
// the trailing item.
expect(currentThumbScale(tester), lessThan(1));
double centerDelta =
tester.getSize(find.text('Child 2')).width * (1 - currentThumbScale(tester)) / 2;
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dy,
moreOrLessEquals(tester.getCenter(find.text('Child 2')).dy, epsilon: 0.01),
);
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
moreOrLessEquals(tester.getCenter(find.text('Child 2')).dx - centerDelta, epsilon: 0.01),
);
await tester.pumpAndSettle();
expect(currentThumbScale(tester), moreOrLessEquals(0.95, epsilon: 0.01));
centerDelta = tester.getSize(find.text('Child 2')).width * (1 - currentThumbScale(tester));
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dy,
moreOrLessEquals(tester.getCenter(find.text('Child 2')).dy, epsilon: 0.01),
);
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
moreOrLessEquals(tester.getCenter(find.text('Child 2')).dx - centerDelta / 2, epsilon: 0.01),
);
// Drag to Child 1.
await gesture.moveTo(tester.getCenter(find.text('Child 1')));
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
// Moved slightly to the left
expect(currentThumbScale(tester), moreOrLessEquals(0.95, epsilon: 0.01));
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
lessThan(tester.getCenter(find.text('Child 2')).dx),
);
await tester.pumpAndSettle();
expect(currentThumbScale(tester), moreOrLessEquals(0.95, epsilon: 0.01));
centerDelta = tester.getSize(find.text('Child 1')).width * (1 - currentThumbScale(tester));
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dy,
moreOrLessEquals(tester.getCenter(find.text('Child 1')).dy, epsilon: 0.01),
);
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
moreOrLessEquals(tester.getCenter(find.text('Child 1')).dx + centerDelta / 2, epsilon: 0.01),
);
await gesture.up();
await tester.pump();
await tester.pump(const Duration(milliseconds: 10));
expect(currentThumbScale(tester), greaterThan(0.95));
await tester.pumpAndSettle();
expect(currentThumbScale(tester), moreOrLessEquals(1, epsilon: 0.01));
});
testWidgets('Thumb does not go out of bounds in animation', (WidgetTester tester) async {
const children = <int, Widget>{
0: Text('Child 1', maxLines: 1),
1: Text('wiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiide Child 2', maxLines: 1),
2: SizedBox(height: 400),
};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
final Rect initialThumbRect = currentUnscaledThumbRect(tester, useGlobalCoordinate: true);
// Starts animating towards 1.
setState!(() {
groupValue = 1;
});
await tester.pump(const Duration(milliseconds: 10));
const newChildren = <int, Widget>{0: Text('C1', maxLines: 1), 1: Text('C2', maxLines: 1)};
// Now let the segments shrink.
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: newChildren,
groupValue: 1,
onValueChanged: defaultCallback,
);
},
),
);
final RenderBox renderSegmentedControl = getRenderSegmentedControl(tester);
final Offset segmentedControlOrigin = renderSegmentedControl.localToGlobal(Offset.zero);
// Expect the segmented control to be much narrower.
expect(segmentedControlOrigin.dx, greaterThan(initialThumbRect.left));
final Rect thumbRect = currentUnscaledThumbRect(tester, useGlobalCoordinate: true);
expect(initialThumbRect.size.height, 400);
expect(thumbRect.size.height, lessThan(100));
// The new thumbRect should fit in the segmentedControl. The -1 and the +1
// are to account for the thumb's vertical EdgeInsets.
expect(segmentedControlOrigin.dx - 1, lessThanOrEqualTo(thumbRect.left));
expect(
segmentedControlOrigin.dx + renderSegmentedControl.size.width + 1,
greaterThanOrEqualTo(thumbRect.right),
);
});
testWidgets('Transition is triggered while a transition is already occurring', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('A'), 1: Text('B'), 2: Text('C')};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
await tester.tap(find.text('B'));
await tester.pump();
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));
// Between A and B.
final Rect initialThumbRect = currentUnscaledThumbRect(tester, useGlobalCoordinate: true);
expect(initialThumbRect.center.dx, greaterThan(tester.getCenter(find.text('A')).dx));
expect(initialThumbRect.center.dx, lessThan(tester.getCenter(find.text('B')).dx));
// While A to B transition is occurring, press on C.
await tester.tap(find.text('C'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));
final Rect secondThumbRect = currentUnscaledThumbRect(tester, useGlobalCoordinate: true);
// Between the initial Rect and B.
expect(secondThumbRect.center.dx, greaterThan(initialThumbRect.center.dx));
expect(secondThumbRect.center.dx, lessThan(tester.getCenter(find.text('B')).dx));
await tester.pump(const Duration(milliseconds: 500));
// Eventually moves to C.
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center,
offsetMoreOrLessEquals(tester.getCenter(find.text('C')), epsilon: 0.01),
);
});
testWidgets('Insert segment while animation is running', (WidgetTester tester) async {
final Map<int, Widget> children = SplayTreeMap<int, Widget>((int a, int b) => a - b);
children[0] = const Text('A');
children[2] = const Text('C');
children[3] = const Text('D');
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
await tester.tap(find.text('D'));
await tester.pump();
await tester.pump(const Duration(milliseconds: 40));
children[1] = const Text('B');
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
await tester.pumpAndSettle();
// Eventually moves to D.
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center,
offsetMoreOrLessEquals(tester.getCenter(find.text('D')), epsilon: 0.01),
);
});
testWidgets('change selection programmatically when dragging', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('A'), 1: Text('B'), 2: Text('C')};
var callbackCalled = false;
void onValueChanged(int? newValue) {
callbackCalled = true;
}
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: onValueChanged,
);
},
),
);
// Start dragging.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Change selection programmatically.
setState!(() {
groupValue = 1;
});
await tester.pump();
await tester.pumpAndSettle();
// The ongoing drag gesture should veto the programmatic change.
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dy,
moreOrLessEquals(tester.getCenter(find.text('A')).dy, epsilon: 0.01),
);
final double centerDelta =
tester.getSize(find.text('A')).width * (1 - currentThumbScale(tester)) / 2;
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
moreOrLessEquals(tester.getCenter(find.text('A')).dx + centerDelta, epsilon: 0.01),
);
// Move the pointer to 'B'. The onValueChanged callback will be called but
// since the parent widget thinks we're already at 'B', it will not trigger
// a rebuild for us.
await gesture.moveTo(tester.getCenter(find.text('B')));
await gesture.up();
await tester.pump();
await tester.pumpAndSettle();
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center,
offsetMoreOrLessEquals(tester.getCenter(find.text('B')), epsilon: 0.01),
);
expect(callbackCalled, isFalse);
});
testWidgets('Disable "highlighted" segment during drag, highlight stays', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('A'), 1: Text('B'), 2: Text('C')};
var disabledChildren = <int>{};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
disabledChildren: disabledChildren,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
// Start dragging.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Move pointer to B.
await gesture.moveTo(tester.getCenter(find.text('B')));
await tester.pumpAndSettle();
// Disable B.
setState!(() {
disabledChildren = <int>{1};
});
await tester.pumpAndSettle();
// During dragging, we can still see the "highlighted" segment.
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center,
offsetMoreOrLessEquals(tester.getCenter(find.text('B')), epsilon: 0.01),
);
await gesture.up();
await tester.pump();
await tester.pumpAndSettle();
// When dragging stops, highlight stays.
expect(getHighlightedIndex(tester), 1);
});
testWidgets('Disable "highlighted" segment during drag, onValueChanged is still called', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('A'), 1: Text('B'), 2: Text('C')};
var disabledChildren = <int>{};
var callbackCalled = 0;
void onValueChanged(int? newValue) {
callbackCalled += 1;
setState!(() {
groupValue = newValue;
});
}
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
disabledChildren: disabledChildren,
groupValue: groupValue,
onValueChanged: onValueChanged,
);
},
),
);
// Start dragging.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Drag to B.
await gesture.moveTo(tester.getCenter(find.text('B')));
await tester.pump();
await tester.pumpAndSettle();
// Disable B.
setState!(() {
disabledChildren = <int>{1};
});
await tester.pumpAndSettle();
// Stop dragging.
await gesture.up();
await tester.pump();
await tester.pumpAndSettle();
expect(getHighlightedIndex(tester), 1);
expect(callbackCalled, 1);
});
testWidgets('Dragging out of bound does not cause out of range exception', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCC')};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
proportionalWidth: true,
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
Size getChildSize(int index) {
return tester.getSize(
find.ancestor(of: find.byWidget(children[index]!), matching: find.byType(MetaData)),
);
}
expect(getChildSize(0).width, 33.0);
expect(getChildSize(2).width, 59.0);
// Start dragging.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Dragging to left until out of bound.
await gesture.moveTo(const Offset(-100, 0));
await tester.pump();
expect(getHighlightedIndex(tester), 0);
// Move the pointer to the last child and continue dragging until out of bound.
final Offset thirdChild = tester.getCenter(find.text('CCC'));
await gesture.moveTo(thirdChild);
await tester.pump();
await gesture.moveTo(thirdChild + const Offset(100, 0));
await tester.pump();
await gesture.up();
await tester.pumpAndSettle();
expect(getHighlightedIndex(tester), 2);
expect(tester.takeException(), isNull);
});
testWidgets('Disallow new gesture when dragging', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('A'), 1: Text('B'), 2: Text('C')};
var callbackCalled = false;
void onValueChanged(int? newValue) {
callbackCalled = true;
}
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: onValueChanged,
);
},
),
);
// Start dragging.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Tap a different segment.
await tester.tap(find.text('C'));
await tester.pump();
await tester.pumpAndSettle();
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dy,
moreOrLessEquals(tester.getCenter(find.text('A')).dy, epsilon: 0.01),
);
double centerDelta = tester.getSize(find.text('A')).width * (1 - currentThumbScale(tester)) / 2;
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
moreOrLessEquals(tester.getCenter(find.text('A')).dx + centerDelta, epsilon: 0.01),
);
// A different drag.
await tester.drag(find.text('A'), const Offset(300, 0));
await tester.pump();
await tester.pumpAndSettle();
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dy,
moreOrLessEquals(tester.getCenter(find.text('A')).dy, epsilon: 0.01),
);
centerDelta = tester.getSize(find.text('A')).width * (1 - currentThumbScale(tester)) / 2;
expect(
currentUnscaledThumbRect(tester, useGlobalCoordinate: true).center.dx,
moreOrLessEquals(tester.getCenter(find.text('A')).dx + centerDelta, epsilon: 0.01),
);
await gesture.up();
expect(callbackCalled, isFalse);
});
testWidgets('gesture outlives the widget', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/63338.
const children = <int, Widget>{0: Text('A'), 1: Text('B'), 2: Text('C')};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
// Start dragging.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
await tester.pumpWidget(const Placeholder());
await gesture.moveBy(const Offset(200, 0));
await tester.pump();
await tester.pump();
await gesture.up();
await tester.pump();
expect(tester.takeException(), isNull);
});
testWidgets('computeDryLayout is pure', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/73362.
const children = <int, Widget>{0: Text('A'), 1: Text('B'), 2: Text('C')};
const Key key = ValueKey<int>(1);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: 10,
child: CupertinoSlidingSegmentedControl<int>(
key: key,
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
),
),
),
),
);
final RenderBox renderBox = getRenderSegmentedControl(tester);
final Size size = renderBox.getDryLayout(const BoxConstraints());
expect(size.width, greaterThan(10));
expect(tester.takeException(), isNull);
});
testWidgets('Has consistent size, independent of groupValue', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/62063.
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
groupValue = null;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
final RenderBox renderBox = getRenderSegmentedControl(tester);
final Size size = renderBox.size;
for (final int value in children.keys) {
setState!(() {
groupValue = value;
});
await tester.pump();
await tester.pumpAndSettle();
expect(renderBox.size, size);
}
});
testWidgets('ScrollView + SlidingSegmentedControl interaction', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('Child 1'), 1: Text('Child 2')};
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: ListView(
controller: scrollController,
children: <Widget>[
const SizedBox(height: 100),
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
const SizedBox(height: 1000),
],
),
),
);
// Tapping still works.
await tester.tap(find.text('Child 2'));
await tester.pump();
expect(groupValue, 1);
// Vertical drag works for the scroll view.
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Child 1')));
// The first moveBy doesn't actually move the scrollable. It's there to make
// sure VerticalDragGestureRecognizer wins the arena. This is due to
// startBehavior being set to DragStartBehavior.start.
await gesture.moveBy(const Offset(0, -100));
await gesture.moveBy(const Offset(0, -100));
await tester.pump();
expect(scrollController.offset, 100);
// Does not affect the segmented control.
expect(groupValue, 1);
await gesture.moveBy(const Offset(0, 100));
await gesture.up();
await tester.pump();
expect(scrollController.offset, 0);
expect(groupValue, 1);
// Long press vertical drag is recognized by the segmented control.
await gesture.down(tester.getCenter(find.text('Child 1')));
await tester.pump(const Duration(milliseconds: 600));
await gesture.moveBy(const Offset(0, -100));
await gesture.moveBy(const Offset(0, -100));
await tester.pump();
// Should not scroll.
expect(scrollController.offset, 0);
expect(groupValue, 1);
await gesture.moveBy(const Offset(0, 100));
await gesture.moveBy(const Offset(0, 100));
await gesture.up();
await tester.pump();
expect(scrollController.offset, 0);
expect(groupValue, 0);
// Horizontal drag is recognized by the segmentedControl.
await gesture.down(tester.getCenter(find.text('Child 1')));
await gesture.moveBy(const Offset(50, 0));
await gesture.moveTo(tester.getCenter(find.text('Child 2')));
await gesture.up();
await tester.pump();
expect(scrollController.offset, 0);
expect(groupValue, 1);
});
testWidgets(
'Hovering over Cupertino sliding segmented control updates cursor to clickable on Web',
(WidgetTester tester) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
key: const ValueKey<String>('Segmented Control'),
children: children,
onValueChanged: defaultCallback,
);
},
),
);
final TestGesture gesture = await tester.createGesture(
kind: PointerDeviceKind.mouse,
pointer: 1,
);
await gesture.addPointer(location: const Offset(10, 10));
await tester.pumpAndSettle();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
SystemMouseCursors.basic,
);
final Offset firstChild = tester.getCenter(find.text('A'));
await gesture.moveTo(firstChild);
await tester.pumpAndSettle();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
},
);
testWidgets('CupertinoSlidingSegmentedControl defaults - no selection', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
Widget buildSlidingSegmentedControl({Brightness? brightness}) {
return CupertinoApp(
theme: CupertinoThemeData(brightness: brightness ?? Brightness.light),
home: CupertinoPageScaffold(
child: Center(
child: CupertinoSlidingSegmentedControl<int>(
children: children,
onValueChanged: defaultCallback,
),
),
),
);
}
// Light theme
await tester.pumpWidget(buildSlidingSegmentedControl());
await expectLater(
find.byType(CupertinoSlidingSegmentedControl<int>),
matchesGoldenFile('cupertino_sliding_segmented_control.light_theme.png'),
);
// Dark theme
await tester.pumpWidget(buildSlidingSegmentedControl(brightness: Brightness.dark));
await expectLater(
find.byType(CupertinoSlidingSegmentedControl<int>),
matchesGoldenFile('cupertino_sliding_segmented_control.dark_theme.png'),
);
});
testWidgets('CupertinoSlidingSegmentedControl defaults - group value is not null', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
Widget buildSlidingSegmentedControl({Brightness? brightness}) {
return CupertinoApp(
theme: CupertinoThemeData(brightness: brightness ?? Brightness.light),
home: CupertinoPageScaffold(
child: Center(
child: CupertinoSlidingSegmentedControl<int>(
groupValue: 1,
children: children,
onValueChanged: defaultCallback,
),
),
),
);
}
// Light theme
await tester.pumpWidget(buildSlidingSegmentedControl());
await expectLater(
find.byType(CupertinoSlidingSegmentedControl<int>),
matchesGoldenFile('cupertino_sliding_segmented_control.with_selection.light_theme.png'),
);
// Dark theme
await tester.pumpWidget(buildSlidingSegmentedControl(brightness: Brightness.dark));
await expectLater(
find.byType(CupertinoSlidingSegmentedControl<int>),
matchesGoldenFile('cupertino_sliding_segmented_control.with_selection.dark_theme.png'),
);
});
testWidgets('CupertinoSlidingSegmentedControl defaults - disabled', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
Widget buildSlidingSegmentedControl({Brightness? brightness}) {
return CupertinoApp(
theme: CupertinoThemeData(brightness: brightness ?? Brightness.light),
home: CupertinoPageScaffold(
child: Center(
child: CupertinoSlidingSegmentedControl<int>(
disabledChildren: const <int>{0},
children: children,
onValueChanged: defaultCallback,
),
),
),
);
}
// Light theme
await tester.pumpWidget(buildSlidingSegmentedControl());
await expectLater(
find.byType(CupertinoSlidingSegmentedControl<int>),
matchesGoldenFile('cupertino_sliding_segmented_control.disabled.light_theme.png'),
);
// Dark theme
await tester.pumpWidget(buildSlidingSegmentedControl(brightness: Brightness.dark));
await expectLater(
find.byType(CupertinoSlidingSegmentedControl<int>),
matchesGoldenFile('cupertino_sliding_segmented_control.disabled.dark_theme.png'),
);
});
testWidgets('Segment can be disabled', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
groupValue = 1;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
disabledChildren: const <int>{0},
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(getHighlightedIndex(tester), 1);
// Tap disabled segment
await tester.tap(find.text('A'));
await tester.pumpAndSettle();
expect(getHighlightedIndex(tester), 1); // The highlighted index doesn't change
// Tap enabled segment
await tester.tap(find.text('CCCC'));
await tester.pumpAndSettle();
expect(getHighlightedIndex(tester), 2);
// Tap disabled segment
await tester.tap(find.text('A'));
await tester.pumpAndSettle();
expect(getHighlightedIndex(tester), 2); // The highlighted index doesn't change
});
testWidgets('Several segments can be disabled', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
var onValueChangedCalled = 0;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
disabledChildren: const <int>{0, 1, 2},
children: children,
groupValue: groupValue,
onValueChanged: (int? value) {
onValueChangedCalled += 1;
defaultCallback.call(value);
},
);
},
),
);
// All segments are disabled, so onValueChangedCalled should always be 0.
await tester.tap(find.text('A'));
await tester.pumpAndSettle();
expect(onValueChangedCalled, 0);
await tester.tap(find.text('CCCC'));
await tester.pumpAndSettle();
expect(onValueChangedCalled, 0);
await tester.tap(find.text('BB'));
await tester.pumpAndSettle();
expect(onValueChangedCalled, 0);
});
testWidgets('CupertinoSlidingSegmentedControl can be momentary', (WidgetTester tester) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
groupValue = 1;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
isMomentary: true,
children: children,
groupValue: groupValue,
onValueChanged: defaultCallback,
);
},
),
);
expect(getHighlightedIndex(tester), null);
// Tap first segment.
await tester.tap(find.text('A'));
await tester.pumpAndSettle();
// The highlighted index doesn't change.
expect(getHighlightedIndex(tester), null);
});
testWidgets('CupertinoSlidingSegmentedControl with momentary scales up selected segment', (
WidgetTester tester,
) async {
const children = <int, Widget>{0: Text('A'), 1: Text('BB'), 2: Text('CCCC')};
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
isMomentary: true,
children: children,
onValueChanged: defaultCallback,
);
},
),
);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('A')));
final Finder scaleTransition = find.ancestor(
of: find.text('A'),
matching: find.byType(ScaleTransition),
);
await tester.pumpAndSettle();
double scale = tester.widget<ScaleTransition>(scaleTransition).scale.value;
expect(scale, greaterThan(1.0));
await gesture.up();
await tester.pumpAndSettle();
scale = tester.widget<ScaleTransition>(scaleTransition).scale.value;
expect(scale, moreOrLessEquals(1.0));
});
testWidgets('Sliding segmented control can use arrow keys', (WidgetTester tester) async {
final children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
children[2] = const Text('Child 3');
groupValue = 0;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
onValueChanged: defaultCallback,
groupValue: groupValue,
);
},
),
);
expect(groupValue, 0);
await tester.tap(find.text('Child 1'));
await tester.pumpAndSettle();
expect(groupValue, 0);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(groupValue, 1);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pumpAndSettle();
expect(groupValue, 2);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(groupValue, 0);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
expect(groupValue, 2);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pumpAndSettle();
expect(groupValue, 1);
});
testWidgets('Sliding segmented control skips disabled segments with keyboard', (
WidgetTester tester,
) async {
final children = <int, Widget>{};
children[0] = const Text('Child 1');
children[1] = const Text('Child 2');
children[2] = const Text('Child 3');
groupValue = 0;
await tester.pumpWidget(
boilerplate(
builder: (BuildContext context) {
return CupertinoSlidingSegmentedControl<int>(
children: children,
onValueChanged: defaultCallback,
groupValue: groupValue,
disabledChildren: const <int>{1},
);
},
),
);
expect(groupValue, 0);
await tester.tap(find.text('Child 1'));
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
await tester.pumpAndSettle();
expect(groupValue, 2);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
await tester.pumpAndSettle();
expect(groupValue, 0);
});
}