// 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(); testWidgets('PaginatedDataTable paging', (WidgetTester tester) async { final TestDataSource source = TestDataSource(); final List<String> 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 control test', (WidgetTester tester) async { TestDataSource source = TestDataSource() ..generation = 42; final List<String> 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; 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: TestDataSource(), 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 String headerText = 'HEADER'; final List<Widget> actions = <Widget>[ IconButton(onPressed: () {}, icon: const Icon(Icons.add)), ]; Widget buildTable({String? header, List<Widget>? actions}) => MaterialApp( home: PaginatedDataTable( header: header != null ? Text(header) : null, actions: actions, source: TestDataSource(allowSelection: true), 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 { final TestDataSource source = TestDataSource(); await tester.pumpWidget(MaterialApp( home: MediaQuery( data: const MediaQueryData( textScaleFactor: 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 { final TestDataSource source = TestDataSource(); 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.pump(); 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 { final TestDataSource source = TestDataSource(); 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 double defaultHorizontalMargin = 24.0; const double defaultColumnSpacing = 56.0; const double customHorizontalMargin = 10.0; const double 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 TestDataSource source = TestDataSource(allowSelection: true); 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 double defaultHorizontalMargin = 24.0; const double defaultColumnSpacing = 56.0; const double customHorizontalMargin = 10.0; const double customColumnSpacing = 15.0; final TestDataSource source = TestDataSource(); 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 { final TestDataSource source = TestDataSource(); // 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)); Widget buildTable(bool checkbox) => MaterialApp( home: PaginatedDataTable( header: const Text('Test table'), source: TestDataSource(allowSelection: true), 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() { return MaterialApp( theme: ThemeData.light().copyWith( dataTableTheme: const DataTableThemeData( decoration: BoxDecoration(color: Colors.white), ), ), home: PaginatedDataTable( header: const Text('Test table'), source: TestDataSource(allowSelection: true), 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('PaginatedDataTable custom checkboxHorizontalMargin properly applied', (WidgetTester tester) async { const double customCheckboxHorizontalMargin = 15.0; const double 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 TestDataSource source = TestDataSource(allowSelection: true); 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 Color selectedTextColor = Color(0xff00ddff); final ColorScheme colors = const ColorScheme.light().copyWith(secondary: selectedTextColor); final ThemeData theme = ThemeData.from(colorScheme: colors); Widget buildTable() { return MaterialApp( theme: theme, home: PaginatedDataTable( header: const Text('Test table'), source: TestDataSource(allowSelection: true), 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 Color arrowHeadColor = Color(0xFFE53935); await tester.pumpWidget( MaterialApp( home: PaginatedDataTable( arrowHeadColor: arrowHeadColor, showFirstLastButtons: true, header: const Text('Test table'), source: TestDataSource(), columns: const <DataColumn>[ DataColumn(label: Text('Name')), DataColumn(label: Text('Calories'), numeric: true), DataColumn(label: Text('Generation')), ], ), ) ); final Iterable<Icon> icons = tester.widgetList(find.byType(Icon)); 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: TestDataSource(), 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 TestDataSource source = TestDataSource(); final ScrollController scrollController = ScrollController(); 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 ScrollController primaryScrollController = ScrollController(); final TestDataSource source = TestDataSource(); 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); }); }