checkLayout function
Future<LayoutReport>
checkLayout(
- WidgetTester tester,
- Widget screen, {
- List<
DeviceSize> devices = DeviceSize.common, - List<
double> textScales = const <double>[1.0], - bool settle = false,
- bool wrapInMaterialApp = true,
Render screen on every size in devices (optionally at several text
scales) inside a widget test and report which combinations overflow.
This is the CI-facing entry point. Call it from inside testWidgets:
testWidgets('checkout fits every phone', (tester) async {
final report = await checkLayout(
tester,
const CheckoutScreen(),
devices: DeviceSize.all,
textScales: const [1.0, 1.3], // also test large-font users
settle: true, // let async content finish loading
);
expectNoOverflow(report);
});
devices: which screen sizes to render at. Defaults to DeviceSize.common.textScales: font scale factors to test at. Large-font accessibility settings are a common overflow cause, so testing at e.g.1.3catches bugs a default-size check misses. RequireswrapInMaterialApp= true for any value other than1.0.settle: if true, callspumpAndSettleafter the first frame so screens that load data asynchronously render their real content before the check.wrapInMaterialApp: wrapsscreenin aMaterialAppfor realistic full-window constraints. Set false ifscreenis already a full app.
Implementation
Future<LayoutReport> checkLayout(
WidgetTester tester,
Widget screen, {
List<DeviceSize> devices = DeviceSize.common,
List<double> textScales = const <double>[1.0],
bool settle = false,
bool wrapInMaterialApp = true,
}) async {
assert(textScales.isNotEmpty, 'Provide at least one text scale.');
assert(
wrapInMaterialApp || textScales.every((s) => s == 1.0),
'textScales other than 1.0 require wrapInMaterialApp: true.',
);
final results = <DeviceResult>[];
// Make sure the fake screen is restored no matter what the test does next.
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
for (final device in devices) {
for (final scale in textScales) {
final recorder = OverflowRecorder()..start();
tester.view.devicePixelRatio = device.pixelRatio;
tester.view.physicalSize = device.physicalSize;
final Widget content = wrapInMaterialApp
? MaterialApp(key: UniqueKey(), home: _withTextScale(screen, scale))
: KeyedSubtree(key: UniqueKey(), child: screen);
try {
// Tear the previous tree down and give the new one a unique key so
// Flutter builds FRESH render objects for every combination. Without
// this, render objects are reused and Flutter de-duplicates the
// overflow error -- so only the first overflowing size is caught.
await tester.pumpWidget(const SizedBox.shrink());
await tester.pumpWidget(content);
if (settle) {
await tester.pumpAndSettle();
}
} finally {
recorder.stop();
// Overflow was intercepted by the recorder, so nothing is queued --
// but drain defensively so a stray non-overflow error can't fail a
// later step of the test unexpectedly.
tester.takeException();
}
results.add(
DeviceResult(
device: device,
textScale: scale,
overflows: recorder.issues,
),
);
}
}
return LayoutReport(results);
}