I have a method in my cubit
that captures a widget as an image and I am testing this method. Long story short this method calls _captureFromWidget
(see below, the code is copied from package screenshot), which accepts the Widget
and returns a Uint8List
. (I am not testing that the package works correctly, I am testing that my method and its parameters work correctly).
The problem is, that in real app the widget is captured correctly, but in the test, the font of all the Text
widgets is not rendered correctly, and boxes are shown instead of letters.
I know the reason of this, see here, and here.
I tried loading the font as they suggested:
in pubspec.yaml:
assets:
- assets/fonts/
and I have in the assets/fonts
folder my font: AbrilFatface-Regular.ttf
in my test:
final Future<ByteData> abrilFatFaceFontData = rootBundle.load('assets/fonts/AbrilFatface-Regular.ttf');
final FontLoader fontLoader = FontLoader('AbrilFatFace-Regular')..addFont(abrilFatFaceFontData);
await fontLoader.load();
Text text = ...; // text that uses custom font from above
cubit.captureWidget(...); // will invoke _captureFromWidget
but still, boxes are shown instead of letters in the captured image:
this is the result of calling the same methods with the same arguments from the real app:
So how to provide the font to the test correctly?
Here is the code that captures the widget:
/// [context] parameter is used to Inherit App Theme and MediaQuery data.
Future<Uint8List> _captureFromWidget(
widgets.Widget widget, {
required Duration delay,
double? pixelRatio,
widgets.BuildContext? context,
}) async {
// Retry counter
int retryCounter = 3;
bool isDirty = false;
widgets.Widget child = widget;
if (context != null) {
// Inherit Theme and MediaQuery of app
child = widgets.InheritedTheme.captureAll(
context,
widgets.MediaQuery(data: widgets.MediaQuery.of(context), child: child),
);
}
final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
Size logicalSize = ui.window.physicalSize / ui.window.devicePixelRatio;
Size imageSize = ui.window.physicalSize;
assert(logicalSize.aspectRatio.toPrecision(5) == imageSize.aspectRatio.toPrecision(5));
final RenderView renderView = RenderView(
window: ui.window,
child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
configuration: ViewConfiguration(
size: logicalSize,
devicePixelRatio: pixelRatio ?? 1.0,
),
);
final PipelineOwner pipelineOwner = PipelineOwner();
final widgets.BuildOwner buildOwner = widgets.BuildOwner(
focusManager: widgets.FocusManager(),
onBuildScheduled: () {
///
///current render is dirty, mark it.
///
isDirty = true;
});
pipelineOwner.rootNode = renderView;
renderView.prepareInitialFrame();
final widgets.RenderObjectToWidgetElement<RenderBox> rootElement = widgets.RenderObjectToWidgetAdapter<RenderBox>(
container: repaintBoundary,
child: widgets.Directionality(
textDirection: TextDirection.ltr,
child: child,
)).attachToRenderTree(
buildOwner,
);
// Render Widget
buildOwner.buildScope(
rootElement,
);
buildOwner.finalizeTree();
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
ui.Image? image;
do {
// Reset the dirty flag
isDirty = false;
image = await repaintBoundary.toImage(pixelRatio: pixelRatio ?? (imageSize.width / logicalSize.width));
// This delay should increase with Widget tree Size
await Future.delayed(delay);
// Check does this require rebuild
if (isDirty) {
// Previous capture has been updated, re-render again.
buildOwner.buildScope(
rootElement,
);
buildOwner.finalizeTree();
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
}
retryCounter--;
//retry until capture is successful
} while (isDirty && retryCounter >= 0);
final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
return byteData!.buffer.asUint8List();
}