In Flutter golden tests, how do you cover a receipt widget in light and dark themes and at larger text scales without duplicating tests?
answer
- one test body, several configurations
- ValueVariant and currentValue
- variant value in the golden file name
- textScaleFactorTestValue or a MediaQuery
- reset overrides in addTearDown
basics
~20 sParameterise one testWidgets body with a ValueVariant (or a loop) over theme and text scale, build the MaterialApp from the current value, and include that value in the golden file name so each combination gets its own baseline.
solid answer
~40 sI describe each case as a value - a record like `(brightness: Brightness.dark, scale: 2.0)` - and pass `variant: ValueVariant<ReceiptCase>(cases)` to `testWidgets`, which runs the body once per value and appends it to the test name. Inside, `variant.currentValue!` picks `ThemeData(brightness: ...)` and the text scale, applied either with `tester.platformDispatcher.textScaleFactorTestValue = 2.0` (cleared with `addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue)`) or with a `MediaQuery` override of `textScaler: TextScaler.linear(2.0)` via the app's `builder`. The golden name encodes the case - `goldens/receipt_dark_x2.png` - so every combination has its own reviewed baseline. Keep the matrix small: light and dark at 1.0 and one large scale catch most overflow and contrast regressions.
code
dart · 37 linesimport 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shop/receipt/receipt_card.dart';
typedef ReceiptCase = ({Brightness brightness, double scale});
final ValueVariant<ReceiptCase> receiptCases = ValueVariant<ReceiptCase>(<ReceiptCase>{
(brightness: Brightness.light, scale: 1.0),
(brightness: Brightness.dark, scale: 1.0),
(brightness: Brightness.light, scale: 2.0),
});
void main() {
testWidgets('receipt card golden', (WidgetTester tester) async {
final ReceiptCase c = receiptCases.currentValue!;
tester.platformDispatcher.textScaleFactorTestValue = c.scale;
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(brightness: c.brightness),
home: Scaffold(
body: RepaintBoundary(
key: const Key('receipt'),
child: ReceiptCard(receipt: sampleReceipt),
),
),
),
);
final String name = '${c.brightness.name}_x${c.scale.toInt()}';
await expectLater(
find.byKey(const Key('receipt')),
matchesGoldenFile('goldens/receipt_$name.png'),
);
}, variant: receiptCases);
}go deeper
Recall that one golden test can run once per theme or text scale, and that each case needs its own golden file name.
Explain ValueVariant's currentValue and per-case naming, and the two ways to apply text scale: the platform dispatcher test value or a MediaQuery textScaler override.
Keep the matrix small and meaningful, reset every override in addTearDown, and pair large-scale goldens with overflow assertions so failures explain themselves.
Decide the supported configuration matrix with design and accessibility owners, since each case adds a baseline to maintain on every visual change.
## The problem A receipt card has to look right in several configurations: light theme, dark theme, and larger system text sizes where item names wrap and prices can overflow. Copy-pasting a golden test per combination multiplies code, and the copies drift apart. `flutter_test` gives you two tidy ways to run one test body several times. ## Option 1: `ValueVariant` `testWidgets` has a `variant` parameter of type `TestVariant<Object?>`. A **`ValueVariant<T>`** takes a `Set<T>` of values; for each value the framework: 1. calls the variant's `setUp`, which stores the value in `currentValue`; 2. runs the test body, whose name gets the value's description appended, so reports show which case failed; 3. calls `tearDown`. Inside the body, `variant.currentValue` tells you which configuration to build. Because each value becomes its own test run, failures are reported per case and each can be re-run by name. ## Option 2: a loop of `testWidgets` A `for` loop over a list of cases that calls `testWidgets` with an interpolated name works too and is sometimes clearer when each case needs a different setup. The key rule is the same: **one golden file per case**. ## Applying the theme Build the `MaterialApp` from the case: `theme: ThemeData(brightness: c.brightness)`. Alternatively pass both `theme` and `darkTheme` and set `themeMode` to `ThemeMode.light` or `ThemeMode.dark`. Either way the receipt reads its colours from `Theme.of(context)`, so both goldens exercise the real theme wiring instead of a hand-picked colour. ## Applying a text scale Two approaches: - **Platform override**: `tester.platformDispatcher.textScaleFactorTestValue = 2.0`, with `addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue)`. The `MediaQuery` that `MaterialApp` builds from the view picks it up, exactly as a user's system setting would reach the app. - **Widget override**: in `MaterialApp.builder`, wrap `child` in a `MediaQuery` whose data is `MediaQuery.of(context).copyWith(textScaler: TextScaler.linear(2.0))`. The platform override tests the path from system setting to app, including any clamping the app applies; the widget override is more local and explicit. Either one must be undone so it does not leak into the next test. ## Naming and sizing | Case | Golden file | |---|---| | light, 1.0 | `goldens/receipt_light_x1.png` | | dark, 1.0 | `goldens/receipt_dark_x1.png` | | light, 2.0 | `goldens/receipt_light_x2.png` | | dark, 2.0 | `goldens/receipt_dark_x2.png` | - Put the case in the file name, never reuse one name across cases. - Keep the view size fixed for all cases (`tester.view.physicalSize`, reset with `addTearDown(tester.view.reset)`) so the only variable is the one you meant. - Capture a keyed `RepaintBoundary` around the receipt so larger text that grows the card grows the image, and a clipped total becomes visible. ## Choosing the matrix Every case is another baseline to regenerate and review whenever the receipt changes. A good default is light and dark at the normal scale plus one large scale (2.0 is a common choice) in one theme; add more only where a bug has already appeared. Overflow at large text is often better asserted with a plain widget test as well - `tester.takeException()` returning a `FlutterError` about overflow - so the failure message names the problem instead of showing only a diff. ## What the goldens will and will not show - **Will show**: wrapping and overflow at larger text, contrast problems in dark theme, a colour hard-coded instead of read from the theme. - **Will not show**: whether the real font looks right (unless fonts are loaded), or whether a screen reader announces the total - that needs semantics checks, not pixels.
- Why must the case appear in the golden file name?All runs share one test body. With a fixed name, each run would compare against - or with `--update-goldens`, overwrite - the same PNG, so only the last case would have a baseline and the others would always fail against it.
- Why clear textScaleFactorTestValue in addTearDown?It is set on the test platform dispatcher, which outlives a single test body. Without clearing it, the next test in the file would render at the enlarged scale and its goldens or layout assertions would fail for reasons unrelated to that test.
- How would you also assert that the total does not overflow at scale 2.0?After pumping, check `tester.takeException()` is null, since a `RenderFlex` overflow is reported as a `FlutterError` during layout in tests. Pair it with the golden so the failure names the overflow instead of only showing a pixel diff.
A ValueVariant golden suite is like a print shop proofing one receipt layout on white paper, on black paper and in large print: one design, several proofs, each filed under its own name so a reviewer compares like with like.
saying these in an interview costs you the question
- Reusing one golden file name for every theme and scale case
- Setting a test text scale and never resetting it
- Hard-coding dark colours in the test instead of switching the theme
- Covering every scale from 1.0 to 3.0 in steps of 0.1 with goldens
- One light-theme golden also proves the dark theme is correct