Advanced¶
Deep configuration for golden_matrix: typed scenarios, filtering rules, RTL, tolerance, skipping, wrappers, dependency injection, post-pump state, custom theme data, dry-run previews, and font loading.
See also: Sampling · Devices · Reports · CI integration · Migration guide · Home.
Typed scenarios¶
A common case is one widget rendered across several state-manager states
(loading / loaded / error / empty from a Cubit, Bloc, Riverpod provider, or
ChangeNotifier). MatrixScenario.typed<T> attaches a compile-time-checked
payload to each scenario and feeds it to a builder you can reuse across all
of them — no stringly-typed switch on the scenario name, no per-scenario
BlocProvider boilerplate.
sealed class UserState { ... } // your state-manager state
Widget buildUserList(UserState state) => BlocProvider<UserCubit>(
create: (_) => UserCubit()..emit(state),
child: const UserListScreen(),
);
matrixGolden(
'UserList',
scenarios: [
MatrixScenario.typed('loading', payload: const UserState.loading(), builder: buildUserList),
MatrixScenario.typed('loaded', payload: UserState.loaded([alice, bob]), builder: buildUserList),
MatrixScenario.typed('error', payload: const UserState.error('Timeout'), builder: buildUserList),
MatrixScenario.typed('empty', payload: const UserState.empty(), builder: buildUserList),
],
axes: const MatrixAxes(themes: [MatrixTheme.light, MatrixTheme.dark]),
);
T is inferred from payload/builder (write MatrixScenario.typed<UserState>(...)
to pin it). Each scenario can carry a different payload type — the typedness is
per-scenario, so it composes with every axis (themes × locales × devices × scales).
Non-breaking
MatrixScenario.typed is additive — the plain MatrixScenario(name, builder: () => widget)
constructor is unchanged. Internally the typed builder is wrapped into the
zero-argument builder (it closes over the payload), so nothing else in the
matrix changes. The payload is also exposed as scenario.payload (typed as
Object?) for introspection.
Mocks, GetIt, and per-combination setup¶
Each combination is its own testWidgets block, so setUp/tearDown run
per-combination (fresh mocks) and setUpAll runs once per matrix. That layers
cleanly with the typed builder, which is the right home for per-scenario stub
variations:
late MockUserRepository repo;
setUpAll(() => provideDummy<Either<Failure, List<User>>>(const Right([])));
setUp(() {
repo = MockUserRepository();
GetIt.I.registerSingleton<UserRepository>(repo);
});
tearDown(() => GetIt.I.reset());
Widget build(UserPayload p) {
when(repo.fetchUsers()).thenAnswer((_) async =>
p.error != null ? Left(Failure(p.error!)) : Right(p.users));
return BlocProvider<UserCubit>(
create: (_) => UserCubit(repo: repo)..loadUsers(),
child: const UserListScreen(),
);
}
matrixGolden('UserList', scenarios: [
MatrixScenario.typed('loaded', payload: UserPayload(users: [alice, bob]), builder: build),
MatrixScenario.typed('empty', payload: const UserPayload(), builder: build),
MatrixScenario.typed('error', payload: const UserPayload(error: 'net'), builder: build),
]);
Rules¶
Filter the generated combinations with predicates. Rules compose and run sequentially.
MatrixRule.exclude((c) => c.theme.name == 'dark' && c.textScale > 1.5)
MatrixRule.includeOnly((c) => c.device.name == 'phoneSmall' || c.device.name == 'tablet')
Passed to a test via the rules list:
matrixGolden(
'ProfileCard',
scenarios: [
MatrixScenario('loading', builder: () => const ProfileCard.loading()),
MatrixScenario('data', builder: () => ProfileCard(user: fakeUser)),
MatrixScenario('error', builder: () => const ProfileCard.error('Timeout')),
],
axes: MatrixAxes(
themes: [MatrixTheme.light, MatrixTheme.dark],
locales: [Locale('en'), Locale('ru'), Locale('ar')],
textScales: [1.0, 2.0],
devices: [MatrixDevice.iphoneSE, MatrixDevice.galaxyA51, MatrixDevice.tablet],
),
rules: [
MatrixRule.exclude((c) => c.locale.languageCode != 'ar' && c.direction == TextDirection.rtl),
],
);
Direction / RTL auto-inference¶
Arabic, Hebrew, and Farsi locales automatically get TextDirection.rtl — no manual setup. Combinations expose c.direction, which you can read in rules (see above) or flip on a one-off combination with copyWith:
Tolerance¶
Allow small pixel differences for stable CI:
matrixGolden(
'Widget',
scenarios: [...],
axes: axes,
tolerance: 0.05 / 100, // 0.05% pixel diff allowed
);
Skip¶
Conditionally skip tests (e.g. platform-specific golden files):
Custom Wrapper (wrapChild)¶
Override the default Scaffold(body: Center(child:)) layout. wrapChild runs inside the auto-built MaterialApp.home, so it's the right level for layout shells (padding, alignment), Theme overrides, or scoped providers that live below MaterialApp:
matrixGolden(
'Widget',
scenarios: [...],
axes: axes,
wrapChild: (child) => child, // no Scaffold, no Center
);
App-level decorator (wrapApp)¶
Wrap the auto-built MaterialApp from outside. This is the seam for DI providers that must sit above MaterialApp — ProviderScope (Riverpod), BlocProvider / MultiBlocProvider, MultiProvider, or any root-level InheritedWidget (e.g. brand theme scopes). The callback receives the current combination so providers can vary per scenario:
matrixGolden(
'ProfileCard',
scenarios: [...],
axes: axes,
// Riverpod
wrapApp: (app, combination) => ProviderScope(
overrides: [
userRepoProvider.overrideWithValue(FakeUserRepo()),
],
child: app,
),
);
matrixGolden(
'CounterCard',
scenarios: [
MatrixScenario('zero', builder: () => const CounterCard()),
MatrixScenario('high', builder: () => const CounterCard()),
],
axes: axes,
// Bloc, varying state by scenario
wrapApp: (app, c) => BlocProvider<CounterBloc>.value(
value: FakeCounterBloc(c.scenario.name == 'high' ? 99 : 0),
child: app,
),
);
wrapApp is complementary to wrapChild:
wrapAppsits above MaterialApp (DI providers)wrapChildsits inside MaterialApp.home (layout shells)
Use them together when needed. When wrapApp is omitted, the widget tree is identical to previous versions — existing goldens unaffected.
For full-screen tests where you want even more control, use screenMatrixGolden with its appBuilder.
Post-pump state: setup, freezeAnimations, captureAfter¶
Three orthogonal parameters (available on both matrixGolden and screenMatrixGolden) for snapshotting non-initial states.
setup — interact before capture¶
matrixGolden(
'LoginForm',
scenarios: [MatrixScenario('validation_error', builder: () => const LoginForm())],
axes: axes,
setup: (tester, combination) async {
await tester.enterText(find.byKey(emailKey), 'bad-email');
await tester.tap(find.byKey(submitKey));
await tester.pumpAndSettle();
},
);
Runs after pumpAndSettle, before the golden is captured. Use to tap, scroll, enter text, expand menus — anything needed to bring the widget into the visual state you want to snapshot.
freezeAnimations — kill infinite shimmer/skeletons¶
matrixGolden(
'UserCardSkeleton',
scenarios: [MatrixScenario('loading', builder: () => const UserCardSkeleton())],
axes: axes,
freezeAnimations: true, // halts Tickers below — snapshot is stable
);
Wraps the widget tree in TickerMode(enabled: false). Halts every AnimationController / Ticker, including shimmer, skeleton loaders, Lottie, breathing dots, marquee — all the things that otherwise make pumpAndSettle hang or produce non-deterministic frames.
captureAfter — snapshot a specific frame¶
matrixGolden(
'SlideInDialog',
scenarios: [MatrixScenario('mid_slide', builder: () => const SlideInDialog())],
axes: axes,
captureAfter: const Duration(milliseconds: 150), // catch dialog half-open
);
Pumps the test clock for the given duration after settling (and after setup), before capture. Use to catch a deterministic mid-animation frame.
Composing them¶
All three combine cleanly:
matrixGolden(
'FormAfterSubmit',
scenarios: [...],
axes: axes,
setup: (tester, _) async {
await tester.tap(submitButton);
await tester.pump(); // one frame so the loader appears
},
freezeAnimations: true, // freeze the loader spinner
);
Custom Theme Data¶
Attach arbitrary context to themes — custom theme systems, brand configs, feature flags:
matrixGolden(
'Widget',
scenarios: [...],
axes: MatrixAxes(
themes: [
MatrixTheme.custom('light', ThemeData.light(), data: MyTheme.light()),
MatrixTheme.custom('dark', ThemeData.dark(), data: MyTheme.dark()),
],
),
);
// Access in screenMatrixGolden appBuilder:
appBuilder: (combination) {
final myTheme = combination.theme.data as MyTheme;
return MyThemeProvider(theme: myTheme, child: MaterialApp(...));
}
Dry-run preview¶
Inspect what a matrixGolden / screenMatrixGolden call would do — combination counts, sampled list, golden paths, collisions — without rendering widgets or writing files:
final preview = previewMatrixGolden(
name: 'PrimaryButton',
scenarios: [MatrixScenario('default', builder: () => const PrimaryButton())],
axes: MatrixAxes(
themes: [MatrixTheme.light, MatrixTheme.dark],
locales: [Locale('en'), Locale('ar')],
),
sampling: MatrixSampling.pairwise,
);
print(preview);
// PrimaryButton
// Scenarios: 1 (default)
// Raw combinations: 4
// After rules: 4
// After sampling (pairwise): 4
// Combinations:
// 1. default | light ltr en 1.0x phoneSmall
// -> goldens/primarybutton/default/light_en_ltr_1x_phonesmall.png
// ...
preview.afterSamplingCount; // 4
preview.goldenPaths; // list of paths the runner would write
preview.duplicatePaths; // non-empty when scenarios collide on the same path
Use it to sanity-check scenarioTags, estimate CI cost before adding a new axis, or catch golden-path collisions before they silently overwrite each other.
Font loading¶
Set up font loading once in test/flutter_test_config.dart so real fonts (Roboto + app fonts) render instead of Ahem squares:
// test/flutter_test_config.dart
import 'dart:async';
import 'package:golden_matrix/golden_matrix.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
await loadAppFonts();
return testMain();
}
Layout-deterministic tests (since 0.18.0)
loadAppFonts(textFonts: false) loads only icon fonts and uses Ahem
placeholders for text. Text geometry becomes predictable across
macOS/Linux CI, while icons still render with real glyphs for review.