Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing AlertDialog/`showDialog` in a widget test ("Which: means none were found but one was expected")

I'm trying to write a widget test to ensure an AlertDialog is shown to the user.

I've got a small example to reproduce this. When manually used by a user, the widget shows the AlertDialog, but it fails to show in a widget test.

enter image description here

I've tried a few things:

  • Using different methods to retrieve the button: find.byKey, find.byIcon,

  • Using different methods to press the button: tester.tap, tester.press

  • Using some arbitrary delay after pressing the button: await tester.pumpAndSettle(const Duration(milliseconds: 1000));

  • Checking different expected elements: expect(find.byElementType(AlertDialog), findsOneWidget);, putting a distinct icon e.g. pokeball and getting it: expect(find.byIcon(Icons.catching_pokemon), findsOneWidget)

  • Setting useDialog's useRootNavigator:false

  • tip: if you run flutter run main.dart, it will run test visually on the device screen

  • tip 2: you can run the app by commenting out some code (see main function)

Short example

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';

const buttonKey = Key("button");
const alertDialogKey = Key("alertDialog");

class MyApp extends StatelessWidget {
  showAppDialog(BuildContext context) async {
    print("Showing app dialog");
    await showDialog(
        context: context,
        builder: (context) {
          return AlertDialog(
            key: alertDialogKey,
            title: const Text(
              "You can see this dialog, but you can't catch it with WidgetTester.",
            ),
            icon: const Icon(Icons.catching_pokemon),
            actions: [
              TextButton(
                onPressed: () {
                  // Navigator.of(context).pop();
                },
                child: const Text("Oops"),
              ),
            ],
          );
        });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dialog',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(body: SafeArea(child: Builder(builder: (context) {
        return TextButton(
          key: buttonKey,
          child: const Text("Show dialog"),
          onPressed: () async => await showAppDialog(context),
        );
      }))),
    );
  }
}

void mainTest() {
  testWidgets(
    "When button is pressed, dialog is shown.",
    (tester) async {
      final widget = MyApp();
      await tester.pumpWidget(widget);

      final button = find.byKey(buttonKey);
      expect(button, findsOneWidget);

      await tester.press(button);
      await tester.pumpAndSettle();

      // None of these work:
      expect(find.byKey(alertDialogKey), findsOneWidget);
      expect(find.byIcon(Icons.catching_pokemon), findsOneWidget);
      expect(find.byElementType(AlertDialog), findsOneWidget);
    },
  );
}

void main() {
  // Uncomment to run the app manually
  // runApp(MyApp());
  // Comment out to run the app manually.
  mainTest();
}
like image 831
Ben Butterworth Avatar asked Sep 19 '25 13:09

Ben Butterworth


2 Answers

I needed to do 2 things:

  • Trigger the button inside await tester.runAsync(() async {}) because showDialog is an async function. By default, Flutter doesn't actually run asynchronous work in tests, for performance reasons. 🤓
  • use tester.tap instead of tester.press because press doesn't actually release button, so .press doesn't trigger onPressed callback 😈.
void mainTest() {
  testWidgets(
    "When button is pressed, dialog is shown.",
    (tester) async {
      final widget = MyApp();
      await tester.pumpWidget(widget);

      final button = find.byKey(buttonKey);
      expect(button, findsOneWidget);

      await tester.runAsync(() async {
        await tester.tap(button);
//         Or alternatively press then "up":
//         final response = await tester.press(button);
//         await response.up();
      });
      await tester.pumpAndSettle();

      // These all work now
      expect(find.byKey(alertDialogKey), findsOneWidget);
      expect(find.byIcon(Icons.catching_pokemon), findsOneWidget);
      expect(find.byType(AlertDialog), findsOneWidget);
    },
  );
}

Extra tip: run your tests on a device to visualise what's happening. Run flutter run test/name_of_test.dart. This helps you see where the problem is: for example, is it the AlertDialog not showing, or the find.byType not finding the AlertDialog?

like image 146
Ben Butterworth Avatar answered Sep 22 '25 05:09

Ben Butterworth


I had a very similar issue and all I needed to do was remove the 'await' from when I 'showDialog'. The reason for this seams to be that the pumpAndSettle is awaiting for the dialog to close before it continues, which it doesn't.

like image 27
Matt Booth Avatar answered Sep 22 '25 05:09

Matt Booth