blob: 3fbd4e7a938f04c80393557cd75a1402c9175591 [file] [log] [blame]
// 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);
TableViewParentData parentData = parentDataOf(
viewport.firstChild!,
);
expect(parentData.vicinity, vicinity);
expect(parentData.layoutOffset, Offset.zero);
expect(parentData.isVisible, isTrue);
// after first child
vicinity = const TableVicinity(column: 1, row: 0);
parentData = parentDataOf(
viewport.childAfter(viewport.firstChild!)!,
);
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);
parentData = parentDataOf(viewport.lastChild!);
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);
parentData = parentDataOf(
viewport.childBefore(viewport.lastChild!)!,
);
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;
}
}