skip to content

Why does text in a Flutter golden image render as solid boxes, and how do you make goldens show the app's real fonts?

level: middleimportance: should knowfreq 36%

answer

  1. flutter test substitutes a test font
  2. FlutterTest replaced Ahem in 3.10
  3. unregistered families fall back
  4. FontLoader then load()
  5. flutter_test_config.dart testExecutable

basics

~20 s

Under flutter test, text uses a test font whose glyphs are boxes: FlutterTest since Flutter 3.10, Ahem before. Unregistered font families fall back to it. Load real fonts with FontLoader, often once in flutter_test_config.dart, accepting that goldens become platform-sensitive.

solid answer

~40 s

The `flutter test` engine resolves text through a test font manager: when no `fontFamily` is given, or the family is not registered in the test, it renders with the `FlutterTest` font, whose glyphs are boxes filling the em square. Since Flutter 3.10 that replaced `Ahem`, which is still available by name; some `flutter_test` doc comments still say Ahem, but the engine source lists FlutterTest first. Box glyphs are deliberate: their metrics are stable across machines, so layout goldens do not drift. To see real text, load the font before pumping - `final FontLoader loader = FontLoader('Inter')..addFont(rootBundle.load('assets/fonts/Inter-Regular.ttf')); await loader.load();` - and use that family name in the theme. Doing it in `setUpAll` inside `testExecutable` in `flutter_test_config.dart` covers a whole directory. The cost: real fonts rasterise differently per OS and engine version.

code

dart · 17 lines
dart
// test/flutter_test_config.dart
import 'dart:async';

import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

Future<void> testExecutable(FutureOr<void> Function() testMain) async {
  setUpAll(() async {
    TestWidgetsFlutterBinding.ensureInitialized();
    final FontLoader inter = FontLoader('Inter')
      ..addFont(rootBundle.load('assets/fonts/Inter-Regular.ttf'))
      ..addFont(rootBundle.load('assets/fonts/Inter-Bold.ttf'));
    await inter.load();
  });

  await testMain();
}

go deeper

for a junior

Recall that flutter test draws text with a box-glyph test font and that FontLoader is how you register a real font before pumping.

for a middle

Explain the fallback rule - unspecified or unregistered families use FlutterTest, Ahem before 3.10 - and how flutter_test_config.dart's testExecutable loads fonts for a directory.

for a senior

Weigh box-font determinism against real-glyph coverage and keep real-font goldens few and pinned to one platform so they do not flap across machines.

for a principal

Set a policy for which goldens use real fonts, tied to where they are generated, so font upgrades and host changes cost one reviewed update, not a flood.

## What you see and why A first golden of a receipt card usually surprises people: every letter and digit is a filled rectangle. That is not a bug. When tests run under `flutter test`, the engine installs a **test font manager** that maps font families to a small set of bundled test fonts. Any text whose `fontFamily` is not specified, or names a family that has not been registered in the test, renders with the default test font. ## FlutterTest and Ahem | Font | Role today | Glyphs | Notes | |---|---|---|---| | `FlutterTest` | default test font since Flutter 3.10 | box filling the em square | ascent 0.75 em, descent 0.25 em; metrics designed to be font-engine agnostic | | `Ahem` | the old default, still usable by name | box | ascent 0.8 em, descent 0.2 em | The switch is recorded as a breaking change: `FlutterTest` produces more precise glyph metrics, so baselines and underline positions moved slightly and many goldens were regenerated. Part of `flutter_test`'s own documentation (the `matchesGoldenFile` doc comment) still says the default is `Ahem`; the engine source registers `FlutterTest`, `Ahem` and `Cough` and falls back to the first, `FlutterTest`. When an interviewer says "Ahem", the idea is the same: a box font chosen for determinism. ## Why boxes are a feature - **Stable metrics.** Each glyph is a box of known size, so the same text lays out to the same pixels on macOS, Linux and Windows. A golden then pins **layout** - alignment, wrapping, spacing, overflow - without depending on how an OS rasterises curves. - **Layout bugs still show.** A price that overflows its column, a total aligned left instead of right, or a line that wraps at a larger text scale all change the box pattern. - **What it misses.** Wrong typeface, wrong weight glyphs, or a character missing from the real font do not show up, because no real font is involved. ## Loading real fonts `FontLoader` from `package:flutter/services.dart` registers a font family at run time: 1. Create it with the **family name** the widgets use: `FontLoader('Inter')`. 2. Add one or more `Future<ByteData>` sources with `addFont(rootBundle.load('assets/fonts/Inter-Regular.ttf'))`. The asset must be declared in `pubspec.yaml`. 3. `await loader.load()` before the widget is pumped. 4. Make sure the theme's `fontFamily` matches the registered name exactly; an unregistered name silently falls back to `FlutterTest`. To do this for every test in a directory, use **`flutter_test_config.dart`**: `flutter test` looks for that file in the test's directory and its parents and, if present, calls its `testExecutable(FutureOr<void> Function() testMain)` with the test's `main`. Load the fonts in a `setUpAll` there and then `await testMain()`. ## The trade-off you are accepting Real fonts bring back everything the box font removed: - **Per-OS rasterisation.** Anti-aliasing and hinting differ between macOS, Linux and Windows, so a golden recorded on one host can fail on another by a few percent of pixels. - **Engine and version drift.** A Flutter upgrade that changes text shaping or the renderer can move glyph edges by a pixel. - **Icon fonts too.** An icon whose font family was not registered in the test also falls back to the test font. A common split is to keep most goldens on the default test font and render a small, deliberately chosen set - a typography sample, the receipt's total line - with real fonts, generated and compared on a single, fixed platform. ## Checklist - Boxes in the image: expected; decide whether you need real glyphs at all. - Real font requested but boxes remain: the family name does not match, or `load()` was not awaited before pumping. - Real fonts enabled and CI now fails with tiny diffs: generate goldens on the CI platform, not on laptops.

  • You loaded a font but the receipt still shows boxes; what do you check?
    That the family name passed to `FontLoader` equals the `fontFamily` the theme or `TextStyle` uses, that `await loader.load()` finished before `pumpWidget`, and that the asset path is declared in `pubspec.yaml`. Any unregistered family falls back to the test font without an error.
  • Why might a team keep the box font for most goldens even after solving font loading?
    Box glyphs make goldens independent of the host's text rasterisation, so they pass on any developer machine and CI host. Layout regressions still show. Real fonts are reserved for a few typography-sensitive goldens produced on one fixed platform.

saying these in an interview costs you the question

  • Boxes in the golden mean the app's font assets are broken
  • Declaring the font in pubspec.yaml is enough for flutter test to use it
  • Ahem is still the default test font in current Flutter
  • Box-font goldens cannot catch any layout regressions
  • Real fonts render identically on macOS, Linux and Windows