fluttercommunity / font_awesome_flutter

The Font Awesome Icon pack available as Flutter Icons
Other
834 stars 236 forks source link

Default finders from `flutter_test` don't work with `FaIcon` #198

Open bartekpacia opened 2 years ago

bartekpacia commented 2 years ago

The problem

Let's say that I have the following Widget:

IconButton(
    iconSize: 20,
    icon: FaIcon(FontAwesomeIcons.eye),
    onPressed: () {/* change PasswordTextField visibility */},
  );

If I want to test that this Widget is visible, I'd normally use

// password_text_field_test.dart

 testWidgets(
      'tapping on password visibility button toggles it',
      (tester) async {
        final eyeFinder = find.widgetWithIcon(IconButton, FontAwesomeIcons.eye);
        expect(eyeFinder, findsOneWidget);
});

But the above will fail:

tapping on password visibility button toggles it
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: exactly one matching node in the widget tree
  Actual: _AncestorFinder:<zero widgets with type "IconButton" which is an ancestor of icon
"IconData(U+0F06E)">
   Which: means none were found but one was expected

The reason is that FaIcon is not an Icon (i.e FaIcon extends StatelessWidget), so find.widgetWithIcon can't find it.

The solution 1/2

To make find.widgetWithIcon work with FaIcon, we have to create a custom implementation of Flutter's WidgetIconFinder:

class WidgetFaIconFinder extends MatchFinder {
  WidgetFaIconFinder(this.icon, {bool skipOffstage = true})
      : super(skipOffstage: skipOffstage);

  final IconData icon;

  @override
  String get description => 'Font Awesome icon "$icon"';

  @override
  bool matches(Element candidate) {
    final widget = candidate.widget;
    return widget is FaIcon && widget.icon == icon;
  }
}

Then, to make it shorter for use in tests, we can create our own CustomFinders class, basing on Flutter's CommonFinders:

// finders.dart

const customFind = CustomFinders._();

class CustomFinders {
  const CustomFinders._();

  /// Like [CommonFinders.byIcon], but for FontAwesomeIcons.
  Finder byFaIcon(IconData icon, {bool skipOffstage = true}) =>
      WidgetFaIconFinder(icon, skipOffstage: skipOffstage);

  /// Like [CommonFinders.widgetWithIcon], but for FontAwesomeIcons.
  Finder widgetWithFaIcon(
    Type widgetType,
    IconData icon, {
    bool skipOffstage = true,
  }) {
    return find.ancestor(
      of: customFind.byFaIcon(icon),
      matching: find.byType(widgetType),
    );
  }
}

The solution 2/2

Then, we can use our custom finders just like we'd use the default ones

// password_text_field_test.dart

 testWidgets(
      'tapping on password visibility button toggles it',
      (tester) async {
        final eyeFinder = customFind.widgetWithFaIcon(IconButton, FontAwesomeIcons.eye);
        expect(eyeFinder, findsOneWidget);
});

Further steps

I see 2 ways: