Testing
How to widget-test screens that navigate and use stores, with the fakes from package:inlay/testing.dart.
Screens in an add-to-app module rely on two singletons that talk to the host over
platform channels: InlayNavigator and KeyValueStorage. In a plain testWidgets
run there is no host on the other side, so every call fails with
MissingPluginException. package:inlay/ provides in-memory fakes for
both, and both singletons accept a test override.
import 'package:inlay/inlay.dart';
import 'package:inlay/testing.dart';
setUp(() {
KeyValueStorage.instance = FakeKeyValueStorage();
InlayNavigator.instance = FakeInlayNavigator();
});Nothing else about the screen under test changes - it keeps reading
KeyValueStorage. and calling InlayNavigator. exactly as
in production.
Asserting on navigation
FakeInlayNavigator records every call instead of crossing the boundary:
pushedRoutes- typed routes passed topush, in call order.pushedPages- rawPageSettingsfrom all dispatch methods, so native-route, dialog, andpushForResult()pushes are visible too.resultsByRouteId- scripted results for the*ForResultmethods, keyed byrouteId. Missing entries resolve tonull, like a screen dismissed without a result. The generated key isrouteNameon Flutter routes (the path template) and dialog routes (the camel-cased class name); native routes expose it asrouteId.popCount- container pops;maybePopstill pops an in-Flutter route when it can.
testWidgets('awaits the Counter result and shows it', (tester) async {
final navigator = FakeInlayNavigator();
InlayNavigator.instance = navigator;
// Script the typed result the fake hands back to pushForResult().
navigator.resultsByRouteId[CounterPage.routeName] =
CounterPage.encodeResult(7);
await tester.pumpWidget(
const MaterialApp(
home: GreetingScreen(name: 'Test', style: GreetingStyle.casual),
),
);
await tester.pumpAndSettle();
await tester.tap(find.text('Open Counter (new engine, await result)'));
await tester.pumpAndSettle();
expect(navigator.pushedPages.single.routeId, CounterPage.routeName);
expect(find.text('Counter returned: 7'), findsOneWidget);
});Because the recorded pages use the generated wire format, assertions can go deeper
than just checking that something was pushed - you can decode the recorded
PageSettings and assert on the actual params that would have crossed the
boundary. The full version of this test
lives in example_module/; it also mocks
package_info_plus, which GreetingScreen reads on init (see the last section).
Testing screens that use stores
FakeKeyValueStorage is an in-memory map with the same semantics as the real storage.
This includes the part that matters most for correctness: your own writes don't notify
your own stream, because the real platform suppresses self-notifications. There are
three touch points:
data- the raw backing map, for inspection or low-level seeding.- Seeding through the generated store - the natural way to arrange state, since it uses the same key encoding as production code.
simulateExternalChange(entries)- plays the role of another engine or native code writing to storage: it applies the entries and fires the stream, which is exactly what a cross-engine sync looks like from this isolate.
testWidgets('reacts to a change from native code', (tester) async {
final storage = FakeKeyValueStorage();
KeyValueStorage.instance = storage;
await UserPreferencesStore(storage, userId: '42').setDisplayName('Marcin');
await tester.pumpWidget(const MaterialApp(home: ProfileScreen(userId: '42')));
await tester.pumpAndSettle();
expect(find.text('Display name: Marcin'), findsOneWidget);
storage.simulateExternalChange([
StorageEntry(key: 'user_preferences/42/displayName', value: 'From native'),
]);
await tester.pumpAndSettle();
expect(find.text('Display name: From native'), findsOneWidget);
});This test covers the whole reactive chain (store, InlayCubit, widget rebuild) without
a platform on either end.
What the fakes don't cover
Plugins are a separate concern - a screen calling package_info_plus or any other
plugin still needs that plugin's own test hooks (PackageInfo.
and friends). Widget tests also can't tell you whether the native side decodes your
routes correctly. For that you need a thin native UI test that crosses into a Flutter
screen - see Plugins in a multi-engine setup for the pattern.
The example module ships both kinds of tests: widget tests in example_module/,
and native UI tests in both hosts (XCUITests in the iOS host and UiAutomator tests
in the Android host).