automation_keys_gen 0.1.3
automation_keys_gen: ^0.1.3 copied to clipboard
Enforces a consistent automation key contract for Flutter UI tests, with CLI validation and generated Markdown documentation.
example/lib/main.dart
import 'package:automation_keys_gen/automation_keys_gen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const ExampleApp());
}
class ExampleApp extends StatelessWidget {
const ExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/onboarding/welcome',
routes: {
'/onboarding/welcome': (_) => const WelcomePage(),
'/onboarding/intro': (_) => const IntroPage(),
'/profile/settings': (_) => const SettingsPage(),
'/catalog/list': (_) => const CatalogListPage(),
'/catalog/detail': (_) => const CatalogDetailPage(),
},
);
}
}
/// ONBOARDING / welcome
///
/// Demonstrates a simple page without screen variants.
@AutomationKeyDescription('Onboarding welcome screen (entry point).')
class WelcomePage extends StatelessWidget with AutomationKeyHelper {
const WelcomePage({super.key});
@override
String get moduleName => 'onboarding';
@override
String get featureName => 'welcome';
@override
Widget build(BuildContext context) {
@AutomationKeyDescription('Page title text in the AppBar.')
final pageTitleKey = textKey('page_title');
@AutomationKeyDescription('Main headline text on the welcome page.')
final titleKey = textKey('headline');
@AutomationKeyDescription('Supporting subtitle text below headline.')
final subtitleKey = textKey('subtitle');
@AutomationKeyDescription('Root content container for the welcome page.')
final contentContainerKey = containerKey('content');
@AutomationKeyDescription('Primary CTA button to continue onboarding.')
final continueKey = buttonKey('continue');
return Scaffold(
key: pageKey,
appBar: AppBar(
title: Text('Welcome', key: pageTitleKey),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
key: contentContainerKey,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Welcome!', key: titleKey, style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 12),
Text('This example shows how to build stable automation keys.', key: subtitleKey),
const Spacer(),
ElevatedButton(
key: continueKey,
onPressed: () => Navigator.of(context).pushNamed('/onboarding/intro'),
child: const Text('Continue'),
),
],
),
),
);
}
}
/// ONBOARDING / intro
///
/// Demonstrates a screen variant via [screenName] and an overlay key.
@AutomationKeyDescription('Onboarding intro flow (step one). Demonstrates screen variant + overlays.')
class IntroPage extends StatelessWidget with AutomationKeyHelper {
const IntroPage({super.key});
@override
String get moduleName => 'onboarding';
@override
String get featureName => 'intro';
@override
String? get screenName => 'step_one';
@override
Widget build(BuildContext context) {
@AutomationKeyDescription('Page title text in the AppBar.')
final pageTitleKey = textKey('page_title');
@AutomationKeyDescription('Info icon in the AppBar. Opens info dialog.')
final infoIconKey = iconKey('info');
@AutomationKeyDescription('Info dialog container key (overlay).')
final infoDialogKey = dialogKey('info');
@AutomationKeyDescription('Info dialog title text.')
final dialogTitleKey = textKey('dialog_title');
@AutomationKeyDescription('Info dialog body text.')
final dialogBodyKey = textKey('dialog_body');
@AutomationKeyDescription('Info dialog close button.')
final dialogCloseKey = buttonKey('dialog_close');
@AutomationKeyDescription('Help floating action button on the intro page.')
final helpFabKey = fabKey('help');
@AutomationKeyDescription('Intro step title text.')
final titleKey = textKey('title');
@AutomationKeyDescription('Language dropdown control in this step.')
final languageDropdownKey = dropdownKey('language');
@AutomationKeyDescription('Next button in onboarding intro step one.')
final nextKey = buttonKey('next');
return Scaffold(
key: pageKey,
appBar: AppBar(
title: Text('Intro', key: pageTitleKey),
actions: [
IconButton(
key: infoIconKey,
icon: const Icon(Icons.info_outline),
onPressed: () {
showDialog<void>(
context: context,
builder: (_) => AlertDialog(
key: infoDialogKey,
title: Text('Info', key: dialogTitleKey),
content: Text('This dialog demonstrates overlay keys.', key: dialogBodyKey),
actions: [
TextButton(
key: dialogCloseKey,
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
),
);
},
),
],
),
floatingActionButton: FloatingActionButton(
key: helpFabKey,
onPressed: () {},
child: const Icon(Icons.help_outline),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Step 1', key: titleKey),
const SizedBox(height: 12),
DropdownButton<String>(
key: languageDropdownKey,
value: 'en',
items: const [
DropdownMenuItem(value: 'en', child: Text('English')),
DropdownMenuItem(value: 'id', child: Text('Bahasa')),
],
onChanged: (_) {},
),
const Spacer(),
ElevatedButton(
key: nextKey,
onPressed: () => Navigator.of(context).pushNamed('/profile/settings'),
child: const Text('Next'),
),
],
),
),
);
}
}
/// PROFILE / settings
///
/// Demonstrates appNameOverride (page-level override)
/// and common form controls.
@AutomationKeyDescription('User settings screen (profile module). Demonstrates appNameOverride + form controls.')
class SettingsPage extends StatelessWidget with AutomationKeyHelper {
const SettingsPage({super.key});
@override
String? get appNameOverride => 'demo';
@override
String get moduleName => 'profile';
@override
String get featureName => 'settings';
@override
Widget build(BuildContext context) {
@AutomationKeyDescription('Page title text in the AppBar.')
final pageTitleKey = textKey('page_title');
@AutomationKeyDescription('Main form container for settings.')
final formContainerKey = containerKey('form');
@AutomationKeyDescription('Section title: Preferences.')
final sectionTitleKey = textKey('section_title');
@AutomationKeyDescription('Display name input field.')
final displayNameKey = inputKey('display_name');
@AutomationKeyDescription('Notifications toggle switch.')
final notificationsKey = toggleKey('notifications');
@AutomationKeyDescription('Agree to terms checkbox.')
final agreeTermsKey = checkboxKey('agree_terms');
@AutomationKeyDescription('Text label for agree-to-terms checkbox.')
final agreeTermsLabelKey = textKey('agree_terms_label');
@AutomationKeyDescription('Save button. Opens confirmation bottom sheet.')
final saveKey = buttonKey('save');
@AutomationKeyDescription('Bottom sheet shown after saving settings.')
final savedSheetKey = bottomSheetKey('saved_confirmation');
@AutomationKeyDescription('Bottom sheet title text.')
final savedSheetTitleKey = textKey('bottom_sheet_title');
@AutomationKeyDescription('Info banner shown on settings page.')
final infoBannerKey = bannerKey('info');
return Scaffold(
key: pageKey,
appBar: AppBar(
title: Text('Settings', key: pageTitleKey),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
key: formContainerKey,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Preferences', key: sectionTitleKey),
const SizedBox(height: 12),
TextField(
key: displayNameKey,
decoration: const InputDecoration(labelText: 'Display name'),
),
const SizedBox(height: 12),
SwitchListTile(
key: notificationsKey,
value: true,
onChanged: (_) {},
title: const Text('Enable notifications'),
),
const SizedBox(height: 12),
Row(
children: [
Checkbox(
key: agreeTermsKey,
value: true,
onChanged: (_) {},
),
Expanded(
child: Text('I agree to terms', key: agreeTermsLabelKey),
),
],
),
const SizedBox(height: 12),
ElevatedButton(
key: saveKey,
onPressed: () {
showModalBottomSheet<void>(
context: context,
builder: (_) => SizedBox(
key: savedSheetKey,
height: 140,
child: Center(
child: Text('Saved!', key: savedSheetTitleKey),
),
),
);
},
child: const Text('Save'),
),
const SizedBox(height: 12),
Banner(
key: infoBannerKey,
message: 'Info',
location: BannerLocation.topEnd,
child: const SizedBox(height: 0),
),
],
),
),
);
}
}
/// CATALOG / list
///
/// Demonstrates stable list keys.
@AutomationKeyDescription('Catalog list screen. Demonstrates stable list keys for automation.')
class CatalogListPage extends StatelessWidget with AutomationKeyHelper {
const CatalogListPage({super.key});
@override
String get moduleName => 'catalog';
@override
String get featureName => 'list';
@override
Widget build(BuildContext context) {
return Scaffold(
key: pageKey,
appBar: AppBar(
/// @AutomationKeyDescription: Page title text in the AppBar.
title: Text('Catalog', key: textKey('page_title')),
),
body: ListView(
children: [
/// @AutomationKeyDescription: Catalog list tile for item sku_1001 (stable id).
ListTile(
key: listItemKey('sku_1001'),
/// @AutomationKeyDescription: Title text for item sku_1001.
title: Text('Item A', key: textKey('item_title_sku_1001')),
trailing: Card(
/// @AutomationKeyDescription: Card container inside list tile for item sku_1001.
key: listCardKey('sku_1001'),
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.chevron_right),
),
),
onTap: () => Navigator.of(context).pushNamed('/catalog/detail'),
),
/// @AutomationKeyDescription: Catalog list tile for item sku_1002 (stable id).
ListTile(
key: listItemKey('sku_1002'),
/// @AutomationKeyDescription: Title text for item sku_1002.
title: Text('Item B', key: textKey('item_title_sku_1002')),
trailing: Card(
/// @AutomationKeyDescription: Card container inside list tile for item sku_1002.
key: listCardKey('sku_1002'),
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.chevron_right),
),
),
onTap: () => Navigator.of(context).pushNamed('/catalog/detail'),
),
/// @AutomationKeyDescription: Catalog list tile for item sku_1003 (stable id).
ListTile(
key: listItemKey('sku_1003'),
/// @AutomationKeyDescription: Title text for item sku_1003.
title: Text('Item C', key: textKey('item_title_sku_1003')),
trailing: Card(
/// @AutomationKeyDescription: Card container inside list tile for item sku_1003.
key: listCardKey('sku_1003'),
child: const Padding(
padding: EdgeInsets.all(8),
child: Icon(Icons.chevron_right),
),
),
onTap: () => Navigator.of(context).pushNamed('/catalog/detail'),
),
],
),
);
}
}
/// CATALOG / detail
///
/// Demonstrates an error state container key.
@AutomationKeyDescription('Catalog detail screen (default variant). Demonstrates image + error state keys.')
class CatalogDetailPage extends StatelessWidget with AutomationKeyHelper {
const CatalogDetailPage({super.key});
@override
String get moduleName => 'catalog';
@override
String get featureName => 'detail';
@override
String? get screenName => 'default';
@override
Widget build(BuildContext context) {
@AutomationKeyDescription('Page title text in the AppBar.')
final pageTitleKey = textKey('page_title');
@AutomationKeyDescription('Hero image on the detail page.')
final heroImageKey = imageKey('hero');
@AutomationKeyDescription('Error state container shown when item is out of stock.')
final outOfStockKey = errorStateKey('out_of_stock');
@AutomationKeyDescription('Out-of-stock error message text.')
final errorMessageKey = textKey('error_message');
@AutomationKeyDescription('Back button to return to catalog list.')
final backKey = buttonKey('back_to_list');
return Scaffold(
key: pageKey,
appBar: AppBar(
title: Text('Detail', key: pageTitleKey),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Image(
key: heroImageKey,
image: const AssetImage('assets/placeholder.png'),
errorBuilder: (_, __, ___) => const SizedBox.shrink(),
),
const SizedBox(height: 12),
Container(
key: outOfStockKey,
padding: const EdgeInsets.all(12),
color: Colors.red.shade50,
child: Text('Out of stock', key: errorMessageKey),
),
const Spacer(),
ElevatedButton(
key: backKey,
onPressed: () => Navigator.of(context).pushNamed('/catalog/list'),
child: const Text('Back'),
),
],
),
),
);
}
}