Skip to content

Commit 782e898

Browse files
berosoAndré Sousa
authored andcommitted
Add tests for pinned cells hit test.
1 parent 90f14e4 commit 782e898

1 file changed

Lines changed: 278 additions & 0 deletions

File tree

packages/two_dimensional_scrollables/test/table_view/table_test.dart

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4631,6 +4631,284 @@ void main() {
46314631
expect(mergedRect.top, 200);
46324632
expect(mergedRect.bottom, 400);
46334633
});
4634+
4635+
group('Table pinned cells hit test', () {
4636+
// Regression tests for https://github.com/flutter/flutter/issues/186876
4637+
// Trailing pinned cells must take precedence over underlying scrollable cells
4638+
// that have scrolled into the same screen rect.
4639+
//
4640+
// Geometry shared by all four tests (10 columns × 10 rows, 100 × 100 px,
4641+
// viewport 400 × 400 px):
4642+
//
4643+
// paintOffset.dx(col k) = k × 100 − scroll (for non-pinned columns)
4644+
//
4645+
// At scroll = 525:
4646+
// • _targetTrailingColumnPixel = 825, so col 8 (trailingOffset = 900) is
4647+
// the last non-pinned column built (900 ≥ 825).
4648+
// • col 8 lands at x = 8×100 − 525 = 275 → paint rect x = 275..375.
4649+
// • Trailing pinned col 9 always sits at x = 300..400.
4650+
// • Both rects contain x = 350, so without the _sectionClipRectFor fix
4651+
// the forward child-list scan hits col 8 first and swallows the tap.
4652+
//
4653+
// At scroll = 500 the bug is NOT triggered: _targetTrailingColumnPixel = 800
4654+
// causes col 7 (trailingOffset = 800) to be the last non-pinned column built
4655+
// (col 8 is never added to the child list), so there is no overlap.
4656+
// The same argument applies symmetrically for rows.
4657+
4658+
testWidgets(
4659+
'Trailing pinned column takes precedence over scrolled-under regular cell',
4660+
(WidgetTester tester) async {
4661+
final horizontalController = ScrollController();
4662+
TableVicinity? lastTapped;
4663+
4664+
await tester.pumpWidget(
4665+
MaterialApp(
4666+
home: Scaffold(
4667+
body: SizedBox(
4668+
height: 400,
4669+
width: 400,
4670+
child: TableView.builder(
4671+
cacheExtent: 0.0,
4672+
columnCount: 10,
4673+
rowCount: 1,
4674+
trailingPinnedColumnCount: 1,
4675+
horizontalDetails: ScrollableDetails.horizontal(
4676+
controller: horizontalController,
4677+
),
4678+
columnBuilder: (_) =>
4679+
const TableSpan(extent: FixedTableSpanExtent(100)),
4680+
rowBuilder: (_) =>
4681+
const TableSpan(extent: FixedTableSpanExtent(100)),
4682+
cellBuilder: (_, TableVicinity vicinity) {
4683+
return TableViewCell(
4684+
child: GestureDetector(
4685+
behavior: HitTestBehavior.opaque,
4686+
onTap: () => lastTapped = vicinity,
4687+
child: const SizedBox.expand(),
4688+
),
4689+
);
4690+
},
4691+
),
4692+
),
4693+
),
4694+
),
4695+
);
4696+
4697+
// col 8 (regular) lands at x = 275..375, overlapping the trailing
4698+
// pinned col 9 (x = 300..400). A tap at x = 350 hits both; col 9 wins.
4699+
horizontalController.jumpTo(525);
4700+
await tester.pump();
4701+
4702+
await tester.tapAt(const Offset(350, 50));
4703+
await tester.pumpAndSettle();
4704+
4705+
expect(
4706+
lastTapped?.column,
4707+
9,
4708+
reason:
4709+
'Trailing pinned column 9 must receive the tap; column 8 (scrolled underneath) must not.',
4710+
);
4711+
},
4712+
);
4713+
4714+
testWidgets(
4715+
'Trailing pinned row takes precedence over scrolled-under regular cell',
4716+
(WidgetTester tester) async {
4717+
final verticalController = ScrollController();
4718+
TableVicinity? lastTapped;
4719+
4720+
await tester.pumpWidget(
4721+
MaterialApp(
4722+
home: Scaffold(
4723+
body: SizedBox(
4724+
height: 400,
4725+
width: 400,
4726+
child: TableView.builder(
4727+
cacheExtent: 0.0,
4728+
columnCount: 1,
4729+
rowCount: 10,
4730+
trailingPinnedRowCount: 1,
4731+
verticalDetails: ScrollableDetails.vertical(
4732+
controller: verticalController,
4733+
),
4734+
columnBuilder: (_) =>
4735+
const TableSpan(extent: FixedTableSpanExtent(100)),
4736+
rowBuilder: (_) =>
4737+
const TableSpan(extent: FixedTableSpanExtent(100)),
4738+
cellBuilder: (_, TableVicinity vicinity) {
4739+
return TableViewCell(
4740+
child: GestureDetector(
4741+
behavior: HitTestBehavior.opaque,
4742+
onTap: () => lastTapped = vicinity,
4743+
child: const SizedBox.expand(),
4744+
),
4745+
);
4746+
},
4747+
),
4748+
),
4749+
),
4750+
),
4751+
);
4752+
4753+
// row 8 (regular) lands at y = 275..375, overlapping the trailing
4754+
// pinned row 9 (y = 300..400). A tap at y = 350 hits both; row 9 wins.
4755+
verticalController.jumpTo(525);
4756+
await tester.pump();
4757+
4758+
await tester.tapAt(const Offset(50, 350));
4759+
await tester.pumpAndSettle();
4760+
4761+
expect(
4762+
lastTapped?.row,
4763+
9,
4764+
reason:
4765+
'Trailing pinned row 9 must receive the tap; row 8 (scrolled underneath) must not.',
4766+
);
4767+
},
4768+
);
4769+
4770+
testWidgets(
4771+
'Trailing pinned corner cell takes precedence over scrolled-under regular cells',
4772+
(WidgetTester tester) async {
4773+
final horizontalController = ScrollController();
4774+
final verticalController = ScrollController();
4775+
TableVicinity? lastTapped;
4776+
4777+
await tester.pumpWidget(
4778+
MaterialApp(
4779+
home: Scaffold(
4780+
body: SizedBox(
4781+
height: 400,
4782+
width: 400,
4783+
child: TableView.builder(
4784+
cacheExtent: 0.0,
4785+
columnCount: 10,
4786+
rowCount: 10,
4787+
trailingPinnedColumnCount: 1,
4788+
trailingPinnedRowCount: 1,
4789+
horizontalDetails: ScrollableDetails.horizontal(
4790+
controller: horizontalController,
4791+
),
4792+
verticalDetails: ScrollableDetails.vertical(
4793+
controller: verticalController,
4794+
),
4795+
columnBuilder: (_) =>
4796+
const TableSpan(extent: FixedTableSpanExtent(100)),
4797+
rowBuilder: (_) =>
4798+
const TableSpan(extent: FixedTableSpanExtent(100)),
4799+
cellBuilder: (_, TableVicinity vicinity) {
4800+
return TableViewCell(
4801+
child: GestureDetector(
4802+
behavior: HitTestBehavior.opaque,
4803+
onTap: () => lastTapped = vicinity,
4804+
child: const SizedBox.expand(),
4805+
),
4806+
);
4807+
},
4808+
),
4809+
),
4810+
),
4811+
),
4812+
);
4813+
4814+
// Regular cell (row 8, col 8) lands at x = 275..375, y = 275..375,
4815+
// overlapping the trailing corner cell (row 9, col 9) at x = 300..400,
4816+
// y = 300..400. A tap at (350, 350) hits both; (row 9, col 9) wins.
4817+
horizontalController.jumpTo(525);
4818+
verticalController.jumpTo(525);
4819+
await tester.pump();
4820+
4821+
await tester.tapAt(const Offset(350, 350));
4822+
await tester.pumpAndSettle();
4823+
4824+
expect(
4825+
lastTapped,
4826+
const TableVicinity(column: 9, row: 9),
4827+
reason:
4828+
'Trailing pinned corner cell (row 9, col 9) must receive the tap; the regular cell (row 8, col 8) scrolled underneath must not.',
4829+
);
4830+
},
4831+
);
4832+
4833+
testWidgets(
4834+
'Leading and trailing pinned columns each clip to their own viewport band',
4835+
(WidgetTester tester) async {
4836+
final horizontalController = ScrollController();
4837+
final tappedColumns = <int>[];
4838+
4839+
await tester.pumpWidget(
4840+
MaterialApp(
4841+
home: Scaffold(
4842+
body: SizedBox(
4843+
height: 100,
4844+
width: 400,
4845+
child: TableView.builder(
4846+
cacheExtent: 0.0,
4847+
columnCount: 10,
4848+
rowCount: 1,
4849+
// col 0 → always at x = 0..100
4850+
pinnedColumnCount: 1,
4851+
// col 9 → always at x = 300..400
4852+
trailingPinnedColumnCount: 1,
4853+
horizontalDetails: ScrollableDetails.horizontal(
4854+
controller: horizontalController,
4855+
),
4856+
columnBuilder: (_) =>
4857+
const TableSpan(extent: FixedTableSpanExtent(100)),
4858+
rowBuilder: (_) =>
4859+
const TableSpan(extent: FixedTableSpanExtent(100)),
4860+
cellBuilder: (_, TableVicinity vicinity) {
4861+
return TableViewCell(
4862+
child: GestureDetector(
4863+
behavior: HitTestBehavior.opaque,
4864+
onTap: () => tappedColumns.add(vicinity.column),
4865+
child: const SizedBox.expand(),
4866+
),
4867+
);
4868+
},
4869+
),
4870+
),
4871+
),
4872+
),
4873+
);
4874+
4875+
// With leading pinned extent = 100 and trailing pinned extent = 100 the
4876+
// non-pinned band is x = 100..300 (200 px wide). The formula becomes
4877+
// paintOffset.dx(col k) = k×100 − scroll
4878+
// so col 8 lands at x = 275..375 at scroll = 525, overlapping trailing
4879+
// col 9 (x = 300..400) at x = 300..375.
4880+
horizontalController.jumpTo(525);
4881+
await tester.pump();
4882+
4883+
// 1. Tap the trailing band (x = 350) → col 9, NOT the underlying col 8.
4884+
await tester.tapAt(const Offset(350, 50));
4885+
await tester.pumpAndSettle();
4886+
expect(
4887+
tappedColumns.last,
4888+
9,
4889+
reason: 'Tap at x=350 should hit trailing pinned col 9, not col 8.',
4890+
);
4891+
4892+
// 2. Tap the leading band (x = 50) → col 0.
4893+
await tester.tapAt(const Offset(50, 50));
4894+
await tester.pumpAndSettle();
4895+
expect(
4896+
tappedColumns.last,
4897+
0,
4898+
reason: 'Tap at x=50 should hit leading pinned col 0.',
4899+
);
4900+
4901+
// 3. Tap the middle of the non-pinned band (x = 200) → neither pinned col.
4902+
await tester.tapAt(const Offset(200, 50));
4903+
await tester.pumpAndSettle();
4904+
expect(
4905+
tappedColumns.last,
4906+
isNot(anyOf(0, 9)),
4907+
reason: 'Tap at x=200 should hit a non-pinned column.',
4908+
);
4909+
},
4910+
);
4911+
});
46344912
}
46354913

46364914
class _NullBuildContext implements BuildContext, TwoDimensionalChildManager {

0 commit comments

Comments
 (0)