samedi 14 décembre 2019

Flutter's WidgetTester.pumpandSettle() does not wait for rendering finished?

I have a StatefulWidget which state renders different Widget depending on loading state (Loading -> Loaded/Error):

// widget
class ListNotesScreen extends StatefulWidget {
  static const route = '/listNotes';
  static navigateTo(BuildContext context, [bool cleanStack = true]) =>
      Navigator.pushNamedAndRemoveUntil(context, route, (_) => !cleanStack);

  final String title;
  final ListNotesUseCase _useCase;
  final VoidCallback _addNoteCallback;
  ListNotesScreen(this._useCase, this._addNoteCallback, {Key key, this.title}) : super(key: key);

  @override
  _ListNotesScreenState createState() => _ListNotesScreenState();
}

// state
class _ListNotesScreenState extends State<ListNotesScreen> {
  ListNotesLoadState _state;

  Future<ListNotesResponse> _fetchNotes() async {
    return widget._useCase.listNotes();
  }

  @override
  initState() {
    super.initState();
    _loadNotes();
  }

  _loadNotes() {
    setState(() {
      _state = ListNotesLoadingState();
    });

    _fetchNotes().then((response) {
      setState(() {
        _state = ListNotesLoadedState(response.notes);
      });
    }).catchError((error) {
      setState(() {
        _state = ListNotesLoadErrorState(error);
      });
    });
  }

  @override
  Widget build(BuildContext context) => Scaffold(
    appBar: AppBar(
      title: Text('Notes list'),
      actions: <Widget>[
        IconButton(icon: Icon(Icons.add), onPressed: widget._addNoteCallback),
        IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadNotes())
      ],
    ),
    body: _state.getWidget());
}

// loading states
// State:
@sealed
abstract class ListNotesLoadState {
  Widget getWidget();
}

// Loading
class ListNotesLoadingState extends ListNotesLoadState {
  @override
  Widget getWidget() => Center(child: CircularProgressIndicator(value: null));
}

// Loaded
class ListNotesLoadedState extends ListNotesLoadState {
  final List<Note> _notes;
  ListNotesLoadedState(this._notes);

  @override
  Widget getWidget() => ListView.builder(
    itemBuilder: (_, int index) => NoteItemWidget(this._notes[index]),
    itemCount: this._notes.length,
    padding: EdgeInsets.all(18.0));
}

Here is the test for the widget:

void main() {
  testWidgets('Notes list is shown', (WidgetTester tester) async {
    final title1 = 'Title1';
    final title2 = 'Title2';
    final body1 = 'Body1';
    final body2 = 'Body2';
    var notes = [
      Note('1', title1, body1),
      Note('2', title2, body2),
    ];
    final listUseCase = TestListNotesInteractor(notes);
    final widget = ListNotesScreen(listUseCase, null, title: 'List notes');
    await tester.pumpWidget(widget);
    await tester.pumpAndSettle();

    expect(find.text('someInvalidString'), findsNothing);
    expect(find.text(title1), findsOneWidget);
    expect(find.text(title2), findsOneWidget);
    expect(find.text(body1), findsOneWidget);
    expect(find.text(body2), findsOneWidget);

    // TODO: fix the test (tested manually and it works)
  });
}

So widget tester is expected to wait until the state it set to loading in initState(), then _loadNotes moves it to ListNotesLoadedState and ListNotesLoadedState.getWidget() to return ListView with expected string (NoteItemWidget root and few Text with expected string).

However the test fails. What's the reason (i was able to use test interactors in the app and visually see expected texts)? How can i analyze the actual Widgets tree on test failure?

I tend to think that WidgetTester did not wait for Future to be completed (though it's expected to be mocked and be sync behind the scenes, please correct me).

One can find the project on Github (make sure to call flutter packages pub run build_runner build to generate json de-/serialize code).

Aucun commentaire:

Enregistrer un commentaire