genui_mock 0.1.1
genui_mock: ^0.1.1 copied to clipboard
Record, save, and replay GenUI streaming sessions as deterministic, git-diffable JSON fixtures. Test Catalog widgets against real recorded model output with zero network calls.
genui_mock #
Record a real GenUI streaming session once, save it as a small git-diffable JSON fixture, and replay it later with zero network calls, no API key, and fully deterministic output.
Unofficial, not affiliated with Google.
genui_mockis an independent, community-built testing utility. It is not published, endorsed, or supported by Google or the Flutter team, and it has no special access to thegenuipackage beyond what's publicly published. The name referencesgenui's namespace purely to describe what this package is for.
Why #
genui is alpha, and its API moves often. Every time you tweak a widget in
your Catalog, checking whether it renders correctly usually means burning a
real API call and waiting on network latency — and even then, the model's
output isn't deterministic enough to assert against directly.
genui_mock lets you record what the model actually streamed back once, and
replay that exact byte-for-byte output as many times as you want, instantly,
in CI, with no API key required.
What genui version was this built against? #
genui 0.10.1 (the latest published version as of writing). The one
integration point this package depends on — the Transport interface — was
introduced in genui 0.8.0 and has not changed shape since (still four
methods: incomingText, incomingMessages, sendRequest, dispose); see
Design below for why that's the seam this package is built on.
genui 0.10.0 did move the A2UI message types (A2uiMessage,
CreateSurfaceMessage, etc.) out into package:a2ui_core, which is why
genui_mock now depends on a2ui_core directly too.
Install #
dev_dependencies:
genui_mock: ^0.1.1
genui_mock depends on genui (and, transitively, a2ui_core) directly
rather than just as a dev dependency, since GenUiMockTransport implements
genui's real Transport interface.
Usage #
1. Record a real session, once #
Wrap whatever Stream<String> you already feed into genui (e.g. the raw
chunks from your model API call, before A2uiTransportAdapter.addChunk) with
a GenUiSessionRecorder. It forwards every chunk through unchanged — your
app behaves exactly as it does today — while timing and collecting them.
import 'dart:io';
import 'package:genui_mock/genui_mock.dart';
final recorder = GenUiSessionRecorder(
name: 'weather_forecast',
description: 'user asks for a 3-day forecast',
);
// For each user turn, replace the stream you hand to your transport with
// the recorded version — recording is transparent to the rest of your app:
final chunks = recorder.recordTurn(rawChunksFromYourModelCall());
await for (final chunk in chunks) {
transportAdapter.addChunk(chunk); // unchanged
}
// Once the session is over, save it:
final fixture = recorder.toFixture();
await File('test/fixtures/weather_forecast.json').writeAsString(fixture.encode());
The saved fixture is plain, pretty-printed JSON — readable and diffable in code review, so you can see exactly what changed when you re-record it:
{
"formatVersion": 1,
"name": "weather_forecast",
"description": "user asks for a 3-day forecast",
"recordedAt": "2026-07-02T10:15:00.000Z",
"turns": [
{
"chunks": [
{ "text": "{\"beginRendering\": ...", "delayMs": 412 },
{ "text": "{\"surfaceUpdate\": ...", "delayMs": 96 }
]
}
]
}
2. Replay the fixture in a test #
import 'dart:io';
import 'package:genui/genui.dart';
import 'package:genui_mock/genui_mock.dart';
final fixture = GenUiFixture.decode(
await File('test/fixtures/weather_forecast.json').readAsString(),
);
final transport = GenUiMockTransport(fixture);
final conversation = Conversation(controller: myController, transport: transport);
await conversation.sendRequest(ChatMessage.user('what is the weather?'));
// `myController`'s surfaces now update exactly as they did in the real,
// recorded session — no network access, no API key, fully deterministic.
Each call to sendRequest replays the fixture's next recorded turn, so a
multi-turn fixture drives a multi-turn conversation the same way it was
recorded.
See the example for a complete runnable app that replays a fixture through GenUI's real widgets.
3. Choose how fast to replay #
// Instant (default) — for fast unit/widget tests:
GenUiMockTransport(fixture);
// Paced to the original timing — for testing loading/skeleton states:
GenUiMockTransport(fixture, mode: PlaybackMode.paced);
// Paced, but 5x faster than real time:
GenUiMockTransport(fixture, mode: PlaybackMode.paced, speedMultiplier: 5);
Design #
genui's API has shifted a lot between versions — compare the 0.5.x
line (which coupled UI rendering directly to a ContentGenerator/model
client) to 0.8.0+, which introduced Transport: a small, four-method
interface (incomingText, incomingMessages, sendRequest, dispose)
that explicitly exists to decouple genui's rendering engine from any
particular network client. That's the most stable point of contact
available in the currently published API, so it's the only thing this
package integrates against.
Concretely, genui_mock is split in two:
- Fixture / recorder / player (
GenUiFixture,GenUiSessionRecorder,GenUiFixturePlayer) — plain Dart, zero dependency onpackage:genui. These only know aboutStream<String>chunks and their timing, so they can't be broken by agenuiAPI change and are fully unit-testable without agenuidependency at all. GenUiMockTransport(lib/src/genui_mock_transport.dart) — the only file in this package that importspackage:genui. It implementsTransportby replaying a fixture throughgenui's ownA2uiTransportAdapter, so parsing the A2UI wire format out of raw text chunks is always delegated togenuiitself — this package never has to track or reimplement that protocol.
If a future genui release changes Transport, only
genui_mock_transport.dart should need to change.
What's recorded #
A fixture is a sequence of turns; each turn is the ordered list of raw text
chunks (and inter-chunk delays) that arrived after one sendRequest call —
exactly what a real Transport streams through incomingText /
incomingMessages, before any A2UI parsing happens. Recording at this layer
means the fixture captures precisely what the model said, with no lossy
intermediate representation.
License #
MIT — see LICENSE.