Introduction
I tried to do something pretty simple in my opinion. Have a widget handle tap, long press, double tap, and right click events. Making it work is rather easy, but it’s way harder to test than I ever imagined. Here is my approach on how to handle tap gestures in widget tests.
Testing tap events
I started with a ListTile
which offers onTap and onLongPress to begin with. Things went smoothly and here is a working widget test:
testWidgets("Test", (tester) async {
bool tapped = false;
bool longPressed = false;
final widgetToTest = MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
ListTile(
onTap: () {
tapped = true;
},
onLongPress: () {
longPressed = true;
},
title: SizedBox(
width: 200,
height: 100,
child: Text("my list tile"),
),
),
],
),
),
),
);
await tester.pumpWidget(widgetToTest);
await tester.pumpAndSettle();
await tester.tap(find.byType(Text));
await tester.pump();
expect(tapped, true, reason: "NOT TAPPED");
await tester.longPress(find.byType(Text));
await tester.pump();
expect(longPressed, true, reason: "NOT LONG PRESSED");
});
But then I wanted to have right click and double click events for desktop app support. ListTile
doesn’t offer this behavior so I wrapped it in a GestureDetector
which offers everything I wanted. Here is the updated example code:
testWidgets("Test", (tester) async {
bool tapped = false;
bool longPressed = false;
bool doubleTapped = false;
final widgetToTest = MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
GestureDetector(
onDoubleTap: () {
doubleTapped = true;
},
child: ListTile(
onTap: () {
tapped = true;
},
onLongPress: () {
longPressed = true;
},
title: SizedBox(
width: 200,
height: 100,
child: Text("my list tile"),
),
),
),
],
),
),
),
);
await tester.pumpWidget(widgetToTest);
await tester.pumpAndSettle();
await tester.tap(find.byType(Text));
await tester.pump();
expect(tapped, true, reason: "NOT TAPPED");
await tester.longPress(find.byType(Text));
await tester.pump();
expect(longPressed, true, reason: "NOT LONG PRESSED");
await tester.tap(find.byType(Text));
await tester.tap(find.byType(Text));
await tester.pump();
expect(doubleTapped, true, reason: "NOT DOUBLE TAPPED");
});
Should work, but doesn’t.

It’s interesting that the tap is failing. So it seems that ListTile
and GestureDetector
don’t work well together.
Sure, there must be a way to test this. So I started digging around.
List of not working approaches
I am a bit disappointed at this point because I couldn’t get it to work. And I tried a lot. Here are some approaches that I tried.
Move ListTile events to GestureDetector
So instead of
GestureDetector(
onDoubleTap: () {
doubleTapped = true;
},
child: ListTile(
onTap: () {
tapped = true;
},
onLongPress: () {
longPressed = true;
},
title: SizedBox(
width: 200,
height: 100,
child: Text("my list tile"),
),
),
),
I tried
GestureDetector(
onDoubleTap: () {
doubleTapped = true;
},
onTap: () {
tapped = true;
},
onLongPress: () {
longPressed = true;
},
child: ListTile(
title: SizedBox(
width: 200,
height: 100,
child: Text("my list tile"),
),
),
),
Result was the same. All taps weren’t recognized as expected.
Replace ListTile with a Container
Maybe I could get around the problem by replacing the ListTile
with something that does not handle any tap gestures. I tried various widgets but sadly, no improvements in the results.
In addition, I was really surprised that I couldn’t find any bug reports or articles about that topic across the internet. Is nobody writing widget tests or just not sharing their experiences?
I came to the conclusion that GestureDetector
is not the way to go. Moving on to…
InkWell to the rescue! Oh wait…
InkWell
is essentially a GestureDetector
with effects in simple words.
Btw how cool is that widget name? A fountain of paint that has splash effects. Naming done right by the Flutter team! ❤️
I introduced InkWell
… and nothing changed. NOTHING. The same errors, the same problems. With or without the ListTile
.
Time to fight with the big guns. I let Claude Sonnet 4 loose. This is always my last resort because it’s better than GPT 4.1 but my requests to Sonnet are limited in my GitHub Copilot license.
AI tries its luck
Claude Sonnet 4 gave everything but after 40 minutes of mostly trying what I had already tried (yes, I explained in the prompt which approaches I had already tried), I told it to revert all changes to its original state.
💡 Tip
Always commit before giving control to an AI agent. Claude couldn’t restore the initial state. I had to do it manually.
I was about to give up but with a last search I found a working solution!
The hacky but working solution
My greetings go out to StackOverflow. I hope it never dies because it’s a goldmine for strange problems and solutions.
The idea is to not use the widget tester to tap the InkWell
or GestureDetector
but invoke the methods on the widgets.
I had no idea that this was even possible. Here is the working test code:
testWidgets("Test", (tester) async {
bool tapped = false;
bool longPressed = false;
bool doubleTapped = false;
ValueKey key = ValueKey("a");
final widgetToTest = MaterialApp(
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
GestureDetector(
key: key,
onDoubleTap: () {
doubleTapped = true;
},
onTap: () {
tapped = true;
},
onLongPress: () {
longPressed = true;
},
child: ListTile(
title: SizedBox(
width: 200,
height: 100,
child: Text("my list tile"),
),
),
),
],
),
),
),
);
await tester.pumpWidget(widgetToTest);
await tester.pumpAndSettle();
final gd = find.byKey(key).evaluate().first.widget as GestureDetector;
gd.onTap!();
await tester.pump();
expect(tapped, true, reason: "NOT TAPPED");
gd.onLongPress!();
await tester.pump();
expect(longPressed, true, reason: "NOT LONG PRESSED");
gd.onDoubleTap!();
await tester.pump();
expect(doubleTapped, true, reason: "NOT DOUBLE TAPPED");
});
Your InkWell
/GestureDetector
needs a key to identify it. The type doesn’t matter, it works with Key
, ValueKey
, UniqueKey
, or GlobalKey
.
Then grab the widget and cast it to the corresponding type. The last step is to call the desired methods directly. Not pretty or intuitive but after hours of trying, I am pragmatic in that regard.
I hope this article saves someone the pain and frustration that I encountered!
Conclusion
In this article, I talked about how to handle tap gestures in widget tests. There is always a solution for every problem in the Flutter world, but this time it is not pretty. When you have issues with InkWell
or GestureDetector
, then this article should help you.
Related articles

