Creating Scenarios

This page focuses on implementing scenarios for widget tests. For concepts and motivation, see the Testing Overview.

Create Test Scenarios

Define scenarios directly in a story's scenarios list. This is where you describe each state you want to test. In scenarios, values are fixed on purpose. Unlike adjustable story controls via Args, test scenarios should hard-code widget parameters so each run stays deterministic and reproducible.

The following example defines three scenarios: Default, Hello, and Dark.

final $Default = _Story(
  scenarios: [
    _Scenario(
      name: 'Default',
    ),
    _Scenario(
      name: 'Hello',
      args: _Args.fixed(
        text: 'Hello, Widgetbook!',
      ),
    ),
    _Scenario(
      name: 'Dark',
      modes: [
        MaterialThemeMode('Dark', ThemeData.dark()),
      ],
    ),
  ]
);

Create Interaction Scenarios

Use the run callback in a Scenario to define the full test flow before snapshots are captured. This can include user interactions, pumping frames, waiting for UI updates, and assertions.

final $Counter = _Story(
  scenarios: [
    _Scenario(
      name: 'Incremented',
      run: (tester, args) async {
        await tester.tap(find.byIcon(Icons.add));
        await tester.pumpAndSettle();

        expect(
          find.text('${args.initialValue + 1}'),
          findsOneWidget,
        );
      },
    ),
  ],
);

For animated widgets, you can pump specific frame durations to verify intermediate states before the final frame:

final $AnimatedCard = _Story(
  scenarios: [
    _Scenario(
      name: 'Mid Animation',
      run: (tester, args) async {
        await tester.tap(find.text('Expand'));

        // Start the animation.
        await tester.pump();

        // Move to an intermediate frame.
        await tester.pump(const Duration(milliseconds: 150));

        expect(find.text('Animating...'), findsOneWidget);
      },
    ),
    _Scenario(
      name: 'Animation Completed',
      run: (tester, args) async {
        await tester.tap(find.text('Expand'));

        // Wait for all animation frames to complete.
        await tester.pumpAndSettle();

        expect(find.text('Expanded Content'), findsOneWidget);
      },
    ),
  ],
);

Define Global Scenario Definitions

Use global scenario definitions when the same scenario setup should be available across many components. Configure them in widgetbook.config.dart.

This works especially well for validating common cross-cutting setups, such as themes, locales, or viewports.

final config = Config(
  // ...
  scenarios: [
    ScenarioDefinition(
      name: 'Dark Mode',
      modes: [MaterialThemeMode('Dark', ThemeData.dark())],
    ),
    ScenarioDefinition(
      name: 'Light Mode',
      modes: [MaterialThemeMode('Light', ThemeData.light())],
    ),
  ],
);

How Global Scenario Definitions Are Applied

Global scenario definitions are applied to every story. During execution, Widgetbook builds the effective list as:

  • all global scenario definitions from config.scenarios
  • followed by local scenarios defined in the story

Each global definition becomes its own scenario per story; global definitions are not merged into a single local scenario object.

How Modes Are Merged

Modes lock addon values to fixed settings for testing. For each scenario (global or local), modes are resolved by merging:

  • story-level modes
  • scenario-level modes

By default, scenario-level modes take precedence when both define the same mode type.

Example: Inheritance + Merge

If your config defines a global scenario:

ScenarioDefinition(
  name: 'Dark Mode',
  modes: [MaterialThemeMode('Dark', ThemeData.dark())],
)

and a story defines:

final $Button = _Story(
  modes: [
    ViewportMode(const ViewportData.constrained(name: '800w', maxWidth: 800)),
  ],
  scenarios: [
    _Scenario(
      name: 'Loading',
      args: _Args.fixed(isLoading: true),
    ),
  ],
);

then the story runs with two scenarios:

  • Dark Mode (inherited from global definitions)
  • Loading (local scenario)

The inherited Dark Mode scenario also receives the story viewport mode (800w) because story and scenario modes are merged for each scenario.