blob: b765d240f0ca72a0fa898d57240fc507fef64cf9 [file] [log] [blame]
// Copyright 2018 The Chromium 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/gestures.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
group('$ReorderableListView', () {
const double itemHeight = 48.0;
const List<String> originalListItems = <String>['Item 1', 'Item 2', 'Item 3', 'Item 4'];
List<String> listItems;
void onReorder(int oldIndex, int newIndex) {
if (oldIndex < newIndex) {
newIndex -= 1;
}
final String element = listItems.removeAt(oldIndex);
listItems.insert(newIndex, element);
}
Widget listItemToWidget(String listItem) {
return SizedBox(
key: Key(listItem),
height: itemHeight,
width: itemHeight,
child: Text(listItem),
);
}
Widget build({ Widget header, Axis scrollDirection = Axis.vertical, TextDirection textDirection = TextDirection.ltr }) {
return MaterialApp(
home: Directionality(
textDirection: textDirection,
child: SizedBox(
height: itemHeight * 10,
width: itemHeight * 10,
child: ReorderableListView(
header: header,
children: listItems.map<Widget>(listItemToWidget).toList(),
scrollDirection: scrollDirection,
onReorder: onReorder,
),
),
),
);
}
setUp(() {
// Copy the original list into listItems.
listItems = originalListItems.toList();
});
group('in vertical mode', () {
testWidgets('reorders its contents only when a drag finishes', (WidgetTester tester) async {
await tester.pumpWidget(build());
expect(listItems, orderedEquals(originalListItems));
final TestGesture drag = await tester.startGesture(tester.getCenter(find.text('Item 1')));
await tester.pump(kLongPressTimeout + kPressTimeout);
expect(listItems, orderedEquals(originalListItems));
await drag.moveTo(tester.getCenter(find.text('Item 4')));
expect(listItems, orderedEquals(originalListItems));
await drag.up();
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 1', 'Item 4']));
});
testWidgets('allows reordering from the very top to the very bottom', (WidgetTester tester) async {
await tester.pumpWidget(build());
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 1')),
tester.getCenter(find.text('Item 4')) + const Offset(0.0, itemHeight * 2),
);
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1']));
});
testWidgets('allows reordering from the very bottom to the very top', (WidgetTester tester) async {
await tester.pumpWidget(build());
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 4')),
tester.getCenter(find.text('Item 1')),
);
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 2', 'Item 3']));
});
testWidgets('allows reordering inside the middle of the widget', (WidgetTester tester) async {
await tester.pumpWidget(build());
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 3')),
tester.getCenter(find.text('Item 2')),
);
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 2', 'Item 4']));
});
testWidgets('properly reorders with a header', (WidgetTester tester) async {
await tester.pumpWidget(build(header: const Text('Header Text')));
expect(find.text('Header Text'), findsOneWidget);
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 1')),
tester.getCenter(find.text('Item 4')) + const Offset(0.0, itemHeight * 2),
);
expect(find.text('Header Text'), findsOneWidget);
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1']));
});
testWidgets('properly determines the vertical drop area extents', (WidgetTester tester) async {
final Widget reorderableListView = ReorderableListView(
children: const <Widget>[
SizedBox(
key: Key('Normal item'),
height: itemHeight,
child: Text('Normal item'),
),
SizedBox(
key: Key('Tall item'),
height: itemHeight * 2,
child: Text('Tall item'),
),
SizedBox(
key: Key('Last item'),
height: itemHeight,
child: Text('Last item'),
),
],
scrollDirection: Axis.vertical,
onReorder: (int oldIndex, int newIndex) { },
);
await tester.pumpWidget(MaterialApp(
home: SizedBox(
height: itemHeight * 10,
child: reorderableListView,
),
));
Element getContentElement() {
final SingleChildScrollView listScrollView = tester.widget(find.byType(SingleChildScrollView));
final Widget scrollContents = listScrollView.child;
final Element contentElement = tester.element(find.byElementPredicate((Element element) => element.widget == scrollContents));
return contentElement;
}
const double kNonDraggingListHeight = 292.0;
// The list view pads the drop area by 8dp.
const double kDraggingListHeight = 300.0;
// Drag a normal text item
expect(getContentElement().size.height, kNonDraggingListHeight);
TestGesture drag = await tester.startGesture(tester.getCenter(find.text('Normal item')));
await tester.pump(kLongPressTimeout + kPressTimeout);
await tester.pumpAndSettle();
expect(getContentElement().size.height, kDraggingListHeight);
// Move it
await drag.moveTo(tester.getCenter(find.text('Last item')));
await tester.pumpAndSettle();
expect(getContentElement().size.height, kDraggingListHeight);
// Drop it
await drag.up();
await tester.pumpAndSettle();
expect(getContentElement().size.height, kNonDraggingListHeight);
// Drag a tall item
drag = await tester.startGesture(tester.getCenter(find.text('Tall item')));
await tester.pump(kLongPressTimeout + kPressTimeout);
await tester.pumpAndSettle();
expect(getContentElement().size.height, kDraggingListHeight);
// Move it
await drag.moveTo(tester.getCenter(find.text('Last item')));
await tester.pumpAndSettle();
expect(getContentElement().size.height, kDraggingListHeight);
// Drop it
await drag.up();
await tester.pumpAndSettle();
expect(getContentElement().size.height, kNonDraggingListHeight);
});
testWidgets('Preserves children states when the list parent changes the order', (WidgetTester tester) async {
_StatefulState findState(Key key) {
return find.byElementPredicate((Element element) => element.ancestorWidgetOfExactType(_Stateful)?.key == key)
.evaluate()
.first
.ancestorStateOfType(const TypeMatcher<_StatefulState>());
}
await tester.pumpWidget(MaterialApp(
home: ReorderableListView(
children: <Widget>[
_Stateful(key: const Key('A')),
_Stateful(key: const Key('B')),
_Stateful(key: const Key('C')),
],
onReorder: (int oldIndex, int newIndex) { },
),
));
await tester.tap(find.byKey(const Key('A')));
await tester.pumpAndSettle();
// Only the 'A' widget should be checked.
expect(findState(const Key('A')).checked, true);
expect(findState(const Key('B')).checked, false);
expect(findState(const Key('C')).checked, false);
await tester.pumpWidget(MaterialApp(
home: ReorderableListView(
children: <Widget>[
_Stateful(key: const Key('B')),
_Stateful(key: const Key('C')),
_Stateful(key: const Key('A')),
],
onReorder: (int oldIndex, int newIndex) { },
),
));
// Only the 'A' widget should be checked.
expect(findState(const Key('B')).checked, false);
expect(findState(const Key('C')).checked, false);
expect(findState(const Key('A')).checked, true);
});
testWidgets('Uses the PrimaryScrollController when available', (WidgetTester tester) async {
final ScrollController primary = ScrollController();
final Widget reorderableList = ReorderableListView(
children: const <Widget>[
SizedBox(width: 100.0, height: 100.0, child: Text('C'), key: Key('C')),
SizedBox(width: 100.0, height: 100.0, child: Text('B'), key: Key('B')),
SizedBox(width: 100.0, height: 100.0, child: Text('A'), key: Key('A')),
],
onReorder: (int oldIndex, int newIndex) { },
);
Widget buildWithScrollController(ScrollController controller) {
return MaterialApp(
home: PrimaryScrollController(
controller: controller,
child: SizedBox(
height: 100.0,
width: 100.0,
child: reorderableList,
),
),
);
}
await tester.pumpWidget(buildWithScrollController(primary));
SingleChildScrollView scrollView = tester.widget(
find.byType(SingleChildScrollView),
);
expect(scrollView.controller, primary);
// Now try changing the primary scroll controller and checking that the scroll view gets updated.
final ScrollController primary2 = ScrollController();
await tester.pumpWidget(buildWithScrollController(primary2));
scrollView = tester.widget(
find.byType(SingleChildScrollView),
);
expect(scrollView.controller, primary2);
});
testWidgets('Still builds when no PrimaryScrollController is available', (WidgetTester tester) async {
final Widget reorderableList = ReorderableListView(
children: const <Widget>[
SizedBox(width: 100.0, height: 100.0, child: Text('C'), key: Key('C')),
SizedBox(width: 100.0, height: 100.0, child: Text('B'), key: Key('B')),
SizedBox(width: 100.0, height: 100.0, child: Text('A'), key: Key('A')),
],
onReorder: (int oldIndex, int newIndex) { },
);
final Widget boilerplate = Localizations(
locale: const Locale('en'),
delegates: const <LocalizationsDelegate<dynamic>>[
DefaultMaterialLocalizations.delegate,
DefaultWidgetsLocalizations.delegate,
],
child:SizedBox(
width: 100.0,
height: 100.0,
child: Directionality(
textDirection: TextDirection.ltr,
child: reorderableList,
),
),
);
try {
await tester.pumpWidget(boilerplate);
} catch (e) {
fail('Expected no error, but got $e');
}
// Expect that we have build *a* ScrollController for use in the view.
final SingleChildScrollView scrollView = tester.widget(
find.byType(SingleChildScrollView),
);
expect(scrollView.controller, isNotNull);
});
group('Accessibility (a11y/Semantics)', () {
Map<CustomSemanticsAction, VoidCallback> getSemanticsActions(int index) {
final Semantics semantics = find.ancestor(
of: find.byKey(Key(listItems[index])),
matching: find.byType(Semantics),
).evaluate().first.widget;
return semantics.properties.customSemanticsActions;
}
const CustomSemanticsAction moveToStart = CustomSemanticsAction(label: 'Move to the start');
const CustomSemanticsAction moveToEnd = CustomSemanticsAction(label: 'Move to the end');
const CustomSemanticsAction moveUp = CustomSemanticsAction(label: 'Move up');
const CustomSemanticsAction moveDown = CustomSemanticsAction(label: 'Move down');
testWidgets('Provides the correct accessibility actions in LTR and RTL modes', (WidgetTester tester) async {
// The a11y actions for a vertical list are the same in LTR and RTL modes.
final SemanticsHandle handle = tester.ensureSemantics();
for (TextDirection direction in TextDirection.values) {
await tester.pumpWidget(build());
// The first item can be moved down or to the end.
final Map<CustomSemanticsAction, VoidCallback> firstSemanticsActions = getSemanticsActions(0);
expect(firstSemanticsActions.length, 2, reason: 'The first list item should have 2 custom actions with $direction.');
expect(firstSemanticsActions.containsKey(moveToStart), false, reason: 'The first item cannot `Move to the start` with $direction.');
expect(firstSemanticsActions.containsKey(moveUp), false, reason: 'The first item cannot `Move up` with $direction.');
expect(firstSemanticsActions.containsKey(moveDown), true, reason: 'The first item should be able to `Move down` with $direction.');
expect(firstSemanticsActions.containsKey(moveToEnd), true, reason: 'The first item should be able to `Move to the end` with $direction.');
// Items in the middle can be moved to the start, end, up or down.
for (int i = 1; i < listItems.length - 1; i += 1) {
final Map<CustomSemanticsAction, VoidCallback> ithSemanticsActions = getSemanticsActions(i);
expect(ithSemanticsActions.length, 4, reason: 'List item $i should have 4 custom actions with $direction.');
expect(ithSemanticsActions.containsKey(moveToStart), true, reason: 'List item $i should be able to `Move to the start` with $direction.');
expect(ithSemanticsActions.containsKey(moveUp), true, reason: 'List item $i should be able to `Move up` with $direction.');
expect(ithSemanticsActions.containsKey(moveDown), true, reason: 'List item $i should be able to `Move down` with $direction.');
expect(ithSemanticsActions.containsKey(moveToEnd), true, reason: 'List item $i should be able to `Move to the end` with $direction.');
}
// The last item can be moved up or to the start.
final Map<CustomSemanticsAction, VoidCallback> lastSemanticsActions = getSemanticsActions(listItems.length - 1);
expect(lastSemanticsActions.length, 2, reason: 'The last list item should have 2 custom actions with $direction.');
expect(lastSemanticsActions.containsKey(moveToStart), true, reason: 'The last item should be able to `Move to the start` with $direction.');
expect(lastSemanticsActions.containsKey(moveUp), true, reason: 'The last item should be able to `Move up` with $direction.');
expect(lastSemanticsActions.containsKey(moveDown), false, reason: 'The last item cannot `Move down` with $direction.');
expect(lastSemanticsActions.containsKey(moveToEnd), false, reason: 'The last item cannot `Move to the end` with $direction.');
}
handle.dispose();
});
testWidgets('First item accessibility (a11y) actions work', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to end: move Item 1 to the end of the list.
await tester.pumpWidget(build());
Map<CustomSemanticsAction, VoidCallback> firstSemanticsActions = getSemanticsActions(0);
firstSemanticsActions[moveToEnd]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1']));
// Test out move after: move Item 2 (the current first item) one space down.
await tester.pumpWidget(build());
firstSemanticsActions = getSemanticsActions(0);
firstSemanticsActions[moveDown]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 3', 'Item 2', 'Item 4', 'Item 1']));
handle.dispose();
});
testWidgets('Middle item accessibility (a11y) actions work', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to end: move Item 2 to the end of the list.
await tester.pumpWidget(build());
Map<CustomSemanticsAction, VoidCallback> middleSemanticsActions = getSemanticsActions(1);
middleSemanticsActions[moveToEnd]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 4', 'Item 2']));
// Test out move after: move Item 3 (the current second item) one space down.
await tester.pumpWidget(build());
middleSemanticsActions = getSemanticsActions(1);
middleSemanticsActions[moveDown]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 4', 'Item 3', 'Item 2']));
// Test out move after: move Item 3 (the current third item) one space up.
await tester.pumpWidget(build());
middleSemanticsActions = getSemanticsActions(2);
middleSemanticsActions[moveUp]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 4', 'Item 2']));
// Test out move to start: move Item 4 (the current third item) to the start of the list.
await tester.pumpWidget(build());
middleSemanticsActions = getSemanticsActions(2);
middleSemanticsActions[moveToStart]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 3', 'Item 2']));
handle.dispose();
});
testWidgets('Last item accessibility (a11y) actions work', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to start: move Item 4 to the start of the list.
await tester.pumpWidget(build());
Map<CustomSemanticsAction, VoidCallback> lastSemanticsActions = getSemanticsActions(listItems.length - 1);
lastSemanticsActions[moveToStart]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 2', 'Item 3']));
// Test out move up: move Item 3 (the current last item) one space up.
await tester.pumpWidget(build());
lastSemanticsActions = getSemanticsActions(listItems.length - 1);
lastSemanticsActions[moveUp]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 3', 'Item 2']));
handle.dispose();
});
testWidgets("Doesn't hide accessibility when a child declares its own semantics", (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
final Widget reorderableListView = ReorderableListView(
children: <Widget>[
const SizedBox(
key: Key('List tile 1'),
height: itemHeight,
child: Text('List tile 1'),
),
SizedBox(
key: const Key('Switch tile'),
height: itemHeight,
child: Material(
child: SwitchListTile(
title: const Text('Switch tile'),
value: true,
onChanged: (bool newValue) { },
),
),
),
const SizedBox(
key: Key('List tile 2'),
height: itemHeight,
child: Text('List tile 2'),
),
],
scrollDirection: Axis.vertical,
onReorder: (int oldIndex, int newIndex) { },
);
await tester.pumpWidget(MaterialApp(
home: SizedBox(
height: itemHeight * 10,
child: reorderableListView,
),
));
// Get the switch tile's semantics:
final SemanticsNode semanticsNode = tester.getSemantics(find.byKey(const Key('Switch tile')));
// Check for properties of both SwitchTile semantics and the ReorderableListView custom semantics actions.
expect(semanticsNode, matchesSemantics(
hasToggledState: true,
isToggled: true,
isEnabled: true,
hasEnabledState: true,
label: 'Switch tile',
hasTapAction: true,
customActions: const <CustomSemanticsAction>[
CustomSemanticsAction(label: 'Move up'),
CustomSemanticsAction(label: 'Move down'),
CustomSemanticsAction(label: 'Move to the end'),
CustomSemanticsAction(label: 'Move to the start'),
],
));
handle.dispose();
});
});
});
group('in horizontal mode', () {
testWidgets('allows reordering from the very top to the very bottom', (WidgetTester tester) async {
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 1')),
tester.getCenter(find.text('Item 4')) + const Offset(itemHeight * 2, 0.0),
);
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1']));
});
testWidgets('allows reordering from the very bottom to the very top', (WidgetTester tester) async {
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 4')),
tester.getCenter(find.text('Item 1')),
);
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 2', 'Item 3']));
});
testWidgets('allows reordering inside the middle of the widget', (WidgetTester tester) async {
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 3')),
tester.getCenter(find.text('Item 2')),
);
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 2', 'Item 4']));
});
testWidgets('properly reorders with a header', (WidgetTester tester) async {
await tester.pumpWidget(build(header: const Text('Header Text'), scrollDirection: Axis.horizontal));
expect(find.text('Header Text'), findsOneWidget);
expect(listItems, orderedEquals(originalListItems));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 1')),
tester.getCenter(find.text('Item 4')) + const Offset(itemHeight * 2, 0.0),
);
await tester.pumpAndSettle();
expect(find.text('Header Text'), findsOneWidget);
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1']));
await tester.pumpWidget(build(header: const Text('Header Text'), scrollDirection: Axis.horizontal));
await longPressDrag(
tester,
tester.getCenter(find.text('Item 4')),
tester.getCenter(find.text('Item 3')),
);
expect(find.text('Header Text'), findsOneWidget);
expect(listItems, orderedEquals(<String>['Item 2', 'Item 4', 'Item 3', 'Item 1']));
});
testWidgets('properly determines the horizontal drop area extents', (WidgetTester tester) async {
final Widget reorderableListView = ReorderableListView(
children: const <Widget>[
SizedBox(
key: Key('Normal item'),
width: itemHeight,
child: Text('Normal item'),
),
SizedBox(
key: Key('Tall item'),
width: itemHeight * 2,
child: Text('Tall item'),
),
SizedBox(
key: Key('Last item'),
width: itemHeight,
child: Text('Last item'),
),
],
scrollDirection: Axis.horizontal,
onReorder: (int oldIndex, int newIndex) { },
);
await tester.pumpWidget(MaterialApp(
home: SizedBox(
width: itemHeight * 10,
child: reorderableListView,
),
));
Element getContentElement() {
final SingleChildScrollView listScrollView = tester.widget(find.byType(SingleChildScrollView));
final Widget scrollContents = listScrollView.child;
final Element contentElement = tester.element(find.byElementPredicate((Element element) => element.widget == scrollContents));
return contentElement;
}
const double kNonDraggingListWidth = 292.0;
// The list view pads the drop area by 8dp.
const double kDraggingListWidth = 300.0;
// Drag a normal text item
expect(getContentElement().size.width, kNonDraggingListWidth);
TestGesture drag = await tester.startGesture(tester.getCenter(find.text('Normal item')));
await tester.pump(kLongPressTimeout + kPressTimeout);
await tester.pumpAndSettle();
expect(getContentElement().size.width, kDraggingListWidth);
// Move it
await drag.moveTo(tester.getCenter(find.text('Last item')));
await tester.pumpAndSettle();
expect(getContentElement().size.width, kDraggingListWidth);
// Drop it
await drag.up();
await tester.pumpAndSettle();
expect(getContentElement().size.width, kNonDraggingListWidth);
// Drag a tall item
drag = await tester.startGesture(tester.getCenter(find.text('Tall item')));
await tester.pump(kLongPressTimeout + kPressTimeout);
await tester.pumpAndSettle();
expect(getContentElement().size.width, kDraggingListWidth);
// Move it
await drag.moveTo(tester.getCenter(find.text('Last item')));
await tester.pumpAndSettle();
expect(getContentElement().size.width, kDraggingListWidth);
// Drop it
await drag.up();
await tester.pumpAndSettle();
expect(getContentElement().size.width, kNonDraggingListWidth);
});
testWidgets('Preserves children states when the list parent changes the order', (WidgetTester tester) async {
_StatefulState findState(Key key) {
return find.byElementPredicate((Element element) => element.ancestorWidgetOfExactType(_Stateful)?.key == key)
.evaluate()
.first
.ancestorStateOfType(const TypeMatcher<_StatefulState>());
}
await tester.pumpWidget(MaterialApp(
home: ReorderableListView(
children: <Widget>[
_Stateful(key: const Key('A')),
_Stateful(key: const Key('B')),
_Stateful(key: const Key('C')),
],
onReorder: (int oldIndex, int newIndex) { },
scrollDirection: Axis.horizontal,
),
));
await tester.tap(find.byKey(const Key('A')));
await tester.pumpAndSettle();
// Only the 'A' widget should be checked.
expect(findState(const Key('A')).checked, true);
expect(findState(const Key('B')).checked, false);
expect(findState(const Key('C')).checked, false);
await tester.pumpWidget(MaterialApp(
home: ReorderableListView(
children: <Widget>[
_Stateful(key: const Key('B')),
_Stateful(key: const Key('C')),
_Stateful(key: const Key('A')),
],
onReorder: (int oldIndex, int newIndex) { },
scrollDirection: Axis.horizontal,
),
));
// Only the 'A' widget should be checked.
expect(findState(const Key('B')).checked, false);
expect(findState(const Key('C')).checked, false);
expect(findState(const Key('A')).checked, true);
});
group('Accessibility (a11y/Semantics)', () {
Map<CustomSemanticsAction, VoidCallback> getSemanticsActions(int index) {
final Semantics semantics = find.ancestor(
of: find.byKey(Key(listItems[index])),
matching: find.byType(Semantics),
).evaluate().first.widget;
return semantics.properties.customSemanticsActions;
}
const CustomSemanticsAction moveToStart = CustomSemanticsAction(label: 'Move to the start');
const CustomSemanticsAction moveToEnd = CustomSemanticsAction(label: 'Move to the end');
const CustomSemanticsAction moveLeft = CustomSemanticsAction(label: 'Move left');
const CustomSemanticsAction moveRight = CustomSemanticsAction(label: 'Move right');
testWidgets('Provides the correct accessibility actions in LTR mode', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
// The first item can be moved right or to the end.
final Map<CustomSemanticsAction, VoidCallback> firstSemanticsActions = getSemanticsActions(0);
expect(firstSemanticsActions.length, 2, reason: 'The first list item should have 2 custom actions.');
expect(firstSemanticsActions.containsKey(moveToStart), false, reason: 'The first item cannot `Move to the start`.');
expect(firstSemanticsActions.containsKey(moveLeft), false, reason: 'The first item cannot `Move left`.');
expect(firstSemanticsActions.containsKey(moveRight), true, reason: 'The first item should be able to `Move right`.');
expect(firstSemanticsActions.containsKey(moveToEnd), true, reason: 'The first item should be able to `Move to the end`.');
// Items in the middle can be moved to the start, end, left or right.
for (int i = 1; i < listItems.length - 1; i += 1) {
final Map<CustomSemanticsAction, VoidCallback> ithSemanticsActions = getSemanticsActions(i);
expect(ithSemanticsActions.length, 4, reason: 'List item $i should have 4 custom actions.');
expect(ithSemanticsActions.containsKey(moveToStart), true, reason: 'List item $i should be able to `Move to the start`.');
expect(ithSemanticsActions.containsKey(moveLeft), true, reason: 'List item $i should be able to `Move left`.');
expect(ithSemanticsActions.containsKey(moveRight), true, reason: 'List item $i should be able to `Move right`.');
expect(ithSemanticsActions.containsKey(moveToEnd), true, reason: 'List item $i should be able to `Move to the end`.');
}
// The last item can be moved left or to the start.
final Map<CustomSemanticsAction, VoidCallback> lastSemanticsActions = getSemanticsActions(listItems.length - 1);
expect(lastSemanticsActions.length, 2, reason: 'The last list item should have 2 custom actions.');
expect(lastSemanticsActions.containsKey(moveToStart), true, reason: 'The last item should be able to `Move to the start`.');
expect(lastSemanticsActions.containsKey(moveLeft), true, reason: 'The last item should be able to `Move left`.');
expect(lastSemanticsActions.containsKey(moveRight), false, reason: 'The last item cannot `Move right`.');
expect(lastSemanticsActions.containsKey(moveToEnd), false, reason: 'The last item cannot `Move to the end`.');
handle.dispose();
});
testWidgets('Provides the correct accessibility actions in Right-To-Left directionality', (WidgetTester tester) async {
// In RTL mode, the right is the start and the left is the end.
// The array representation is unchanged (LTR), but the direction of the motion actions is reversed.
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
// The first item can be moved right or to the end.
final Map<CustomSemanticsAction, VoidCallback> firstSemanticsActions = getSemanticsActions(0);
expect(firstSemanticsActions.length, 2, reason: 'The first list item should have 2 custom actions.');
expect(firstSemanticsActions.containsKey(moveToStart), false, reason: 'The first item cannot `Move to the start`.');
expect(firstSemanticsActions.containsKey(moveRight), false, reason: 'The first item cannot `Move right`.');
expect(firstSemanticsActions.containsKey(moveLeft), true, reason: 'The first item should be able to `Move left`.');
expect(firstSemanticsActions.containsKey(moveToEnd), true, reason: 'The first item should be able to `Move to the end`.');
// Items in the middle can be moved to the start, end, left or right.
for (int i = 1; i < listItems.length - 1; i += 1) {
final Map<CustomSemanticsAction, VoidCallback> ithSemanticsActions = getSemanticsActions(i);
expect(ithSemanticsActions.length, 4, reason: 'List item $i should have 4 custom actions.');
expect(ithSemanticsActions.containsKey(moveToStart), true, reason: 'List item $i should be able to `Move to the start`.');
expect(ithSemanticsActions.containsKey(moveRight), true, reason: 'List item $i should be able to `Move right`.');
expect(ithSemanticsActions.containsKey(moveLeft), true, reason: 'List item $i should be able to `Move left`.');
expect(ithSemanticsActions.containsKey(moveToEnd), true, reason: 'List item $i should be able to `Move to the end`.');
}
// The last item can be moved left or to the start.
final Map<CustomSemanticsAction, VoidCallback> lastSemanticsActions = getSemanticsActions(listItems.length - 1);
expect(lastSemanticsActions.length, 2, reason: 'The last list item should have 2 custom actions.');
expect(lastSemanticsActions.containsKey(moveToStart), true, reason: 'The last item should be able to `Move to the start`.');
expect(lastSemanticsActions.containsKey(moveRight), true, reason: 'The last item should be able to `Move right`.');
expect(lastSemanticsActions.containsKey(moveLeft), false, reason: 'The last item cannot `Move left`.');
expect(lastSemanticsActions.containsKey(moveToEnd), false, reason: 'The last item cannot `Move to the end`.');
handle.dispose();
});
testWidgets('First item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to end: move Item 1 to the end of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
Map<CustomSemanticsAction, VoidCallback> firstSemanticsActions = getSemanticsActions(0);
firstSemanticsActions[moveToEnd]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1']));
// Test out move after: move Item 2 (the current first item) one space to the right.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
firstSemanticsActions = getSemanticsActions(0);
firstSemanticsActions[moveRight]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 3', 'Item 2', 'Item 4', 'Item 1']));
handle.dispose();
});
testWidgets('First item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async {
// In RTL mode, the right is the start and the left is the end.
// The array representation is unchanged (LTR), but the direction of the motion actions is reversed.
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to end: move Item 1 to the end of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
Map<CustomSemanticsAction, VoidCallback> firstSemanticsActions = getSemanticsActions(0);
firstSemanticsActions[moveToEnd]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 2', 'Item 3', 'Item 4', 'Item 1']));
// Test out move after: move Item 2 (the current first item) one space to the left.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
firstSemanticsActions = getSemanticsActions(0);
firstSemanticsActions[moveLeft]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 3', 'Item 2', 'Item 4', 'Item 1']));
handle.dispose();
});
testWidgets('Middle item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to end: move Item 2 to the end of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
Map<CustomSemanticsAction, VoidCallback> middleSemanticsActions = getSemanticsActions(1);
middleSemanticsActions[moveToEnd]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 4', 'Item 2']));
// Test out move after: move Item 3 (the current second item) one space to the right.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
middleSemanticsActions = getSemanticsActions(1);
middleSemanticsActions[moveRight]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 4', 'Item 3', 'Item 2']));
// Test out move after: move Item 3 (the current third item) one space to the left.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
middleSemanticsActions = getSemanticsActions(2);
middleSemanticsActions[moveLeft]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 4', 'Item 2']));
// Test out move to start: move Item 4 (the current third item) to the start of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
middleSemanticsActions = getSemanticsActions(2);
middleSemanticsActions[moveToStart]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 3', 'Item 2']));
handle.dispose();
});
testWidgets('Middle item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async {
// In RTL mode, the right is the start and the left is the end.
// The array representation is unchanged (LTR), but the direction of the motion actions is reversed.
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to end: move Item 2 to the end of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
Map<CustomSemanticsAction, VoidCallback> middleSemanticsActions = getSemanticsActions(1);
middleSemanticsActions[moveToEnd]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 4', 'Item 2']));
// Test out move after: move Item 3 (the current second item) one space to the left.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
middleSemanticsActions = getSemanticsActions(1);
middleSemanticsActions[moveLeft]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 4', 'Item 3', 'Item 2']));
// Test out move after: move Item 3 (the current third item) one space to the right.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
middleSemanticsActions = getSemanticsActions(2);
middleSemanticsActions[moveRight]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 1', 'Item 3', 'Item 4', 'Item 2']));
// Test out move to start: move Item 4 (the current third item) to the start of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
middleSemanticsActions = getSemanticsActions(2);
middleSemanticsActions[moveToStart]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 3', 'Item 2']));
handle.dispose();
});
testWidgets('Last item accessibility (a11y) actions work in LTR mode', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to start: move Item 4 to the start of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
Map<CustomSemanticsAction, VoidCallback> lastSemanticsActions = getSemanticsActions(listItems.length - 1);
lastSemanticsActions[moveToStart]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 2', 'Item 3']));
// Test out move before: move Item 3 (the current last item) one space to the left.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal));
lastSemanticsActions = getSemanticsActions(listItems.length - 1);
lastSemanticsActions[moveLeft]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 3', 'Item 2']));
handle.dispose();
});
testWidgets('Last item accessibility (a11y) actions work in Right-To-Left directionality', (WidgetTester tester) async {
// In RTL mode, the right is the start and the left is the end.
// The array representation is unchanged (LTR), but the direction of the motion actions is reversed.
final SemanticsHandle handle = tester.ensureSemantics();
expect(listItems, orderedEquals(originalListItems));
// Test out move to start: move Item 4 to the start of the list.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
Map<CustomSemanticsAction, VoidCallback> lastSemanticsActions = getSemanticsActions(listItems.length - 1);
lastSemanticsActions[moveToStart]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 2', 'Item 3']));
// Test out move before: move Item 3 (the current last item) one space to the right.
await tester.pumpWidget(build(scrollDirection: Axis.horizontal, textDirection: TextDirection.rtl));
lastSemanticsActions = getSemanticsActions(listItems.length - 1);
lastSemanticsActions[moveRight]();
await tester.pumpAndSettle();
expect(listItems, orderedEquals(<String>['Item 4', 'Item 1', 'Item 3', 'Item 2']));
handle.dispose();
});
});
});
testWidgets('ReorderableListView can be reversed', (WidgetTester tester) async {
final Widget reorderableListView = ReorderableListView(
children: const <Widget>[
SizedBox(
key: Key('A'),
child: Text('A'),
),
SizedBox(
key: Key('B'),
child: Text('B'),
),
SizedBox(
key: Key('C'),
child: Text('C'),
),
],
reverse: true,
onReorder: (int oldIndex, int newIndex) { },
);
await tester.pumpWidget(MaterialApp(
home: reorderableListView,
));
expect(tester.getCenter(find.text('A')).dy, lessThan(tester.getCenter(find.text('B')).dy));
});
// TODO(djshuckerow): figure out how to write a test for scrolling the list.
});
}
Future<void> longPressDrag(WidgetTester tester, Offset start, Offset end) async {
final TestGesture drag = await tester.startGesture(start);
await tester.pump(kLongPressTimeout + kPressTimeout);
await drag.moveTo(end);
await tester.pump(kPressTimeout);
await drag.up();
}
class _Stateful extends StatefulWidget {
// Ignoring the preference for const constructors because we want to test with regular non-const instances.
// ignore:prefer_const_constructors_in_immutables
_Stateful({Key key}) : super(key: key);
@override
State<StatefulWidget> createState() => _StatefulState();
}
class _StatefulState extends State<_Stateful> {
bool checked = false;
@override
Widget build(BuildContext context) {
return Container(
width: 48.0,
height: 48.0,
child: Material(
child: Checkbox(
value: checked,
onChanged: (bool newValue) => checked = newValue,
),
),
);
}
}