Search code examples
flutterflutter-test

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();
}

Solution

  • 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?