Rutger Huijsmans
Rutger Huijsmans

Reputation: 2408

Widget not rebuilding on notifyListeners during test

I'm trying to build a Widget test for a screen that's using the Provider framework.

The app has 1 screen with 1 button, when I tap the button it will trigger a function to update the state. Once the state is updated a string with the key Key('LoadedString') will appear.
When manually testing in the simulator this works as expected.

When I'm trying to use a widget test to verify the expected behavior it seems like the UI is not updating.

This is the Widget:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'provider_page_state_controller.dart';

class ProviderPageWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(providers: [
      ChangeNotifierProvider<ProviderPageStateController>(
          create: (context) => ProviderPageStateController())
    ], child: HomePage());
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var screenHeight = MediaQuery.of(context).size.height;
    ScreenState state =
        Provider.of<ProviderPageStateController>(context).pageState;

    // Loading state
    if (state == ScreenState.Loading) {
      return Scaffold(
          body: Container(
        color: Colors.purple,
        child: Center(child: Text('Loading...')),
      ));
    }
    // SuccessfullyLoaded state
    if (state == ScreenState.SuccessfullyLoaded) {
      return Scaffold(
          body: Container(
              color: Colors.green,
              child: Center(
                  child: Column(
                children: [
                  Padding(
                    padding:
                    EdgeInsets.fromLTRB(0, (screenHeight / 2) - 100,     0, 0),
                  ),
                  Text('Loaded!', key: Key('LoadedString')),
                  Container(
                    margin: EdgeInsets.all(25),
                    color: Colors.blueAccent,
                    child: TextButton(
                      child: Text(
                        'Reset',
                    style: TextStyle(color: Colors.white, fontSize:     20.0),
                      ),
                      onPressed: () {
                        Provider.of<ProviderPageStateController>(context,
                                listen: false)
                            .updateState(ScreenState.InitialState);
                      },
                    ),
                  ),
                ],
              ))));
    }
    // InitialState (=default state)
    return Scaffold(
        body: Container(
      color: Colors.white,
      child: Center(
        child: Column(
          children: [
            Padding(
          padding: EdgeInsets.fromLTRB(0, (screenHeight / 2) - 100,     0, 0),
            ),
            Container(
              key: Key('ButtonA'),
              margin: EdgeInsets.all(25),
              color: Colors.blueAccent,
              child: TextButton(
                child: Text(
                  'Button A',
                  style: TextStyle(color: Colors.white, fontSize: 20.0),
                ),
                onPressed: () {
                  Provider.of<ProviderPageStateController>(context,
                          listen: false)
                      .functionA();
                },
              ),
            ),
          ],
        ),
      ),
    ));
  }
}

This is the Controller:

import 'package:flutter/material.dart';

enum ScreenState { InitialState, Loading, SuccessfullyLoaded }

class ProviderPageStateController extends ChangeNotifier {
  var pageState = ScreenState.InitialState;

  void updateState(ScreenState screenState) {
    pageState = screenState;
    notifyListeners();
  }

  Future<void> functionA() async {
    updateState(ScreenState.Loading);
    Future.delayed(Duration(milliseconds: 1000), () {
      updateState(ScreenState.SuccessfullyLoaded);
    });
  }

  Future<void> reset() async {
    updateState(ScreenState.InitialState);
  }
}

This is the Widget test:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import '../provider_page/provider_page_state_controller.dart';
import '../provider_page/provider_page_widget.dart';

void main() {
  testWidgets('ProviderPageWidget SuccessfullyLoaded test',
      (WidgetTester tester) async {
    final providerPageStateController = ProviderPageStateController();

    await tester.pumpWidget(
      ListenableProvider<ProviderPageStateController>.value(
        value: providerPageStateController,
        child: MaterialApp(
          home: ProviderPageWidget(),
        ),
      ),
    );

    await providerPageStateController.functionA();
    expect(providerPageStateController.pageState, ScreenState.Loading);
    await tester.pumpAndSettle(Duration(seconds: 2));
    expect(
    providerPageStateController.pageState,     ScreenState.SuccessfullyLoaded);

    // Why does this assert fail?
    expect(find.byKey(Key('LoadedString')), findsOneWidget);
  });
}

Why is the last expect of my Widget test failing?

Upvotes: 1

Views: 1002

Answers (2)

Rutger Huijsmans
Rutger Huijsmans

Reputation: 2408

I found out what the issue was. I was not pumping the correct widget in the widget test.

Updating the widget test to the following solved the issue:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import '../provider_page/provider_page_state_controller.dart';
import '../provider_page/provider_page_widget.dart';

void main() {
  testWidgets('ProviderPageWidget SuccessfullyLoaded test',
      (WidgetTester tester) async {
    final providerPageStateController = ProviderPageStateController();

    await tester.pumpWidget(
      ListenableProvider<ProviderPageStateController>.value(
        value: providerPageStateController,
        child: MaterialApp(
          home: HomePage(),
        ),
      ),
    );

    await providerPageStateController.functionA();
    expect(providerPageStateController.pageState, ScreenState.Loading);
    await tester.pumpAndSettle(Duration(seconds: 2));
    expect(providerPageStateController.pageState, ScreenState.SuccessfullyLoaded);

    // Why does this assert fail?
    expect(find.byKey(Key('LoadedString')), findsOneWidget);
  });
}

The change is on the line:

home: HomePage(),

Upvotes: -1

hitshy_dev
hitshy_dev

Reputation: 133

Use the Provider.value constructor, since you are using a variable instead of instantiating an object. And to make sure all dependants are updated eagerly use the the lazy: false flag in tests, even when the value is not accessed. The resulting changes are:

void main() {
  final providerPageState = ProviderPageState();

...
      Provider<ProviderPageState>.value(
        value: providerPageState,
        child: MaterialApp(
          home: ProviderPageWidget(),
        ),
        lazy: false,
      ),
    );

I would also recommend switching to ChangeNotifierProvider, when using provider to store states. Make sure your ProviderPageState is extending ChangeNotifier and implementing the needed methods, as reference you can use the implementation of ValueNotifier.

Upvotes: 1

Related Questions