# Creating Scenarios

This page focuses on implementing scenarios for widget tests. For concepts and motivation, see the [Testing Overview](/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`.

```dart
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.

```dart
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:

```dart
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.

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

### How Global Scenario Definitions Are Applied

Each definition has a `strategy` that controls how it is expanded for every story:

- `ScenarioStrategy.perScenario` (default): the definition is crossed with each
  of the story's local scenarios. Every local scenario gets one variant per
  definition that keeps the local scenario's args and `run` callback and merges
  in the definition's modes. The bare local scenarios are replaced by their
  variants. For stories without local scenarios, the definition is applied to
  the story's default state instead, producing one scenario named after the
  definition.
- `ScenarioStrategy.perStory`: one standalone scenario per story, built with
  the story's default args and without a `run` callback. The definition's modes
  are not applied to the story's local scenarios.
- `ScenarioStrategy.both`: the union of the two. For stories without local
  scenarios, only the standalone scenario is created.

With the config above, a story with local scenarios `Loading` and `Error` runs
four scenarios: `Loading • Dark Mode`, `Loading • Light Mode`,
`Error • Dark Mode`, and `Error • Light Mode`. A story without local scenarios
runs two: `Dark Mode` and `Light Mode`.

Use `perScenario` (the default) for dimensions that every state should be
tested in, such as themes. Use `perStory` for setups where one snapshot per
story is enough, such as viewports:

```dart
ScenarioDefinition(
  name: 'iPhone 13',
  modes: [ViewportMode(IosViewports.iPhone13)],
  strategy: ScenarioStrategy.perStory,
),
```

### How Scenario Names Are Built

Crossed scenarios are named by the `nameBuilder` of your `ScenarioConfig`. The
default combines the local scenario's name with the definition's name, e.g.
`Loading • Dark Mode`. When a story has no local scenarios, the scenario is
named after the definition only, e.g. `Dark Mode`. Standalone scenarios from
`perStory` definitions are always named after the definition.

Provide your own `nameBuilder` to customize the format:

```dart
scenarioConfig: ScenarioConfig(
  definitions: [ /* ... */ ],
  nameBuilder: (definition, story, scenario) =>
      '${scenario.name} (${definition.name})',
),
```

Scenario names must not contain `/`, which is reserved for scenario paths.

### How Modes Are Merged

[Modes](/addons/modes) lock addon values to fixed settings for testing. For
each scenario, modes are resolved by merging:

- local scenario modes (highest precedence)
- definition modes (for crossed scenarios)
- story-level modes (lowest precedence)

When two levels define the same mode type, the more specific level wins. For
example, a local scenario that pins a `MaterialThemeMode` keeps it even when
crossed with a `Dark Mode` definition.

### Example: Inheritance + Merge

If your config defines a global scenario definition:

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

and a story defines:

```dart
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 one scenario, `Loading • Dark Mode`, which combines
all three levels:

- the loading args from the local scenario
- the dark theme mode from the definition
- the `800w` viewport mode from the story
