| // Copyright 2013 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 'dart:io' show Platform; |
| |
| import 'package:flutter/gestures.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart'; |
| |
| const TableSpan span = TableSpan(extent: FixedTableSpanExtent(100)); |
| const Widget cell = SizedBox.shrink(); |
| |
| TableSpan getTappableSpan(int index, VoidCallback callback) { |
| return TableSpan( |
| extent: const FixedTableSpanExtent(100), |
| recognizerFactories: <Type, GestureRecognizerFactory>{ |
| TapGestureRecognizer: |
| GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>( |
| () => TapGestureRecognizer(), |
| (TapGestureRecognizer t) => t.onTap = () => callback(), |
| ), |
| }, |
| ); |
| } |
| |
| TableSpan getMouseTrackingSpan( |
| int index, { |
| PointerEnterEventListener? onEnter, |
| PointerExitEventListener? onExit, |
| }) { |
| return TableSpan( |
| extent: const FixedTableSpanExtent(100), |
| onEnter: onEnter, |
| onExit: onExit, |
| cursor: SystemMouseCursors.cell, |
| ); |
| } |
| |
| final bool masterChannel = !Platform.environment.containsKey('CHANNEL') || |
| Platform.environment['CHANNEL'] == 'master'; |
| |
| // TODO(Piinks): Remove once painting can be validated by mock_canvas in |
| // flutter_test, and re-enable web tests in https://github.com/flutter/flutter/issues/132782 |
| // Regenerate goldens on a Mac computer by running `flutter test --update-goldens` |
| final bool runGoldens = Platform.isMacOS && masterChannel; |
| |
| void main() { |
| group('TableView.builder', () { |
| test('creates correct delegate', () { |
| final TableView tableView = TableView.builder( |
| columnCount: 3, |
| rowCount: 2, |
| rowBuilder: (_) => span, |
| columnBuilder: (_) => span, |
| cellBuilder: (_, __) => cell, |
| ); |
| final TableCellBuilderDelegate delegate = |
| tableView.delegate as TableCellBuilderDelegate; |
| expect(delegate.pinnedRowCount, 0); |
| expect(delegate.pinnedRowCount, 0); |
| expect(delegate.rowCount, 2); |
| expect(delegate.columnCount, 3); |
| expect(delegate.buildColumn(0), span); |
| expect(delegate.buildRow(0), span); |
| expect( |
| delegate.builder( |
| _NullBuildContext(), |
| const TableVicinity(row: 0, column: 0), |
| ), |
| cell, |
| ); |
| }); |
| |
| test('asserts correct counts', () { |
| TableView? tableView; |
| expect( |
| () { |
| tableView = TableView.builder( |
| cellBuilder: (_, __) => cell, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| columnCount: 1, |
| rowCount: 1, |
| pinnedColumnCount: -1, // asserts |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('pinnedColumnCount >= 0'), |
| ), |
| ), |
| ); |
| expect( |
| () { |
| tableView = TableView.builder( |
| cellBuilder: (_, __) => cell, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| columnCount: 1, |
| rowCount: 1, |
| pinnedRowCount: -1, // asserts |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('pinnedRowCount >= 0'), |
| ), |
| ), |
| ); |
| expect( |
| () { |
| tableView = TableView.builder( |
| cellBuilder: (_, __) => cell, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| columnCount: 1, |
| rowCount: -1, // asserts |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('rowCount >= 0'), |
| ), |
| ), |
| ); |
| expect( |
| () { |
| tableView = TableView.builder( |
| cellBuilder: (_, __) => cell, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| columnCount: -1, // asserts |
| rowCount: 1, |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('columnCount >= 0'), |
| ), |
| ), |
| ); |
| expect( |
| () { |
| tableView = TableView.builder( |
| cellBuilder: (_, __) => cell, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| columnCount: 1, |
| pinnedColumnCount: 2, // asserts |
| rowCount: 1, |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('columnCount >= pinnedColumnCount'), |
| ), |
| ), |
| ); |
| expect( |
| () { |
| tableView = TableView.builder( |
| cellBuilder: (_, __) => cell, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| columnCount: 1, |
| pinnedRowCount: 2, // asserts |
| rowCount: 1, |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('rowCount >= pinnedRowCount'), |
| ), |
| ), |
| ); |
| expect(tableView, isNull); |
| }); |
| }); |
| |
| group('TableView.list', () { |
| test('creates correct delegate', () { |
| final TableView tableView = TableView.list( |
| rowBuilder: (_) => span, |
| columnBuilder: (_) => span, |
| cells: const <List<Widget>>[ |
| <Widget>[cell, cell, cell], |
| <Widget>[cell, cell, cell] |
| ], |
| ); |
| final TableCellListDelegate delegate = |
| tableView.delegate as TableCellListDelegate; |
| expect(delegate.pinnedRowCount, 0); |
| expect(delegate.pinnedRowCount, 0); |
| expect(delegate.rowCount, 2); |
| expect(delegate.columnCount, 3); |
| expect(delegate.buildColumn(0), span); |
| expect(delegate.buildRow(0), span); |
| expect(delegate.children[0][0], cell); |
| }); |
| |
| test('asserts correct counts', () { |
| TableView? tableView; |
| expect( |
| () { |
| tableView = TableView.list( |
| cells: const <List<Widget>>[ |
| <Widget>[cell] |
| ], |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| pinnedColumnCount: -1, // asserts |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('pinnedColumnCount >= 0'), |
| ), |
| ), |
| ); |
| expect( |
| () { |
| tableView = TableView.list( |
| cells: const <List<Widget>>[ |
| <Widget>[cell] |
| ], |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| pinnedRowCount: -1, // asserts |
| ); |
| }, |
| throwsA( |
| isA<AssertionError>().having( |
| (AssertionError error) => error.toString(), |
| 'description', |
| contains('pinnedRowCount >= 0'), |
| ), |
| ), |
| ); |
| expect(tableView, isNull); |
| }); |
| }); |
| |
| group('RenderTableViewport', () { |
| testWidgets('parent data and table vicinities', |
| (WidgetTester tester) async { |
| final Map<TableVicinity, UniqueKey> childKeys = |
| <TableVicinity, UniqueKey>{}; |
| const TableSpan span = TableSpan(extent: FixedTableSpanExtent(200)); |
| final TableView tableView = TableView.builder( |
| rowCount: 5, |
| columnCount: 5, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| childKeys[vicinity] = childKeys[vicinity] ?? UniqueKey(); |
| return SizedBox.square(key: childKeys[vicinity], dimension: 200); |
| }, |
| ); |
| TableViewParentData parentDataOf(RenderBox child) { |
| return child.parentData! as TableViewParentData; |
| } |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| final RenderTwoDimensionalViewport viewport = getViewport( |
| tester, |
| childKeys.values.first, |
| ); |
| expect(viewport.mainAxis, Axis.vertical); |
| // first child |
| TableVicinity vicinity = const TableVicinity(column: 0, row: 0); |
| expect( |
| parentDataOf(viewport.firstChild!).vicinity, |
| vicinity, |
| ); |
| TableViewParentData parentData = parentDataOf( |
| tester.renderObject<RenderBox>(find.byKey(childKeys[vicinity]!)), |
| ); |
| expect(parentData.vicinity, vicinity); |
| expect(parentData.layoutOffset, Offset.zero); |
| expect(parentData.isVisible, isTrue); |
| // after first child |
| vicinity = const TableVicinity(column: 1, row: 0); |
| expect( |
| parentDataOf(viewport.childAfter(viewport.firstChild!)!).vicinity, |
| vicinity, |
| ); |
| parentData = parentDataOf( |
| tester.renderObject<RenderBox>(find.byKey(childKeys[vicinity]!)), |
| ); |
| expect(parentData.vicinity, vicinity); |
| expect(parentData.layoutOffset, const Offset(200, 0.0)); |
| expect(parentData.isVisible, isTrue); |
| // before first child (none) |
| expect( |
| viewport.childBefore(viewport.firstChild!), |
| isNull, |
| ); |
| |
| // last child |
| vicinity = const TableVicinity(column: 4, row: 4); |
| expect( |
| parentDataOf(viewport.lastChild!).vicinity, |
| vicinity, |
| ); |
| parentData = parentDataOf( |
| tester.renderObject<RenderBox>(find.byKey(childKeys[vicinity]!)), |
| ); |
| expect(parentData.vicinity, vicinity); |
| expect(parentData.layoutOffset, const Offset(800.0, 800.0)); |
| expect(parentData.isVisible, isFalse); |
| // after last child (none) |
| expect( |
| viewport.childAfter(viewport.lastChild!), |
| isNull, |
| ); |
| // before last child |
| vicinity = const TableVicinity(column: 3, row: 4); |
| expect( |
| parentDataOf(viewport.childBefore(viewport.lastChild!)!).vicinity, |
| vicinity, |
| ); |
| parentData = parentDataOf( |
| tester.renderObject<RenderBox>(find.byKey(childKeys[vicinity]!)), |
| ); |
| expect(parentData.vicinity, vicinity); |
| expect(parentData.layoutOffset, const Offset(600.0, 800.0)); |
| expect(parentData.isVisible, isFalse); |
| }); |
| |
| testWidgets('TableSpan gesture hit testing', (WidgetTester tester) async { |
| int tapCounter = 0; |
| // Rows |
| TableView tableView = TableView.builder( |
| rowCount: 50, |
| columnCount: 50, |
| columnBuilder: (_) => span, |
| rowBuilder: (int index) => index.isEven |
| ? getTappableSpan( |
| index, |
| () => tapCounter++, |
| ) |
| : span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| |
| // Even rows are set up for taps. |
| expect(tapCounter, 0); |
| // Tap along along a row |
| await tester.tap(find.text('Row: 0 Column: 0')); |
| await tester.tap(find.text('Row: 0 Column: 1')); |
| await tester.tap(find.text('Row: 0 Column: 2')); |
| await tester.tap(find.text('Row: 0 Column: 3')); |
| expect(tapCounter, 4); |
| // Tap along some odd rows |
| await tester.tap(find.text('Row: 1 Column: 0')); |
| await tester.tap(find.text('Row: 1 Column: 1')); |
| await tester.tap(find.text('Row: 3 Column: 2')); |
| await tester.tap(find.text('Row: 5 Column: 3')); |
| expect(tapCounter, 4); |
| // Check other even rows |
| await tester.tap(find.text('Row: 2 Column: 1')); |
| await tester.tap(find.text('Row: 2 Column: 2')); |
| await tester.tap(find.text('Row: 4 Column: 4')); |
| await tester.tap(find.text('Row: 4 Column: 5')); |
| expect(tapCounter, 8); |
| |
| // Columns |
| tapCounter = 0; |
| tableView = TableView.builder( |
| rowCount: 50, |
| columnCount: 50, |
| rowBuilder: (_) => span, |
| columnBuilder: (int index) => index.isEven |
| ? getTappableSpan( |
| index, |
| () => tapCounter++, |
| ) |
| : span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| |
| // Even columns are set up for taps. |
| expect(tapCounter, 0); |
| // Tap along along a column |
| await tester.tap(find.text('Row: 1 Column: 0')); |
| await tester.tap(find.text('Row: 2 Column: 0')); |
| await tester.tap(find.text('Row: 3 Column: 0')); |
| await tester.tap(find.text('Row: 4 Column: 0')); |
| expect(tapCounter, 4); |
| // Tap along some odd columns |
| await tester.tap(find.text('Row: 1 Column: 1')); |
| await tester.tap(find.text('Row: 2 Column: 1')); |
| await tester.tap(find.text('Row: 3 Column: 3')); |
| await tester.tap(find.text('Row: 4 Column: 3')); |
| expect(tapCounter, 4); |
| // Check other even columns |
| await tester.tap(find.text('Row: 2 Column: 2')); |
| await tester.tap(find.text('Row: 3 Column: 2')); |
| await tester.tap(find.text('Row: 4 Column: 4')); |
| await tester.tap(find.text('Row: 5 Column: 4')); |
| expect(tapCounter, 8); |
| |
| // Intersecting - main axis sets precedence |
| int rowTapCounter = 0; |
| int columnTapCounter = 0; |
| tableView = TableView.builder( |
| rowCount: 50, |
| columnCount: 50, |
| rowBuilder: (int index) => index.isEven |
| ? getTappableSpan( |
| index, |
| () => rowTapCounter++, |
| ) |
| : span, |
| columnBuilder: (int index) => index.isEven |
| ? getTappableSpan( |
| index, |
| () => columnTapCounter++, |
| ) |
| : span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| |
| // Even columns and rows are set up for taps, mainAxis is vertical by |
| // default, mening row major order. Rows should take precedence where they |
| // intersect at even indices. |
| expect(columnTapCounter, 0); |
| expect(rowTapCounter, 0); |
| // Tap where non intersecting, but even. |
| await tester.tap(find.text('Row: 2 Column: 3')); // Row |
| await tester.tap(find.text('Row: 4 Column: 5')); // Row |
| await tester.tap(find.text('Row: 3 Column: 2')); // Column |
| await tester.tap(find.text('Row: 1 Column: 6')); // Column |
| expect(columnTapCounter, 2); |
| expect(rowTapCounter, 2); |
| // Tap where both are odd and nothing should receive a tap. |
| await tester.tap(find.text('Row: 1 Column: 1')); |
| await tester.tap(find.text('Row: 3 Column: 1')); |
| await tester.tap(find.text('Row: 3 Column: 3')); |
| await tester.tap(find.text('Row: 5 Column: 3')); |
| expect(columnTapCounter, 2); |
| expect(rowTapCounter, 2); |
| // Check intersections. |
| await tester.tap(find.text('Row: 2 Column: 2')); |
| await tester.tap(find.text('Row: 4 Column: 2')); |
| await tester.tap(find.text('Row: 2 Column: 4')); |
| await tester.tap(find.text('Row: 4 Column: 4')); |
| expect(columnTapCounter, 2); |
| expect(rowTapCounter, 6); // Rows took precedence |
| |
| // Change mainAxis |
| rowTapCounter = 0; |
| columnTapCounter = 0; |
| tableView = TableView.builder( |
| mainAxis: Axis.horizontal, |
| rowCount: 50, |
| columnCount: 50, |
| rowBuilder: (int index) => index.isEven |
| ? getTappableSpan( |
| index, |
| () => rowTapCounter++, |
| ) |
| : span, |
| columnBuilder: (int index) => index.isEven |
| ? getTappableSpan( |
| index, |
| () => columnTapCounter++, |
| ) |
| : span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| expect(rowTapCounter, 0); |
| expect(columnTapCounter, 0); |
| |
| // Check intersections. |
| await tester.tap(find.text('Row: 2 Column: 2')); |
| await tester.tap(find.text('Row: 4 Column: 2')); |
| await tester.tap(find.text('Row: 2 Column: 4')); |
| await tester.tap(find.text('Row: 4 Column: 4')); |
| expect(columnTapCounter, 4); // Columns took precedence |
| expect(rowTapCounter, 0); |
| }); |
| |
| testWidgets('provides correct details in TableSpanExtentDelegate', |
| (WidgetTester tester) async { |
| final TestTableSpanExtent columnExtent = TestTableSpanExtent(); |
| final TestTableSpanExtent rowExtent = TestTableSpanExtent(); |
| final ScrollController verticalController = ScrollController(); |
| final ScrollController horizontalController = ScrollController(); |
| final TableView tableView = TableView.builder( |
| rowCount: 10, |
| columnCount: 10, |
| columnBuilder: (_) => TableSpan(extent: columnExtent), |
| rowBuilder: (_) => TableSpan(extent: rowExtent), |
| cellBuilder: (_, TableVicinity vicinity) { |
| return const SizedBox.square(dimension: 100); |
| }, |
| verticalDetails: ScrollableDetails.vertical( |
| controller: verticalController, |
| ), |
| horizontalDetails: ScrollableDetails.horizontal( |
| controller: horizontalController, |
| ), |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| expect(verticalController.position.pixels, 0.0); |
| expect(horizontalController.position.pixels, 0.0); |
| // Represents the last delegate provided to the last row and column |
| expect(columnExtent.delegate.precedingExtent, 900.0); |
| expect(columnExtent.delegate.viewportExtent, 800.0); |
| expect(rowExtent.delegate.precedingExtent, 900.0); |
| expect(rowExtent.delegate.viewportExtent, 600.0); |
| |
| verticalController.jumpTo(10.0); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 10.0); |
| expect(horizontalController.position.pixels, 0.0); |
| // Represents the last delegate provided to the last row and column |
| expect(columnExtent.delegate.precedingExtent, 900.0); |
| expect(columnExtent.delegate.viewportExtent, 800.0); |
| expect(rowExtent.delegate.precedingExtent, 900.0); |
| expect(rowExtent.delegate.viewportExtent, 600.0); |
| |
| horizontalController.jumpTo(10.0); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 10.0); |
| expect(horizontalController.position.pixels, 10.0); |
| // Represents the last delegate provided to the last row and column |
| expect(columnExtent.delegate.precedingExtent, 900.0); |
| expect(columnExtent.delegate.viewportExtent, 800.0); |
| expect(rowExtent.delegate.precedingExtent, 900.0); |
| expect(rowExtent.delegate.viewportExtent, 600.0); |
| }); |
| |
| testWidgets('regular layout - no pinning', (WidgetTester tester) async { |
| final ScrollController verticalController = ScrollController(); |
| final ScrollController horizontalController = ScrollController(); |
| final TableView tableView = TableView.builder( |
| rowCount: 50, |
| columnCount: 50, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| verticalDetails: ScrollableDetails.vertical( |
| controller: verticalController, |
| ), |
| horizontalDetails: ScrollableDetails.horizontal( |
| controller: horizontalController, |
| ), |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| expect(find.text('Row: 0 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 1'), findsOneWidget); |
| // Within the cacheExtent |
| expect(find.text('Row: 3 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 4 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 4'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 5'), findsOneWidget); |
| // Outside of the cacheExtent |
| expect(find.text('Row: 10 Column: 10'), findsNothing); |
| expect(find.text('Row: 11 Column: 10'), findsNothing); |
| expect(find.text('Row: 10 Column: 11'), findsNothing); |
| expect(find.text('Row: 11 Column: 11'), findsNothing); |
| |
| // Let's scroll! |
| verticalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 0.0); |
| expect(find.text('Row: 49 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 1'), findsOneWidget); |
| // Within the CacheExtent |
| expect(find.text('Row: 49 Column: 4'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 5'), findsOneWidget); |
| // Not around. |
| expect(find.text('Row: 0 Column: 0'), findsNothing); |
| expect(find.text('Row: 1 Column: 0'), findsNothing); |
| expect(find.text('Row: 0 Column: 1'), findsNothing); |
| expect(find.text('Row: 1 Column: 1'), findsNothing); |
| |
| // Let's scroll some more! |
| horizontalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 4400.0); |
| expect(find.text('Row: 49 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 48'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 48'), findsOneWidget); |
| // Nothing within the CacheExtent |
| expect(find.text('Row: 50 Column: 50'), findsNothing); |
| expect(find.text('Row: 51 Column: 51'), findsNothing); |
| // Not around. |
| expect(find.text('Row: 0 Column: 0'), findsNothing); |
| expect(find.text('Row: 1 Column: 0'), findsNothing); |
| expect(find.text('Row: 0 Column: 1'), findsNothing); |
| expect(find.text('Row: 1 Column: 1'), findsNothing); |
| }); |
| |
| testWidgets('pinned rows and columns', (WidgetTester tester) async { |
| // Just pinned rows |
| final ScrollController verticalController = ScrollController(); |
| final ScrollController horizontalController = ScrollController(); |
| TableView tableView = TableView.builder( |
| rowCount: 50, |
| pinnedRowCount: 1, |
| columnCount: 50, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| verticalDetails: ScrollableDetails.vertical( |
| controller: verticalController, |
| ), |
| horizontalDetails: ScrollableDetails.horizontal( |
| controller: horizontalController, |
| ), |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| expect(verticalController.position.pixels, 0.0); |
| expect(horizontalController.position.pixels, 0.0); |
| expect(find.text('Row: 0 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 1'), findsOneWidget); |
| // Within the cacheExtent |
| expect(find.text('Row: 6 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 7 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 8'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 8'), findsOneWidget); |
| // Outside of the cacheExtent |
| expect(find.text('Row: 10 Column: 10'), findsNothing); |
| expect(find.text('Row: 11 Column: 10'), findsNothing); |
| expect(find.text('Row: 10 Column: 11'), findsNothing); |
| expect(find.text('Row: 11 Column: 11'), findsNothing); |
| |
| // Let's scroll! |
| verticalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 0.0); |
| expect(find.text('Row: 49 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 1'), findsOneWidget); |
| // Within the CacheExtent |
| expect(find.text('Row: 49 Column: 8'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 9'), findsOneWidget); |
| // Not around unless pinned. |
| expect(find.text('Row: 0 Column: 0'), findsOneWidget); // Pinned row |
| expect(find.text('Row: 1 Column: 0'), findsNothing); |
| expect(find.text('Row: 0 Column: 1'), findsOneWidget); // Pinned row |
| expect(find.text('Row: 1 Column: 1'), findsNothing); |
| |
| // Let's scroll some more! |
| horizontalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 4400.0); |
| expect(find.text('Row: 49 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 48'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 48'), findsOneWidget); |
| // Nothing within the CacheExtent |
| expect(find.text('Row: 50 Column: 50'), findsNothing); |
| expect(find.text('Row: 51 Column: 51'), findsNothing); |
| // Not around unless pinned. |
| expect(find.text('Row: 0 Column: 49'), findsOneWidget); // Pinned row |
| expect(find.text('Row: 1 Column: 49'), findsNothing); |
| expect(find.text('Row: 0 Column: 48'), findsOneWidget); // Pinned row |
| expect(find.text('Row: 1 Column: 48'), findsNothing); |
| |
| // Just pinned columns |
| verticalController.jumpTo(0.0); |
| horizontalController.jumpTo(0.0); |
| tableView = TableView.builder( |
| rowCount: 50, |
| pinnedColumnCount: 1, |
| columnCount: 50, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| verticalDetails: ScrollableDetails.vertical( |
| controller: verticalController, |
| ), |
| horizontalDetails: ScrollableDetails.horizontal( |
| controller: horizontalController, |
| ), |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| expect(verticalController.position.pixels, 0.0); |
| expect(horizontalController.position.pixels, 0.0); |
| expect(find.text('Row: 0 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 1'), findsOneWidget); |
| // Within the cacheExtent |
| expect(find.text('Row: 6 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 7 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 8'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 9'), findsOneWidget); |
| // Outside of the cacheExtent |
| expect(find.text('Row: 10 Column: 10'), findsNothing); |
| expect(find.text('Row: 11 Column: 10'), findsNothing); |
| expect(find.text('Row: 10 Column: 11'), findsNothing); |
| expect(find.text('Row: 11 Column: 11'), findsNothing); |
| |
| // Let's scroll! |
| verticalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 0.0); |
| expect(find.text('Row: 49 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 1'), findsOneWidget); |
| // Within the CacheExtent |
| expect(find.text('Row: 49 Column: 8'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 9'), findsOneWidget); |
| // Not around unless pinned. |
| expect(find.text('Row: 49 Column: 0'), findsOneWidget); // Pinned column |
| expect(find.text('Row: 48 Column: 0'), findsOneWidget); // Pinned column |
| expect(find.text('Row: 0 Column: 1'), findsNothing); |
| expect(find.text('Row: 1 Column: 1'), findsNothing); |
| |
| // Let's scroll some more! |
| horizontalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 4400.0); |
| expect(find.text('Row: 49 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 48'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 48'), findsOneWidget); |
| // Nothing within the CacheExtent |
| expect(find.text('Row: 50 Column: 50'), findsNothing); |
| expect(find.text('Row: 51 Column: 51'), findsNothing); |
| // Not around. |
| expect(find.text('Row: 49 Column: 0'), findsOneWidget); // Pinned column |
| expect(find.text('Row: 48 Column: 0'), findsOneWidget); // Pinned column |
| expect(find.text('Row: 0 Column: 1'), findsNothing); |
| expect(find.text('Row: 1 Column: 1'), findsNothing); |
| |
| // Both |
| verticalController.jumpTo(0.0); |
| horizontalController.jumpTo(0.0); |
| tableView = TableView.builder( |
| rowCount: 50, |
| pinnedColumnCount: 1, |
| pinnedRowCount: 1, |
| columnCount: 50, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| verticalDetails: ScrollableDetails.vertical( |
| controller: verticalController, |
| ), |
| horizontalDetails: ScrollableDetails.horizontal( |
| controller: horizontalController, |
| ), |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| expect(verticalController.position.pixels, 0.0); |
| expect(horizontalController.position.pixels, 0.0); |
| expect(find.text('Row: 0 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 1'), findsOneWidget); |
| // Within the cacheExtent |
| expect(find.text('Row: 7 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 6 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 0 Column: 8'), findsOneWidget); |
| expect(find.text('Row: 1 Column: 9'), findsOneWidget); |
| // Outside of the cacheExtent |
| expect(find.text('Row: 10 Column: 10'), findsNothing); |
| expect(find.text('Row: 11 Column: 10'), findsNothing); |
| expect(find.text('Row: 10 Column: 11'), findsNothing); |
| expect(find.text('Row: 11 Column: 11'), findsNothing); |
| |
| // Let's scroll! |
| verticalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 0.0); |
| expect(find.text('Row: 49 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 0'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 1'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 1'), findsOneWidget); |
| // Within the CacheExtent |
| expect(find.text('Row: 49 Column: 8'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 9'), findsOneWidget); |
| // Not around unless pinned. |
| expect(find.text('Row: 0 Column: 0'), findsOneWidget); // Pinned |
| expect(find.text('Row: 48 Column: 0'), findsOneWidget); // Pinned |
| expect(find.text('Row: 0 Column: 1'), findsOneWidget); // Pinned |
| expect(find.text('Row: 1 Column: 1'), findsNothing); |
| |
| // Let's scroll some more! |
| horizontalController.jumpTo(verticalController.position.maxScrollExtent); |
| await tester.pump(); |
| expect(verticalController.position.pixels, 4400.0); |
| expect(horizontalController.position.pixels, 4400.0); |
| expect(find.text('Row: 49 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 49'), findsOneWidget); |
| expect(find.text('Row: 49 Column: 48'), findsOneWidget); |
| expect(find.text('Row: 48 Column: 48'), findsOneWidget); |
| // Nothing within the CacheExtent |
| expect(find.text('Row: 50 Column: 50'), findsNothing); |
| expect(find.text('Row: 51 Column: 51'), findsNothing); |
| // Not around unless pinned. |
| expect(find.text('Row: 0 Column: 0'), findsOneWidget); // Pinned |
| expect(find.text('Row: 49 Column: 0'), findsOneWidget); // Pinned |
| expect(find.text('Row: 0 Column: 49'), findsOneWidget); // Pinned |
| expect(find.text('Row: 1 Column: 1'), findsNothing); |
| }); |
| |
| testWidgets('only paints visible cells', (WidgetTester tester) async { |
| final ScrollController verticalController = ScrollController(); |
| final ScrollController horizontalController = ScrollController(); |
| final TableView tableView = TableView.builder( |
| rowCount: 50, |
| columnCount: 50, |
| columnBuilder: (_) => span, |
| rowBuilder: (_) => span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| verticalDetails: ScrollableDetails.vertical( |
| controller: verticalController, |
| ), |
| horizontalDetails: ScrollableDetails.horizontal( |
| controller: horizontalController, |
| ), |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| |
| bool cellNeedsPaint(String cell) { |
| return find.text(cell).evaluate().first.renderObject!.debugNeedsPaint; |
| } |
| |
| expect(cellNeedsPaint('Row: 0 Column: 0'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 1'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 2'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 3'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 4'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 5'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 6'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 7'), isFalse); |
| expect(cellNeedsPaint('Row: 0 Column: 8'), isTrue); // cacheExtent |
| expect(cellNeedsPaint('Row: 0 Column: 9'), isTrue); // cacheExtent |
| expect(cellNeedsPaint('Row: 0 Column: 10'), isTrue); // cacheExtent |
| expect( |
| find.text('Row: 0 Column: 11'), |
| findsNothing, |
| ); // outside of cacheExtent |
| |
| expect(cellNeedsPaint('Row: 1 Column: 0'), isFalse); |
| expect(cellNeedsPaint('Row: 2 Column: 0'), isFalse); |
| expect(cellNeedsPaint('Row: 3 Column: 0'), isFalse); |
| expect(cellNeedsPaint('Row: 4 Column: 0'), isFalse); |
| expect(cellNeedsPaint('Row: 5 Column: 0'), isFalse); |
| expect(cellNeedsPaint('Row: 6 Column: 0'), isTrue); // cacheExtent |
| expect(cellNeedsPaint('Row: 7 Column: 0'), isTrue); // cacheExtent |
| expect(cellNeedsPaint('Row: 8 Column: 0'), isTrue); // cacheExtent |
| expect( |
| find.text('Row: 9 Column: 0'), |
| findsNothing, |
| ); // outside of cacheExtent |
| |
| // Check a couple other cells |
| expect(cellNeedsPaint('Row: 5 Column: 7'), isFalse); // last visible cell |
| expect(cellNeedsPaint('Row: 6 Column: 7'), isTrue); // also in cacheExtent |
| expect(cellNeedsPaint('Row: 5 Column: 8'), isTrue); // also in cacheExtent |
| expect(cellNeedsPaint('Row: 6 Column: 8'), isTrue); // also in cacheExtent |
| }); |
| |
| testWidgets('paints decorations in correct order', |
| (WidgetTester tester) async { |
| // TODO(Piinks): Rewrite this to remove golden files from this repo when |
| // mock_canvas is public - https://github.com/flutter/flutter/pull/131631 |
| // foreground, background, and precedence per mainAxis |
| TableView tableView = TableView.builder( |
| rowCount: 2, |
| columnCount: 2, |
| columnBuilder: (int index) => TableSpan( |
| extent: const FixedTableSpanExtent(200.0), |
| foregroundDecoration: const TableSpanDecoration( |
| border: TableSpanBorder( |
| trailing: BorderSide( |
| color: Colors.orange, |
| width: 3, |
| ))), |
| backgroundDecoration: TableSpanDecoration( |
| color: index.isEven ? Colors.red : null, |
| ), |
| ), |
| rowBuilder: (int index) => TableSpan( |
| extent: const FixedTableSpanExtent(200.0), |
| foregroundDecoration: const TableSpanDecoration( |
| border: TableSpanBorder( |
| leading: BorderSide( |
| color: Colors.green, |
| width: 3, |
| ))), |
| backgroundDecoration: TableSpanDecoration( |
| color: index.isOdd ? Colors.blue : null, |
| ), |
| ), |
| cellBuilder: (_, TableVicinity vicinity) { |
| return const SizedBox.square( |
| dimension: 200, |
| child: Center(child: FlutterLogo()), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| await expectLater( |
| find.byType(TableView), |
| matchesGoldenFile('goldens/tableSpanDecoration.defaultMainAxis.png'), |
| skip: !runGoldens, |
| ); |
| |
| // Switch main axis |
| tableView = TableView.builder( |
| mainAxis: Axis.horizontal, |
| rowCount: 2, |
| columnCount: 2, |
| columnBuilder: (int index) => TableSpan( |
| extent: const FixedTableSpanExtent(200.0), |
| foregroundDecoration: const TableSpanDecoration( |
| border: TableSpanBorder( |
| trailing: BorderSide( |
| color: Colors.orange, |
| width: 3, |
| ))), |
| backgroundDecoration: TableSpanDecoration( |
| color: index.isEven ? Colors.red : null, |
| ), |
| ), |
| rowBuilder: (int index) => TableSpan( |
| extent: const FixedTableSpanExtent(200.0), |
| foregroundDecoration: const TableSpanDecoration( |
| border: TableSpanBorder( |
| leading: BorderSide( |
| color: Colors.green, |
| width: 3, |
| ))), |
| backgroundDecoration: TableSpanDecoration( |
| color: index.isOdd ? Colors.blue : null, |
| ), |
| ), |
| cellBuilder: (_, TableVicinity vicinity) { |
| return const SizedBox.square( |
| dimension: 200, |
| child: Center(child: FlutterLogo()), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| await expectLater( |
| find.byType(TableView), |
| matchesGoldenFile('goldens/tableSpanDecoration.horizontalMainAxis.png'), |
| skip: !runGoldens, |
| ); |
| }); |
| |
| testWidgets('mouse handling', (WidgetTester tester) async { |
| int enterCounter = 0; |
| int exitCounter = 0; |
| final TableView tableView = TableView.builder( |
| rowCount: 50, |
| columnCount: 50, |
| columnBuilder: (_) => span, |
| rowBuilder: (int index) => index.isEven |
| ? getMouseTrackingSpan( |
| index, |
| onEnter: (_) => enterCounter++, |
| onExit: (_) => exitCounter++, |
| ) |
| : span, |
| cellBuilder: (_, TableVicinity vicinity) { |
| return SizedBox.square( |
| dimension: 100, |
| child: Text('Row: ${vicinity.row} Column: ${vicinity.column}'), |
| ); |
| }, |
| ); |
| |
| await tester.pumpWidget(MaterialApp(home: tableView)); |
| await tester.pumpAndSettle(); |
| // Even rows will respond to mouse, odd will not |
| final Offset evenRow = tester.getCenter(find.text('Row: 2 Column: 2')); |
| final Offset oddRow = tester.getCenter(find.text('Row: 3 Column: 2')); |
| final TestGesture gesture = await tester.createGesture( |
| kind: PointerDeviceKind.mouse, |
| ); |
| await gesture.addPointer(location: oddRow); |
| expect(enterCounter, 0); |
| expect(exitCounter, 0); |
| expect( |
| RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
| SystemMouseCursors.basic, |
| ); |
| await gesture.moveTo(evenRow); |
| await tester.pumpAndSettle(); |
| expect(enterCounter, 1); |
| expect(exitCounter, 0); |
| expect( |
| RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
| SystemMouseCursors.cell, |
| ); |
| await gesture.moveTo(oddRow); |
| await tester.pumpAndSettle(); |
| expect(enterCounter, 1); |
| expect(exitCounter, 1); |
| expect( |
| RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), |
| SystemMouseCursors.basic, |
| ); |
| }); |
| }); |
| } |
| |
| class _NullBuildContext implements BuildContext, TwoDimensionalChildManager { |
| @override |
| dynamic noSuchMethod(Invocation invocation) => throw UnimplementedError(); |
| } |
| |
| RenderTableViewport getViewport(WidgetTester tester, Key childKey) { |
| return RenderAbstractViewport.of(tester.renderObject(find.byKey(childKey))) |
| as RenderTableViewport; |
| } |
| |
| class TestTableSpanExtent extends TableSpanExtent { |
| late TableSpanExtentDelegate delegate; |
| |
| @override |
| double calculateExtent(TableSpanExtentDelegate delegate) { |
| this.delegate = delegate; |
| return 100; |
| } |
| } |