From ccdf82646620da3e84788826adf8d7c36cb68760 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 14 Aug 2023 17:55:07 -0700 Subject: [PATCH] PaginatedDataTable improvements (#131374) - slightly improved assert message when row cell counts don't match column count. - more breadcrumbs in API documentation. more documentation in general. - added more documentation for the direction of the "ascending" arrow. - two samples for PaginatedDataTable. - make PaginatedDataTable support hot reloading across changes to the number of columns. - introduce matrix3MoreOrLessEquals. An earlier version of this PR used it in tests, but eventually it was not needed. The function seems useful to keep though. --- .../paginated_data_table.0.dart | 86 +++++ .../paginated_data_table.1.dart | 307 ++++++++++++++++++ .../paginated_data_table.0_test.dart | 13 + .../paginated_data_table.1_test.dart | 23 ++ .../flutter/lib/src/material/data_table.dart | 6 +- .../lib/src/material/data_table_source.dart | 23 +- .../src/material/paginated_data_table.dart | 56 +++- .../test/material/data_table_test.dart | 6 +- packages/flutter_test/lib/src/matchers.dart | 23 ++ packages/flutter_test/test/matchers_test.dart | 30 ++ 10 files changed, 555 insertions(+), 18 deletions(-) create mode 100644 examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart create mode 100644 examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart create mode 100644 examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart create mode 100644 examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart diff --git a/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart b/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart new file mode 100644 index 0000000000..8249d5e62f --- /dev/null +++ b/examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart @@ -0,0 +1,86 @@ +// 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/material.dart'; + +/// Flutter code sample for [PaginatedDataTable]. + +class MyDataSource extends DataTableSource { + @override + int get rowCount => 3; + + @override + DataRow? getRow(int index) { + switch (index) { + case 0: return const DataRow( + cells: [ + DataCell(Text('Sarah')), + DataCell(Text('19')), + DataCell(Text('Student')), + ], + ); + case 1: return const DataRow( + cells: [ + DataCell(Text('Janine')), + DataCell(Text('43')), + DataCell(Text('Professor')), + ], + ); + case 2: return const DataRow( + cells: [ + DataCell(Text('William')), + DataCell(Text('27')), + DataCell(Text('Associate Professor')), + ], + ); + default: return null; + } + } + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => 0; +} + +final DataTableSource dataSource = MyDataSource(); + +void main() => runApp(const DataTableExampleApp()); + +class DataTableExampleApp extends StatelessWidget { + const DataTableExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: SingleChildScrollView( + padding: EdgeInsets.all(12.0), + child: DataTableExample(), + ), + ); + } +} + +class DataTableExample extends StatelessWidget { + const DataTableExample({super.key}); + + @override + Widget build(BuildContext context) { + return PaginatedDataTable( + columns: const [ + DataColumn( + label: Text('Name'), + ), + DataColumn( + label: Text('Age'), + ), + DataColumn( + label: Text('Role'), + ), + ], + source: dataSource, + ); + } +} diff --git a/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart b/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart new file mode 100644 index 0000000000..bc4eff6a93 --- /dev/null +++ b/examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart @@ -0,0 +1,307 @@ +// 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/material.dart'; + +/// Flutter code sample for [PaginatedDataTable]. + +class MyDataSource extends DataTableSource { + static const List _displayIndexToRawIndex = [ 0, 3, 4, 5, 6 ]; + + late List>> sortedData; + void setData(List>> rawData, int sortColumn, bool sortAscending) { + sortedData = rawData.toList()..sort((List> a, List> b) { + final Comparable cellA = a[_displayIndexToRawIndex[sortColumn]]; + final Comparable cellB = b[_displayIndexToRawIndex[sortColumn]]; + return cellA.compareTo(cellB) * (sortAscending ? 1 : -1); + }); + notifyListeners(); + } + + @override + int get rowCount => sortedData.length; + + static DataCell cellFor(Object data) { + String value; + if (data is DateTime) { + value = '${data.year}-${data.month.toString().padLeft(2, '0')}-${data.day.toString().padLeft(2, '0')}'; + } else { + value = data.toString(); + } + return DataCell(Text(value)); + } + + @override + DataRow? getRow(int index) { + return DataRow.byIndex( + index: sortedData[index][0] as int, + cells: [ + cellFor('S${sortedData[index][1]}E${sortedData[index][2].toString().padLeft(2, '0')}'), + cellFor(sortedData[index][3]), + cellFor(sortedData[index][4]), + cellFor(sortedData[index][5]), + cellFor(sortedData[index][6]), + ], + ); + } + + @override + bool get isRowCountApproximate => false; + + @override + int get selectedRowCount => 0; +} + +void main() => runApp(const DataTableExampleApp()); + +class DataTableExampleApp extends StatelessWidget { + const DataTableExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: SingleChildScrollView( + padding: EdgeInsets.all(12.0), + child: DataTableExample(), + ), + ); + } +} + +class DataTableExample extends StatefulWidget { + const DataTableExample({super.key}); + + @override + State createState() => _DataTableExampleState(); +} + +class _DataTableExampleState extends State { + final MyDataSource dataSource = MyDataSource() + ..setData(episodes, 0, true); + + int _columnIndex = 0; + bool _columnAscending = true; + + void _sort(int columnIndex, bool ascending) { + setState(() { + _columnIndex = columnIndex; + _columnAscending = ascending; + dataSource.setData(episodes, _columnIndex, _columnAscending); + }); + } + + @override + Widget build(BuildContext context) { + return PaginatedDataTable( + sortColumnIndex: _columnIndex, + sortAscending: _columnAscending, + columns: [ + DataColumn( + label: const Text('Episode'), + onSort: _sort, + ), + DataColumn( + label: const Text('Title'), + onSort: _sort, + ), + DataColumn( + label: const Text('Director'), + onSort: _sort, + ), + DataColumn( + label: const Text('Writer(s)'), + onSort: _sort, + ), + DataColumn( + label: const Text('Air Date'), + onSort: _sort, + ), + ], + source: dataSource, + ); + } +} + +final List>> episodes = >>[ + >[ + 1, + 1, + 1, + 'Strange New Worlds', + 'Akiva Goldsman', + 'Akiva Goldsman, Alex Kurtzman, Jenny Lumet', + DateTime(2022, 5, 5), + ], + >[ + 2, + 1, + 2, + 'Children of the Comet', + 'Maja Vrvilo', + 'Henry Alonso Myers, Sarah Tarkoff', + DateTime(2022, 5, 12), + ], + >[ + 3, + 1, + 3, + 'Ghosts of Illyria', + 'Leslie Hope', + 'Akela Cooper, Bill Wolkoff', + DateTime(2022, 5, 19), + ], + >[ + 4, + 1, + 4, + 'Memento Mori', + 'Dan Liu', + 'Davy Perez, Beau DeMayo', + DateTime(2022, 5, 26), + ], + >[ + 5, + 1, + 5, + 'Spock Amok', + 'Rachel Leiterman', + 'Henry Alonso Myers, Robin Wasserman', + DateTime(2022, 6, 2), + ], + >[ + 6, + 1, + 6, + 'Lift Us Where Suffering Cannot Reach', + 'Andi Armaganian', + 'Robin Wasserman, Bill Wolkoff', + DateTime(2022, 6, 9), + ], + >[ + 7, + 1, + 7, + 'The Serene Squall', + 'Sydney Freeland', + 'Beau DeMayo, Sarah Tarkoff', + DateTime(2022, 6, 16), + ], + >[ + 8, + 1, + 8, + 'The Elysian Kingdom', + 'Amanda Row', + 'Akela Cooper, Onitra Johnson', + DateTime(2022, 6, 23), + ], + >[ + 9, + 1, + 9, + 'All Those Who Wander', + 'Christopher J. Byrne', + 'Davy Perez', + DateTime(2022, 6, 30), + ], + >[ + 10, + 2, + 10, + 'A Quality of Mercy', + 'Chris Fisher', + 'Henry Alonso Myers, Akiva Goldsman', + DateTime(2022, 7, 7), + ], + >[ + 11, + 2, + 1, + 'The Broken Circle', + 'Chris Fisher', + 'Henry Alonso Myers, Akiva Goldsman', + DateTime(2023, 6, 15), + ], + >[ + 12, + 2, + 2, + 'Ad Astra per Aspera', + 'Valerie Weiss', + 'Dana Horgan', + DateTime(2023, 6, 22), + ], + >[ + 13, + 2, + 3, + 'Tomorrow and Tomorrow and Tomorrow', + 'Amanda Row', + 'David Reed', + DateTime(2023, 6, 29), + ], + >[ + 14, + 2, + 4, + 'Among the Lotus Eaters', + 'Eduardo Sánchez', + 'Kirsten Beyer, Davy Perez', + DateTime(2023, 7, 6), + ], + >[ + 15, + 2, + 5, + 'Charades', + 'Jordan Canning', + 'Kathryn Lyn, Henry Alonso Myers', + DateTime(2023, 7, 13), + ], + >[ + 16, + 2, + 6, + 'Lost in Translation', + 'Dan Liu', + 'Onitra Johnson, David Reed', + DateTime(2023, 7, 20), + ], + >[ + 17, + 2, + 7, + 'Those Old Scientists', + 'Jonathan Frakes', + 'Kathryn Lyn, Bill Wolkoff', + DateTime(2023, 7, 22), + ], + >[ + 18, + 2, + 8, + 'Under the Cloak of War', + '', + 'Davy Perez', + DateTime(2023, 7, 27), + ], + >[ + 19, + 2, + 9, + 'Subspace Rhapsody', + '', + 'Dana Horgan, Bill Wolkoff', + DateTime(2023, 8, 3), + ], + >[ + 20, + 2, + 10, + 'Hegemony', + '', + 'Henry Alonso Myers', + DateTime(2023, 8, 10), + ], +]; diff --git a/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart b/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart new file mode 100644 index 0000000000..31084fc263 --- /dev/null +++ b/examples/api/test/material/paginated_data_table/paginated_data_table.0_test.dart @@ -0,0 +1,13 @@ +// 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_api_samples/material/paginated_data_table/paginated_data_table.0.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('PaginatedDataTable 0', (WidgetTester tester) async { + await tester.pumpWidget(const example.DataTableExampleApp()); + expect(find.text('Associate Professor'), findsOneWidget); + }); +} diff --git a/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart b/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart new file mode 100644 index 0000000000..7a3bb91685 --- /dev/null +++ b/examples/api/test/material/paginated_data_table/paginated_data_table.1_test.dart @@ -0,0 +1,23 @@ +// 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/material.dart'; +import 'package:flutter_api_samples/material/paginated_data_table/paginated_data_table.1.dart' as example; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('PaginatedDataTable 1', (WidgetTester tester) async { + await tester.pumpWidget(const example.DataTableExampleApp()); + expect(find.text('Strange New Worlds'), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_upward).at(1)); + await tester.pump(); + expect(find.text('Strange New Worlds'), findsNothing); + await tester.tap(find.byIcon(Icons.chevron_right)); + await tester.pump(); + expect(find.text('Strange New Worlds'), findsOneWidget); + await tester.tap(find.byIcon(Icons.arrow_upward).at(1)); + await tester.pump(); + expect(find.text('Strange New Worlds'), findsNothing); + }); +} diff --git a/packages/flutter/lib/src/material/data_table.dart b/packages/flutter/lib/src/material/data_table.dart index b1c01ce207..a7ed565f3a 100644 --- a/packages/flutter/lib/src/material/data_table.dart +++ b/packages/flutter/lib/src/material/data_table.dart @@ -444,7 +444,7 @@ class DataTable extends StatelessWidget { this.clipBehavior = Clip.none, }) : assert(columns.isNotEmpty), assert(sortColumnIndex == null || (sortColumnIndex >= 0 && sortColumnIndex < columns.length)), - assert(!rows.any((DataRow row) => row.cells.length != columns.length)), + assert(!rows.any((DataRow row) => row.cells.length != columns.length), 'All rows must have the same number of cells as there are header cells (${columns.length})'), assert(dividerThickness == null || dividerThickness >= 0), assert(dataRowMinHeight == null || dataRowMaxHeight == null || dataRowMaxHeight >= dataRowMinHeight), assert(dataRowHeight == null || (dataRowMinHeight == null && dataRowMaxHeight == null), @@ -467,6 +467,8 @@ class DataTable extends StatelessWidget { /// /// When this is null, it implies that the table's sort order does /// not correspond to any of the columns. + /// + /// The direction of the sort is specified using [sortAscending]. final int? sortColumnIndex; /// Whether the column mentioned in [sortColumnIndex], if any, is sorted @@ -479,6 +481,8 @@ class DataTable extends StatelessWidget { /// If false, the order is descending (meaning the rows with the /// smallest values for the current sort column are last in the /// table). + /// + /// Ascending order is represented by an upwards-facing arrow. final bool sortAscending; /// Invoked when the user selects or unselects every row, using the diff --git a/packages/flutter/lib/src/material/data_table_source.dart b/packages/flutter/lib/src/material/data_table_source.dart index dd7e8b9feb..7b51441f96 100644 --- a/packages/flutter/lib/src/material/data_table_source.dart +++ b/packages/flutter/lib/src/material/data_table_source.dart @@ -18,21 +18,32 @@ import 'data_table.dart'; /// /// DataTableSource objects are expected to be long-lived, not recreated with /// each build. +/// +/// If a [DataTableSource] is used with a [PaginatedDataTable] that supports +/// sortable columns (see [DataColumn.onSort] and +/// [PaginatedDataTable.sortColumnIndex]), the rows reported by the data source +/// must be reported in the sorted order. abstract class DataTableSource extends ChangeNotifier { /// Called to obtain the data about a particular row. /// + /// Rows should be keyed so that state can be maintained when the data source + /// is sorted (e.g. in response to [DataColumn.onSort]). Keys should be + /// consistent for a given [DataRow] regardless of the sort order (i.e. the + /// key represents the data's identity, not the row position). + /// /// The [DataRow.byIndex] constructor provides a convenient way to construct - /// [DataRow] objects for this callback's purposes without having to worry about - /// independently keying each row. + /// [DataRow] objects for this method's purposes without having to worry about + /// independently keying each row. The index passed to that constructor is the + /// index of the underlying data, which is different than the `index` + /// parameter for [getRow], which represents the _sorted_ position. /// /// If the given index does not correspond to a row, or if no data is yet /// available for a row, then return null. The row will be left blank and a /// loading indicator will be displayed over the table. Once data is available /// or once it is firmly established that the row index in question is beyond - /// the end of the table, call [notifyListeners]. + /// the end of the table, call [notifyListeners]. (See [rowCount].) /// - /// Data returned from this method must be consistent for the lifetime of the - /// object. If the row count changes, then a new delegate must be provided. + /// If the underlying data changes, call [notifyListeners]. DataRow? getRow(int index); /// Called to obtain the number of rows to tell the user are available. @@ -58,5 +69,7 @@ abstract class DataTableSource extends ChangeNotifier { /// Called to obtain the number of rows that are currently selected. /// /// If the selected row count changes, call [notifyListeners]. + /// + /// Selected rows are those whose [DataRow.selected] property is set to true. int get selectedRowCount; } diff --git a/packages/flutter/lib/src/material/paginated_data_table.dart b/packages/flutter/lib/src/material/paginated_data_table.dart index ef85796455..f30cff9903 100644 --- a/packages/flutter/lib/src/material/paginated_data_table.dart +++ b/packages/flutter/lib/src/material/paginated_data_table.dart @@ -31,6 +31,23 @@ import 'theme.dart'; /// If the [key] is a [PageStorageKey], the [initialFirstRowIndex] is persisted /// to [PageStorage]. /// +/// {@tool dartpad} +/// +/// This sample shows how to display a [DataTable] with three columns: name, +/// age, and role. The columns are defined by three [DataColumn] objects. The +/// table contains three rows of data for three example users, the data for +/// which is defined by three [DataRow] objects. +/// +/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// +/// This example shows how paginated data tables can supported sorted data. +/// +/// ** See code in examples/api/lib/material/paginated_data_table/paginated_data_table.1.dart ** +/// {@end-tool} +/// /// See also: /// /// * [DataTable], which is not paginated. @@ -142,13 +159,15 @@ class PaginatedDataTable extends StatefulWidget { /// The current primary sort key's column. /// - /// See [DataTable.sortColumnIndex]. + /// See [DataTable.sortColumnIndex] for details. + /// + /// The direction of the sort is specified using [sortAscending]. final int? sortColumnIndex; /// Whether the column mentioned in [sortColumnIndex], if any, is sorted /// in ascending order. /// - /// See [DataTable.sortAscending]. + /// See [DataTable.sortAscending] for details. final bool sortAscending; /// Invoked when the user selects or unselects every row, using the @@ -297,10 +316,27 @@ class PaginatedDataTableState extends State { if (oldWidget.source != widget.source) { oldWidget.source.removeListener(_handleDataSourceChanged); widget.source.addListener(_handleDataSourceChanged); - _handleDataSourceChanged(); + _updateCaches(); } } + @override + void reassemble() { + super.reassemble(); + // This function is called during hot reload. + // + // Normally, if the data source changes, it would notify its listeners and + // thus trigger _handleDataSourceChanged(), which clears the row cache and + // causes the widget to rebuild. + // + // During a hot reload, though, a data source can change in ways that will + // invalidate the row cache (e.g. adding or removing columns) without ever + // triggering a notification, leaving the PaginatedDataTable in an invalid + // state. This method handles this case by clearing the cache any time the + // widget is involved in a hot reload. + _updateCaches(); + } + @override void dispose() { widget.source.removeListener(_handleDataSourceChanged); @@ -308,12 +344,14 @@ class PaginatedDataTableState extends State { } void _handleDataSourceChanged() { - setState(() { - _rowCount = widget.source.rowCount; - _rowCountApproximate = widget.source.isRowCountApproximate; - _selectedRowCount = widget.source.selectedRowCount; - _rows.clear(); - }); + setState(_updateCaches); + } + + void _updateCaches() { + _rowCount = widget.source.rowCount; + _rowCountApproximate = widget.source.isRowCountApproximate; + _selectedRowCount = widget.source.selectedRowCount; + _rows.clear(); } /// Ensures that the given row is visible. diff --git a/packages/flutter/test/material/data_table_test.dart b/packages/flutter/test/material/data_table_test.dart index 8b71d9eca4..2863d8a606 100644 --- a/packages/flutter/test/material/data_table_test.dart +++ b/packages/flutter/test/material/data_table_test.dart @@ -469,11 +469,11 @@ void main() { 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), ); + // The `tester.widget` ensures that there is exactly one upward arrow. Transform transformOfArrow = tester.widget(iconFinder); expect( transformOfArrow.transform.getRotation(), @@ -521,11 +521,11 @@ void main() { 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), ); + // The `tester.widget` ensures that there is exactly one upward arrow. Transform transformOfArrow = tester.widget(iconFinder); expect( transformOfArrow.transform.getRotation(), @@ -574,11 +574,11 @@ void main() { 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), ); + // The `tester.widget` ensures that there is exactly one upward arrow. Transform transformOfArrow = tester.widget(iconFinder); expect( transformOfArrow.transform.getRotation(), diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 54d02bf540..9742411bee 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -12,6 +12,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:matcher/expect.dart'; import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports +import 'package:vector_math/vector_math_64.dart' show Matrix3; import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage; import 'accessibility.dart'; @@ -345,10 +346,24 @@ Matcher rectMoreOrLessEquals(Rect value, { double epsilon = precisionErrorTolera /// /// * [moreOrLessEquals], which is for [double]s. /// * [offsetMoreOrLessEquals], which is for [Offset]s. +/// * [matrix3MoreOrLessEquals], which is for [Matrix3]s. Matcher matrixMoreOrLessEquals(Matrix4 value, { double epsilon = precisionErrorTolerance }) { return _IsWithinDistance(_matrixDistance, value, epsilon); } +/// Asserts that two [Matrix3]s are equal, within some tolerated error. +/// +/// {@macro flutter.flutter_test.moreOrLessEquals} +/// +/// See also: +/// +/// * [moreOrLessEquals], which is for [double]s. +/// * [offsetMoreOrLessEquals], which is for [Offset]s. +/// * [matrixMoreOrLessEquals], which is for [Matrix4]s. +Matcher matrix3MoreOrLessEquals(Matrix3 value, { double epsilon = precisionErrorTolerance }) { + return _IsWithinDistance(_matrix3Distance, value, epsilon); +} + /// Asserts that two [Offset]s are equal, within some tolerated error. /// /// {@macro flutter.flutter_test.moreOrLessEquals} @@ -1443,6 +1458,14 @@ double _matrixDistance(Matrix4 a, Matrix4 b) { return delta; } +double _matrix3Distance(Matrix3 a, Matrix3 b) { + double delta = 0.0; + for (int i = 0; i < 9; i += 1) { + delta = math.max((a[i] - b[i]).abs(), delta); + } + return delta; +} + double _sizeDistance(Size a, Size b) { // TODO(a14n): remove ignore when lint is updated, https://github.com/dart-lang/linter/issues/1843 // ignore: unnecessary_parenthesis diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 7a5aa13fa5..7e37125c80 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -10,6 +10,7 @@ import 'dart:typed_data'; import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:vector_math/vector_math_64.dart' show Matrix3; /// Class that makes it easy to mock common toStringDeep behavior. class _MockToStringDeep { @@ -251,6 +252,35 @@ void main() { ); }); + test('matrix3MoreOrLessEquals', () { + expect( + Matrix3.rotationZ(math.pi), + matrix3MoreOrLessEquals(Matrix3.fromList([ + -1, 0, 0, + 0, -1, 0, + 0, 0, 1, + ])) + ); + + expect( + Matrix3.rotationZ(math.pi), + matrix3MoreOrLessEquals(Matrix3.fromList([ + -2, 0, 0, + 0, -2, 0, + 0, 0, 1, + ]), epsilon: 2) + ); + + expect( + Matrix3.rotationZ(math.pi), + isNot(matrix3MoreOrLessEquals(Matrix3.fromList([ + -2, 0, 0, + 0, -2, 0, + 0, 0, 1, + ]))) + ); + }); + test('rectMoreOrLessEquals', () { expect( const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),