Skip to main content

Test widget apps

Widget tests run through the same message and rendering pipeline as a real app. You can mount a widget, send keyboard or mouse input, inspect the rendered terminal, and reproduce tricky resize or flicker bugs without opening an interactive session.

Start with WidgetTester

The WidgetTester is the primary tool for testing widgets. It is designed to feel familiar to Flutter developers while adapting to the terminal environment.

What it can do

  • Production Pipeline: Unlike simple state-to-view tests, WidgetTester runs a real Program with a mock terminal. Every event follows the full path: send(msg) -> update(msg) -> view() -> render().
  • Deterministic Frames: Capture snapshots of the rendered view after each message or pump.
  • Input Simulation: Send keys, paste text, simulate mouse clicks, and drag gestures.
  • Render-Tree Hit Testing: Interact with widgets using their actual coordinates on the terminal screen.

Basic Usage

Use the testWidgets helper to manage the tester lifecycle:

void main() {
testWidgets('increments counter on tap', (tester) async {
// Mount the widget
await tester.pumpWidget(MyCounterWidget());

// Assert on initial state
expect(tester.find.text('Count: 0'), isTrue);

// Find and tap a widget by its text content
tester.tap(tester.find.textLocation('Increment'));

// Assert on updated state
expect(tester.find.text('Count: 1'), isTrue);
});
}

Finding Widgets

The tester.find helper provides several ways to locate elements:

  • find.text('hello'): Check if text is visible.
  • find.textLocation('button'): Get coordinates for tapping.
  • find.byType<MyWidget>(): Find by widget class.
  • find.byKey(MyKey): Find by explicit widget key.

Stress Testing (Widget Storms)

To ensure robustness, Artisanal includes "Storm" testing—the ability to flood a widget with high-frequency, deterministic events.

Storm Patterns

  • keyboardStorm: Rapid-fire character input.
  • mouseFlood: High-frequency mouse motion.
  • mixedBurst: Interleaved keys, mouse events, resizes, and pumps.
  • pathologicalResize: Rapidly oscillating between extreme viewport sizes (e.g., 1x1 to 200x80).
testWidgets('handles pathological resizes', (tester) async {
await tester.pumpWidget(MyComplexLayout());

final result = tester.runStorm(
WidgetStormProfile.pathologicalResize(count: 50),
);

expect(result.passed, isTrue);
});

Flicker Analysis

Visual quality is critical in TUI apps. The FlickerAnalyzer scans the raw terminal output emitted during tests to detect patterns that cause visual artifacts or "tearing."

Detected Issues

  • Partial Clears: Using EL (Erase Line) or ED (Erase Display) in the middle of a frame, which might briefly expose blank cells before text is written over them.
  • Unsynchronized Output: Writing visible characters outside of "synchronized update" (DECSET 2026) brackets.
  • Interleaved Writes: Starting a new frame before the previous one has finished its sync bracket.
testWidgets('renders without flicker', (tester) async {
await tester.pumpWidget(MyWidget());

// Run some interactions...
tester.sendKey('a');

// Analyze the raw terminal output
final analysis = tester.analyzeFlicker(requireSynchronizedOutput: true);
analysis.assertFlickerFree();
});

The Widget Gauntlet

The WidgetGauntlet is a high-level composite check that runs multiple storm profiles and a flicker analysis in a single pass, producing a structured "evidence bundle."

final gauntlet = WidgetGauntlet(
config: WidgetGauntletConfig(
analyzeFlicker: true,
stormProfiles: [
WidgetStormProfile.mixedBurst(),
WidgetStormProfile.pathologicalResize(),
],
),
);

final result = await gauntlet.run(tester: tester, widget: MyWidget());
expect(result.passed, isTrue);

Deterministic Testing Helpers

  • ManualClock: Control time for animation tests. Use tester.advanceAnimation(controllerId, clock, delta: Duration(...)) to precisely step through frames.
  • HarnessArtifactManifest: Structured metadata about a test run, including logs, frame snapshots, and replay scripts.