Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect when tap target is covered #61

Merged
merged 27 commits into from
Jun 27, 2024
Merged
Changes from 1 commit
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3e45cc3
Add optimized search for interactable widget positions
danielmolnar Jun 21, 2024
ac9a157
Add test "Finds widgets after dragging down and up"
danielmolnar Jun 21, 2024
7f6dfb8
Add print warning
danielmolnar Jun 21, 2024
a79007f
Add CustomTappableArea
danielmolnar Jun 22, 2024
e4c8bb4
Add test "Partially covered, finds tappable area"
danielmolnar Jun 22, 2024
6a00a46
Add MeasureSize PokeTestWidget
danielmolnar Jun 22, 2024
bc1de17
Adjust test to setup
danielmolnar Jun 22, 2024
a6d7389
Add test Poke test widget throws without defined tappable spots
danielmolnar Jun 22, 2024
3cce03c
Add docs to PokeTestWidget
danielmolnar Jun 22, 2024
6657c76
Improve test naming, constellation
danielmolnar Jun 24, 2024
c919bf1
Make captureConsoleOutput public
danielmolnar Jun 20, 2024
ef71574
Add test "Warn about using and finding alternative tappable area."
danielmolnar Jun 24, 2024
7df7e4f
Add todo
danielmolnar Jun 24, 2024
1f18195
Refactor act
danielmolnar Jun 24, 2024
f5df08d
Return HitTestFailure if no position is found
danielmolnar Jun 24, 2024
4ca1113
Fix test not running on flutter 3.10
danielmolnar Jun 24, 2024
9faf92c
Improve naming
danielmolnar Jun 24, 2024
7b4ca4d
Use positioned.fill instead of measure size
danielmolnar Jun 25, 2024
717b842
Create a useful error message when an InkWell covers the tap target
passsy Jun 25, 2024
1d93a41
Report useful error when tap target has 0x0 pixels in size
passsy Jun 26, 2024
531121f
Revert "Return HitTestFailure if no position is found"
danielmolnar Jun 26, 2024
b42d443
Improve getting the location of a widget in code using public APIs :t…
passsy Jun 27, 2024
8210279
Merge branch 'detect-pokable-positions' into tap-text-in-elevated-button
passsy Jun 27, 2024
3eff69e
Merge remote-tracking branch 'origin/main' into tap-text-in-elevated-…
passsy Jun 27, 2024
dc63f28
Delete MeasureSize
passsy Jun 27, 2024
f28a83c
Cleanup
passsy Jun 27, 2024
ac9b6d3
Print the actual widget, not the selector in tap error messages
passsy Jun 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add test "Partially covered, finds tappable area"
danielmolnar committed Jun 22, 2024
commit e4c8bb4520b337f5507a207733c3d6f1c9411e12
36 changes: 26 additions & 10 deletions lib/src/act/act.dart
Original file line number Diff line number Diff line change
@@ -80,24 +80,32 @@ class Act {
final renderBox = _getRenderBoxOrThrow(selector);
_validateViewBounds(renderBox, selector: selector);

final centerPosition =
renderBox.localToGlobal(renderBox.size.center(Offset.zero));
final tappedPosition = _findPokablePosition(
widgetSelector: selector,
snapshot: snapshot,
);

if (tappedPosition == null) {
throw TestFailure(
"Widget '${selector.toStringBreadcrumb()}' is not interactable.",
);
}

// Before tapping the widget, we need to make sure that the widget is not
// covered by another widget, or outside the viewport.
_pokeRenderObject(
position: centerPosition,
position: tappedPosition,
target: renderBox,
snapshot: snapshot,
);

final binding = TestWidgetsFlutterBinding.instance;

// Finally, tap the widget by sending a down and up event.
final downEvent = PointerDownEvent(position: centerPosition);
final downEvent = PointerDownEvent(position: tappedPosition);
binding.handlePointerEvent(downEvent);

final upEvent = PointerUpEvent(position: centerPosition);
final upEvent = PointerUpEvent(position: tappedPosition);
binding.handlePointerEvent(upEvent);

await binding.pump();
@@ -325,22 +333,30 @@ class Act {
renderBox.size.topRight(Offset.zero),
renderBox.size.bottomRight(Offset.zero),
];

int iterations = 0;
final firstPosition = renderBox.localToGlobal(initialPosition);
for (final localPosition in mostLikelyHitRegions) {
if (iterations == 1) {
// ignore: avoid_print
print(
"Widget failed hit test at its center ($firstPosition). Trying to find passing region within the widget's boundaries.",
);
}
final Offset globalPosition = renderBox.localToGlobal(localPosition);
if (_canBePoked(
position: globalPosition,
target: renderBox,
snapshot: snapshot,
)) {
if (globalPosition != initialPosition) {
if (globalPosition != firstPosition) {
// ignore: avoid_print
print(
'Warning: The widget is not interactable at the center but was interactable at $globalPosition',
'Found hit testable position of widget at $globalPosition',
);
}
return globalPosition;
}
iterations++;
}

// No luck with the most likely hit regions, let's try a grid pattern
@@ -361,10 +377,10 @@ class Act {
target: renderBox,
snapshot: snapshot,
)) {
if (globalPosition != initialPosition) {
if (globalPosition != firstPosition) {
// ignore: avoid_print
print(
'Warning: The widget is not interactable at the center but was interactable at $globalPosition',
'Found hit testable position of widget at $globalPosition.',
);
}
return globalPosition;
27 changes: 27 additions & 0 deletions test/act/find_tappable_area_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spot/spot.dart';

import '../widgets/custom_tappable_area_widget.dart';

void main() {
testWidgets('Partially covered, finds tappable area', (tester) async {
bool gotTapped = false;
await tester.pumpWidget(
MaterialApp(
home: Center(
child: CustomTappableAreaWidget(
tappablePosition: const MapEntry(15, 15),
onTap: (tapped) {
gotTapped = tapped;
},
),
),
),
);
final WidgetSelector<WidgetToPoke> button = spot<WidgetToPoke>()
..existsOnce();
await act.tap(button);
expect(gotTapped, isTrue);
});
}
99 changes: 72 additions & 27 deletions test/widgets/custom_tappable_area_widget.dart
Original file line number Diff line number Diff line change
@@ -3,40 +3,85 @@ import 'package:flutter/material.dart';
class CustomTappableAreaWidget extends StatelessWidget {
const CustomTappableAreaWidget({
super.key,
required this.tapOffset,
MapEntry<int, int>? tappablePosition,
required this.onTap,
this.areaSize = 50.0,
});
Size? boardSize,
MapEntry<int, int>? cubeCount,
}) : _boardSize = boardSize ?? const Size(400, 400),
_tappablePosition = tappablePosition ?? const MapEntry(1, 1),
_cubeCount = cubeCount ?? const MapEntry<int, int>(16, 16);

final Size _boardSize;

final Offset tapOffset;
final double areaSize;
final VoidCallback onTap;
final MapEntry<int, int> _cubeCount;
final MapEntry<int, int>? _tappablePosition;
final void Function(bool) onTap;

double get _totalWidth => _boardSize.width;
double get _totalHeight => _boardSize.height;
double get _cubeWidth => _totalWidth / _cubeCount.key;
double get _cubeHeight => _totalWidth / _cubeCount.value;

@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
const IgnorePointer(
child: ColoredBox(
color: Colors.transparent,
),
),
Positioned(
left: tapOffset.dx - areaSize / 2,
top: tapOffset.dy - areaSize / 2,
child: GestureDetector(
onTap: onTap,
child: Container(
width: areaSize,
height: areaSize,
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.5),
shape: BoxShape.circle,
),
return ConstrainedBox(
constraints: BoxConstraints(
maxWidth: _totalWidth,
maxHeight: _totalHeight,
),
child: Stack(
children: [
GestureDetector(
onTap: () {
onTap.call(true);
},
child: WidgetToPoke(
width: _totalWidth,
height: _totalHeight,
),
),
),
],
Column(
children: List.generate(_cubeCount.value, (row) {
return Row(
children: List.generate(_cubeCount.key, (col) {
final bool isTappable = _tappablePosition?.key == row &&
_tappablePosition?.value == col;
return IgnorePointer(
ignoring: isTappable,
child: Container(
width: _cubeWidth,
height: _cubeHeight,
color: isTappable
? Colors.transparent
: ((row + col).isEven ? Colors.white : Colors.black),
),
);
}),
);
}),
),
],
),
);
}
}

class WidgetToPoke extends StatelessWidget {
const WidgetToPoke({
super.key,
required this.width,
required this.height,
});

final double width;
final double height;

@override
Widget build(BuildContext context) {
return Container(
width: width,
height: height,
color: Colors.green,
);
}
}