933 lines
31 KiB
Dart
933 lines
31 KiB
Dart
// 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.
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../rendering/rendering_tester.dart';
|
|
import '../widgets/semantics_tester.dart';
|
|
|
|
class SpyFixedExtentScrollController extends FixedExtentScrollController {
|
|
/// Override for test visibility only.
|
|
@override
|
|
bool get hasListeners => super.hasListeners;
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('Picker respects theme styling', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
itemExtent: 50.0,
|
|
onSelectedItemChanged: (_) {},
|
|
children: List<Widget>.generate(3, (int index) {
|
|
return SizedBox(height: 50.0, width: 300.0, child: Text(index.toString()));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
final RenderParagraph paragraph = tester.renderObject(find.text('1'));
|
|
|
|
expect(paragraph.text.style!.color, isSameColorAs(CupertinoColors.black));
|
|
expect(
|
|
paragraph.text.style!.copyWith(color: CupertinoColors.black),
|
|
const TextStyle(
|
|
inherit: false,
|
|
fontFamily: 'CupertinoSystemDisplay',
|
|
fontSize: 21.0,
|
|
fontWeight: FontWeight.w400,
|
|
letterSpacing: -0.6,
|
|
color: CupertinoColors.black,
|
|
),
|
|
);
|
|
});
|
|
|
|
testWidgets('Picker semantics', (WidgetTester tester) async {
|
|
final semantics = SemanticsTester(tester);
|
|
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
itemExtent: 50.0,
|
|
onSelectedItemChanged: (_) {},
|
|
children: List<Widget>.generate(13, (int index) {
|
|
return SizedBox(height: 50.0, width: 300.0, child: Text(index.toString()));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(
|
|
semantics,
|
|
includesNodeWith(
|
|
value: '0',
|
|
increasedValue: '1',
|
|
actions: <SemanticsAction>[SemanticsAction.increase],
|
|
),
|
|
);
|
|
|
|
final hourListController =
|
|
tester.widget<ListWheelScrollView>(find.byType(ListWheelScrollView)).controller!
|
|
as FixedExtentScrollController;
|
|
|
|
hourListController.jumpToItem(11);
|
|
await tester.pumpAndSettle();
|
|
expect(
|
|
semantics,
|
|
includesNodeWith(
|
|
value: '11',
|
|
increasedValue: '12',
|
|
decreasedValue: '10',
|
|
actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease],
|
|
),
|
|
);
|
|
semantics.dispose();
|
|
});
|
|
|
|
testWidgets('Picker semantics excludes current item with empty label', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// When the current item has an empty label (e.g., wrapped with ExcludeSemantics),
|
|
// the picker should not set any value, increasedValue, decreasedValue, or actions.
|
|
final semantics = SemanticsTester(tester);
|
|
final controller = FixedExtentScrollController(initialItem: 1);
|
|
addTearDown(controller.dispose);
|
|
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
scrollController: controller,
|
|
itemExtent: 50.0,
|
|
onSelectedItemChanged: (_) {},
|
|
children: const <Widget>[
|
|
Text('0'),
|
|
// Item at index 1 is excluded from semantics (simulating a disabled item).
|
|
ExcludeSemantics(child: Text('1')),
|
|
Text('2'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// When the current item (index 1) has an empty label due to ExcludeSemantics,
|
|
// the picker should not have any value or actions set.
|
|
expect(semantics, isNot(includesNodeWith(value: '1')));
|
|
// Also verify that no increase/decrease actions are set for this item.
|
|
expect(
|
|
semantics,
|
|
isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.increase])),
|
|
);
|
|
expect(
|
|
semantics,
|
|
isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.decrease])),
|
|
);
|
|
|
|
// Scroll to item 0 which has a valid label.
|
|
controller.jumpToItem(0);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Now the picker should have value '0' but no increase action
|
|
// because the next item (1) has an empty label.
|
|
expect(semantics, includesNodeWith(value: '0'));
|
|
expect(
|
|
semantics,
|
|
isNot(includesNodeWith(value: '0', actions: <SemanticsAction>[SemanticsAction.increase])),
|
|
);
|
|
|
|
// Scroll to item 2 which has a valid label.
|
|
controller.jumpToItem(2);
|
|
await tester.pumpAndSettle();
|
|
|
|
// Now the picker should have value '2' but no decrease action
|
|
// because the previous item (1) has an empty label.
|
|
expect(semantics, includesNodeWith(value: '2'));
|
|
expect(
|
|
semantics,
|
|
isNot(includesNodeWith(value: '2', actions: <SemanticsAction>[SemanticsAction.decrease])),
|
|
);
|
|
|
|
semantics.dispose();
|
|
});
|
|
|
|
group('layout', () {
|
|
// Regression test for https://github.com/flutter/flutter/issues/22999
|
|
testWidgets('CupertinoPicker.builder test', (WidgetTester tester) async {
|
|
Widget buildFrame(int childCount) {
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoPicker.builder(
|
|
itemExtent: 50.0,
|
|
onSelectedItemChanged: (_) {},
|
|
itemBuilder: (BuildContext context, int index) {
|
|
return Text('$index');
|
|
},
|
|
childCount: childCount,
|
|
),
|
|
);
|
|
}
|
|
|
|
await tester.pumpWidget(buildFrame(1));
|
|
expect(tester.renderObject(find.text('0')).attached, true);
|
|
|
|
await tester.pumpWidget(buildFrame(2));
|
|
expect(tester.renderObject(find.text('0')).attached, true);
|
|
expect(tester.renderObject(find.text('1')).attached, true);
|
|
});
|
|
|
|
testWidgets('selected item is in the middle', (WidgetTester tester) async {
|
|
final controller = FixedExtentScrollController(initialItem: 1);
|
|
addTearDown(controller.dispose);
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
scrollController: controller,
|
|
itemExtent: 50.0,
|
|
onSelectedItemChanged: (_) {},
|
|
children: List<Widget>.generate(3, (int index) {
|
|
return SizedBox(height: 50.0, width: 300.0, child: Text(index.toString()));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(tester.getTopLeft(find.widgetWithText(SizedBox, '1').first), const Offset(0.0, 125.0));
|
|
|
|
controller.jumpToItem(0);
|
|
await tester.pump();
|
|
|
|
expect(
|
|
tester.getTopLeft(find.widgetWithText(SizedBox, '1').first),
|
|
offsetMoreOrLessEquals(const Offset(0.0, 170.0), epsilon: 0.5),
|
|
);
|
|
expect(tester.getTopLeft(find.widgetWithText(SizedBox, '0').first), const Offset(0.0, 125.0));
|
|
});
|
|
});
|
|
|
|
testWidgets('picker dark mode', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
theme: const CupertinoThemeData(brightness: Brightness.light),
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
backgroundColor: const CupertinoDynamicColor.withBrightness(
|
|
color: Color(
|
|
0xFF123456,
|
|
), // Set alpha channel to FF to disable under magnifier painting.
|
|
darkColor: Color(0xFF654321),
|
|
),
|
|
itemExtent: 15.0,
|
|
children: const <Widget>[Text('1'), Text('1')],
|
|
onSelectedItemChanged: (int i) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
find.byType(CupertinoPicker),
|
|
paints..rsuperellipse(color: const Color.fromARGB(30, 118, 118, 128)),
|
|
);
|
|
expect(find.byType(CupertinoPicker), paints..rect(color: const Color(0xFF123456)));
|
|
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
theme: const CupertinoThemeData(brightness: Brightness.dark),
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
backgroundColor: const CupertinoDynamicColor.withBrightness(
|
|
color: Color(0xFF123456),
|
|
darkColor: Color(0xFF654321),
|
|
),
|
|
itemExtent: 15.0,
|
|
children: const <Widget>[Text('1'), Text('1')],
|
|
onSelectedItemChanged: (int i) {},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(
|
|
find.byType(CupertinoPicker),
|
|
paints..rsuperellipse(color: const Color.fromARGB(61, 118, 118, 128)),
|
|
);
|
|
expect(find.byType(CupertinoPicker), paints..rect(color: const Color(0xFF654321)));
|
|
});
|
|
|
|
testWidgets('picker selectionOverlay', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
theme: const CupertinoThemeData(brightness: Brightness.light),
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
itemExtent: 15.0,
|
|
onSelectedItemChanged: (int i) {},
|
|
selectionOverlay: const CupertinoPickerDefaultSelectionOverlay(
|
|
background: Color(0x12345678),
|
|
),
|
|
children: const <Widget>[Text('1'), Text('1')],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(CupertinoPicker), paints..rsuperellipse(color: const Color(0x12345678)));
|
|
});
|
|
|
|
testWidgets('CupertinoPicker.selectionOverlay is nullable', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
theme: const CupertinoThemeData(brightness: Brightness.light),
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
itemExtent: 15.0,
|
|
onSelectedItemChanged: (int i) {},
|
|
selectionOverlay: null,
|
|
children: const <Widget>[Text('1'), Text('1')],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(find.byType(CupertinoPicker), isNot(paints..rsuperellipse()));
|
|
});
|
|
|
|
group('scroll', () {
|
|
testWidgets(
|
|
'scrolling calls onSelectedItemChanged and triggers haptic feedback when scroll passes middle of item',
|
|
(WidgetTester tester) async {
|
|
final selectedItems = <int>[];
|
|
final systemCalls = <MethodCall>[];
|
|
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
|
MethodCall methodCall,
|
|
) async {
|
|
systemCalls.add(methodCall);
|
|
return null;
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoPicker(
|
|
itemExtent: 100.0,
|
|
onSelectedItemChanged: (int index) {
|
|
selectedItems.add(index);
|
|
},
|
|
children: List<Widget>.generate(100, (int index) {
|
|
return Center(
|
|
child: SizedBox(width: 400.0, height: 100.0, child: Text(index.toString())),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
// Drag to almost the middle of the next item.
|
|
await tester.drag(
|
|
find.text('0'),
|
|
const Offset(0.0, -90.0),
|
|
warnIfMissed: false,
|
|
); // has an IgnorePointer
|
|
// Expect that the item changed, but haptics were not triggered yet,
|
|
// since we are not in the middle of the item.
|
|
expect(selectedItems, <int>[1]);
|
|
expect(systemCalls, isEmpty);
|
|
|
|
// Let the scroll settle and end up in the middle of the item.
|
|
await tester.pumpAndSettle();
|
|
expect(systemCalls, hasLength(2));
|
|
// Check that the haptic feedback and ticking sound were triggered.
|
|
expect(
|
|
systemCalls[0],
|
|
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
|
|
);
|
|
expect(systemCalls[1], isMethodCall('SystemSound.play', arguments: 'SystemSoundType.tick'));
|
|
|
|
// Overscroll a little to pass the middle of the item.
|
|
await tester.drag(
|
|
find.text('0'),
|
|
const Offset(0.0, 110.0),
|
|
warnIfMissed: false,
|
|
); // has an IgnorePointer
|
|
expect(selectedItems, <int>[1, 0]);
|
|
expect(systemCalls, hasLength(4));
|
|
expect(
|
|
systemCalls[2],
|
|
isMethodCall('HapticFeedback.vibrate', arguments: 'HapticFeedbackType.selectionClick'),
|
|
);
|
|
expect(systemCalls[3], isMethodCall('SystemSound.play', arguments: 'SystemSoundType.tick'));
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets('scrolling with new behavior calls onSelectedItemChanged only when scroll ends', (
|
|
WidgetTester tester,
|
|
) async {
|
|
final selectedItems = <int>[];
|
|
final systemCalls = <MethodCall>[];
|
|
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
|
MethodCall methodCall,
|
|
) async {
|
|
systemCalls.add(methodCall);
|
|
return null;
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoPicker(
|
|
itemExtent: 100.0,
|
|
changeReportingBehavior: ChangeReportingBehavior.onScrollEnd,
|
|
onSelectedItemChanged: (int index) {
|
|
selectedItems.add(index);
|
|
},
|
|
children: List<Widget>.generate(100, (int index) {
|
|
return Center(
|
|
child: SizedBox(width: 400.0, height: 100.0, child: Text(index.toString())),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
final Offset initialOffset = tester.getTopLeft(find.text('0'));
|
|
// Drag to almost the middle of the next item.
|
|
final TestGesture scrollGesture = await tester.startGesture(initialOffset);
|
|
// Item 0 is still closest to the center. No updates.
|
|
await scrollGesture.moveBy(const Offset(0.0, -49.0));
|
|
expect(selectedItems.isEmpty, true);
|
|
|
|
// Now item 1 is closest to the center.
|
|
await scrollGesture.moveBy(const Offset(0.0, -1.0));
|
|
expect(selectedItems, <int>[]);
|
|
|
|
// Now item 1 is still closest to the center for another full itemExtent (100px).
|
|
await scrollGesture.moveBy(const Offset(0.0, -99.0));
|
|
expect(selectedItems, <int>[]);
|
|
|
|
await scrollGesture.moveBy(const Offset(0.0, -1.0));
|
|
await scrollGesture.up();
|
|
await tester.pumpAndSettle();
|
|
expect(selectedItems, <int>[2]);
|
|
|
|
await scrollGesture.down(initialOffset);
|
|
await scrollGesture.moveBy(const Offset(0.0, 100.0));
|
|
expect(selectedItems, <int>[2]);
|
|
|
|
await scrollGesture.up();
|
|
expect(selectedItems, <int>[2, 1]);
|
|
});
|
|
|
|
testWidgets(
|
|
'does not trigger haptics or sounds when scrolling by tapping on the item',
|
|
(WidgetTester tester) async {
|
|
final selectedItems = <int>[];
|
|
final systemCalls = <MethodCall>[];
|
|
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
|
MethodCall methodCall,
|
|
) async {
|
|
systemCalls.add(methodCall);
|
|
return null;
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoPicker(
|
|
itemExtent: 100.0,
|
|
onSelectedItemChanged: (int index) {
|
|
selectedItems.add(index);
|
|
},
|
|
children: List<Widget>.generate(100, (int index) {
|
|
return Center(
|
|
child: SizedBox(width: 400.0, height: 100.0, child: Text(index.toString())),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('2'), warnIfMissed: false); // has an IgnorePointer
|
|
await tester.pumpAndSettle(const Duration(milliseconds: 10));
|
|
|
|
// Expect that the item changed, but haptics were not triggered.
|
|
expect(selectedItems, <int>[1, 2]);
|
|
expect(systemCalls, isEmpty);
|
|
|
|
await tester.drag(find.text('2'), const Offset(0.0, -30.0), warnIfMissed: false);
|
|
await tester.pumpAndSettle(const Duration(milliseconds: 10));
|
|
// Expect that moving within the item does not trigger haptics after animating scroll.
|
|
expect(selectedItems, <int>[1, 2]);
|
|
expect(systemCalls, isEmpty);
|
|
},
|
|
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
|
|
);
|
|
|
|
testWidgets(
|
|
'do not trigger haptic or sounds on non-iOS devices',
|
|
(WidgetTester tester) async {
|
|
final selectedItems = <int>[];
|
|
final systemCalls = <MethodCall>[];
|
|
|
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
|
|
MethodCall methodCall,
|
|
) async {
|
|
systemCalls.add(methodCall);
|
|
return null;
|
|
});
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoPicker(
|
|
itemExtent: 100.0,
|
|
onSelectedItemChanged: (int index) {
|
|
selectedItems.add(index);
|
|
},
|
|
children: List<Widget>.generate(100, (int index) {
|
|
return Center(
|
|
child: SizedBox(width: 400.0, height: 100.0, child: Text(index.toString())),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
await tester.drag(
|
|
find.text('0'),
|
|
const Offset(0.0, -100.0),
|
|
warnIfMissed: false,
|
|
); // has an IgnorePointer
|
|
|
|
// Allow the scroll to settle in the middle of the item.
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(selectedItems, <int>[1]);
|
|
expect(systemCalls, isEmpty);
|
|
},
|
|
variant: TargetPlatformVariant(
|
|
TargetPlatform.values
|
|
.where((TargetPlatform platform) => platform != TargetPlatform.iOS)
|
|
.toSet(),
|
|
),
|
|
);
|
|
|
|
testWidgets(
|
|
'a drag in between items settles back',
|
|
(WidgetTester tester) async {
|
|
final controller = FixedExtentScrollController(initialItem: 10);
|
|
addTearDown(controller.dispose);
|
|
final selectedItems = <int>[];
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoPicker(
|
|
scrollController: controller,
|
|
itemExtent: 100.0,
|
|
onSelectedItemChanged: (int index) {
|
|
selectedItems.add(index);
|
|
},
|
|
children: List<Widget>.generate(100, (int index) {
|
|
return Center(
|
|
child: SizedBox(width: 400.0, height: 100.0, child: Text(index.toString())),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Drag it by a bit but not enough to move to the next item.
|
|
await tester.drag(
|
|
find.text('10'),
|
|
const Offset(0.0, 30.0),
|
|
pointer: 1,
|
|
touchSlopY: 0.0,
|
|
warnIfMissed: false,
|
|
); // has an IgnorePointer
|
|
|
|
// The item that was in the center now moved a bit.
|
|
expect(tester.getTopLeft(find.widgetWithText(SizedBox, '10')), const Offset(200.0, 250.0));
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getTopLeft(find.widgetWithText(SizedBox, '10')).dy,
|
|
moreOrLessEquals(250.0, epsilon: 0.5),
|
|
);
|
|
expect(selectedItems.isEmpty, true);
|
|
|
|
// Drag it by enough to move to the next item.
|
|
await tester.drag(
|
|
find.text('10'),
|
|
const Offset(0.0, 70.0),
|
|
pointer: 1,
|
|
touchSlopY: 0.0,
|
|
warnIfMissed: false,
|
|
); // has an IgnorePointer
|
|
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getTopLeft(find.widgetWithText(SizedBox, '10')).dy,
|
|
// It's down by 100.0 now.
|
|
moreOrLessEquals(340.0, epsilon: 0.5),
|
|
);
|
|
expect(selectedItems, <int>[9]);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
|
|
testWidgets(
|
|
'a big fling that overscrolls springs back',
|
|
(WidgetTester tester) async {
|
|
final controller = FixedExtentScrollController(initialItem: 10);
|
|
addTearDown(controller.dispose);
|
|
final selectedItems = <int>[];
|
|
|
|
await tester.pumpWidget(
|
|
Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: CupertinoPicker(
|
|
scrollController: controller,
|
|
itemExtent: 100.0,
|
|
onSelectedItemChanged: (int index) {
|
|
selectedItems.add(index);
|
|
},
|
|
children: List<Widget>.generate(100, (int index) {
|
|
return Center(
|
|
child: SizedBox(width: 400.0, height: 100.0, child: Text(index.toString())),
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
);
|
|
|
|
// A wild throw appears.
|
|
await tester.fling(
|
|
find.text('10'),
|
|
const Offset(0.0, 10000.0),
|
|
1000.0,
|
|
warnIfMissed: false, // has an IgnorePointer
|
|
);
|
|
|
|
if (debugDefaultTargetPlatformOverride == TargetPlatform.iOS) {
|
|
// Should have been flung far enough that even the first item goes off
|
|
// screen and gets removed.
|
|
expect(find.widgetWithText(SizedBox, '0').evaluate().isEmpty, true);
|
|
}
|
|
|
|
expect(
|
|
selectedItems,
|
|
// This specific throw was fast enough that each scroll update landed
|
|
// on every second item.
|
|
<int>[8, 6, 4, 2, 0],
|
|
);
|
|
|
|
// Let it spring back.
|
|
await tester.pumpAndSettle();
|
|
|
|
expect(
|
|
tester.getTopLeft(find.widgetWithText(SizedBox, '0')).dy,
|
|
// Should have sprung back to the middle now.
|
|
moreOrLessEquals(250.0),
|
|
);
|
|
expect(
|
|
selectedItems,
|
|
// Falling back to 0 shouldn't produce more callbacks.
|
|
<int>[8, 6, 4, 2, 0],
|
|
);
|
|
},
|
|
variant: const TargetPlatformVariant(<TargetPlatform>{
|
|
TargetPlatform.iOS,
|
|
TargetPlatform.macOS,
|
|
}),
|
|
);
|
|
});
|
|
|
|
// TODO(justinmc): Don't test Material interactions in Cupertino tests.
|
|
// https://github.com/flutter/flutter/issues/177028
|
|
testWidgets('Picker adapts to MaterialApp dark mode', (WidgetTester tester) async {
|
|
Widget buildCupertinoPicker(Brightness brightness) {
|
|
return MaterialApp(
|
|
theme: ThemeData(brightness: brightness),
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: SizedBox(
|
|
height: 300.0,
|
|
width: 300.0,
|
|
child: CupertinoPicker(
|
|
itemExtent: 50.0,
|
|
onSelectedItemChanged: (_) {},
|
|
children: List<Widget>.generate(3, (int index) {
|
|
return SizedBox(height: 50.0, width: 300.0, child: Text(index.toString()));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// CupertinoPicker with light theme.
|
|
await tester.pumpWidget(buildCupertinoPicker(Brightness.light));
|
|
RenderParagraph paragraph = tester.renderObject(find.text('1'));
|
|
expect(paragraph.text.style!.color, CupertinoColors.label);
|
|
// Text style should not return unresolved color.
|
|
expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse);
|
|
|
|
// CupertinoPicker with dark theme.
|
|
await tester.pumpWidget(buildCupertinoPicker(Brightness.dark));
|
|
paragraph = tester.renderObject(find.text('1'));
|
|
expect(paragraph.text.style!.color, CupertinoColors.label);
|
|
// Text style should not return unresolved color.
|
|
expect(paragraph.text.style!.color.toString().contains('UNRESOLVED'), isFalse);
|
|
});
|
|
|
|
group('CupertinoPickerDefaultSelectionOverlay', () {
|
|
testWidgets('should be using directional decoration', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
theme: const CupertinoThemeData(brightness: Brightness.light),
|
|
home: CupertinoPicker(
|
|
itemExtent: 15.0,
|
|
onSelectedItemChanged: (int i) {},
|
|
selectionOverlay: const CupertinoPickerDefaultSelectionOverlay(
|
|
background: Color(0x12345678),
|
|
),
|
|
children: const <Widget>[Text('1'), Text('1')],
|
|
),
|
|
),
|
|
);
|
|
|
|
final Finder selectionContainer = find.byType(Container);
|
|
final Container container = tester.firstWidget<Container>(selectionContainer);
|
|
final EdgeInsetsGeometry? margin = container.margin;
|
|
final BorderRadiusGeometry? borderRadius =
|
|
((container.decoration as ShapeDecoration?)?.shape as RoundedSuperellipseBorder?)
|
|
?.borderRadius;
|
|
|
|
expect(margin, isA<EdgeInsetsDirectional>());
|
|
expect(borderRadius, isA<BorderRadiusDirectional>());
|
|
});
|
|
});
|
|
|
|
testWidgets('Scroll controller is detached upon dispose', (WidgetTester tester) async {
|
|
final controller = SpyFixedExtentScrollController();
|
|
addTearDown(controller.dispose);
|
|
expect(controller.hasListeners, false);
|
|
expect(controller.positions.length, 0);
|
|
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Center(
|
|
child: CupertinoPicker(
|
|
scrollController: controller,
|
|
itemExtent: 50.0,
|
|
onSelectedItemChanged: (_) {},
|
|
children: List<Widget>.generate(3, (int index) {
|
|
return SizedBox(width: 300.0, child: Text(index.toString()));
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(controller.hasListeners, true);
|
|
expect(controller.positions.length, 1);
|
|
|
|
await tester.pumpWidget(const SizedBox.expand());
|
|
expect(controller.hasListeners, false);
|
|
expect(controller.positions.length, 0);
|
|
});
|
|
|
|
testWidgets('Registers taps and does not crash with certain diameterRatio', (
|
|
WidgetTester tester,
|
|
) async {
|
|
// Regression test for https://github.com/flutter/flutter/issues/126491
|
|
|
|
final children = List<int>.generate(100, (int index) => index);
|
|
final paintedChildren = <int>[];
|
|
final tappedChildren = <int>{};
|
|
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Align(
|
|
alignment: Alignment.topLeft,
|
|
child: Center(
|
|
child: SizedBox(
|
|
height: 120,
|
|
child: CupertinoPicker(
|
|
itemExtent: 55,
|
|
diameterRatio: 0.9,
|
|
onSelectedItemChanged: (int index) {},
|
|
children: children
|
|
.map<Widget>(
|
|
(int index) => GestureDetector(
|
|
key: ValueKey<int>(index),
|
|
onTap: () {
|
|
tappedChildren.add(index);
|
|
},
|
|
child: SizedBox(
|
|
width: 55,
|
|
height: 55,
|
|
child: CustomPaint(
|
|
painter: TestCallbackPainter(
|
|
onPaint: () {
|
|
paintedChildren.add(index);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
// Children are painted two times for whatever reason
|
|
expect(paintedChildren, <int>[0, 1, 0, 1]);
|
|
|
|
// Expect hitting 0 and 1, which are painted
|
|
await tester.tap(find.byKey(const ValueKey<int>(0)));
|
|
expect(tappedChildren, const <int>[0]);
|
|
|
|
await tester.tap(find.byKey(const ValueKey<int>(1)));
|
|
expect(tappedChildren, const <int>[0, 1]);
|
|
|
|
// The third child is not painted, so is not hit
|
|
await tester.tap(find.byKey(const ValueKey<int>(2)), warnIfMissed: false);
|
|
expect(tappedChildren, const <int>[0, 1]);
|
|
});
|
|
|
|
testWidgets('Tapping on child in a CupertinoPicker selects that child', (
|
|
WidgetTester tester,
|
|
) async {
|
|
var selectedItem = 0;
|
|
const tapScrollDuration = Duration(milliseconds: 300);
|
|
// The tap animation is set to 300ms, but add an extra 1µs to complete the scroll animation.
|
|
const infinitesimalPause = Duration(microseconds: 1);
|
|
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: CupertinoPicker(
|
|
itemExtent: 10.0,
|
|
onSelectedItemChanged: (int i) {
|
|
selectedItem = i;
|
|
},
|
|
children: const <Widget>[Text('0'), Text('1'), Text('2'), Text('3')],
|
|
),
|
|
),
|
|
);
|
|
|
|
expect(selectedItem, equals(0));
|
|
// Tap on the item at index 1.
|
|
await tester.tap(find.text('1'));
|
|
await tester.pump();
|
|
await tester.pump(tapScrollDuration + infinitesimalPause);
|
|
expect(selectedItem, equals(1));
|
|
|
|
// Skip to the item at index 3.
|
|
await tester.tap(find.text('3'));
|
|
await tester.pump();
|
|
await tester.pump(tapScrollDuration + infinitesimalPause);
|
|
expect(selectedItem, equals(3));
|
|
|
|
// Tap on the item at index 0.
|
|
await tester.tap(find.text('0'));
|
|
await tester.pump();
|
|
await tester.pump(tapScrollDuration + infinitesimalPause);
|
|
expect(selectedItem, equals(0));
|
|
|
|
// Skip to the item at index 2.
|
|
await tester.tap(find.text('2'));
|
|
await tester.pump();
|
|
await tester.pump(tapScrollDuration + infinitesimalPause);
|
|
expect(selectedItem, equals(2));
|
|
});
|
|
|
|
testWidgets('CupertinoPickerDefaultSelectionOverlay does not crash at zero area', (
|
|
WidgetTester tester,
|
|
) async {
|
|
tester.view.physicalSize = Size.zero;
|
|
addTearDown(tester.view.reset);
|
|
await tester.pumpWidget(
|
|
const CupertinoApp(home: Center(child: CupertinoPickerDefaultSelectionOverlay())),
|
|
);
|
|
expect(tester.getSize(find.byType(CupertinoPickerDefaultSelectionOverlay)), Size.zero);
|
|
});
|
|
|
|
testWidgets('CupertinoPicker does not crash at zero area', (WidgetTester tester) async {
|
|
await tester.pumpWidget(
|
|
CupertinoApp(
|
|
home: Center(
|
|
child: SizedBox.shrink(
|
|
child: CupertinoPicker(
|
|
itemExtent: 2.0,
|
|
onSelectedItemChanged: (_) {},
|
|
children: const <Widget>[Text('X'), Text('Y')],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
expect(tester.getSize(find.byType(CupertinoPicker)), Size.zero);
|
|
});
|
|
}
|