Search code examples
flutterwidget-test-flutter

Widget testing DropdownButton finds duplicate DropdownMenuItems


I'm trying to write widget tests for a DropdownButton in my app. I noticed that after tapping the button to open it the call to find.byType(DropdownMenuItem) is returning double the expected number of DropdownMenuItems.

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

enum MyColor {
  blue,
  green,
  red,
  yellow,
  black,
  pink
}

Future<void> main() async {
//   runApp(MyApp());
  
  // tests
  group('dropdown tests', () {
    testWidgets('how many elements should be found?', (tester) async {
      await tester.pumpWidget(MyApp());
      await tester.pumpAndSettle();
      
      expect(find.byType(DropdownButton<MyColor>), findsOneWidget);
      await tester.tap(find.byType(DropdownButton<MyColor>));
      await tester.pumpAndSettle();
      
      // fails
      // expect(find.byType(DropdownMenuItem<MyColor>), findsNWidgets(MyColor.values.length));
      
      // passes
      expect(find.byType(DropdownMenuItem<MyColor>), findsNWidgets(MyColor.values.length * 2));
    });
  });
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        body: Center(
          child: MyWidget(),
        ),
      ),
    );
  }
}

class MyWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  MyColor selected = MyColor.blue;
  
  @override
  Widget build(BuildContext context) {
    return DropdownButton<MyColor>(
      value: selected,
      items: MyColor.values.map((col) {
        return DropdownMenuItem<MyColor>(
          child: Text(col.name),
          value: col,
        );
      }).toList(),
      onChanged: (value) {
        if (value == null) {
          return;
        }
        
        print('${value.name} selected');
        setState(() {
          selected = value;
        });
      }
    );
  } 
}

Dartpad: https://dartpad.dev/?id=ce3eadff6bd98e6005817c70883451a0

I suspect that this has something to do with how Flutter renders the scene. I looked into the widget tests for the dropdown in the Flutter repo but I don't see any difference between my setup and theirs, but I also don't see any calls to find.byType(DropdownMenuItem). Does anyone know why this happens? Or is there an error in my code?


Solution

  • When an DropdownButton is rendered initially all items are rendered with IndexedStack and based on the selected value we see one visible item at the top

    • At that stage find.byType(DropdownMenuItem<MyColor>) will find 6 items

    Once you tap on DropdownButton a _DropdownRoute route is pushed with all the items

    • At that stage find.byType(DropdownMenuItem<MyColor>) will find 12 items (the first 6 items are from IndexedStack and the second 6 items are from the new route) So the number of items should be double at this stage as documented in the flutter tests as well

    // Each item appears twice, once in the menu and once // in the dropdown button's IndexedStack.

    https://github.com/flutter/flutter/blob/504e66920005937b6ffbc3ccd6b59d594b0e98c4/packages/flutter/test/material/dropdown_test.dart#L2230

    Once you tap on of the DropdownMenuItem items the number of found widgets will go back to 6