flutter_beacon_widget_extension

Automatic E2E UI mapping for Flutter. Scans your presentation layer for interactive widgets and generates *.beacon.dart files — so AI tools (Claude + Maestro) can write stable E2E flows without brittle text-matching.

How it works

The build_runner builder scans every .dart file matching your configured glob and produces a *.beacon.dart alongside it. Each generated file lists every interactive widget found, with its stable key, semantic type, valid actions, and a pre-built Maestro YAML snippet.

Three ways a widget gets tracked

1. Native Flutter widgets — just add a ValueKey

No annotation needed. The builder recognizes 50+ native interactive widgets:

TextField(
  key: const ValueKey('login_email_field'),
  controller: _controller,
)

ElevatedButton(
  key: const ValueKey('login_submit_button'),
  onPressed: _submit,
  child: const Text('Entrar'),
)

ListView(
  key: const ValueKey('results_list'),
  children: [...],
)

Supported: GestureDetector, InkWell, TextField, TextFormField, ElevatedButton, TextButton, OutlinedButton, FilledButton, IconButton, FloatingActionButton, ListView, GridView, PageView, Switch, Checkbox, Radio, Slider, ChoiceChip, FilterChip, and 30+ more.

2. BeaconWidget — for custom components

BeaconWidget(
  id: 'radar_card',
  description: 'Radar alert card — tap for details, swipe to dismiss',
  type: BeaconType.card,
  actions: [BeaconAction.tap, BeaconAction.swipe],
  child: RadarCard(radar: radar),
)

3. @Beacon — screen-level documentation

@Beacon(
  description: 'Login screen — email + password form, errors as SnackBar',
  type: BeaconType.screen,
)
class LoginPage extends StatefulWidget { ... }

Setup

pubspec.yaml:

dependencies:
  flutter_beacon_widget_extension: ^0.1.0

dev_dependencies:
  build_runner: ^2.0.0

build.yaml (scope the generator to your UI layer):

targets:
  $default:
    builders:
      flutter_beacon_widget_extension|flutter_beacon_widget_extension:
        enabled: true
        generate_for:
          - lib/features/**/presentation/**/*.dart

.gitignore (generated files are reproducible — don't commit them):

*.beacon.dart

Generate:

flutter pub run build_runner build

Generated output

login_page.dartlogin_page.beacon.dart:

// GENERATED BY flutter_beacon_widget_extension — DO NOT EDIT.
import 'package:flutter_beacon_widget_extension/flutter_beacon_widget_extension.dart';

const List<BeaconInfo> loginPageBeacons = [
  BeaconInfo(
    key: 'login_page',
    description: 'Login screen — email + password form',
    type: BeaconType.screen,
    actions: [BeaconAction.assertVisible],
    widgetClass: 'LoginPage',
    sourcePath: 'lib/features/auth/presentation/pages/login_page.dart',
    sourceLine: 42,
  ),
  BeaconInfo(
    key: 'login_email_field',
    description: 'Login email field',
    type: BeaconType.textField,
    actions: [BeaconAction.tap, BeaconAction.input, BeaconAction.assertVisible],
    widgetClass: 'TextField',
    sourcePath: 'lib/features/auth/presentation/pages/login_page.dart',
    sourceLine: 91,
  ),
  ...
];

Each BeaconInfo has a maestroHint getter that returns ready-to-paste Maestro YAML:

loginPageBeacons[1].maestroHint
// - tapOn:
//     id: "login_email_field"
// - inputText:
//     id: "login_email_field"
//     text: "<value>"
// - assertVisible:
//     id: "login_email_field"

Using with Maestro

Read the .beacon.dart file before writing a flow. Use the key as id: — never text-match:

# login_flow.yaml
appId: com.example.myapp
---
- assertVisible:
    id: "login_page"
- tapOn:
    id: "login_email_field"
- inputText: "user@example.com"
- tapOn:
    id: "login_submit_button"

CLAUDE.md rule

Add this to your project's CLAUDE.md so Claude always reads beacon files before writing flows:

Before writing any Maestro flow for a screen, read the *.beacon.dart file
generated alongside the page file. Use the key field as id: in Maestro — never text-match.

Libraries

builder
flutter_beacon_widget_extension
flutter_beacon — automatic E2E UI mapping for AI-assisted testing.