// 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. @TestOn('!chrome') library; import 'dart:math' as math; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:vector_math/vector_math_64.dart' show Matrix3; import '../foundation/leak_tracking.dart'; import 'data_table_test_utils.dart'; void main() { testWidgetsWithLeakTracking('DataTable control test', (WidgetTester tester) async { final List<String> log = <String>[]; Widget buildTable({ int? sortColumnIndex, bool sortAscending = true }) { return DataTable( sortColumnIndex: sortColumnIndex, sortAscending: sortAscending, onSelectAll: (bool? value) { log.add('select-all: $value'); }, 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'); }, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) { log.add('row-selected: ${dessert.name}'); }, onLongPress: () { log.add('onLongPress: ${dessert.name}'); }, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () { log.add('cell-tap: ${dessert.calories}'); }, onDoubleTap: () { log.add('cell-doubleTap: ${dessert.calories}'); }, onLongPress: () { log.add('cell-longPress: ${dessert.calories}'); }, onTapCancel: () { log.add('cell-tapCancel: ${dessert.calories}'); }, onTapDown: (TapDownDetails details) { log.add('cell-tapDown: ${dessert.calories}'); }, ), ], ); }).toList(), ); } await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); await tester.tap(find.byType(Checkbox).first); expect(log, <String>['select-all: true']); log.clear(); await tester.tap(find.text('Cupcake')); expect(log, <String>['row-selected: Cupcake']); log.clear(); await tester.longPress(find.text('Cupcake')); expect(log, <String>['onLongPress: Cupcake']); log.clear(); await tester.tap(find.text('Calories')); expect(log, <String>['column-sort: 1 true']); log.clear(); await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(sortColumnIndex: 1)), )); await tester.pumpAndSettle(const Duration(milliseconds: 200)); await tester.tap(find.text('Calories')); expect(log, <String>['column-sort: 1 false']); log.clear(); await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(sortColumnIndex: 1, sortAscending: false)), )); await tester.pumpAndSettle(const Duration(milliseconds: 200)); await tester.tap(find.text('375')); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.text('375')); expect(log, <String>['cell-doubleTap: 375']); log.clear(); await tester.longPress(find.text('375')); // The tap down is triggered on gesture down. // Then, the cancel is triggered when the gesture arena // recognizes that the long press overrides the tap event // so it triggers a tap cancel, followed by the long press. expect(log,<String>['cell-tapDown: 375' ,'cell-tapCancel: 375', 'cell-longPress: 375']); log.clear(); TestGesture gesture = await tester.startGesture( tester.getRect(find.text('375')).center, ); await tester.pump(const Duration(milliseconds: 100)); // onTapDown callback is registered. expect(log, equals(<String>['cell-tapDown: 375'])); await gesture.up(); await tester.pump(const Duration(seconds: 1)); // onTap callback is registered after the gesture is removed. expect(log, equals(<String>['cell-tapDown: 375', 'cell-tap: 375'])); log.clear(); // dragging off the bounds of the cell calls the cancel callback gesture = await tester.startGesture(tester.getRect(find.text('375')).center); await tester.pump(const Duration(milliseconds: 100)); await gesture.moveBy(const Offset(0.0, 200.0)); await gesture.cancel(); expect(log, equals(<String>['cell-tapDown: 375', 'cell-tapCancel: 375'])); log.clear(); await tester.tap(find.byType(Checkbox).last); expect(log, <String>['row-selected: KitKat']); log.clear(); }); testWidgetsWithLeakTracking('DataTable control test - tristate', (WidgetTester tester) async { final List<String> log = <String>[]; const int numItems = 3; Widget buildTable(List<bool> selected, {int? disabledIndex}) { return DataTable( onSelectAll: (bool? value) { log.add('select-all: $value'); }, columns: const <DataColumn>[ DataColumn( label: Text('Name'), tooltip: 'Name', ), ], rows: List<DataRow>.generate( numItems, (int index) => DataRow( cells: <DataCell>[DataCell(Text('Row $index'))], selected: selected[index], onSelectChanged: index == disabledIndex ? null : (bool? value) { log.add('row-selected: $index'); }, ), ), ); } // Tapping the parent checkbox when no rows are selected, selects all. await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(<bool>[false, false, false])), )); await tester.tap(find.byType(Checkbox).first); expect(log, <String>['select-all: true']); log.clear(); // Tapping the parent checkbox when some rows are selected, selects all. await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(<bool>[true, false, true])), )); await tester.tap(find.byType(Checkbox).first); expect(log, <String>['select-all: true']); log.clear(); // Tapping the parent checkbox when all rows are selected, deselects all. await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(<bool>[true, true, true])), )); await tester.tap(find.byType(Checkbox).first); expect(log, <String>['select-all: false']); log.clear(); // Tapping the parent checkbox when all rows are selected and one is // disabled, deselects all. await tester.pumpWidget(MaterialApp( home: Material( child: buildTable( <bool>[true, true, false], disabledIndex: 2, ), ), )); await tester.tap(find.byType(Checkbox).first); expect(log, <String>['select-all: false']); log.clear(); }); testWidgetsWithLeakTracking('DataTable control test - no checkboxes', (WidgetTester tester) async { final List<String> log = <String>[]; Widget buildTable({ bool checkboxes = false }) { return DataTable( showCheckboxColumn: checkboxes, onSelectAll: (bool? value) { log.add('select-all: $value'); }, columns: const <DataColumn>[ DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: Text('Calories'), tooltip: 'Calories', numeric: true, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) { log.add('row-selected: ${dessert.name}'); }, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () { log.add('cell-tap: ${dessert.calories}'); }, ), ], ); }).toList(), ); } await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); expect(find.byType(Checkbox), findsNothing); await tester.tap(find.text('Cupcake')); expect(log, <String>['row-selected: Cupcake']); log.clear(); await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(checkboxes: true)), )); await tester.pumpAndSettle(const Duration(milliseconds: 200)); final Finder checkboxes = find.byType(Checkbox); expect(checkboxes, findsNWidgets(11)); await tester.tap(checkboxes.first); expect(log, <String>['select-all: true']); log.clear(); }); testWidgetsWithLeakTracking('DataTable overflow test - header', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( headingTextStyle: const TextStyle( fontSize: 14.0, letterSpacing: 0.0, // Will overflow if letter spacing is larger than 0.0. ), columns: <DataColumn>[ DataColumn( label: Text('X' * 2000), ), ], rows: const <DataRow>[ DataRow( cells: <DataCell>[ DataCell( Text('X'), ), ], ), ], ), ), ), ); expect(tester.renderObject<RenderBox>(find.byType(Text).first).size.width, greaterThan(800.0)); expect(tester.renderObject<RenderBox>(find.byType(Row).first).size.width, greaterThan(800.0)); expect(tester.takeException(), isNull); // column overflows table, but text doesn't overflow cell }); testWidgetsWithLeakTracking('DataTable overflow test - header with spaces', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: <DataColumn>[ DataColumn( label: Text('X ' * 2000), // has soft wrap points, but they should be ignored ), ], rows: const <DataRow>[ DataRow( cells: <DataCell>[ DataCell( Text('X'), ), ], ), ], ), ), ), ); expect(tester.renderObject<RenderBox>(find.byType(Text).first).size.width, greaterThan(800.0)); expect(tester.renderObject<RenderBox>(find.byType(Row).first).size.width, greaterThan(800.0)); expect(tester.takeException(), isNull); // column overflows table, but text doesn't overflow cell }, skip: true); // https://github.com/flutter/flutter/issues/13512 testWidgetsWithLeakTracking('DataTable overflow test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('X'), ), ], rows: <DataRow>[ DataRow( cells: <DataCell>[ DataCell( Text('X' * 2000), ), ], ), ], ), ), ), ); expect(tester.renderObject<RenderBox>(find.byType(Text).first).size.width, lessThan(800.0)); expect(tester.renderObject<RenderBox>(find.byType(Row).first).size.width, greaterThan(800.0)); expect(tester.takeException(), isNull); // cell overflows table, but text doesn't overflow cell }); testWidgetsWithLeakTracking('DataTable overflow test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('X'), ), ], rows: <DataRow>[ DataRow( cells: <DataCell>[ DataCell( Text('X ' * 2000), // wraps ), ], ), ], ), ), ), ); expect(tester.renderObject<RenderBox>(find.byType(Text).first).size.width, lessThan(800.0)); expect(tester.renderObject<RenderBox>(find.byType(Row).first).size.width, lessThan(800.0)); expect(tester.takeException(), isNull); }); testWidgetsWithLeakTracking('DataTable column onSort test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('Dessert'), ), ], rows: const <DataRow>[ DataRow( cells: <DataCell>[ DataCell( Text('Lollipop'), // wraps ), ], ), ], ), ), ), ); await tester.tap(find.text('Dessert')); await tester.pump(); expect(tester.takeException(), isNull); }); testWidgetsWithLeakTracking('DataTable sort indicator orientation', (WidgetTester tester) async { Widget buildTable({ bool sortAscending = true }) { return DataTable( sortColumnIndex: 0, sortAscending: sortAscending, columns: <DataColumn>[ DataColumn( label: const Text('Name'), tooltip: 'Name', onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( cells: <DataCell>[ DataCell( Text(dessert.name), ), ], ); }).toList(), ); } // Check for ascending list await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); // The `tester.widget` ensures that there is exactly one upward arrow. final Finder iconFinder = find.descendant( of: find.byType(DataTable), matching: find.widgetWithIcon(Transform, Icons.arrow_upward), ); Transform transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), equals(Matrix3.identity()), ); // Check for descending list. await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(sortAscending: false)), )); await tester.pumpAndSettle(); // The `tester.widget` ensures that there is exactly one upward arrow. transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), equals(Matrix3.rotationZ(math.pi)), ); }); testWidgetsWithLeakTracking('DataTable sort indicator orientation does not change on state update', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/43724 Widget buildTable({String title = 'Name1'}) { return DataTable( sortColumnIndex: 0, columns: <DataColumn>[ DataColumn( label: Text(title), tooltip: 'Name', onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( cells: <DataCell>[ DataCell( Text(dessert.name), ), ], ); }).toList(), ); } // Check for ascending list await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); // The `tester.widget` ensures that there is exactly one upward arrow. final Finder iconFinder = find.descendant( of: find.byType(DataTable), matching: find.widgetWithIcon(Transform, Icons.arrow_upward), ); Transform transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), equals(Matrix3.identity()), ); // Cause a rebuild by updating the widget await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(title: 'Name2')), )); await tester.pumpAndSettle(); // The `tester.widget` ensures that there is exactly one upward arrow. transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), equals(Matrix3.identity()), // Should not have changed ); }); testWidgetsWithLeakTracking('DataTable sort indicator orientation does not change on state update - reverse', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/43724 Widget buildTable({String title = 'Name1'}) { return DataTable( sortColumnIndex: 0, sortAscending: false, columns: <DataColumn>[ DataColumn( label: Text(title), tooltip: 'Name', onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( cells: <DataCell>[ DataCell( Text(dessert.name), ), ], ); }).toList(), ); } // Check for ascending list await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); // The `tester.widget` ensures that there is exactly one upward arrow. final Finder iconFinder = find.descendant( of: find.byType(DataTable), matching: find.widgetWithIcon(Transform, Icons.arrow_upward), ); Transform transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), equals(Matrix3.rotationZ(math.pi)), ); // Cause a rebuild by updating the widget await tester.pumpWidget(MaterialApp( home: Material(child: buildTable(title: 'Name2')), )); await tester.pumpAndSettle(); // The `tester.widget` ensures that there is exactly one upward arrow. transformOfArrow = tester.widget<Transform>(iconFinder); expect( transformOfArrow.transform.getRotation(), equals(Matrix3.rotationZ(math.pi)), // Should not have changed ); }); testWidgetsWithLeakTracking('DataTable row onSelectChanged test', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('Dessert'), ), ], rows: const <DataRow>[ DataRow( cells: <DataCell>[ DataCell( Text('Lollipop'), // wraps ), ], ), ], ), ), ), ); await tester.tap(find.text('Lollipop')); await tester.pump(); expect(tester.takeException(), isNull); }); testWidgetsWithLeakTracking('DataTable custom row height', (WidgetTester tester) async { Widget buildCustomTable({ int? sortColumnIndex, bool sortAscending = true, double? dataRowMinHeight, double? dataRowMaxHeight, double headingRowHeight = 56.0, }) { return DataTable( sortColumnIndex: sortColumnIndex, sortAscending: sortAscending, onSelectAll: (bool? value) {}, dataRowMinHeight: dataRowMinHeight, dataRowMaxHeight: dataRowMaxHeight, headingRowHeight: headingRowHeight, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) {}, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ); } // DEFAULT VALUES await tester.pumpWidget(MaterialApp( home: Material( child: DataTable( onSelectAll: (bool? value) {}, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) {}, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ), ), )); // The finder matches with the Container of the cell content, as well as the // Container wrapping the whole table. The first one is used to test row // heights. Finder findFirstContainerFor(String text) => find.widgetWithText(Container, text).first; expect(tester.getSize(findFirstContainerFor('Name')).height, 56.0); expect(tester.getSize(findFirstContainerFor('Frozen yogurt')).height, kMinInteractiveDimension); // CUSTOM VALUES await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable(headingRowHeight: 48.0)), )); expect(tester.getSize(findFirstContainerFor('Name')).height, 48.0); await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable(headingRowHeight: 64.0)), )); expect(tester.getSize(findFirstContainerFor('Name')).height, 64.0); await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable(dataRowMinHeight: 30.0, dataRowMaxHeight: 30.0)), )); expect(tester.getSize(findFirstContainerFor('Frozen yogurt')).height, 30.0); await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable(dataRowMinHeight: 0.0, dataRowMaxHeight: double.infinity)), )); expect(tester.getSize(findFirstContainerFor('Frozen yogurt')).height, greaterThan(0.0)); }); testWidgetsWithLeakTracking('DataTable custom row height one row taller than others', (WidgetTester tester) async { const String multilineText = 'Line one.\nLine two.\nLine three.\nLine four.'; Widget buildCustomTable({ double? dataRowMinHeight, double? dataRowMaxHeight, }) { return DataTable( dataRowMinHeight: dataRowMinHeight, dataRowMaxHeight: dataRowMaxHeight, columns: const <DataColumn>[ DataColumn( label: Text('SingleRowColumn'), ), DataColumn( label: Text('MultiRowColumn'), ), ], rows: const <DataRow>[ DataRow(cells: <DataCell>[ DataCell(Text('Data')), DataCell(Column(children: <Widget>[ Text(multilineText), ])), ]), ], ); } Finder findFirstContainerFor(String text) => find.widgetWithText(Container, text).first; await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable(dataRowMinHeight: 0.0, dataRowMaxHeight: double.infinity)), )); final double singleLineRowHeight = tester.getSize(findFirstContainerFor('Data')).height; final double multilineRowHeight = tester.getSize(findFirstContainerFor(multilineText)).height; expect(multilineRowHeight, greaterThan(singleLineRowHeight)); }); testWidgetsWithLeakTracking('DataTable custom row height - separate test for deprecated dataRowHeight', (WidgetTester tester) async { Widget buildCustomTable({ double dataRowHeight = 48.0, }) { return DataTable( onSelectAll: (bool? value) {}, dataRowHeight: dataRowHeight, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) {}, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ); } // The finder matches with the Container of the cell content, as well as the // Container wrapping the whole table. The first one is used to test row // heights. Finder findFirstContainerFor(String text) => find.widgetWithText(Container, text).first; // CUSTOM VALUES await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable(dataRowHeight: 30.0)), )); expect(tester.getSize(findFirstContainerFor('Frozen yogurt')).height, 30.0); }); testWidgetsWithLeakTracking('DataTable 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; Finder cellContent; Finder checkbox; Finder padding; Widget buildDefaultTable({ int? sortColumnIndex, bool sortAscending = true, }) { return DataTable( sortColumnIndex: sortColumnIndex, sortAscending: sortAscending, onSelectAll: (bool? value) {}, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), DataColumn( label: const Text('Fat'), tooltip: 'Fat', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) {}, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), DataCell( Text('${dessert.fat}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ); } // DEFAULT VALUES await tester.pumpWidget(MaterialApp( home: Material(child: buildDefaultTable()), )); // default checkbox padding checkbox = find.byType(Checkbox).first; padding = find.ancestor(of: checkbox, matching: find.byType(Padding)); 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'); cellContent = find.widgetWithText(Align, 'Frozen yogurt'); // 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'); 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, '6.0'); cellContent = find.widgetWithText(Align, '6.0'); expect( tester.getRect(cellContent).left - tester.getRect(padding).left, defaultColumnSpacing / 2, ); expect( tester.getRect(padding).right - tester.getRect(cellContent).right, defaultHorizontalMargin, ); Widget buildCustomTable({ int? sortColumnIndex, bool sortAscending = true, double? horizontalMargin, double? columnSpacing, }) { return DataTable( sortColumnIndex: sortColumnIndex, sortAscending: sortAscending, onSelectAll: (bool? value) {}, horizontalMargin: horizontalMargin, columnSpacing: columnSpacing, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), DataColumn( label: const Text('Fat'), tooltip: 'Fat', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) {}, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), DataCell( Text('${dessert.fat}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ); } // CUSTOM VALUES await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable( horizontalMargin: customHorizontalMargin, columnSpacing: customColumnSpacing, )), )); // custom checkbox padding checkbox = find.byType(Checkbox).first; padding = find.ancestor(of: checkbox, matching: find.byType(Padding)); 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').first; cellContent = find.widgetWithText(Align, 'Frozen yogurt'); // 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'); 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, '6.0'); cellContent = find.widgetWithText(Align, '6.0'); expect( tester.getRect(cellContent).left - tester.getRect(padding).left, customColumnSpacing / 2, ); expect( tester.getRect(padding).right - tester.getRect(cellContent).right, customHorizontalMargin, ); }); testWidgetsWithLeakTracking('DataTable 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; Finder cellContent; Finder padding; Widget buildDefaultTable({ int? sortColumnIndex, bool sortAscending = true, }) { return DataTable( sortColumnIndex: sortColumnIndex, sortAscending: sortAscending, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), DataColumn( label: const Text('Fat'), tooltip: 'Fat', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), DataCell( Text('${dessert.fat}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ); } // DEFAULT VALUES await tester.pumpWidget(MaterialApp( home: Material(child: buildDefaultTable()), )); // default first column padding padding = find.widgetWithText(Padding, 'Frozen yogurt'); cellContent = find.widgetWithText(Align, 'Frozen yogurt'); // 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'); 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, '6.0'); cellContent = find.widgetWithText(Align, '6.0'); expect( tester.getRect(cellContent).left - tester.getRect(padding).left, defaultColumnSpacing / 2, ); expect( tester.getRect(padding).right - tester.getRect(cellContent).right, defaultHorizontalMargin, ); Widget buildCustomTable({ int? sortColumnIndex, bool sortAscending = true, double? horizontalMargin, double? columnSpacing, }) { return DataTable( sortColumnIndex: sortColumnIndex, sortAscending: sortAscending, horizontalMargin: horizontalMargin, columnSpacing: columnSpacing, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), DataColumn( label: const Text('Fat'), tooltip: 'Fat', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), DataCell( Text('${dessert.fat}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ); } // CUSTOM VALUES await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable( horizontalMargin: customHorizontalMargin, columnSpacing: customColumnSpacing, )), )); // custom first column padding padding = find.widgetWithText(Padding, 'Frozen yogurt'); cellContent = find.widgetWithText(Align, 'Frozen yogurt'); // DataTable wraps its DataCells in an Align widget 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'); 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, '6.0'); cellContent = find.widgetWithText(Align, '6.0'); expect( tester.getRect(cellContent).left - tester.getRect(padding).left, customColumnSpacing / 2, ); expect( tester.getRect(padding).right - tester.getRect(cellContent).right, customHorizontalMargin, ); }); testWidgetsWithLeakTracking('DataTable set border width test', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), ]; const List<DataCell> cells = <DataCell>[ DataCell(Text('cell1')), DataCell(Text('cell2')), ]; const List<DataRow> rows = <DataRow>[ DataRow(cells: cells), DataRow(cells: cells), ]; // no thickness provided - border should be default: i.e "1.0" as it // set in DataTable constructor await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: columns, rows: rows, ), ), ), ); Table table = tester.widget(find.byType(Table)); TableRow tableRow = table.children.last; BoxDecoration boxDecoration = tableRow.decoration! as BoxDecoration; expect(boxDecoration.border!.top.width, 1.0); const double thickness = 4.2; await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( dividerThickness: thickness, columns: columns, rows: rows, ), ), ), ); table = tester.widget(find.byType(Table)); tableRow = table.children.last; boxDecoration = tableRow.decoration! as BoxDecoration; expect(boxDecoration.border!.top.width, thickness); }); testWidgetsWithLeakTracking('DataTable set show bottom border', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), ]; const List<DataCell> cells = <DataCell>[ DataCell(Text('cell1')), DataCell(Text('cell2')), ]; const List<DataRow> rows = <DataRow>[ DataRow(cells: cells), DataRow(cells: cells), ]; await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( showBottomBorder: true, columns: columns, rows: rows, ), ), ), ); Table table = tester.widget(find.byType(Table)); TableRow tableRow = table.children.last; BoxDecoration boxDecoration = tableRow.decoration! as BoxDecoration; expect(boxDecoration.border!.bottom.width, 1.0); await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: columns, rows: rows, ), ), ), ); table = tester.widget(find.byType(Table)); tableRow = table.children.last; boxDecoration = tableRow.decoration! as BoxDecoration; expect(boxDecoration.border!.bottom.width, 0.0); }); testWidgetsWithLeakTracking('DataTable column heading cell - with and without sorting', (WidgetTester tester) async { Widget buildTable({ int? sortColumnIndex, bool sortEnabled = true }) { return DataTable( sortColumnIndex: sortColumnIndex, columns: <DataColumn>[ DataColumn( label: const Expanded(child: Center(child: Text('Name'))), tooltip: 'Name', onSort: sortEnabled ? (_, __) {} : null, ), ], rows: const <DataRow>[ DataRow( cells: <DataCell>[ DataCell(Text('A long desert name')), ], ), ], ); } // Start with without sorting await tester.pumpWidget(MaterialApp( home: Material(child: buildTable( sortEnabled: false, )), )); { final Finder nameText = find.text('Name'); expect(nameText, findsOneWidget); final Finder nameCell = find.ancestor(of: find.text('Name'), matching: find.byType(Container)).first; expect(tester.getCenter(nameText), equals(tester.getCenter(nameCell))); expect(find.descendant(of: nameCell, matching: find.byType(Icon)), findsNothing); } // Turn on sorting await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); { final Finder nameText = find.text('Name'); expect(nameText, findsOneWidget); final Finder nameCell = find.ancestor(of: find.text('Name'), matching: find.byType(Container)).first; expect(find.descendant(of: nameCell, matching: find.byType(Icon)), findsOneWidget); } // Turn off sorting again await tester.pumpWidget(MaterialApp( home: Material(child: buildTable( sortEnabled: false, )), )); { final Finder nameText = find.text('Name'); expect(nameText, findsOneWidget); final Finder nameCell = find.ancestor(of: find.text('Name'), matching: find.byType(Container)).first; expect(tester.getCenter(nameText), equals(tester.getCenter(nameCell))); expect(find.descendant(of: nameCell, matching: find.byType(Icon)), findsNothing); } }); testWidgetsWithLeakTracking('DataTable correctly renders with a mouse', (WidgetTester tester) async { // Regression test for a bug described in // https://github.com/flutter/flutter/pull/43735#issuecomment-589459947 // Filed at https://github.com/flutter/flutter/issues/51152 Widget buildTable({ int? sortColumnIndex }) { return DataTable( sortColumnIndex: sortColumnIndex, columns: <DataColumn>[ const DataColumn( label: Expanded(child: Center(child: Text('column1'))), tooltip: 'Column1', ), DataColumn( label: const Expanded(child: Center(child: Text('column2'))), tooltip: 'Column2', onSort: (_, __) {}, ), ], rows: const <DataRow>[ DataRow( cells: <DataCell>[ DataCell(Text('Content1')), DataCell(Text('Content2')), ], ), ], ); } await tester.pumpWidget(MaterialApp( home: Material(child: buildTable()), )); expect(tester.renderObject(find.text('column1')).attached, true); expect(tester.renderObject(find.text('column2')).attached, true); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); await tester.pumpAndSettle(); expect(tester.renderObject(find.text('column1')).attached, true); expect(tester.renderObject(find.text('column2')).attached, true); // Wait for the tooltip timer to expire to prevent it scheduling a new frame // after the view is destroyed, which causes exceptions. await tester.pumpAndSettle(const Duration(seconds: 1)); }); testWidgetsWithLeakTracking('DataRow renders default selected row colors', (WidgetTester tester) async { final ThemeData themeData = ThemeData.light(); Widget buildTable({bool selected = false}) { return MaterialApp( theme: themeData, home: Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('Column1'), ), ], rows: <DataRow>[ DataRow( onSelectChanged: (bool? checked) {}, selected: selected, cells: const <DataCell>[ DataCell(Text('Content1')), ], ), ], ), ), ); } BoxDecoration lastTableRowBoxDecoration() { final Table table = tester.widget(find.byType(Table)); final TableRow tableRow = table.children.last; return tableRow.decoration! as BoxDecoration; } await tester.pumpWidget(buildTable()); expect(lastTableRowBoxDecoration().color, null); await tester.pumpWidget(buildTable(selected: true)); expect( lastTableRowBoxDecoration().color, themeData.colorScheme.primary.withOpacity(0.08), ); }); testWidgetsWithLeakTracking('DataRow renders checkbox with colors from CheckboxTheme', (WidgetTester tester) async { const Color fillColor = Color(0xFF00FF00); const Color checkColor = Color(0xFF0000FF); final ThemeData themeData = ThemeData( checkboxTheme: const CheckboxThemeData( fillColor: MaterialStatePropertyAll<Color?>(fillColor), checkColor: MaterialStatePropertyAll<Color?>(checkColor), ), ); Widget buildTable() { return MaterialApp( theme: themeData, home: Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('Column1'), ), ], rows: <DataRow>[ DataRow( selected: true, onSelectChanged: (bool? checked) {}, cells: const <DataCell>[ DataCell(Text('Content1')), ], ), ], ), ), ); } await tester.pumpWidget(buildTable()); expect( Material.of(tester.element(find.byType(Checkbox).last)), paints ..path() ..path(color: fillColor) ..path(color: checkColor), ); }); testWidgetsWithLeakTracking('DataRow renders custom colors when selected', (WidgetTester tester) async { const Color selectedColor = Colors.green; const Color defaultColor = Colors.red; Widget buildTable({bool selected = false}) { return Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('Column1'), ), ], rows: <DataRow>[ DataRow( selected: selected, color: MaterialStateProperty.resolveWith<Color>( (Set<MaterialState> states) { if (states.contains(MaterialState.selected)) { return selectedColor; } return defaultColor; }, ), cells: const <DataCell>[ DataCell(Text('Content1')), ], ), ], ), ); } BoxDecoration lastTableRowBoxDecoration() { final Table table = tester.widget(find.byType(Table)); final TableRow tableRow = table.children.last; return tableRow.decoration! as BoxDecoration; } await tester.pumpWidget(MaterialApp( home: buildTable(), )); expect(lastTableRowBoxDecoration().color, defaultColor); await tester.pumpWidget(MaterialApp( home: buildTable(selected: true), )); expect(lastTableRowBoxDecoration().color, selectedColor); }); testWidgetsWithLeakTracking('DataRow renders custom colors when disabled', (WidgetTester tester) async { const Color disabledColor = Colors.grey; const Color defaultColor = Colors.red; Widget buildTable({bool disabled = false}) { return Material( child: DataTable( columns: const <DataColumn>[ DataColumn( label: Text('Column1'), ), ], rows: <DataRow>[ DataRow( cells: const <DataCell>[ DataCell(Text('Content1')), ], onSelectChanged: (bool? value) {}, ), DataRow( color: MaterialStateProperty.resolveWith<Color>( (Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return disabledColor; } return defaultColor; }, ), cells: const <DataCell>[ DataCell(Text('Content2')), ], onSelectChanged: disabled ? null : (bool? value) {}, ), ], ), ); } BoxDecoration lastTableRowBoxDecoration() { final Table table = tester.widget(find.byType(Table)); final TableRow tableRow = table.children.last; return tableRow.decoration! as BoxDecoration; } await tester.pumpWidget(MaterialApp( home: buildTable(), )); expect(lastTableRowBoxDecoration().color, defaultColor); await tester.pumpWidget(MaterialApp( home: buildTable(disabled: true), )); expect(lastTableRowBoxDecoration().color, disabledColor); }); testWidgetsWithLeakTracking('DataRow renders custom colors when pressed', (WidgetTester tester) async { const Color pressedColor = Color(0xff4caf50); Widget buildTable() { return DataTable( columns: const <DataColumn>[ DataColumn( label: Text('Column1'), ), ], rows: <DataRow>[ DataRow( color: MaterialStateProperty.resolveWith<Color>( (Set<MaterialState> states) { if (states.contains(MaterialState.pressed)) { return pressedColor; } return Colors.transparent; }, ), onSelectChanged: (bool? value) {}, cells: const <DataCell>[ DataCell(Text('Content1')), ], ), ], ); } await tester.pumpWidget(MaterialApp( theme: ThemeData(useMaterial3: false), home: Material(child: buildTable()), )); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Content1'))); await tester.pump(const Duration(milliseconds: 200)); // splash is well underway final RenderBox box = Material.of(tester.element(find.byType(InkWell)))as RenderBox; expect(box, paints..circle(x: 68.0, y: 24.0, color: pressedColor)); await gesture.up(); }); testWidgetsWithLeakTracking('DataTable can render inside an AlertDialog', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: AlertDialog( content: DataTable( columns: const <DataColumn>[ DataColumn(label: Text('Col1')), ], rows: const <DataRow>[ DataRow(cells: <DataCell>[DataCell(Text('1'))]), ], ), scrollable: true, ), ), ), ); expect(tester.takeException(), isNull); }); testWidgetsWithLeakTracking('DataTable renders with border and background decoration', (WidgetTester tester) async { const double width = 800; const double height = 600; const double borderHorizontal = 5.0; const double borderVertical = 10.0; const Color borderColor = Color(0xff2196f3); const Color backgroundColor = Color(0xfff5f5f5); await tester.pumpWidget( MaterialApp( home: DataTable( decoration: const BoxDecoration( color: backgroundColor, border: Border.symmetric( vertical: BorderSide(width: borderVertical, color: borderColor), horizontal: BorderSide(width: borderHorizontal, color: borderColor), ), ), columns: const <DataColumn>[ DataColumn(label: Text('Col1')), ], rows: const <DataRow>[ DataRow(cells: <DataCell>[DataCell(Text('1'))]), ], ), ), ); expect( find.ancestor(of: find.byType(Table), matching: find.byType(Container)), paints..rect( rect: const Rect.fromLTRB(0.0, 0.0, width, height), color: backgroundColor, ), ); expect( find.ancestor(of: find.byType(Table), matching: find.byType(Container)), paints..drrect(color: borderColor), ); expect( tester.getTopLeft(find.byType(Table)), const Offset(borderVertical, borderHorizontal), ); expect( tester.getBottomRight(find.byType(Table)), const Offset(width - borderVertical, height - borderHorizontal), ); }); testWidgetsWithLeakTracking('checkboxHorizontalMargin properly applied', (WidgetTester tester) async { const double customCheckboxHorizontalMargin = 15.0; const double customHorizontalMargin = 10.0; Finder cellContent; Finder checkbox; Finder padding; Widget buildCustomTable({ int? sortColumnIndex, bool sortAscending = true, double? horizontalMargin, double? checkboxHorizontalMargin, }) { return DataTable( sortColumnIndex: sortColumnIndex, sortAscending: sortAscending, onSelectAll: (bool? value) {}, horizontalMargin: horizontalMargin, checkboxHorizontalMargin: checkboxHorizontalMargin, columns: <DataColumn>[ const DataColumn( label: Text('Name'), tooltip: 'Name', ), DataColumn( label: const Text('Calories'), tooltip: 'Calories', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), DataColumn( label: const Text('Fat'), tooltip: 'Fat', numeric: true, onSort: (int columnIndex, bool ascending) {}, ), ], rows: kDesserts.map<DataRow>((Dessert dessert) { return DataRow( key: ValueKey<String>(dessert.name), onSelectChanged: (bool? selected) {}, cells: <DataCell>[ DataCell( Text(dessert.name), ), DataCell( Text('${dessert.calories}'), showEditIcon: true, onTap: () {}, ), DataCell( Text('${dessert.fat}'), showEditIcon: true, onTap: () {}, ), ], ); }).toList(), ); } await tester.pumpWidget(MaterialApp( home: Material(child: buildCustomTable( checkboxHorizontalMargin: customCheckboxHorizontalMargin, horizontalMargin: customHorizontalMargin, )), )); // Custom checkbox padding. checkbox = find.byType(Checkbox).first; padding = find.ancestor(of: checkbox, matching: find.byType(Padding)); expect( tester.getRect(checkbox).left - tester.getRect(padding).left, customCheckboxHorizontalMargin, ); expect( tester.getRect(padding).right - tester.getRect(checkbox).right, customCheckboxHorizontalMargin, ); // First column padding. padding = find.widgetWithText(Padding, 'Frozen yogurt').first; cellContent = find.widgetWithText(Align, 'Frozen yogurt'); // DataTable wraps its DataCells in an Align widget. expect( tester.getRect(cellContent).left - tester.getRect(padding).left, customHorizontalMargin, ); }); testWidgetsWithLeakTracking('DataRow is disabled when onSelectChanged is not set', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: const <DataColumn>[ DataColumn(label: Text('Col1')), DataColumn(label: Text('Col2')), ], rows: <DataRow>[ DataRow(cells: const <DataCell>[ DataCell(Text('Hello')), DataCell(Text('world')), ], onSelectChanged: (bool? value) {}, ), const DataRow(cells: <DataCell>[ DataCell(Text('Bug')), DataCell(Text('report')), ]), const DataRow(cells: <DataCell>[ DataCell(Text('GitHub')), DataCell(Text('issue')), ]), ], ), ), ), ); expect(find.widgetWithText(TableRowInkWell, 'Hello'), findsOneWidget); expect(find.widgetWithText(TableRowInkWell, 'Bug'), findsNothing); expect(find.widgetWithText(TableRowInkWell, 'GitHub'), findsNothing); }); testWidgetsWithLeakTracking('DataTable set interior border test', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), ]; const List<DataCell> cells = <DataCell>[ DataCell(Text('cell1')), DataCell(Text('cell2')), ]; const List<DataRow> rows = <DataRow>[ DataRow(cells: cells), DataRow(cells: cells), ]; await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( border: TableBorder.all(width: 2, color: Colors.red), columns: columns, rows: rows, ), ), ), ); final Finder finder = find.byType(DataTable); expect(tester.getSize(finder), equals(const Size(800, 600))); await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( border: TableBorder.all(color: Colors.red), columns: columns, rows: rows, ), ), ), ); Table table = tester.widget(find.byType(Table)); TableBorder? tableBorder = table.border; expect(tableBorder!.top.color, Colors.red); expect(tableBorder.bottom.width, 1); await tester.pumpWidget( MaterialApp( home: Material( child: DataTable( columns: columns, rows: rows, ), ), ), ); table = tester.widget(find.byType(Table)); tableBorder = table.border; expect(tableBorder?.bottom.width, null); expect(tableBorder?.top.color, null); }); // Regression test for https://github.com/flutter/flutter/issues/100952 testWidgetsWithLeakTracking('Do not crashes when paint borders in a narrow space', (WidgetTester tester) async { const List<DataColumn> columns = <DataColumn>[ DataColumn(label: Text('column1')), DataColumn(label: Text('column2')), ]; const List<DataCell> cells = <DataCell>[ DataCell(Text('cell1')), DataCell(Text('cell2')), ]; const List<DataRow> rows = <DataRow>[ DataRow(cells: cells), DataRow(cells: cells), ]; await tester.pumpWidget( MaterialApp( home: Material( child: Center( child: SizedBox( width: 117.0, child: DataTable( border: TableBorder.all(width: 2, color: Colors.red), columns: columns, rows: rows, ), ), ), ), ), ); // Go without crashes. }); testWidgetsWithLeakTracking('DataTable clip behavior', (WidgetTester tester) async { const Color selectedColor = Colors.green; const Color defaultColor = Colors.red; const BorderRadius borderRadius = BorderRadius.all(Radius.circular(30)); Widget buildTable({bool selected = false, required Clip clipBehavior}) { return Material( child: DataTable( clipBehavior: clipBehavior, border: TableBorder.all(borderRadius: borderRadius), columns: const <DataColumn>[ DataColumn( label: Text('Column1'), ), ], rows: <DataRow>[ DataRow( selected: selected, color: MaterialStateProperty.resolveWith<Color>( (Set<MaterialState> states) { if (states.contains(MaterialState.selected)) { return selectedColor; } return defaultColor; }, ), cells: const <DataCell>[ DataCell(Text('Content1')), ], ), ], ), ); } // Test default clip behavior. await tester.pumpWidget(MaterialApp(home: buildTable(clipBehavior: Clip.none))); Material material = tester.widget<Material>(find.byType(Material).last); expect(material.clipBehavior, Clip.none); expect(material.borderRadius, borderRadius); await tester.pumpWidget(MaterialApp(home: buildTable(clipBehavior: Clip.hardEdge))); material = tester.widget<Material>(find.byType(Material).last); expect(material.clipBehavior, Clip.hardEdge); expect(material.borderRadius, borderRadius); }); testWidgetsWithLeakTracking('DataTable dataRowMinHeight smaller or equal dataRowMaxHeight validation', (WidgetTester tester) async { DataTable createDataTable() => DataTable( columns: const <DataColumn>[DataColumn(label: Text('Column1'))], rows: const <DataRow>[], dataRowMinHeight: 2.0, dataRowMaxHeight: 1.0, ); expect(() => createDataTable(), throwsA(predicate((AssertionError e) => e.toString().contains('dataRowMaxHeight >= dataRowMinHeight')))); }); testWidgetsWithLeakTracking('DataTable dataRowHeight is not used together with dataRowMinHeight or dataRowMaxHeight', (WidgetTester tester) async { DataTable createDataTable({double? dataRowHeight, double? dataRowMinHeight, double? dataRowMaxHeight}) => DataTable( columns: const <DataColumn>[DataColumn(label: Text('Column1'))], rows: const <DataRow>[], dataRowHeight: dataRowHeight, dataRowMinHeight: dataRowMinHeight, dataRowMaxHeight: dataRowMaxHeight, ); expect(() => createDataTable(dataRowHeight: 1.0, dataRowMinHeight: 2.0, dataRowMaxHeight: 2.0), throwsA(predicate((AssertionError e) => e.toString().contains('dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null)')))); expect(() => createDataTable(dataRowHeight: 1.0, dataRowMaxHeight: 2.0), throwsA(predicate((AssertionError e) => e.toString().contains('dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null)')))); expect(() => createDataTable(dataRowHeight: 1.0, dataRowMinHeight: 2.0), throwsA(predicate((AssertionError e) => e.toString().contains('dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null)')))); }); group('TableRowInkWell', () { testWidgetsWithLeakTracking('can handle secondary taps', (WidgetTester tester) async { bool secondaryTapped = false; bool secondaryTappedDown = false; await tester.pumpWidget(MaterialApp( home: Material( child: Table( children: <TableRow>[ TableRow( children: <Widget>[ TableRowInkWell( onSecondaryTap: () { secondaryTapped = true; }, onSecondaryTapDown: (TapDownDetails details) { secondaryTappedDown = true; }, child: const SizedBox( width: 100.0, height: 100.0, ), ), ], ), ], ), ), )); expect(secondaryTapped, isFalse); expect(secondaryTappedDown, isFalse); expect(find.byType(TableRowInkWell), findsOneWidget); await tester.tap( find.byType(TableRowInkWell), buttons: kSecondaryMouseButton, ); await tester.pumpAndSettle(); expect(secondaryTapped, isTrue); expect(secondaryTappedDown, isTrue); }); }); testWidgetsWithLeakTracking('Heading cell cursor resolves MaterialStateMouseCursor correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DataTable( sortColumnIndex: 0, columns: <DataColumn>[ // This column can be sorted. DataColumn( mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return SystemMouseCursors.forbidden; } return SystemMouseCursors.copy; }), onSort: (int columnIndex, bool ascending) {}, label: const Text('A'), ), // This column cannot be sorted. DataColumn( mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.disabled)) { return SystemMouseCursors.forbidden; } return SystemMouseCursors.copy; }), label: const Text('B'), ), ], rows: const <DataRow>[ DataRow( cells: <DataCell>[ DataCell(Text('Data 1')), DataCell(Text('Data 2')), ], ), DataRow( cells: <DataCell>[ DataCell(Text('Data 3')), DataCell(Text('Data 4')), ], ), ], ), ), ), ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: tester.getCenter(find.text('A'))); await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.copy); await gesture.moveTo(tester.getCenter(find.text('B'))); await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); }); testWidgetsWithLeakTracking('DataRow cursor resolves MaterialStateMouseCursor correctly', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DataTable( sortColumnIndex: 0, columns: <DataColumn>[ DataColumn( label: const Text('A'), onSort: (int columnIndex, bool ascending) {}, ), const DataColumn(label: Text('B')), ], rows: <DataRow>[ // This row can be selected. DataRow( mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.selected)) { return SystemMouseCursors.copy; } return SystemMouseCursors.forbidden; }), onSelectChanged: (bool? selected) {}, cells: const <DataCell>[ DataCell(Text('Data 1')), DataCell(Text('Data 2')), ], ), // This row is selected. DataRow( selected: true, onSelectChanged: (bool? selected) {}, mouseCursor: MaterialStateProperty.resolveWith((Set<MaterialState> states) { if (states.contains(MaterialState.selected)) { return SystemMouseCursors.copy; } return SystemMouseCursors.forbidden; }), cells: const <DataCell>[ DataCell(Text('Data 3')), DataCell(Text('Data 4')), ], ), ], ), ), ), ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: tester.getCenter(find.text('Data 1'))); await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); await gesture.moveTo(tester.getCenter(find.text('Data 3'))); await tester.pump(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.copy); }); testWidgetsWithLeakTracking("DataRow cursor doesn't update checkbox cursor", (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: DataTable( sortColumnIndex: 0, columns: <DataColumn>[ DataColumn( label: const Text('A'), onSort: (int columnIndex, bool ascending) {}, ), const DataColumn(label: Text('B')), ], rows: <DataRow>[ DataRow( onSelectChanged: (bool? selected) {}, mouseCursor: const MaterialStatePropertyAll<MouseCursor>(SystemMouseCursors.copy), cells: const <DataCell>[ DataCell(Text('Data')), DataCell(Text('Data 2')), ], ), ], ), ), ), ); final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); await gesture.addPointer(location: tester.getCenter(find.byType(Checkbox).last)); await tester.pump(); // Test that the checkbox cursor is not changed. expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); await gesture.moveTo(tester.getCenter(find.text('Data'))); await tester.pump(); // Test that cursor is updated for the row. expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.copy); }); }