// 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 _selectedRows = {}; 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(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 = []; await tester.pumpWidget( MaterialApp( home: PaginatedDataTable( header: const Text('Test table'), source: source, rowsPerPage: 2, showFirstLastButtons: true, availableRowsPerPage: const [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(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), DataColumn(label: Text('Generation')), ], ), ), ); await tester.tap(find.byTooltip('Next page')); expect(log, ['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, ['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(lastPageButton).onPressed, isNotNull); await tester.tap(lastPageButton); expect(log, ['page-changed: 498']); log.clear(); await tester.pump(); expect(tester.widget(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(firstPageButton).onPressed, isNotNull); await tester.tap(firstPageButton); expect(log, ['page-changed: 0']); log.clear(); await tester.pump(); expect(tester.widget(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, ['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 [2, 3, 4, 5, 7, 8], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, columns: const [ 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(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(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(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(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(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 [3, 6, 7, 8, 9], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, columns: const [ 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 = []; Widget buildTable(TestDataSource source) { return PaginatedDataTable( header: const Text('Test table'), source: source, onPageChanged: (int rowIndex) { log.add('page-changed: $rowIndex'); }, columns: [ 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: [ 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, ['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, ['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 [8, 9], onRowsPerPageChanged: (int? rowsPerPage) {}, columns: const [ 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 = [IconButton(onPressed: () {}, icon: const Icon(Icons.add))]; final source = TestDataSource(allowSelection: true); addTearDown(source.dispose); Widget buildTable({String? header, List? actions}) => MaterialApp( home: PaginatedDataTable( header: header != null ? Text(header) : null, actions: actions, source: source, columns: const [ 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 [501], onRowsPerPageChanged: (int? rowsPerPage) {}, columns: const [ 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 [5], onRowsPerPageChanged: (int? rowsPerPage) {}, columns: const [ 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 [2, 4, 8, 16], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, columns: const [ 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 [2, 4, 8, 16], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, columns: const [ DataColumn(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), DataColumn(label: Text('Generation')), ], ), ), ); expect( tester.renderObject(find.widgetWithText(Container, 'Name').first).size.height, 56.0, ); // This is the header row height expect( tester .renderObject(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(find.widgetWithText(Container, 'Name').first).size.height, 48.0, ); await tester.pumpWidget( MaterialApp(home: Material(child: buildCustomHeightPaginatedTable(headingRowHeight: 64.0))), ); expect( tester.renderObject(find.widgetWithText(Container, 'Name').first).size.height, 64.0, ); await tester.pumpWidget( MaterialApp(home: Material(child: buildCustomHeightPaginatedTable(dataRowHeight: 30.0))), ); expect( tester .renderObject(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(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(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 [2, 4], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, onSelectAll: (bool? value) {}, columns: const [ 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 [2, 4], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, onSelectAll: (bool? value) {}, columns: const [ 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 [2, 4, 8, 16], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, columns: const [ 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 [2, 4, 8, 16], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, columns: const [ 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 [2, 4, 8, 16], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, columns: const [ 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(find.byType(Card).first).size.width; // Widths should be equal before we resize... expect( tester.renderObject(find.byType(DataTable).first).size.width, moreOrLessEquals(cardWidth - cardMargin), ); await binding.setSurfaceSize(const Size(expandedWidth, height)); await tester.pumpWidget(buildWidget()); cardWidth = tester.renderObject(find.byType(Card).first).size.width; // ... and should still be equal after the resize. expect( tester.renderObject(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(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(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(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(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), DataColumn(label: Text('Generation')), ], ), ), ); final Container rowContainer = tester.widget( 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 [2, 4], onRowsPerPageChanged: (int? rowsPerPage) {}, onPageChanged: (int rowIndex) {}, onSelectAll: (bool? value) {}, columns: const [ 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(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(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(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), DataColumn(label: Text('Generation')), ], ), ), ); final Iterable 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(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: [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(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(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 headingRowColor = MaterialStatePropertyAll( 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(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({})); }); 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(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()); 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(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()); 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(label: Text('X')), DataColumn(label: Text('Y')), DataColumn(label: Text('Z')), ], source: source, ), ), ), ), ); expect(tester.getSize(find.byType(PaginatedDataTable)), Size.zero); }); }