mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Fixed an issue #169659 This PR improves the StretchingOverscrollIndicator to better match native Android behavior. - Previously, the stretch effect worked well only for slow dragging, and during fast flings it was absent or felt clipped. - Implemented a Simulation-based animation that responds naturally to both slow drags and high-velocity flings. - The result closely resembles the native Android overscroll behavior while maintaining Flutter's performance characteristics. This change ensures that momentum-based gestures produce a more natural and intuitive overscroll experience, especially for users accustomed to native Android scroll views. ## Fling [fling](https://github.com/user-attachments/assets/3944eef3-baaa-47df-82ca-9c49d4283fe4) ## Pull And Reduce Also, a very similar animation is implemented when pulling as well. [pull](https://github.com/user-attachments/assets/2a3025dc-0996-403d-b58b-d07f25487cda) ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --------- Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com> Co-authored-by: Victor Sanni <victorsanniay@gmail.com>
1538 lines
50 KiB
Dart
1538 lines
50 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/gestures.dart' show DragStartBehavior;
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter/rendering.dart';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
|
||
import 'data_table_test_utils.dart';
|
||
|
||
class TestDataSource extends DataTableSource {
|
||
TestDataSource({this.allowSelection = false});
|
||
|
||
final bool allowSelection;
|
||
|
||
int get generation => _generation;
|
||
int _generation = 0;
|
||
set generation(int value) {
|
||
if (_generation == value) {
|
||
return;
|
||
}
|
||
_generation = value;
|
||
notifyListeners();
|
||
}
|
||
|
||
final Set<int> _selectedRows = <int>{};
|
||
|
||
void _handleSelected(int index, bool? selected) {
|
||
if (selected ?? false) {
|
||
_selectedRows.add(index);
|
||
} else {
|
||
_selectedRows.remove(index);
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
@override
|
||
DataRow getRow(int index) {
|
||
final Dessert dessert = kDesserts[index % kDesserts.length];
|
||
final int page = index ~/ kDesserts.length;
|
||
return DataRow.byIndex(
|
||
index: index,
|
||
selected: _selectedRows.contains(index),
|
||
cells: <DataCell>[
|
||
DataCell(Text('${dessert.name} ($page)')),
|
||
DataCell(Text('${dessert.calories}')),
|
||
DataCell(Text('$generation')),
|
||
],
|
||
onSelectChanged: allowSelection ? (bool? selected) => _handleSelected(index, selected) : null,
|
||
);
|
||
}
|
||
|
||
@override
|
||
int get rowCount => 50 * kDesserts.length;
|
||
|
||
@override
|
||
bool get isRowCountApproximate => false;
|
||
|
||
@override
|
||
int get selectedRowCount => _selectedRows.length;
|
||
}
|
||
|
||
void main() {
|
||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
|
||
|
||
late TestDataSource source;
|
||
setUp(() => source = TestDataSource());
|
||
tearDown(() => source.dispose());
|
||
|
||
testWidgets('PaginatedDataTable paging', (WidgetTester tester) async {
|
||
final log = <String>[];
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
showFirstLastButtons: true,
|
||
availableRowsPerPage: const <int>[2, 4, 8, 16],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {
|
||
log.add('rows-per-page-changed: $rowsPerPage');
|
||
},
|
||
onPageChanged: (int rowIndex) {
|
||
log.add('page-changed: $rowIndex');
|
||
},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
await tester.tap(find.byTooltip('Next page'));
|
||
|
||
expect(log, <String>['page-changed: 2']);
|
||
log.clear();
|
||
|
||
await tester.pump();
|
||
|
||
expect(find.text('Frozen yogurt (0)'), findsNothing);
|
||
expect(find.text('Eclair (0)'), findsOneWidget);
|
||
expect(find.text('Gingerbread (0)'), findsNothing);
|
||
|
||
await tester.tap(find.byIcon(Icons.chevron_left));
|
||
|
||
expect(log, <String>['page-changed: 0']);
|
||
log.clear();
|
||
|
||
await tester.pump();
|
||
|
||
expect(find.text('Frozen yogurt (0)'), findsOneWidget);
|
||
expect(find.text('Eclair (0)'), findsNothing);
|
||
expect(find.text('Gingerbread (0)'), findsNothing);
|
||
|
||
final Finder lastPageButton = find.ancestor(
|
||
of: find.byTooltip('Last page'),
|
||
matching: find.byWidgetPredicate((Widget widget) => widget is IconButton),
|
||
);
|
||
|
||
expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull);
|
||
|
||
await tester.tap(lastPageButton);
|
||
|
||
expect(log, <String>['page-changed: 498']);
|
||
log.clear();
|
||
|
||
await tester.pump();
|
||
|
||
expect(tester.widget<IconButton>(lastPageButton).onPressed, isNull);
|
||
|
||
expect(find.text('Frozen yogurt (0)'), findsNothing);
|
||
expect(find.text('Donut (49)'), findsOneWidget);
|
||
expect(find.text('KitKat (49)'), findsOneWidget);
|
||
|
||
final Finder firstPageButton = find.ancestor(
|
||
of: find.byTooltip('First page'),
|
||
matching: find.byWidgetPredicate((Widget widget) => widget is IconButton),
|
||
);
|
||
|
||
expect(tester.widget<IconButton>(firstPageButton).onPressed, isNotNull);
|
||
|
||
await tester.tap(firstPageButton);
|
||
|
||
expect(log, <String>['page-changed: 0']);
|
||
log.clear();
|
||
|
||
await tester.pump();
|
||
|
||
expect(tester.widget<IconButton>(firstPageButton).onPressed, isNull);
|
||
|
||
expect(find.text('Frozen yogurt (0)'), findsOneWidget);
|
||
expect(find.text('Eclair (0)'), findsNothing);
|
||
expect(find.text('Gingerbread (0)'), findsNothing);
|
||
|
||
await tester.tap(find.byIcon(Icons.chevron_left));
|
||
|
||
expect(log, isEmpty);
|
||
|
||
await tester.tap(find.text('2'));
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||
|
||
await tester.tap(find.text('8').last);
|
||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||
|
||
expect(log, <String>['rows-per-page-changed: 8']);
|
||
log.clear();
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable footer page number', (WidgetTester tester) async {
|
||
var rowsPerPage = 2;
|
||
|
||
Widget buildTable(TestDataSource source, int rowsPerPage) {
|
||
return PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: rowsPerPage,
|
||
showFirstLastButtons: true,
|
||
availableRowsPerPage: const <int>[2, 3, 4, 5, 7, 8],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
|
||
expect(find.text('1–2 of 500'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Next page'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('3–4 of 500'), findsOneWidget);
|
||
|
||
final Finder lastPageButton = find.ancestor(
|
||
of: find.byTooltip('Last page'),
|
||
matching: find.byWidgetPredicate((Widget widget) => widget is IconButton),
|
||
);
|
||
|
||
expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull);
|
||
|
||
await tester.tap(lastPageButton);
|
||
await tester.pump();
|
||
|
||
expect(find.text('499–500 of 500'), findsOneWidget);
|
||
|
||
final PaginatedDataTableState state = tester.state(find.byType(PaginatedDataTable));
|
||
|
||
state.pageTo(1);
|
||
rowsPerPage = 3;
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
|
||
expect(find.textContaining('1–3 of 500'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Next page'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('4–6 of 500'), findsOneWidget);
|
||
expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull);
|
||
|
||
await tester.tap(lastPageButton);
|
||
await tester.pump();
|
||
|
||
expect(find.text('499–500 of 500'), findsOneWidget);
|
||
|
||
state.pageTo(1);
|
||
rowsPerPage = 4;
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
|
||
expect(find.textContaining('1–4 of 500'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Next page'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('5–8 of 500'), findsOneWidget);
|
||
expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull);
|
||
|
||
await tester.tap(lastPageButton);
|
||
await tester.pump();
|
||
|
||
expect(find.text('497–500 of 500'), findsOneWidget);
|
||
|
||
state.pageTo(1);
|
||
rowsPerPage = 5;
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
|
||
expect(find.textContaining('1–5 of 500'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Next page'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('6–10 of 500'), findsOneWidget);
|
||
expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull);
|
||
|
||
await tester.tap(lastPageButton);
|
||
await tester.pump();
|
||
|
||
expect(find.text('496–500 of 500'), findsOneWidget);
|
||
|
||
state.pageTo(1);
|
||
rowsPerPage = 8;
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
|
||
expect(find.textContaining('1–8 of 500'), findsOneWidget);
|
||
|
||
await tester.tap(find.byTooltip('Next page'));
|
||
await tester.pump();
|
||
|
||
expect(find.text('9–16 of 500'), findsOneWidget);
|
||
expect(tester.widget<IconButton>(lastPageButton).onPressed, isNotNull);
|
||
|
||
await tester.tap(lastPageButton);
|
||
await tester.pump();
|
||
|
||
expect(find.text('497–500 of 500'), findsOneWidget);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable Last Page Empty Space', (WidgetTester tester) async {
|
||
final source = TestDataSource();
|
||
var rowsPerPage = 3;
|
||
final int rowCount = source.rowCount;
|
||
addTearDown(source.dispose);
|
||
|
||
Widget buildTable(TestDataSource source, int rowsPerPage) {
|
||
return PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: rowsPerPage,
|
||
showFirstLastButtons: true,
|
||
dataRowHeight: 46,
|
||
availableRowsPerPage: const <int>[3, 6, 7, 8, 9],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
showEmptyRows: false,
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(body: Center(child: buildTable(source, rowsPerPage))),
|
||
),
|
||
);
|
||
|
||
expect(
|
||
find.byWidgetPredicate((Widget widget) => widget is SizedBox && widget.height == 0),
|
||
findsOneWidget,
|
||
);
|
||
await tester.tap(find.byIcon(Icons.skip_next));
|
||
await tester.pump();
|
||
expect(
|
||
find.byWidgetPredicate(
|
||
(Widget widget) =>
|
||
widget is SizedBox && widget.height == (rowsPerPage - (rowCount % rowsPerPage)) * 46.0,
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
|
||
rowsPerPage = 6;
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
|
||
await tester.tap(find.byIcon(Icons.skip_previous));
|
||
await tester.pump();
|
||
expect(
|
||
find.byWidgetPredicate((Widget widget) => widget is SizedBox && widget.height == 0),
|
||
findsOneWidget,
|
||
);
|
||
await tester.tap(find.byIcon(Icons.skip_next));
|
||
await tester.pump();
|
||
expect(
|
||
find.byWidgetPredicate(
|
||
(Widget widget) =>
|
||
widget is SizedBox && widget.height == (rowsPerPage - (rowCount % rowsPerPage)) * 46.0,
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
|
||
rowsPerPage = 7;
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
await tester.tap(find.byIcon(Icons.skip_previous));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
find.byWidgetPredicate((Widget widget) => widget is SizedBox && widget.height == 0),
|
||
findsOneWidget,
|
||
);
|
||
await tester.tap(find.byIcon(Icons.skip_next));
|
||
await tester.pump();
|
||
expect(
|
||
find.byWidgetPredicate(
|
||
(Widget widget) =>
|
||
widget is SizedBox && widget.height == (rowsPerPage - (rowCount % rowsPerPage)) * 46.0,
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
|
||
rowsPerPage = 8;
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source, rowsPerPage)));
|
||
await tester.tap(find.byIcon(Icons.skip_previous));
|
||
await tester.pump();
|
||
|
||
expect(
|
||
find.byWidgetPredicate((Widget widget) => widget is SizedBox && widget.height == 0),
|
||
findsOneWidget,
|
||
);
|
||
await tester.tap(find.byIcon(Icons.skip_next));
|
||
await tester.pump();
|
||
expect(
|
||
find.byWidgetPredicate(
|
||
(Widget widget) =>
|
||
widget is SizedBox && widget.height == (rowsPerPage - (rowCount % rowsPerPage)) * 46.0,
|
||
),
|
||
findsOneWidget,
|
||
);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable control test', (WidgetTester tester) async {
|
||
var source = TestDataSource()..generation = 42;
|
||
addTearDown(source.dispose);
|
||
|
||
final log = <String>[];
|
||
|
||
Widget buildTable(TestDataSource source) {
|
||
return PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
onPageChanged: (int rowIndex) {
|
||
log.add('page-changed: $rowIndex');
|
||
},
|
||
columns: <DataColumn>[
|
||
const DataColumn(label: Text('Name'), tooltip: 'Name'),
|
||
DataColumn(
|
||
label: const Text('Calories'),
|
||
tooltip: 'Calories',
|
||
numeric: true,
|
||
onSort: (int columnIndex, bool ascending) {
|
||
log.add('column-sort: $columnIndex $ascending');
|
||
},
|
||
),
|
||
const DataColumn(label: Text('Generation'), tooltip: 'Generation'),
|
||
],
|
||
actions: <Widget>[
|
||
IconButton(
|
||
icon: const Icon(Icons.adjust),
|
||
onPressed: () {
|
||
log.add('action: adjust');
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source)));
|
||
|
||
// the column overflows because we're forcing it to 600 pixels high
|
||
final dynamic exception = tester.takeException();
|
||
expect(exception, isFlutterError);
|
||
// ignore: avoid_dynamic_calls
|
||
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
|
||
// ignore: avoid_dynamic_calls
|
||
expect(exception.diagnostics.first.toString(), startsWith('A RenderFlex overflowed by '));
|
||
|
||
expect(find.text('Gingerbread (0)'), findsOneWidget);
|
||
expect(find.text('Gingerbread (1)'), findsNothing);
|
||
expect(find.text('42'), findsNWidgets(10));
|
||
|
||
source.generation = 43;
|
||
await tester.pump();
|
||
|
||
expect(find.text('42'), findsNothing);
|
||
expect(find.text('43'), findsNWidgets(10));
|
||
|
||
source = TestDataSource()..generation = 15;
|
||
addTearDown(source.dispose);
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source)));
|
||
|
||
expect(find.text('42'), findsNothing);
|
||
expect(find.text('43'), findsNothing);
|
||
expect(find.text('15'), findsNWidgets(10));
|
||
|
||
final PaginatedDataTableState state = tester.state(find.byType(PaginatedDataTable));
|
||
|
||
expect(log, isEmpty);
|
||
state.pageTo(23);
|
||
expect(log, <String>['page-changed: 20']);
|
||
log.clear();
|
||
|
||
await tester.pump();
|
||
|
||
expect(find.text('Gingerbread (0)'), findsNothing);
|
||
expect(find.text('Gingerbread (1)'), findsNothing);
|
||
expect(find.text('Gingerbread (2)'), findsOneWidget);
|
||
|
||
await tester.tap(find.byIcon(Icons.adjust));
|
||
expect(log, <String>['action: adjust']);
|
||
log.clear();
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable text alignment', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: const Text('HEADER'),
|
||
source: source,
|
||
rowsPerPage: 8,
|
||
availableRowsPerPage: const <int>[8, 9],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('COL1')),
|
||
DataColumn(label: Text('COL2')),
|
||
DataColumn(label: Text('COL3')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
expect(find.text('Rows per page:'), findsOneWidget);
|
||
expect(find.text('8'), findsOneWidget);
|
||
expect(
|
||
tester.getTopRight(find.text('8')).dx,
|
||
tester.getTopRight(find.text('Rows per page:')).dx + 40.0,
|
||
); // per spec
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable with and without header and actions', (
|
||
WidgetTester tester,
|
||
) async {
|
||
await binding.setSurfaceSize(const Size(800, 800));
|
||
const headerText = 'HEADER';
|
||
final actions = <Widget>[IconButton(onPressed: () {}, icon: const Icon(Icons.add))];
|
||
final source = TestDataSource(allowSelection: true);
|
||
addTearDown(source.dispose);
|
||
|
||
Widget buildTable({String? header, List<Widget>? actions}) => MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: header != null ? Text(header) : null,
|
||
actions: actions,
|
||
source: source,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(buildTable(header: headerText));
|
||
expect(find.text(headerText), findsOneWidget);
|
||
expect(find.byIcon(Icons.add), findsNothing);
|
||
|
||
await tester.pumpWidget(buildTable(header: headerText, actions: actions));
|
||
expect(find.text(headerText), findsOneWidget);
|
||
expect(find.byIcon(Icons.add), findsOneWidget);
|
||
|
||
await tester.pumpWidget(buildTable());
|
||
expect(find.text(headerText), findsNothing);
|
||
expect(find.byIcon(Icons.add), findsNothing);
|
||
|
||
expect(() => buildTable(actions: actions), throwsAssertionError);
|
||
|
||
await binding.setSurfaceSize(null);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable with large text', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: MediaQuery.withClampedTextScaling(
|
||
minScaleFactor: 20.0,
|
||
maxScaleFactor: 20.0,
|
||
child: PaginatedDataTable(
|
||
header: const Text('HEADER'),
|
||
source: source,
|
||
rowsPerPage: 501,
|
||
availableRowsPerPage: const <int>[501],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('COL1')),
|
||
DataColumn(label: Text('COL2')),
|
||
DataColumn(label: Text('COL3')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
// the column overflows because we're forcing it to 600 pixels high
|
||
final dynamic exception = tester.takeException();
|
||
expect(exception, isFlutterError);
|
||
// ignore: avoid_dynamic_calls
|
||
expect(exception.diagnostics.first.level, DiagnosticLevel.summary);
|
||
// ignore: avoid_dynamic_calls
|
||
expect(exception.diagnostics.first.toString(), contains('A RenderFlex overflowed by'));
|
||
|
||
expect(find.text('Rows per page:'), findsOneWidget);
|
||
// Test that we will show some options in the drop down even if the lowest option is bigger than the source:
|
||
assert(501 > source.rowCount);
|
||
expect(find.text('501'), findsOneWidget);
|
||
// Test that it fits:
|
||
expect(
|
||
tester.getTopRight(find.text('501')).dx,
|
||
greaterThanOrEqualTo(tester.getTopRight(find.text('Rows per page:')).dx + 40.0),
|
||
);
|
||
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/43433
|
||
|
||
testWidgets('PaginatedDataTable footer scrolls', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Align(
|
||
alignment: Alignment.topLeft,
|
||
child: SizedBox(
|
||
width: 100.0,
|
||
child: PaginatedDataTable(
|
||
header: const Text('HEADER'),
|
||
source: source,
|
||
rowsPerPage: 5,
|
||
dragStartBehavior: DragStartBehavior.down,
|
||
availableRowsPerPage: const <int>[5],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('COL1')),
|
||
DataColumn(label: Text('COL2')),
|
||
DataColumn(label: Text('COL3')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
expect(find.text('Rows per page:'), findsOneWidget);
|
||
expect(tester.getTopLeft(find.text('Rows per page:')).dx, lessThan(0.0)); // off screen
|
||
await tester.dragFrom(
|
||
Offset(50.0, tester.getTopLeft(find.text('Rows per page:')).dy),
|
||
const Offset(1000.0, 0.0),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('Rows per page:'), findsOneWidget);
|
||
expect(
|
||
tester.getTopLeft(find.text('Rows per page:')).dx,
|
||
18.0,
|
||
); // 14 padding in the footer row, 4 padding from the card
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable custom row height', (WidgetTester tester) async {
|
||
Widget buildCustomHeightPaginatedTable({
|
||
double? dataRowHeight,
|
||
double? dataRowMinHeight,
|
||
double? dataRowMaxHeight,
|
||
double headingRowHeight = 56.0,
|
||
}) {
|
||
return PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4, 8, 16],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
dataRowHeight: dataRowHeight,
|
||
dataRowMinHeight: dataRowMinHeight,
|
||
dataRowMaxHeight: dataRowMaxHeight,
|
||
headingRowHeight: headingRowHeight,
|
||
);
|
||
}
|
||
|
||
// DEFAULT VALUES
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4, 8, 16],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
expect(
|
||
tester.renderObject<RenderBox>(find.widgetWithText(Container, 'Name').first).size.height,
|
||
56.0,
|
||
); // This is the header row height
|
||
expect(
|
||
tester
|
||
.renderObject<RenderBox>(find.widgetWithText(Container, 'Frozen yogurt (0)').first)
|
||
.size
|
||
.height,
|
||
48.0,
|
||
); // This is the data row height
|
||
|
||
// CUSTOM VALUES
|
||
await tester.pumpWidget(
|
||
MaterialApp(home: Material(child: buildCustomHeightPaginatedTable(headingRowHeight: 48.0))),
|
||
);
|
||
expect(
|
||
tester.renderObject<RenderBox>(find.widgetWithText(Container, 'Name').first).size.height,
|
||
48.0,
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(home: Material(child: buildCustomHeightPaginatedTable(headingRowHeight: 64.0))),
|
||
);
|
||
expect(
|
||
tester.renderObject<RenderBox>(find.widgetWithText(Container, 'Name').first).size.height,
|
||
64.0,
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(home: Material(child: buildCustomHeightPaginatedTable(dataRowHeight: 30.0))),
|
||
);
|
||
expect(
|
||
tester
|
||
.renderObject<RenderBox>(find.widgetWithText(Container, 'Frozen yogurt (0)').first)
|
||
.size
|
||
.height,
|
||
30.0,
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(home: Material(child: buildCustomHeightPaginatedTable(dataRowHeight: 56.0))),
|
||
);
|
||
expect(
|
||
tester
|
||
.renderObject<RenderBox>(find.widgetWithText(Container, 'Frozen yogurt (0)').first)
|
||
.size
|
||
.height,
|
||
56.0,
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: buildCustomHeightPaginatedTable(dataRowMinHeight: 51.0, dataRowMaxHeight: 51.0),
|
||
),
|
||
),
|
||
);
|
||
expect(
|
||
tester
|
||
.renderObject<RenderBox>(find.widgetWithText(Container, 'Frozen yogurt (0)').first)
|
||
.size
|
||
.height,
|
||
51.0,
|
||
);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable custom horizontal padding - checkbox', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const defaultHorizontalMargin = 24.0;
|
||
const defaultColumnSpacing = 56.0;
|
||
const customHorizontalMargin = 10.0;
|
||
const customColumnSpacing = 15.0;
|
||
|
||
const double width = 400;
|
||
const double height = 400;
|
||
|
||
final Size originalSize = binding.renderView.size;
|
||
|
||
// Ensure the containing Card is small enough that we don't expand too
|
||
// much, resulting in our custom margin being ignored.
|
||
await binding.setSurfaceSize(const Size(width, height));
|
||
|
||
final source = TestDataSource(allowSelection: true);
|
||
addTearDown(source.dispose);
|
||
Finder cellContent;
|
||
Finder checkbox;
|
||
Finder padding;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
onSelectAll: (bool? value) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
// default checkbox padding
|
||
checkbox = find.byType(Checkbox).first;
|
||
padding = find.ancestor(of: checkbox, matching: find.byType(Padding)).first;
|
||
expect(tester.getRect(checkbox).left - tester.getRect(padding).left, defaultHorizontalMargin);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(checkbox).right,
|
||
defaultHorizontalMargin / 2,
|
||
);
|
||
|
||
// default first column padding
|
||
padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
|
||
cellContent = find.widgetWithText(
|
||
Align,
|
||
'Frozen yogurt (0)',
|
||
); // DataTable wraps its DataCells in an Align widget
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
defaultHorizontalMargin / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
|
||
// default middle column padding
|
||
padding = find.widgetWithText(Padding, '159').first;
|
||
cellContent = find.widgetWithText(Align, '159');
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
|
||
// default last column padding
|
||
padding = find.widgetWithText(Padding, '0').first;
|
||
cellContent = find.widgetWithText(Align, '0').first;
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
defaultHorizontalMargin,
|
||
);
|
||
|
||
// CUSTOM VALUES
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
onSelectAll: (bool? value) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
horizontalMargin: customHorizontalMargin,
|
||
columnSpacing: customColumnSpacing,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// custom checkbox padding
|
||
checkbox = find.byType(Checkbox).first;
|
||
padding = find.ancestor(of: checkbox, matching: find.byType(Padding)).first;
|
||
expect(tester.getRect(checkbox).left - tester.getRect(padding).left, customHorizontalMargin);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(checkbox).right,
|
||
customHorizontalMargin / 2,
|
||
);
|
||
|
||
// custom first column padding
|
||
padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
|
||
cellContent = find.widgetWithText(
|
||
Align,
|
||
'Frozen yogurt (0)',
|
||
); // DataTable wraps its DataCells in an Align widget
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
customHorizontalMargin / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
customColumnSpacing / 2,
|
||
);
|
||
|
||
// custom middle column padding
|
||
padding = find.widgetWithText(Padding, '159').first;
|
||
cellContent = find.widgetWithText(Align, '159');
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
customColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
customColumnSpacing / 2,
|
||
);
|
||
|
||
// custom last column padding
|
||
padding = find.widgetWithText(Padding, '0').first;
|
||
cellContent = find.widgetWithText(Align, '0').first;
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
customColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
customHorizontalMargin,
|
||
);
|
||
|
||
// Reset the surface size.
|
||
await binding.setSurfaceSize(originalSize);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable custom horizontal padding - no checkbox', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const defaultHorizontalMargin = 24.0;
|
||
const defaultColumnSpacing = 56.0;
|
||
const customHorizontalMargin = 10.0;
|
||
const customColumnSpacing = 15.0;
|
||
Finder cellContent;
|
||
Finder padding;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4, 8, 16],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
// default first column padding
|
||
padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
|
||
cellContent = find.widgetWithText(
|
||
Align,
|
||
'Frozen yogurt (0)',
|
||
); // DataTable wraps its DataCells in an Align widget
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
defaultHorizontalMargin,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
|
||
// default middle column padding
|
||
padding = find.widgetWithText(Padding, '159').first;
|
||
cellContent = find.widgetWithText(Align, '159');
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
|
||
// default last column padding
|
||
padding = find.widgetWithText(Padding, '0').first;
|
||
cellContent = find.widgetWithText(Align, '0').first;
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
defaultColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
defaultHorizontalMargin,
|
||
);
|
||
|
||
// CUSTOM VALUES
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4, 8, 16],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
horizontalMargin: customHorizontalMargin,
|
||
columnSpacing: customColumnSpacing,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// custom first column padding
|
||
padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
|
||
cellContent = find.widgetWithText(Align, 'Frozen yogurt (0)');
|
||
expect(tester.getRect(cellContent).left - tester.getRect(padding).left, customHorizontalMargin);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
customColumnSpacing / 2,
|
||
);
|
||
|
||
// custom middle column padding
|
||
padding = find.widgetWithText(Padding, '159').first;
|
||
cellContent = find.widgetWithText(Align, '159');
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
customColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
customColumnSpacing / 2,
|
||
);
|
||
|
||
// custom last column padding
|
||
padding = find.widgetWithText(Padding, '0').first;
|
||
cellContent = find.widgetWithText(Align, '0').first;
|
||
expect(
|
||
tester.getRect(cellContent).left - tester.getRect(padding).left,
|
||
customColumnSpacing / 2,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(cellContent).right,
|
||
customHorizontalMargin,
|
||
);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable table fills Card width', (WidgetTester tester) async {
|
||
// 800 is wide enough to ensure that all of the columns fit in the
|
||
// Card. The test makes sure that the DataTable is exactly as wide
|
||
// as the Card, minus the Card's margin.
|
||
const double originalWidth = 800;
|
||
const double expandedWidth = 1600;
|
||
const double height = 400;
|
||
|
||
// By default, the margin of a Card is 4 in all directions, so
|
||
// the size of the DataTable (inside the Card) is horizontally
|
||
// reduced by 4 * 2; the left and right margins.
|
||
const double cardMargin = 8;
|
||
|
||
final Size originalSize = binding.renderView.size;
|
||
|
||
Widget buildWidget() => MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4, 8, 16],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
);
|
||
|
||
await binding.setSurfaceSize(const Size(originalWidth, height));
|
||
await tester.pumpWidget(buildWidget());
|
||
|
||
double cardWidth = tester.renderObject<RenderBox>(find.byType(Card).first).size.width;
|
||
|
||
// Widths should be equal before we resize...
|
||
expect(
|
||
tester.renderObject<RenderBox>(find.byType(DataTable).first).size.width,
|
||
moreOrLessEquals(cardWidth - cardMargin),
|
||
);
|
||
|
||
await binding.setSurfaceSize(const Size(expandedWidth, height));
|
||
await tester.pumpWidget(buildWidget());
|
||
|
||
cardWidth = tester.renderObject<RenderBox>(find.byType(Card).first).size.width;
|
||
|
||
// ... and should still be equal after the resize.
|
||
expect(
|
||
tester.renderObject<RenderBox>(find.byType(DataTable).first).size.width,
|
||
moreOrLessEquals(cardWidth - cardMargin),
|
||
);
|
||
|
||
// Double check to ensure we actually resized the surface properly.
|
||
expect(cardWidth, moreOrLessEquals(expandedWidth));
|
||
|
||
// Reset the surface size.
|
||
await binding.setSurfaceSize(originalSize);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable with optional column checkbox', (WidgetTester tester) async {
|
||
await binding.setSurfaceSize(const Size(800, 800));
|
||
addTearDown(() => binding.setSurfaceSize(null));
|
||
final source = TestDataSource(allowSelection: true);
|
||
addTearDown(source.dispose);
|
||
|
||
Widget buildTable(bool checkbox) => MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
showCheckboxColumn: checkbox,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
);
|
||
|
||
await tester.pumpWidget(buildTable(true));
|
||
expect(find.byType(Checkbox), findsNWidgets(11));
|
||
|
||
await tester.pumpWidget(buildTable(false));
|
||
expect(find.byType(Checkbox), findsNothing);
|
||
});
|
||
|
||
testWidgets('Table should not use decoration from DataTableTheme', (WidgetTester tester) async {
|
||
final Size originalSize = binding.renderView.size;
|
||
await binding.setSurfaceSize(const Size(800, 800));
|
||
|
||
Widget buildTable() {
|
||
final source = TestDataSource(allowSelection: true);
|
||
addTearDown(source.dispose);
|
||
|
||
return MaterialApp(
|
||
theme: ThemeData(
|
||
dataTableTheme: const DataTableThemeData(decoration: BoxDecoration(color: Colors.white)),
|
||
),
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(buildTable());
|
||
final Finder tableContainerFinder = find
|
||
.ancestor(of: find.byType(Table), matching: find.byType(Container))
|
||
.first;
|
||
expect(tester.widget<Container>(tableContainerFinder).decoration, const BoxDecoration());
|
||
|
||
// Reset the surface size.
|
||
await binding.setSurfaceSize(originalSize);
|
||
});
|
||
|
||
testWidgets('dataRowMinHeight & dataRowMaxHeight if not set will use DataTableTheme', (
|
||
WidgetTester tester,
|
||
) async {
|
||
addTearDown(() => binding.setSurfaceSize(null));
|
||
await binding.setSurfaceSize(const Size(800, 800));
|
||
|
||
const minMaxDataRowHeight = 30.0;
|
||
|
||
final source = TestDataSource(allowSelection: true);
|
||
addTearDown(source.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
theme: ThemeData(
|
||
dataTableTheme: const DataTableThemeData(
|
||
dataRowMinHeight: minMaxDataRowHeight,
|
||
dataRowMaxHeight: minMaxDataRowHeight,
|
||
),
|
||
),
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
final Container rowContainer = tester.widget<Container>(
|
||
find.descendant(of: find.byType(Table), matching: find.byType(Container)).last,
|
||
);
|
||
expect(rowContainer.constraints?.minHeight, minMaxDataRowHeight);
|
||
expect(rowContainer.constraints?.maxHeight, minMaxDataRowHeight);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable custom checkboxHorizontalMargin properly applied', (
|
||
WidgetTester tester,
|
||
) async {
|
||
const customCheckboxHorizontalMargin = 15.0;
|
||
const customHorizontalMargin = 10.0;
|
||
|
||
const double width = 400;
|
||
const double height = 400;
|
||
|
||
final Size originalSize = binding.renderView.size;
|
||
|
||
// Ensure the containing Card is small enough that we don't expand too
|
||
// much, resulting in our custom margin being ignored.
|
||
await binding.setSurfaceSize(const Size(width, height));
|
||
|
||
final source = TestDataSource(allowSelection: true);
|
||
addTearDown(source.dispose);
|
||
|
||
Finder cellContent;
|
||
Finder checkbox;
|
||
Finder padding;
|
||
|
||
// CUSTOM VALUES
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Material(
|
||
child: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
availableRowsPerPage: const <int>[2, 4],
|
||
onRowsPerPageChanged: (int? rowsPerPage) {},
|
||
onPageChanged: (int rowIndex) {},
|
||
onSelectAll: (bool? value) {},
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
horizontalMargin: customHorizontalMargin,
|
||
checkboxHorizontalMargin: customCheckboxHorizontalMargin,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// Custom checkbox padding.
|
||
checkbox = find.byType(Checkbox).first;
|
||
padding = find.ancestor(of: checkbox, matching: find.byType(Padding)).first;
|
||
expect(
|
||
tester.getRect(checkbox).left - tester.getRect(padding).left,
|
||
customCheckboxHorizontalMargin,
|
||
);
|
||
expect(
|
||
tester.getRect(padding).right - tester.getRect(checkbox).right,
|
||
customCheckboxHorizontalMargin,
|
||
);
|
||
|
||
// Custom first column padding.
|
||
padding = find.widgetWithText(Padding, 'Frozen yogurt (0)').first;
|
||
cellContent = find.widgetWithText(
|
||
Align,
|
||
'Frozen yogurt (0)',
|
||
); // DataTable wraps its DataCells in an Align widget.
|
||
expect(tester.getRect(cellContent).left - tester.getRect(padding).left, customHorizontalMargin);
|
||
|
||
// Reset the surface size.
|
||
await binding.setSurfaceSize(originalSize);
|
||
});
|
||
|
||
testWidgets('Items selected text uses secondary color', (WidgetTester tester) async {
|
||
const selectedTextColor = Color(0xff00ddff);
|
||
final ColorScheme colors = const ColorScheme.light().copyWith(secondary: selectedTextColor);
|
||
final theme = ThemeData.from(colorScheme: colors);
|
||
|
||
final source = TestDataSource(allowSelection: true);
|
||
addTearDown(source.dispose);
|
||
|
||
Widget buildTable() {
|
||
return MaterialApp(
|
||
theme: theme,
|
||
home: PaginatedDataTable(
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
await binding.setSurfaceSize(const Size(800, 800));
|
||
await tester.pumpWidget(buildTable());
|
||
expect(find.text('Test table'), findsOneWidget);
|
||
|
||
// Select a row with yogurt
|
||
await tester.tap(find.text('Frozen yogurt (0)'));
|
||
await tester.pumpAndSettle();
|
||
|
||
// The header should be replace with a selected text item
|
||
expect(find.text('Test table'), findsNothing);
|
||
expect(find.text('1 item selected'), findsOneWidget);
|
||
|
||
// The color of the selected text item should be the colorScheme.secondary
|
||
final TextStyle selectedTextStyle = tester
|
||
.renderObject<RenderParagraph>(find.text('1 item selected'))
|
||
.text
|
||
.style!;
|
||
expect(selectedTextStyle.color, equals(selectedTextColor));
|
||
|
||
await binding.setSurfaceSize(null);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable arrowHeadColor set properly', (WidgetTester tester) async {
|
||
await binding.setSurfaceSize(const Size(800, 800));
|
||
addTearDown(() => binding.setSurfaceSize(null));
|
||
const arrowHeadColor = Color(0xFFE53935);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: PaginatedDataTable(
|
||
arrowHeadColor: arrowHeadColor,
|
||
showFirstLastButtons: true,
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
|
||
final Iterable<IconButton> icons = tester.widgetList(find.byType(IconButton));
|
||
|
||
expect(icons.elementAt(0).color, arrowHeadColor);
|
||
expect(icons.elementAt(1).color, arrowHeadColor);
|
||
expect(icons.elementAt(2).color, arrowHeadColor);
|
||
expect(icons.elementAt(3).color, arrowHeadColor);
|
||
});
|
||
|
||
testWidgets('OverflowBar header left alignment', (WidgetTester tester) async {
|
||
// Test an old special case that tried to align the first child of a ButtonBar
|
||
// and the left edge of a Text header widget. Still possible with OverflowBar
|
||
// albeit without any special case in the implementation's build method.
|
||
Widget buildFrame(Widget header) {
|
||
return MaterialApp(
|
||
home: PaginatedDataTable(
|
||
header: header,
|
||
rowsPerPage: 2,
|
||
source: source,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(buildFrame(const Text('HEADER')));
|
||
final double headerX = tester.getTopLeft(find.text('HEADER')).dx;
|
||
final Widget overflowBar = OverflowBar(
|
||
children: <Widget>[ElevatedButton(onPressed: () {}, child: const Text('BUTTON'))],
|
||
);
|
||
await tester.pumpWidget(buildFrame(overflowBar));
|
||
expect(headerX, tester.getTopLeft(find.byType(ElevatedButton)).dx);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable can be scrolled using ScrollController', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final scrollController = ScrollController();
|
||
addTearDown(scrollController.dispose);
|
||
|
||
Widget buildTable(TestDataSource source) {
|
||
return Align(
|
||
alignment: Alignment.topLeft,
|
||
child: SizedBox(
|
||
width: 100,
|
||
child: PaginatedDataTable(
|
||
controller: scrollController,
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name'), tooltip: 'Name'),
|
||
DataColumn(label: Text('Calories'), tooltip: 'Calories', numeric: true),
|
||
DataColumn(label: Text('Generation'), tooltip: 'Generation'),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
await tester.pumpWidget(MaterialApp(home: buildTable(source)));
|
||
|
||
// DataTable uses provided ScrollController
|
||
final Scrollable bodyScrollView = tester.widget(find.byType(Scrollable).first);
|
||
expect(bodyScrollView.controller, scrollController);
|
||
|
||
expect(scrollController.offset, 0.0);
|
||
scrollController.jumpTo(50.0);
|
||
await tester.pumpAndSettle();
|
||
|
||
expect(scrollController.offset, 50.0);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable uses PrimaryScrollController when primary ', (
|
||
WidgetTester tester,
|
||
) async {
|
||
final primaryScrollController = ScrollController();
|
||
addTearDown(primaryScrollController.dispose);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: PrimaryScrollController(
|
||
controller: primaryScrollController,
|
||
child: PaginatedDataTable(
|
||
primary: true,
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
// DataTable uses primaryScrollController
|
||
final Scrollable bodyScrollView = tester.widget(find.byType(Scrollable).first);
|
||
expect(bodyScrollView.controller, primaryScrollController);
|
||
|
||
// Footer does not use primaryScrollController
|
||
final Scrollable footerScrollView = tester.widget(find.byType(Scrollable).last);
|
||
expect(footerScrollView.controller, null);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable custom heading row color', (WidgetTester tester) async {
|
||
const WidgetStateProperty<Color> headingRowColor = MaterialStatePropertyAll<Color>(
|
||
Color(0xffFF0000),
|
||
);
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: PaginatedDataTable(
|
||
primary: true,
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
headingRowColor: headingRowColor,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
final Table table = tester.widget(find.byType(Table));
|
||
final TableRow tableRow = table.children[0];
|
||
final tableRowBoxDecoration = tableRow.decoration! as BoxDecoration;
|
||
expect(tableRowBoxDecoration.color, headingRowColor.resolve(<WidgetState>{}));
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable respects custom dividerThickness', (WidgetTester tester) async {
|
||
const dividerThickness = 2.0;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: PaginatedDataTable(
|
||
primary: true,
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
dividerThickness: dividerThickness,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.byType(Table), findsOneWidget);
|
||
final Table table = tester.widget(find.byType(Table));
|
||
final TableRow tableRow = table.children[0];
|
||
final tableRowBoxDecoration = tableRow.decoration as BoxDecoration?;
|
||
expect(tableRowBoxDecoration, isNotNull);
|
||
expect(tableRowBoxDecoration?.border, isA<Border>());
|
||
|
||
final border = tableRowBoxDecoration?.border as Border?;
|
||
expect(border?.bottom.width, dividerThickness);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable respects default dividerThickness', (WidgetTester tester) async {
|
||
const defaultDividerThickness = 1.0;
|
||
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Scaffold(
|
||
body: PaginatedDataTable(
|
||
primary: true,
|
||
header: const Text('Test table'),
|
||
source: source,
|
||
rowsPerPage: 2,
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('Name')),
|
||
DataColumn(label: Text('Calories'), numeric: true),
|
||
DataColumn(label: Text('Generation')),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
|
||
expect(find.byType(Table), findsOneWidget);
|
||
final Table table = tester.widget(find.byType(Table));
|
||
final TableRow tableRow = table.children[0];
|
||
final tableRowBoxDecoration = tableRow.decoration as BoxDecoration?;
|
||
expect(tableRowBoxDecoration, isNotNull);
|
||
expect(tableRowBoxDecoration?.border, isA<Border>());
|
||
|
||
final border = tableRowBoxDecoration?.border as Border?;
|
||
expect(border?.bottom.width, defaultDividerThickness);
|
||
});
|
||
|
||
testWidgets('PaginatedDataTable does not crash at zero area', (WidgetTester tester) async {
|
||
await tester.pumpWidget(
|
||
MaterialApp(
|
||
home: Center(
|
||
child: SizedBox.shrink(
|
||
child: PaginatedDataTable(
|
||
columns: const <DataColumn>[
|
||
DataColumn(label: Text('X')),
|
||
DataColumn(label: Text('Y')),
|
||
DataColumn(label: Text('Z')),
|
||
],
|
||
source: source,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
expect(tester.getSize(find.byType(PaginatedDataTable)), Size.zero);
|
||
});
|
||
}
|