Search code examples
flutterintegration-testing

Integration test failing due to app translations


I have this simple app that I want to test:


Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  AppTheme theme = AppTheme();
  FlutterLocalization localization = FlutterLocalization.instance;


  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

  @override
  void initState(){
    widget.theme.loadprefs();
    widget.localization.init(
      mapLocales: [
        const MapLocale(
          'en',
          AppLocale.EN,
          countryCode: 'US',
          fontFamily: 'Font EN',
        ),

      ],
      initLanguageCode: 'en',
    );
    widget.localization.onTranslatedLanguage = _onTranslatedLanguage;
  }

  void _onTranslatedLanguage(Locale? locale) {
    setState(() {});
  }


  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => widget.theme,
      child: Consumer<AppTheme>(
          builder: (context, state, child) {
          return MaterialApp(
            // routerConfig: _appRouter.config(),
            supportedLocales:  widget.localization.supportedLocales,
            localizationsDelegates:  widget.localization.localizationsDelegates,
            //
            home: MyHomePage(title: "title"),
          );
        }
      ),
    );
  }
}


class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        color: backgroundColor(context),
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              TextButton(onPressed: (){
                bool theme = (Provider.of<AppTheme>(context, listen: false).getCurrentTheme() == THEMES[0].theme);
                int index = theme == true ? 1: 0;
                Provider.of<AppTheme>(context, listen: false).setTheme(index);

              }, child: Text("Change theme")),
              Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '$_counter',
                style: Theme.of(context).textTheme.headlineMedium,
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        // Provide a Key to this button. This allows finding this
        // specific button inside the test suite, and tapping it.
        key: const Key('increment'),
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

Test:

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('end-to-end test', () {
    testWidgets('tap on the floating action button, verify counter',
            (tester) async {
          // Load app widget.

              // var theme = AppTheme();
              // await theme.loadprefs();
          await tester.pumpWidget( MyApp());

          // Verify the counter starts at 0.
          expect(find.text('0'), findsOneWidget);

          // Finds the floating action button to tap on.
              final fab = find.byKey(const ValueKey('increment'));
           int i =0;
           while(i<100) {
             await tester.tap(fab);
             i+=1;
           }

          await Future.delayed(Duration(minutes: 2));
          // Emulate a tap on the floating action button.

          // Trigger a frame.
          await tester.pumpAndSettle();

          // Verify the counter increments by 1.
          expect(find.text('100'), findsOneWidget);
        });
  }); 

AppTheme:


class AppTheme extends ChangeNotifier {

  final String key = "theme";
  SharedPreferences? _prefs;

  late ThemeClass currentTheme;
  List<ThemeClass> themes = THEMES;

  AppTheme({initiate = true}) {
    if (initiate) {
      loadprefs();
    }
  }

  switchtheme(ThemeClass theme) {
    currentTheme = theme;
    _saveprefs();
    notifyListeners();
  }

  _initiateprefs() async {
    _prefs ??= await SharedPreferences.getInstance();
  }

  getTheme(String theme){
    return themes.where((element) => element.name == theme).first;
  }

  bool isCurrent(int index){
    return currentTheme.name == themes[index].name;
  }

  loadprefs() async {
    await _initiateprefs();
    final n = _prefs?.getString(key);
    if(n!= null) {
      currentTheme = getTheme(n);
    } else {
      currentTheme = themes[0];
    }
    notifyListeners();
  }

  _saveprefs() async {
    await _initiateprefs();
    _prefs?.setString(key, currentTheme.name);
  }

  getCurrentTheme() {
    return currentTheme.theme;
  }

  void setTheme(int index) {
    switchtheme(themes[index]);
  }

}
const Color DARK_BACKGROUND = Color(0xff26282D);
const Color THIRD_COLOR_DARK = Color(0xff3080CD);
const Color DARK_SECONDARY = Color(0xff12161C);
const Color DARK_SECONDARY_VARIANT = Color(0xff383A44);
const Color DARK_CONTRAST_COLOR = Color(0xffe2e4ea);
const Color DARK_SHADOW_COLOR = Color(0xff090A0D);


final List<ThemeClass> THEMES = [
  ThemeClass(
      name: 'Dark',
      theme: themeDark
  ),
  ThemeClass(
      name: 'Light',
      theme: themeLight
  ),
];

class ThemeClass{
  final String name;
  final ThemeData theme;

  ThemeClass({required this.name, required this.theme});

}


final ThemeData themeLight = ThemeData(
  bottomAppBarTheme: const BottomAppBarTheme(
    color: Colors.pink,
  ),
  primaryColor: Colors.amber,
);

final ThemeData themeDark = ThemeData(
  bottomAppBarTheme: const BottomAppBarTheme(
    color: DARK_BACKGROUND,
  ),
  primaryColor: THIRD_COLOR_DARK,
  secondaryHeaderColor: DARK_SECONDARY,

  textTheme: darkText,
  dividerColor: DARK_CONTRAST_COLOR,
  indicatorColor: DARK_SECONDARY_VARIANT,
  shadowColor: DARK_SHADOW_COLOR,
);
const TextTheme darkText = TextTheme(
    headlineMedium: TextStyle(
      fontSize: FONT_DISPLAY_LARGE,
      fontWeight: FontWeight.w500,
      color: FONT_COLOR_DARK,
      // Add your font and other styles here
    )
);

AppLocale:

import 'package:intl/intl.dart';

import 'en.dart';

String getAbbreviationWithIndex(String abbr, int index) {
  return '$abbr$index';
}

mixin AppLocale {
  static const String increment = "increment";

  static const Map<String, dynamic> EN = ENG;
  static FlutterLocalization locale = FlutterLocalization.instance;
}

en.dart:

const Map<String, dynamic> ENG = {
  "month": 'Month',
};

I'm encountering an issue with my application where certain tests are failing because they cannot find the increment. After some investigation, I discovered that these failures occur specifically when I add localization to the app. It seems that the introduction of localization is somehow causing the tests to break, but I'm not entirely sure why this is happening.

Could you help me understand the root cause of this issue? Why does adding localization to the application result in test failures, particularly in finding the increment? Additionally, what steps can I take to fix this problem and ensure that my tests pass successfully even with localization enabled?


Solution

  • I made few changes in the test and was able to run the tests successfully. After pumping the app, its still pumping frames to the screen but without waiting for that to complete, you are looking for the element using key. That is causing the problem.

    you can use await tester.pumpAndSettle(); right after await tester.pumpWidget(MyApp());

    await tester.pumpWidget(MyApp());
    await tester.pumpAndSettle();
    

    Also, its better to check for the item before tapping it.

    final fab = find.byKey(const ValueKey('increment'));
    expect(fab, findsOneWidget);
    

    if you want to see whats happening on the screen, give a pumpAndSettle with a duration after tap

     while (i < 100) {
                await tester.tap(fab);
                await tester.pumpAndSettle(const Duration(seconds: 1));
                i += 1;
              }
    

    final test is as follows

    void main() {
      IntegrationTestWidgetsFlutterBinding.ensureInitialized();
    
      group('end-to-end test', () {
        testWidgets('tap on the floating action button, verify counter',
                (tester) async {
    
              await tester.pumpWidget(MyApp());
              await tester.pumpAndSettle();
    
              // Verify the counter starts at 0.
              expect(find.text('0'), findsOneWidget);
    
              // Finds the floating action button to tap on.
              final fab = find.byKey(const ValueKey('increment'));
              int i = 0;
              while (i < 100) {
                await tester.tap(fab);
                //uncomment if you wanna see whats happening on the screen
                //await tester.pumpAndSettle(const Duration(milliseconds: 300));
                i += 1;
              }
              await tester.pumpAndSettle();
              expect(find.text('100'), findsOneWidget);
            });
      });
    }
    

    Suggested Improvements: Its always better using static / constant keys wherever required and use them instead of hardcoding it in the code

    const counterValueKey = 'counterValueKey';
    const incrementButtonKey = 'incrementButtonKey';