How To Handle Tap Gestures In Widget Tests

Flutter category image

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:

Dart
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:

Dart
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.

The onTap event in the widget test did not work as expected
The onTap event in the widget test did not work as expected

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

Dart
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

Dart
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:

Dart
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.


Want More Flutter Content?

Join my bi-weekly newsletter that delivers small Flutter portions right in your inbox. A title, an abstract, a link, and you decide if you want to dive in!

Flutter ❤️ Firebase

Get started with Firebase and learn how to use it in your Flutter apps. My detailed ebook delivers you everything you need to know! Flutter and Firebase are a perfect match!

Become A Testing Expert!

Become a proficient Flutter app tester with my detailed guide. This ebook covers everything from unit tests over widget tests up to dependency mocking.