Flutter Widget test cannot emulate different screen size properly
Asked Answered
P

3

10

Before deploying my Flutter app, I wanted to test it on multiple screen sizes to check if there is any Renderflex overflow for smaller screens.

But I when first modified the screen size during widget testing to match the device I was using during the development, I realized that the widget test is throwing Render overflow errors already, even though it did not have such errors on the real device. So I asked this questions How to fix A RenderFlex overflowed during Widget Test

But I after further investigation and using Flutter golden feature test which snaps png out of widget tests, I narrowed down the problem to a discrepancy in text size.

You can see clearly in the reproducible step below that the text during the widget text is WAY BIGGER (on the right) than the actual text in the real device (on the left).

enter image description here

The bigger text size during Widget test causes the RenderFlex error in my app.

Steps to reproduce:

  1. Now connect a real device and run this code with flutter run

lib/main.dart

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: TextScaleComparaison(),
    ),
  );
}

class TextScaleComparaison extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final widget = Scaffold(
      body: LayoutBuilder(
        builder: (BuildContext context, BoxConstraints constraints) {
          final width = MediaQuery.of(context).size.width;
          final height = MediaQuery.of(context).size.height;
          final dpr = MediaQuery.of(context).devicePixelRatio;
          final textScale = MediaQuery.of(context).textScaleFactor;
          final vi = MediaQuery.of(context).viewInsets;
          final vip = MediaQuery.of(context).viewPadding;
          final font = DefaultTextStyle.of(context).style.fontFamily;
          print("width is $width and height is $height and dpi is $dpr txtScale is $textScale vi is $vi vip is $vip font is $font");
          return Center(child: Text("This cannot be that long!!"));
        },
      ),
    );
    return widget;
  }
}
  1. Check the logs and you should see device screen info:

For me I got :

I/flutter (27450): width is 411.42857142857144 and height is 797.7142857142857 and dpi is 2.625 txtScale is 1.1 vi is EdgeInsets.zero vip is EdgeInsets(0.0, 24.0, 0.0, 0.0) font is Roboto

Copy the screen width and height to and textScale and devicePixelRatio to the next step in the code below.

  1. Edit the code below to add the above setting because we want to simulate this exact screensize in the test.

test/test.dart

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/main.dart';
void main() {

  testWidgets(
    "Emulate real screen size",
    (WidgetTester tester) async {
      // Adjust these to match your actual device screen specs
      final width = 414;
      final height = 846;
      tester.binding.window.devicePixelRatioTestValue = (2.625);
      tester.binding.window.textScaleFactorTestValue = (1.1);
      final dpi = tester.binding.window.devicePixelRatio;
      tester.binding.window.physicalSizeTestValue = Size(width * dpi, height * dpi);
      await tester.pumpWidget(
        MediaQuery(
          data: MediaQueryData(),
          child: MaterialApp(
            home: TextScaleComparaison(),
          ),
        ),
      );
      await expectLater(
        find.byType(TextScaleComparaison),
        matchesGoldenFile("text.png"),
      );
    },
  );
}

Run test.dart with flutter test --update-goldens test/test.dart

This will create a png file at test/text.png

Check the logs: For me it printed:

width is 414.0 and height is 846.0 and dpi is 2.625 txtScale is 1.1 vi is EdgeInsets.zero vip is EdgeInsets.zero font is Roboto

What I am missing ? Why can't the text show exactly the same as the real device?

Partizan answered 18/6, 2020 at 10:28 Comment(0)
P
20

That is because of the font difference used by flutter test and flutter run.

Flutter's default font is Roboto for Android if you did not change it other font.

  1. Default Android: Roboto font and for iOS: San Francisco font
  2. Customize https://flutter.dev/docs/cookbook/design/fonts

Either 1) or 2) these fonts are not available to flutter test by default. Flutter test purposely uses a font called Ahem which is made out of square blocks that you see on your screenshot.

This is a preview:

enter image description here

Ahem font square are wayyy bigger than the normal that you are using. Therefore, it causes the RenderFlex overflow error

Solution

To achieve a near perfect emulation of your device in flutter test you have to download the font data then load the exact font that you are using.

To load a font in widget test, you should do inside the testWidgets function or setUp:

final flamante = rootBundle.load('assets/fonts/Flamante-Roma-Medium.ttf');
final fontLoader = FontLoader('FlamanteRoma')..addFont(flamante);
await fontLoader.load();

Then add this font to the ThemeData before pumping the widget.

 theme: ThemeData(
              fontFamily: 'FlamanteRoma',
 ),

The final test.dart code is:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:example/test/compare_test_size.dart';

void main() {
  testWidgets(
    "Emulate real screen size",
    (WidgetTester tester) async {
      final flamante = rootBundle.load('assets/fonts/Flamante-Roma-Medium.ttf');
      final fontLoader = FontLoader('FlamanteRoma')..addFont(flamante);

      await fontLoader.load();

      // Adjust these to match your actual device screen specs
      final width = 411.4;
      final height = 797.7;
      tester.binding.window.devicePixelRatioTestValue = (2.625);
      tester.binding.window.textScaleFactorTestValue = (1.1);
      final dpi = tester.binding.window.devicePixelRatio;
      tester.binding.window.physicalSizeTestValue = Size(width * dpi, height * dpi);

      await tester.pumpWidget(
        MediaQuery(
          data: MediaQueryData(),
          child: MaterialApp(
            home: TextScaleComparaison(),
            theme: ThemeData(
              fontFamily: 'FlamanteRoma',
            ),
          ),
        ),
      );

      await expectLater(
        find.byType(TextScaleComparaison),
        matchesGoldenFile("text.png"),
      );
    },
  );
}

Now re generate the golden test and check the png. You will see real text, not boxes anymore:

test/test.png

enter image description here

And don't forget to add the same font in your main.dart

runApp(
    MaterialApp(
      home: TextScaleComparaison(),
      theme: ThemeData(
        fontFamily: 'FlamanteRoma',
      ),
    ),
  );

And also don't forget to update pubspec.yaml and run flutter pub get

- family: FlamanteRoma
  fonts:
    - asset: assets/fonts/Flamante-Roma-Medium.ttf
Partizan answered 18/6, 2020 at 22:45 Comment(2)
And as a quick hack you could just change the font-scaling to overcome more generic RenderFlex errors caused by the testing font, e.g. tester.binding.window.textScaleFactorTestValue = (0.8);Kandrakandy
I have not been able to get this to work on Flutter 1.17.5 (nor 1.17.4, nor 1.20.0-7.3.pre-beta). I can do flutter build bundle and it fails if I change pubspec.yaml to non-existing path, so I'm confident that part is correct. Running flutter test fails with Unable to load asset: assets/fonts/Flamante-Roma-Medium.ttf. This is thrown in PlatformAssetBundle.load and I think that's the problem, it shouldn't be PlatformAssetBundle? Anything else that I need to do?Peloquin
C
1

As Curly already mentioned here, you can overwrite the tester.binding.window.textScaleFactorTestValue with a lower value. 0.8 was working well for my used fonts, but depending on your setup, you might use even a lower value. In this way, you don't need to await the loading of your fonts in each test case.

Chongchoo answered 24/8, 2020 at 11:6 Comment(0)
I
1

Since you don't necessarily want to store the font that you want to use in the test in assets/ since it then will be bundled into the app, you can also store it directly in the test directory (or somewhere else) and read it with File.

So say that you have the following font:

test/fonts/Ubuntu-Regular.ttf

And then to use it:

final ubuntuRegular = File('test/fonts/Ubuntu-Regular.ttf');
final content = ByteData.view(
  Uint8List.fromList(ubuntuRegular.readAsBytesSync()).buffer,
);
final fontLoader = FontLoader('UbuntuRegular')
  ..addFont(Future.value(content));
await fontLoader.load();
pumpWidget(
  MaterialApp(
    theme: ThemeData(fontFamily: 'UbuntuRegular'),
    home: Scaffold(body: YourWidget()),
  ),
);

You could also make this into an extension since you'll probably repeat it a lot:

extension WidgetTesterExtension on WidgetTester {
  Future<void> pumpApp(WidgetBuilder builder) async {
    final ubuntuRegular = File('test/fonts/Ubuntu-Regular.ttf');
    final content = ByteData.view(
        Uint8List.fromList(ubuntuRegular.readAsBytesSync()).buffer);
    final fontLoader = FontLoader('UbuntuRegular')
      ..addFont(Future.value(content));
    await fontLoader.load();
    return pumpWidget(
      MaterialApp(
        theme: ThemeData(fontFamily: 'UbuntuRegular'),
        home: Scaffold(body: Builder(builder: builder)),
      ),
    );
  }
}
Impasse answered 21/11, 2023 at 9:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.