diff --git a/.gitignore b/.gitignore
index eb6c05c..e579ee7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ migrate_working_dir/
.flutter-plugins
.flutter-plugins-dependencies
build/
+.cursor/
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 505622d..7165f42 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -12,6 +12,18 @@
"--delete-conflicting-outputs"
],
"cwd": "${workspaceFolder}/example"
+ },
+ {
+ "name": "Debug BDD CLI",
+ "request": "launch",
+ "type": "dart",
+ "program": "${workspaceFolder}/bin/bdd_flutter.dart",
+ "args": [
+ "build",
+ "--verbose",
+ "--delete-conflicting-outputs"
+ ],
+ "cwd": "${workspaceFolder}/example"
}
]
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d88db84..8f1714d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,38 @@
+## 1.0.0
+
+### BREAKING CHANGES
+
+- Removed `build_runner` dependency entirely — use `dart run bdd_flutter build` instead
+- Removed `--no-widget-test` and `--new-only` CLI flags
+- Configuration moved from `build.yaml` to `.bdd_flutter/config.yaml`
+- Generated file extensions changed from `.bdd_scenarios.g.dart` to `.bdd_scenarios.dart` and `.bdd_test.g.dart` to `.bdd_test.dart`
+
+### Features
+
+- New standalone CLI: `dart run bdd_flutter build` and `dart run bdd_flutter test`
+- Incremental test generation with manifest tracking (only regenerates changed scenarios)
+- `--force` flag to regenerate all files
+- `test` command with formatted Feature/Scenario report output
+- Custom test directory via `test_dir` config option
+- Custom scenario class suffix via `scenario_suffix` config option (e.g., `Steps` instead of `Scenario`)
+- Additional imports support via `additional_imports` config option
+- Background steps support
+- `@unitTest` / `@widgetTest` decorators at feature and scenario level
+- Scenario-level Examples tables for parameterized tests
+
+### Improvements
+
+- Clean Architecture restructure (domain, infrastructure, presentation layers)
+- Instance-based scenario classes (supports `late` fields for shared state)
+- Comprehensive documentation across all layers
+
+## 0.3.0
+
+- remove `build_runner` dependency
+- add custom CLI
+- `dart run bdd remane` to remove `.g` from generated files extension
+- `dart run bdd build` to generate files from feature files
+
## 0.2.0
- change output files extension to `.bdd_scenarios.g.dart` and `.bdd_test.g.dart`
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..d989910
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,121 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What This Project Is
+
+bdd_flutter is a Dart CLI tool that generates Flutter/Dart test files from Gherkin `.feature` files. Users write BDD scenarios in `.feature` files, run `dart run bdd_flutter build`, and get `.bdd_scenarios.dart` (step stubs) and `.bdd_test.dart` (runnable tests) files generated alongside the feature file.
+
+## Common Commands
+
+```bash
+# Run package tests (not example — example uses flutter_test)
+dart test test/parsers/ test/builders/
+
+# Run the CLI locally against the example project
+cd example && dart run bdd_flutter build
+
+# Run BDD tests with formatted report
+cd example && dart run bdd_flutter test
+
+# Force regenerate all
+cd example && dart run bdd_flutter build --force
+
+# Analyze code
+dart analyze
+
+# Get dependencies
+dart pub get
+```
+
+## Architecture
+
+Clean Architecture in `lib/src/`. The CLI entry point is `bin/bdd_flutter.dart`.
+
+### Code Generation Pipeline
+
+```
+BDDCLI.run(args)
+ → BDDController.generateFeatureTestCases(options)
+ → ConfigParser.loadConfig() # .bdd_flutter/config.yaml
+ → ManifestParser.loadManifest() # .bdd_flutter/manifest.yaml
+ → FeatureParser.parseFeature(filePath) # .feature → Feature model
+ → ScenariosFileBuilder.buildScenarioFile(feature) # Feature → .bdd_scenarios.dart
+ → TestFileBuilder.buildTestFile(feature) # Feature → .bdd_test.dart
+ → ManifestParser.saveManifest() # update manifest
+```
+
+### Layers
+
+- **`domain/`** — Core models: Feature, Scenario, Step, Decorator, Background, BDDConfig, Manifest, BuildOptions
+- **`infrastructure/parsers/`** — FeatureParser, ConfigParser, ManifestParser
+- **`infrastructure/builders/`** — ScenariosFileBuilder, TestFileBuilder (domain models → Dart code)
+- **`presentation/cli/`** — BDDCLI entry point, argument parsing
+- **`presentation/controllers/`** — BDDController orchestrates config, manifest, parsing, building
+- **`presentation/reporter/`** — BDDTestRunner (CLI `test` command), BDDReportFormatter (output formatting), BDDTestReporter (legacy, exported)
+
+### Domain Models
+
+- **Feature** — name, path, scenarios, decorators, optional background
+- **Scenario** — name, steps, optional examples table, decorators, optional customClassName
+- **Step** — keyword (Given/When/Then/And) + text with `` placeholders
+- **Decorator** — enum: `unitTest`, `widgetTest`
+- **Background** — shared setup steps applied to all scenarios in a feature
+- **BDDConfig** — generate_widget_tests, enable_reporter, ignore_features
+- **Manifest** — tracks generated features, scenario hashes for incremental builds
+- **BuildOptions** — CLI flags: widgetTest, reporter, force, newOnly
+
+### Generated Code Pattern
+
+Scenario classes use **instance methods** (not static), so users can add `late` fields for shared state between steps:
+
+```dart
+// .bdd_scenarios.dart
+class IncrementScenario {
+ Future iHaveACounter(WidgetTester tester) async { ... }
+ Future iIncrementIt(WidgetTester tester) async { ... }
+}
+
+// .bdd_test.dart
+final scenario = IncrementScenario();
+await scenario.iHaveACounter(tester);
+```
+
+### Generation Modes
+
+- **Incremental (default)** — compares scenario hashes against manifest, skips unchanged, appends new scenarios
+- **Force (`--force`)** — regenerates everything
+
+### Decorators
+
+- `@unitTest` / `@widgetTest` — on feature or scenario (scenario overrides feature)
+- Feature files only contain behavior-relevant tags; tooling config lives in `.bdd_flutter/config.yaml`
+
+### CLI Commands
+
+- `build` — Generate test files (incremental by default)
+- `build --force` — Regenerate all files regardless of changes
+- `test` — Run BDD tests with formatted Feature/Scenario report
+
+### Config File
+
+`.bdd_flutter/config.yaml`:
+- `test_dir` (string, default `test/`) — where to scan for `.feature` files
+- `generate_widget_tests` (bool, default true)
+- `ignore_features` (list of paths to skip)
+- `additional_imports` (list of imports added to every generated `_scenarios.dart`)
+- `scenario_suffix` (string, default `Scenario`) — class name suffix (e.g., `Steps`)
+
+### Manifest File
+
+`.bdd_flutter/manifest.yaml` — auto-generated, tracks per-feature paths, timestamps, and scenario hashes for incremental builds.
+
+## Coding Conventions
+
+- Use PascalCase for classes, camelCase for variables/functions, underscore_case for files
+- Always declare explicit types; avoid `dynamic`
+- One export per file
+- Functions should be <20 lines with a single purpose
+- Prefer composition over inheritance
+- Use early returns to avoid deep nesting
+- Follow Arrange-Act-Assert for tests
diff --git a/README.md b/README.md
index acd73ea..19a380b 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,34 @@
-# 🚀 BDD Flutter
+# BDD Flutter
[](https://pub.dev/packages/bdd_flutter)
-[](https://pub.dev/packages/very_good_analysis)
[](https://opensource.org/licenses/MIT)
-[](https://www.buymeacoffee.com/samderlust)
-A powerful Flutter package that simplifies Behavior Driven Development (BDD) by automatically generating test files from Gherkin feature files. Write expressive tests in plain English using Given/When/Then scenarios and let BDD Flutter handle the boilerplate code generation.
+A Flutter package that simplifies Behavior Driven Development (BDD) by automatically generating test files from Gherkin feature files. Write expressive tests in plain English using Given/When/Then scenarios and let BDD Flutter handle the boilerplate code generation.
-## ✨ Features
+## Features
-- 📝 Parse `.feature` files written in Gherkin syntax
-- ⚡ Generate boilerplate test files automatically
-- 🧪 Support for both widget tests and unit tests
-- ⚙️ Configurable test generation
+- Parse `.feature` files written in Gherkin syntax (Given/When/Then/And)
+- Generate boilerplate test files automatically
+- Support for both widget tests and unit tests
+- Incremental generation — new scenarios are appended without overwriting existing implementations
+- Instance-based scenario classes for shared state between steps
+- Background support for shared setup steps
+- Examples tables for parameterized scenarios
+- Configurable via `.bdd_flutter/config.yaml`
+- Manifest tracking in `.bdd_flutter/manifest.yaml`
-## 📦 Installation
+## Installation
-Add the following dependencies to your package's `pubspec.yaml` file:
+Add to your `pubspec.yaml`:
```yaml
dev_dependencies:
- bdd_flutter: any
- build_runner: any
+ bdd_flutter: latest
```
-## 🚀 Quick Start
+## Quick Start
-1. Create a `.feature` file in your project:
+1. Create a `.feature` file in your test folder:
```gherkin
Feature: Counter
@@ -44,150 +46,192 @@ Feature: Counter
2. Generate test files:
```bash
-flutter pub run build_runner build
+dart run bdd_flutter build
```
-3. Run your tests:
+3. Implement the generated step methods in `counter.bdd_scenarios.dart`
-```bash
-flutter test
-```
-
-## 💡 Recommendations
-
-When working with generated test files, follow these best practices:
-
-1. **Generated Files**:
-
- - Generated files will have the `.g.dart` extension (e.g., `counter_test.bdd_test.g.dart` or `counter_scenarios.g.dart`)
- - After implementing your tests, it's recommended to:
- - Remove the `.g` extension from the file name
- - Add an ignore decorator to your feature file (@ignore)
- - This will prevent the generated files from being overwritten by subsequent builds
-
-2. **Feature File Ignore**:
- Add this comment at the top of your feature file:
- ```gherkin
- @ignore
- Feature: Counter
- ```
-
-This approach ensures that:
-
-- Your implemented tests won't be overwritten by subsequent builds
-- Generated files are properly ignored in version control
-- You maintain a clean project structure
+4. Run your tests with BDD report:
-## ⚙️ Configuration
-
-Configure test generation in your `build.yaml`:
-
-```yaml
-targets:
- $default:
- builders:
- bdd_flutter|bdd_test_builder:
- options:
- generate_widget_tests: false # Default: true
- enable_reporter: true # Default: false
- ignore_features:
- - "test/features/ignored.feature"
- - "test/features/another_ignored.feature"
+```bash
+dart run bdd_flutter test
```
-### Configuration Options
+Or run normally with `flutter test`.
-| Option | Type | Default | Description |
-| ----------------------- | ---- | ------- | ------------------------------------------------------ |
-| `generate_widget_tests` | bool | true | Generate widget tests when true, unit tests when false |
-| `enable_reporter` | bool | false | Enable/disable test reporter |
-| `ignore_features` | List | [] | List of feature file paths to ignore during generation |
+## Generated Files
-## 🏷️ Decorators
+The generator creates two files per `.feature` file:
-Control test generation with decorators:
+- **`.bdd_scenarios.dart`** — Scenario classes with step method stubs (your implementation goes here)
+- **`.bdd_test.dart`** — Test orchestration file (auto-generated, do not edit)
-| Decorator | Scope | Description |
-| -------------------------- | ----------------- | --------------------------------------- |
-| `@unitTest` | Feature, Scenario | Generate unit test (overrides config) |
-| `@widgetTest` | Feature, Scenario | Generate widget test (overrides config) |
-| `@className("CustomName")` | Scenario | Generate custom class name |
-| `@enableReporter` | Feature | Enable test reporter |
-| `@disableReporter` | Feature | Disable test reporter |
+### How It Works
-> 💡 Decorators follow a hierarchy: Scenario-level decorators override Feature-level ones.
+- Scenario classes use **instance methods**, so you can add `late` fields for shared state (mocks, widgets, etc.)
+- Each test instantiates a **fresh scenario** for proper test isolation
+- When you add a new scenario to a `.feature` file, only the new scenario class is **appended** — existing implementations are preserved
+- The test file is **always regenerated** (it contains no user code)
-## 📝 Complete Example
+### Example Output
-### 1. Feature File (`counter.feature`)
-
-```gherkin
-Feature: Counter
- Scenario: Increment
- Given I have a counter with value 0
- When I increment the counter by
- Then the counter should have value
- Examples:
- | value | expected_value |
- | 1 | 1 |
- | 2 | 2 |
- | 3 | 3 |
-```
-
-### 2. Generated Files
-
-#### `counter_scenarios.dart`
+#### `counter.bdd_scenarios.dart`
```dart
import 'package:flutter_test/flutter_test.dart';
class IncrementScenario {
- static Future iHaveACounterWithValue0(WidgetTester tester) async {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
// TODO: Implement Given I have a counter with value 0
}
- static Future iIncrementTheCounterBy(WidgetTester tester, dynamic value) async {
+ Future iIncrementTheCounterByValue(WidgetTester tester, String value) async {
// TODO: Implement When I increment the counter by
}
- static Future theCounterShouldHaveValue(WidgetTester tester, dynamic expected_value) async {
+ Future theCounterShouldHaveValueExpectedValue(WidgetTester tester, String expectedValue) async {
// TODO: Implement Then the counter should have value
}
}
```
-#### `counter_test.dart`
+#### `counter.bdd_test.dart`
```dart
import 'package:flutter_test/flutter_test.dart';
-import 'counter_scenarios.dart';
+import 'counter.bdd_scenarios.dart';
void main() {
group('Counter', () {
testWidgets('Increment', (tester) async {
- await IncrementScenario.iHaveACounterWithValue0(tester);
- // Example with values: 1, 1
- await IncrementScenario.iIncrementTheCounterBy(tester, '1');
- await IncrementScenario.theCounterShouldHaveValue(tester, '1');
- // Example with values: 2, 2
- await IncrementScenario.iIncrementTheCounterBy(tester, '2');
- await IncrementScenario.theCounterShouldHaveValue(tester, '2');
- // Example with values: 3, 3
- await IncrementScenario.iIncrementTheCounterBy(tester, '3');
- await IncrementScenario.theCounterShouldHaveValue(tester, '3');
+ final scenario = IncrementScenario();
+ final examples = [
+ {'value': '1', 'expectedValue': '1'},
+ {'value': '2', 'expectedValue': '2'},
+ {'value': '3', 'expectedValue': '3'},
+ ];
+ for (var example in examples) {
+ await scenario.iIncrementTheCounterByValue(tester, example['value']!);
+ await scenario.theCounterShouldHaveValueExpectedValue(tester, example['expectedValue']!);
+ }
});
});
}
```
-## 🤝 Contributing
+## Configuration
+
+Configure the generator in `.bdd_flutter/config.yaml`:
+
+```yaml
+# Where to scan for .feature files
+test_dir: "test/"
+
+# Generate widget tests or unit tests
+generate_widget_tests: true
+
+# Feature files to skip during generation
+ignore_features:
+ - test/features/login.feature
+ - test/features/registration.feature
+
+# Imports added to every generated .bdd_scenarios.dart file
+additional_imports:
+ - "package:mocktail/mocktail.dart"
+ - "test/helpers/test_helpers.dart"
+
+# Suffix for generated scenario class names (default: "Scenario")
+# e.g., "Steps" → IncrementSteps instead of IncrementScenario
+scenario_suffix: "Scenario"
+```
+
+Or use `--force` to regenerate everything:
+
+```bash
+dart run bdd_flutter build --force
+```
+
+### Config Options
+
+| Option | Type | Default | Description |
+| ----------------------- | ------ | ------------ | -------------------------------------------------------- |
+| `test_dir` | String | `test/` | Directory to scan for `.feature` files |
+| `generate_widget_tests` | bool | true | Generate widget tests when true, unit tests when false |
+| `ignore_features` | List | [] | Feature file paths to skip during generation |
+| `additional_imports` | List | [] | Imports added to every generated scenario file |
+| `scenario_suffix` | String | `Scenario` | Class name suffix (e.g., `Steps` for `IncrementSteps`) |
+
+### CLI Flags
+
+### Commands
+
+| Command | Description |
+| --------------- | ---------------------------------------------------- |
+| `build` | Generate test files from `.feature` files |
+| `build --force` | Force regenerate all files (overwrites existing) |
+| `test` | Run BDD tests with formatted Feature/Scenario report |
+
+## Decorators
+
+Control test type with standard Gherkin tags:
+
+| Decorator | Scope | Description |
+| ------------- | ----------------- | --------------------------------------- |
+| `@unitTest` | Feature, Scenario | Generate unit test (overrides config) |
+| `@widgetTest` | Feature, Scenario | Generate widget test (overrides config) |
+
+Scenario-level decorators override Feature-level ones.
+
+To skip generation for specific features, use `ignore_features` in config.
+
+## Generation Modes
+
+### Incremental (Default)
+
+```bash
+dart run bdd_flutter build
+```
+
+- Skips unchanged features (tracked via manifest)
+- New scenarios are **appended** to existing scenario files — implementations are preserved
+- Test files are regenerated to include all scenarios
+
+### Force Regenerate
+
+```bash
+dart run bdd_flutter build --force
+```
+
+- Regenerates all files from scratch
+- **Overwrites** existing scenario implementations — use with caution
+
+## Project Structure
+
+```
+your_project/
+├── .bdd_flutter/
+│ ├── config.yaml # Configuration (commit to version control)
+│ └── manifest.yaml # Generation tracking (commit to version control)
+├── test/
+│ └── login/
+│ ├── login.feature
+│ ├── login.bdd_scenarios.dart # Your implementations
+│ └── login.bdd_test.dart # Auto-generated orchestration
+└── pubspec.yaml
+```
+
+## Best Practices
+
+1. **Version Control** — Keep both `config.yaml` and `manifest.yaml` in version control. The manifest prevents incremental builds from overwriting implemented code on fresh clones.
+
+2. **Scenario Files** — Implement your test logic in `.bdd_scenarios.dart`. Use `late` fields for shared state between steps (mocks, providers, widgets).
+
+3. **Test Files** — Do not edit `.bdd_test.dart` files. They are regenerated automatically and contain no user code.
-We welcome contributions! Please feel free to:
+4. **Adding Scenarios** — When you add new scenarios to a `.feature` file, run `build` — new scenario classes are appended without touching existing ones.
-- Open an issue
-- Submit a pull request
-- Share your feedback
+5. **Feature Files** — Keep feature files clean. Only use `@unitTest`/`@widgetTest` decorators. All other configuration belongs in `.bdd_flutter/config.yaml`.
-## 📄 License
+## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
diff --git a/analysis_options.yaml b/analysis_options.yaml
index a5744c1..5bcd91a 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -1,4 +1,3 @@
include: package:flutter_lints/flutter.yaml
-
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
diff --git a/bin/bdd_flutter.dart b/bin/bdd_flutter.dart
new file mode 100644
index 0000000..6f54556
--- /dev/null
+++ b/bin/bdd_flutter.dart
@@ -0,0 +1,6 @@
+import 'package:bdd_flutter/src/presentation/cli/bbd_cli.dart';
+
+void main(List arguments) async {
+ final cli = BDDCLI();
+ await cli.run(arguments);
+}
diff --git a/build.yaml b/build.yaml
deleted file mode 100644
index 9b65fee..0000000
--- a/build.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-builders:
- bdd_flutter|bdd_test_builder:
- import: "package:bdd_flutter/bdd_flutter.dart"
- builder_factories: ["bddTestBuilder"]
- build_extensions:
- { ".feature": [".bdd_scenarios.g.dart", ".bdd_test.g.dart"] }
- auto_apply: dependents
- build_to: source
diff --git a/example/.bdd_flutter/manifest.yaml b/example/.bdd_flutter/manifest.yaml
new file mode 100644
index 0000000..adfee6a
--- /dev/null
+++ b/example/.bdd_flutter/manifest.yaml
@@ -0,0 +1,103 @@
+version: "1.0"
+last_generated: "2026-03-31T22:40:54.476380"
+features:
+ - path: "test/calculator/calculator.feature"
+ last_modified: "2025-08-16T13:55:30.921"
+ test_file: "test/calculator/calculator.bdd_test.dart"
+ scenarios:
+ - name: "Add two numbers"
+ hash: "f03974460050014a1179e86546325452"
+ test_method: "testAddTwoNumbersScenario"
+ - name: "Subtract two numbers"
+ hash: "6a3adb8f284f822789c89be0e919c7bf"
+ test_method: "testSubtractTwoNumbersScenario"
+ - name: "Subtract two numbers2"
+ hash: "f50907ed69f6ec5526f6acf958f01496"
+ test_method: "testSubtractTwoNumbers2Scenario"
+ - name: "Multiply two numbers"
+ hash: "8dba45ac682e9fa560bc6eefa73bc1f4"
+ test_method: "testMultiplyTwoNumbersScenario"
+ - name: "Divide two numbers"
+ hash: "415425b012fac2d176e72560dac0b8e1"
+ test_method: "testDivideTwoNumbersScenario"
+ - name: "Divide two numbers2"
+ hash: "737c00e721ef5df3f8d9eeb338a51b79"
+ test_method: "testDivideTwoNumbers2Scenario"
+ - path: "test/features/feature2.feature"
+ last_modified: "2025-05-19T14:28:03.988"
+ test_file: "test/features/feature2.bdd_test.dart"
+ scenarios:
+ - name: "Scenario 2"
+ hash: "8023fb9d6a1d88eadee8e007b8f8e967"
+ test_method: "testScenario2Scenario"
+ - name: "Scenario 3"
+ hash: "995fc3dbd32f20d5657d97ba82c0f467"
+ test_method: "testScenario3Scenario"
+ - path: "test/features/feature3.feature"
+ last_modified: "2025-05-14T21:04:20.415"
+ test_file: "test/features/feature3.bdd_test.dart"
+ scenarios:
+ - name: "Scenario 3"
+ hash: "995fc3dbd32f20d5657d97ba82c0f467"
+ test_method: "testScenario3Scenario"
+ - path: "test/features/feature1.feature"
+ last_modified: "2025-05-20T22:28:54.794"
+ test_file: "test/features/feature1.bdd_test.dart"
+ scenarios:
+ - name: "Scenario 1"
+ hash: "0872c6351cf96b32a10cde4c40b50082"
+ test_method: "testScenario1Scenario"
+ - name: "Scenario 2"
+ hash: "8023fb9d6a1d88eadee8e007b8f8e967"
+ test_method: "testScenario2Scenario"
+ - name: "Scenario 3"
+ hash: "995fc3dbd32f20d5657d97ba82c0f467"
+ test_method: "testScenario3Scenario"
+ - name: "Scenario 4"
+ hash: "3616c7a4f65b5c618759cbf467ef932c"
+ test_method: "testScenario4Scenario"
+ - name: "Scenario 5"
+ hash: "8dbde53f132f931117b624fb9df580b0"
+ test_method: "testScenario5Scenario"
+ - path: "test/sample/sample.feature"
+ last_modified: "2025-05-11T21:52:29.585"
+ test_file: "test/sample/sample.bdd_test.dart"
+ scenarios:
+ - name: "Sample"
+ hash: "55f8a2694d752e1c1cb32c7b9b9a56b2"
+ test_method: "testSampleScenario"
+ - name: "Counter"
+ hash: "6fcad53b0190130b41652843710a936f"
+ test_method: "testCounterScenario"
+ - name: "Counter with examples"
+ hash: "9cba8be6501d2cfc9bfb3586c4714cc9"
+ test_method: "testCounterWithExamplesScenario"
+ - name: "Counter with parameters"
+ hash: "bcdc61902c339891d57cf97da815b9c5"
+ test_method: "testCounterWithParametersScenario"
+ - name: "Counter with widget test"
+ hash: "c6e6af68b963a51031605f70a444b308"
+ test_method: "testCounterWithWidgetTestScenario"
+ - path: "test/counter/counter.feature"
+ last_modified: "2026-03-31T21:48:45.677"
+ test_file: "test/counter/counter.bdd_test.dart"
+ scenarios:
+ - name: "Increment"
+ hash: "4ac2c8fb0c941d6c8cde8372ba6f6b5f"
+ test_method: "testIncrementScenario"
+ - name: "Decrement"
+ hash: "1cd52123967c7db75be605bba60fac33"
+ test_method: "testDecrementScenario"
+ - path: "test/login/login.feature"
+ last_modified: "2026-03-31T21:51:19.959"
+ test_file: "test/login/login.bdd_test.dart"
+ scenarios:
+ - name: "Successful login"
+ hash: "5fea2322ced5756d9f1c9707548202f6"
+ test_method: "testSuccessfulLoginScenario"
+ - name: "Failed login shows error"
+ hash: "02d5530d9adf441cc17a732d94fd5158"
+ test_method: "testFailedLoginShowsErrorScenario"
+ - name: "Logout after login"
+ hash: "ab40a81506e5b317c5a888164c886324"
+ test_method: "testLogoutAfterLoginScenario"
diff --git a/example/bdd_ignore.yaml b/example/bdd_ignore.yaml
deleted file mode 100644
index 1bbdd28..0000000
--- a/example/bdd_ignore.yaml
+++ /dev/null
@@ -1,3 +0,0 @@
-ignore_files:
- - test/sample/sample.bdd_scenarios.dart
- - test/sample/sample.bdd_test.dart
diff --git a/example/build.yaml b/example/build.yaml
deleted file mode 100644
index 5c39d90..0000000
--- a/example/build.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-targets:
- $default:
- builders:
- bdd_flutter|bdd_test_builder:
- options:
- # generate_widget_tests:
- # enable_reporter: true
- ignore_features:
- - "test/calculator/calculator.feature"
diff --git a/example/lib/src/auth_provider.dart b/example/lib/src/auth_provider.dart
new file mode 100644
index 0000000..d38efb4
--- /dev/null
+++ b/example/lib/src/auth_provider.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/foundation.dart';
+
+import 'auth_repository.dart';
+
+class AuthProvider extends ChangeNotifier {
+ final AuthRepository _repository;
+
+ AuthProvider(this._repository);
+
+ User? _user;
+ String? _error;
+ bool _isLoading = false;
+
+ User? get user => _user;
+ String? get error => _error;
+ bool get isLoading => _isLoading;
+ bool get isLoggedIn => _user != null;
+
+ Future login(String email, String password) async {
+ _isLoading = true;
+ _error = null;
+ notifyListeners();
+
+ try {
+ _user = await _repository.login(email, password);
+ } catch (e) {
+ _error = e.toString();
+ } finally {
+ _isLoading = false;
+ notifyListeners();
+ }
+ }
+
+ Future logout() async {
+ await _repository.logout();
+ _user = null;
+ _error = null;
+ notifyListeners();
+ }
+}
diff --git a/example/lib/src/auth_repository.dart b/example/lib/src/auth_repository.dart
new file mode 100644
index 0000000..6ae6886
--- /dev/null
+++ b/example/lib/src/auth_repository.dart
@@ -0,0 +1,28 @@
+class User {
+ final String name;
+ final String email;
+
+ User({required this.name, required this.email});
+}
+
+abstract class AuthRepository {
+ Future login(String email, String password);
+ Future logout();
+}
+
+class AuthRepositoryImpl implements AuthRepository {
+ @override
+ Future login(String email, String password) async {
+ // Simulate API call
+ await Future.delayed(const Duration(seconds: 1));
+ if (email == 'test@test.com' && password == 'password') {
+ return User(name: 'Test User', email: email);
+ }
+ throw Exception('Invalid credentials');
+ }
+
+ @override
+ Future logout() async {
+ await Future.delayed(const Duration(milliseconds: 500));
+ }
+}
diff --git a/example/lib/src/login_screen.dart b/example/lib/src/login_screen.dart
new file mode 100644
index 0000000..5ff62ff
--- /dev/null
+++ b/example/lib/src/login_screen.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import 'auth_provider.dart';
+
+class LoginScreen extends StatefulWidget {
+ const LoginScreen({super.key});
+
+ @override
+ State createState() => _LoginScreenState();
+}
+
+class _LoginScreenState extends State {
+ final _emailController = TextEditingController();
+ final _passwordController = TextEditingController();
+
+ @override
+ void dispose() {
+ _emailController.dispose();
+ _passwordController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('Login')),
+ body: Consumer(
+ builder: (context, auth, _) {
+ if (auth.isLoggedIn) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text('Welcome, ${auth.user!.name}!'),
+ const SizedBox(height: 16),
+ ElevatedButton(onPressed: auth.logout, child: const Text('Logout')),
+ ],
+ ),
+ );
+ }
+
+ return Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (auth.error != null) Text(auth.error!, style: const TextStyle(color: Colors.red)),
+ TextField(controller: _emailController, decoration: const InputDecoration(labelText: 'Email')),
+ TextField(
+ controller: _passwordController,
+ decoration: const InputDecoration(labelText: 'Password'),
+ obscureText: true,
+ ),
+ const SizedBox(height: 16),
+ if (auth.isLoading)
+ const CircularProgressIndicator()
+ else
+ ElevatedButton(
+ onPressed: () => auth.login(_emailController.text, _passwordController.text),
+ child: const Text('Login'),
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index bb999fb..03863b0 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -5,23 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
- sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab"
+ sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
url: "https://pub.dev"
source: hosted
- version: "76.0.0"
- _macros:
- dependency: transitive
- description: dart
- source: sdk
- version: "0.3.3"
+ version: "93.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
- sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e"
+ sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
url: "https://pub.dev"
source: hosted
- version: "6.11.0"
+ version: "10.0.1"
args:
dependency: transitive
description:
@@ -44,7 +39,7 @@ packages:
path: ".."
relative: true
source: path
- version: "0.2.0"
+ version: "0.0.1"
boolean_selector:
dependency: transitive
description:
@@ -53,86 +48,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
- build:
- dependency: transitive
- description:
- name: build
- sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
- url: "https://pub.dev"
- source: hosted
- version: "2.4.2"
- build_config:
- dependency: transitive
- description:
- name: build_config
- sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
- url: "https://pub.dev"
- source: hosted
- version: "1.1.2"
- build_daemon:
- dependency: transitive
- description:
- name: build_daemon
- sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
- url: "https://pub.dev"
- source: hosted
- version: "4.0.4"
- build_resolvers:
- dependency: transitive
- description:
- name: build_resolvers
- sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
- url: "https://pub.dev"
- source: hosted
- version: "2.4.4"
- build_runner:
- dependency: "direct dev"
- description:
- name: build_runner
- sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
- url: "https://pub.dev"
- source: hosted
- version: "2.4.15"
- build_runner_core:
- dependency: transitive
- description:
- name: build_runner_core
- sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
- url: "https://pub.dev"
- source: hosted
- version: "8.0.0"
- built_collection:
- dependency: transitive
- description:
- name: built_collection
- sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
- url: "https://pub.dev"
- source: hosted
- version: "5.1.1"
- built_value:
- dependency: transitive
- description:
- name: built_value
- sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4
- url: "https://pub.dev"
- source: hosted
- version: "8.9.5"
characters:
dependency: transitive
description:
name: characters
- sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
- checked_yaml:
+ version: "1.4.1"
+ cli_config:
dependency: transitive
description:
- name: checked_yaml
- sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+ name: cli_config
+ sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
- version: "2.0.3"
+ version: "0.2.0"
clock:
dependency: transitive
description:
@@ -141,14 +72,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.2"
- code_builder:
- dependency: transitive
- description:
- name: code_builder
- sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
- url: "https://pub.dev"
- source: hosted
- version: "4.10.1"
collection:
dependency: transitive
description:
@@ -169,10 +92,10 @@ packages:
dependency: transitive
description:
name: coverage
- sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43
+ sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
- version: "1.11.1"
+ version: "1.15.0"
crypto:
dependency: transitive
description:
@@ -181,22 +104,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.6"
- dart_style:
- dependency: transitive
- description:
- name: dart_style
- sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820"
- url: "https://pub.dev"
- source: hosted
- version: "2.3.8"
fake_async:
dependency: transitive
description:
name: fake_async
- sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc"
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
- version: "1.3.2"
+ version: "1.3.3"
file:
dependency: transitive
description:
@@ -205,14 +120,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
- fixnum:
- dependency: transitive
- description:
- name: fixnum
- sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
- url: "https://pub.dev"
- source: hosted
- version: "1.1.1"
flutter:
dependency: "direct main"
description: flutter
@@ -247,22 +154,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.3"
- graphs:
- dependency: transitive
- description:
- name: graphs
- sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
- url: "https://pub.dev"
- source: hosted
- version: "2.3.2"
- http:
- dependency: transitive
- description:
- name: http
- sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
- url: "https://pub.dev"
- source: hosted
- version: "1.3.0"
http_multi_server:
dependency: transitive
description:
@@ -287,46 +178,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
- js:
- dependency: transitive
- description:
- name: js
- sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
- url: "https://pub.dev"
- source: hosted
- version: "0.7.2"
- json_annotation:
- dependency: transitive
- description:
- name: json_annotation
- sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
- url: "https://pub.dev"
- source: hosted
- version: "4.9.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
- sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
- version: "10.0.8"
+ version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
- sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
- version: "3.0.9"
+ version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
- sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
- version: "3.0.1"
+ version: "3.0.2"
lints:
dependency: transitive
description:
@@ -343,38 +218,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.0"
- macros:
- dependency: transitive
- description:
- name: macros
- sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656"
- url: "https://pub.dev"
- source: hosted
- version: "0.1.3-main.0"
matcher:
dependency: transitive
description:
name: matcher
- sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
- version: "0.12.17"
+ version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
- version: "0.11.1"
+ version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
- sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
- version: "1.16.0"
+ version: "1.17.0"
mime:
dependency: transitive
description:
@@ -383,6 +250,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
+ mocktail:
+ dependency: "direct dev"
+ description:
+ name: mocktail
+ sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ nested:
+ dependency: transitive
+ description:
+ name: nested
+ sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
node_preamble:
dependency: transitive
description:
@@ -415,6 +298,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.1"
+ provider:
+ dependency: "direct main"
+ description:
+ name: provider
+ sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
@@ -423,14 +314,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.2.0"
- pubspec_parse:
- dependency: transitive
- description:
- name: pubspec_parse
- sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
- url: "https://pub.dev"
- source: hosted
- version: "1.5.0"
shelf:
dependency: transitive
description:
@@ -508,14 +391,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
- stream_transform:
- dependency: transitive
- description:
- name: stream_transform
- sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
- url: "https://pub.dev"
- source: hosted
- version: "2.1.1"
string_scanner:
dependency: transitive
description:
@@ -536,34 +411,26 @@ packages:
dependency: "direct dev"
description:
name: test
- sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e"
+ sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
url: "https://pub.dev"
source: hosted
- version: "1.25.15"
+ version: "1.30.0"
test_api:
dependency: transitive
description:
name: test_api
- sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
- version: "0.7.4"
+ version: "0.7.10"
test_core:
dependency: transitive
description:
name: test_core
- sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa"
- url: "https://pub.dev"
- source: hosted
- version: "0.6.8"
- timing:
- dependency: transitive
- description:
- name: timing
- sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
+ sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
url: "https://pub.dev"
source: hosted
- version: "1.0.2"
+ version: "0.6.16"
typed_data:
dependency: transitive
description:
@@ -576,10 +443,10 @@ packages:
dependency: transitive
description:
name: vector_math
- sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
- version: "2.1.4"
+ version: "2.2.0"
vm_service:
dependency: transitive
description:
@@ -608,18 +475,18 @@ packages:
dependency: transitive
description:
name: web_socket
- sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
+ sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
- version: "0.1.6"
+ version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
- sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5"
+ sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
- version: "3.0.2"
+ version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
@@ -637,5 +504,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
- dart: ">=3.7.0 <4.0.0"
+ dart: ">=3.9.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 25bb512..c25df6b 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -9,6 +9,7 @@ environment:
dependencies:
flutter:
sdk: flutter
+ provider: ^6.0.0
dev_dependencies:
flutter_test:
@@ -16,8 +17,8 @@ dev_dependencies:
flutter_lints: ^5.0.0
bdd_flutter:
path: ../
- build_runner: ^2.4.11
test: ^1.20.0
+ mocktail: ^1.0.0
flutter:
uses-material-design: true
diff --git a/example/test/calculator/calculator.bdd_scenarios.dart b/example/test/calculator/calculator.bdd_scenarios.dart
index 12fad21..4fc99e4 100644
--- a/example/test/calculator/calculator.bdd_scenarios.dart
+++ b/example/test/calculator/calculator.bdd_scenarios.dart
@@ -1,57 +1,105 @@
import 'package:flutter_test/flutter_test.dart';
class AddTwoNumbersScenario {
- static Future iHaveTheNumber1(WidgetTester tester) async {
+ Future iHaveTheNumber1(WidgetTester tester) async {
// TODO: Implement Given I have the number 1
}
- static Future iAddThemTogether(WidgetTester tester) async {
+ Future iHaveTheNumber2(WidgetTester tester) async {
+ // TODO: Implement And I have the number 2
+ }
+
+ Future iAddThemTogether(WidgetTester tester) async {
// TODO: Implement When I add them together
}
- static Future theResultShouldBe3(WidgetTester tester) async {
+ Future theResultShouldBe3(WidgetTester tester) async {
// TODO: Implement Then the result should be 3
}
}
class SubtractTwoNumbersScenario {
- static Future iHaveTheNumber5(WidgetTester tester) async {
+ Future iHaveTheNumber5(WidgetTester tester) async {
// TODO: Implement Given I have the number 5
}
- static Future iSubtractThem(WidgetTester tester) async {
+ Future iHaveTheNumber3(WidgetTester tester) async {
+ // TODO: Implement And I have the number 3
+ }
+
+ Future iSubtractThem(WidgetTester tester) async {
// TODO: Implement When I subtract them
}
- static Future theResultShouldBe2(WidgetTester tester) async {
+ Future theResultShouldBe2(WidgetTester tester) async {
// TODO: Implement Then the result should be 2
}
}
+class SubtractTwoNumbers2Scenario {
+ Future iHaveTheNumber6(WidgetTester tester) async {
+ // TODO: Implement Given I have the number 6
+ }
+
+ Future iHaveTheNumber8(WidgetTester tester) async {
+ // TODO: Implement And I have the number 8
+ }
+
+ Future iSubtractThem(WidgetTester tester) async {
+ // TODO: Implement When I subtract them
+ }
+
+ Future theResultShouldBe2(WidgetTester tester) async {
+ // TODO: Implement Then the result should be -2
+ }
+}
+
class MultiplyTwoNumbersScenario {
- static Future iHaveTheNumber2(WidgetTester tester) async {
+ Future iHaveTheNumber2(WidgetTester tester) async {
// TODO: Implement Given I have the number 2
}
- static Future iMultiplyThem(WidgetTester tester) async {
+ Future iHaveTheNumber3(WidgetTester tester) async {
+ // TODO: Implement And I have the number 3
+ }
+
+ Future iMultiplyThem(WidgetTester tester) async {
// TODO: Implement When I multiply them
}
- static Future theResultShouldBe6(WidgetTester tester) async {
+ Future theResultShouldBe6(WidgetTester tester) async {
// TODO: Implement Then the result should be 6
}
}
class DivideTwoNumbersScenario {
- static Future iHaveTheNumber(WidgetTester tester, String number1) async {
+ Future iHaveTheNumberNumber1(WidgetTester tester, String number1) async {
// TODO: Implement Given I have the number
}
- static Future iDivideThem(WidgetTester tester) async {
+ Future iHaveTheNumberNumber2(WidgetTester tester, String number2) async {
+ // TODO: Implement And I have the number
+ }
+
+ Future iDivideThem(WidgetTester tester) async {
// TODO: Implement When I divide them
}
- static Future theResultShouldBe(WidgetTester tester, String result) async {
+ Future theResultShouldBeResult(WidgetTester tester, String result) async {
+ // TODO: Implement Then the result should be
+ }
+}
+
+class DivideTwoNumbers2Scenario {
+ Future iHaveNumber1AndNumber2(WidgetTester tester, String number1, String number2) async {
+ // TODO: Implement Given I have and
+ }
+
+ Future iDivideThemToEachOther(WidgetTester tester) async {
+ // TODO: Implement When I divide them to each other
+ }
+
+ Future theResultShouldBeResult(WidgetTester tester, String result) async {
// TODO: Implement Then the result should be
}
}
diff --git a/example/test/calculator/calculator.bdd_test.dart b/example/test/calculator/calculator.bdd_test.dart
index b23e37b..2e7ca75 100644
--- a/example/test/calculator/calculator.bdd_test.dart
+++ b/example/test/calculator/calculator.bdd_test.dart
@@ -4,33 +4,55 @@ import 'calculator.bdd_scenarios.dart';
void main() {
group('Calculator', () {
testWidgets('Add two numbers', (tester) async {
+ final scenario = AddTwoNumbersScenario();
//Scenario: Add two numbers
// Given I have the number 1
- await AddTwoNumbersScenario.iHaveTheNumber1(tester);
+ await scenario.iHaveTheNumber1(tester);
+ // And I have the number 2
+ await scenario.iHaveTheNumber2(tester);
// When I add them together
- await AddTwoNumbersScenario.iAddThemTogether(tester);
+ await scenario.iAddThemTogether(tester);
// Then the result should be 3
- await AddTwoNumbersScenario.theResultShouldBe3(tester);
+ await scenario.theResultShouldBe3(tester);
});
testWidgets('Subtract two numbers', (tester) async {
+ final scenario = SubtractTwoNumbersScenario();
//Scenario: Subtract two numbers
// Given I have the number 5
- await SubtractTwoNumbersScenario.iHaveTheNumber5(tester);
+ await scenario.iHaveTheNumber5(tester);
+ // And I have the number 3
+ await scenario.iHaveTheNumber3(tester);
// When I subtract them
- await SubtractTwoNumbersScenario.iSubtractThem(tester);
+ await scenario.iSubtractThem(tester);
// Then the result should be 2
- await SubtractTwoNumbersScenario.theResultShouldBe2(tester);
+ await scenario.theResultShouldBe2(tester);
+ });
+ testWidgets('Subtract two numbers2', (tester) async {
+ final scenario = SubtractTwoNumbers2Scenario();
+ //Scenario: Subtract two numbers2
+ // Given I have the number 6
+ await scenario.iHaveTheNumber6(tester);
+ // And I have the number 8
+ await scenario.iHaveTheNumber8(tester);
+ // When I subtract them
+ await scenario.iSubtractThem(tester);
+ // Then the result should be -2
+ await scenario.theResultShouldBe2(tester);
});
testWidgets('Multiply two numbers', (tester) async {
+ final scenario = MultiplyTwoNumbersScenario();
//Scenario: Multiply two numbers
// Given I have the number 2
- await MultiplyTwoNumbersScenario.iHaveTheNumber2(tester);
+ await scenario.iHaveTheNumber2(tester);
+ // And I have the number 3
+ await scenario.iHaveTheNumber3(tester);
// When I multiply them
- await MultiplyTwoNumbersScenario.iMultiplyThem(tester);
+ await scenario.iMultiplyThem(tester);
// Then the result should be 6
- await MultiplyTwoNumbersScenario.theResultShouldBe6(tester);
+ await scenario.theResultShouldBe6(tester);
});
testWidgets('Divide two numbers', (tester) async {
+ final scenario = DivideTwoNumbersScenario();
//Scenario: Divide two numbers
final examples = [
{'number1': '10', 'number2': '2', 'result': '5'},
@@ -39,11 +61,30 @@ void main() {
];
for (var example in examples) {
// Given I have the number
- await DivideTwoNumbersScenario.iHaveTheNumber(tester, example['number1']!);
+ await scenario.iHaveTheNumberNumber1(tester, example['number1']!);
+ // And I have the number
+ await scenario.iHaveTheNumberNumber2(tester, example['number2']!);
// When I divide them
- await DivideTwoNumbersScenario.iDivideThem(tester);
+ await scenario.iDivideThem(tester);
+ // Then the result should be
+ await scenario.theResultShouldBeResult(tester, example['result']!);
+ }
+ });
+ testWidgets('Divide two numbers2', (tester) async {
+ final scenario = DivideTwoNumbers2Scenario();
+ //Scenario: Divide two numbers2
+ final examples = [
+ {'number1': '10', 'number2': '2', 'result': '5'},
+ {'number1': '10', 'number2': '1', 'result': '10'},
+ {'number1': '10', 'number2': '10', 'result': '1'},
+ ];
+ for (var example in examples) {
+ // Given I have and
+ await scenario.iHaveNumber1AndNumber2(tester, example['number1']!, example['number2']!);
+ // When I divide them to each other
+ await scenario.iDivideThemToEachOther(tester);
// Then the result should be
- await DivideTwoNumbersScenario.theResultShouldBe(tester, example['result']!);
+ await scenario.theResultShouldBeResult(tester, example['result']!);
}
});
});
diff --git a/example/test/calculator/calculator.feature b/example/test/calculator/calculator.feature
index 83d3341..9a9610b 100644
--- a/example/test/calculator/calculator.feature
+++ b/example/test/calculator/calculator.feature
@@ -13,6 +13,12 @@ Feature: Calculator
When I subtract them
Then the result should be 2
+ Scenario: Subtract two numbers2
+ Given I have the number 6
+ And I have the number 8
+ When I subtract them
+ Then the result should be -2
+
Scenario: Multiply two numbers
Given I have the number 2
And I have the number 3
@@ -31,6 +37,17 @@ Feature: Calculator
| 10 | 1 | 10 |
| 10 | 10 | 1 |
+ Scenario: Divide two numbers2
+ Given I have and
+ When I divide them to each other
+ Then the result should be
+
+ Examples:
+ | number1 | number2 | result |
+ | 10 | 2 | 5 |
+ | 10 | 1 | 10 |
+ | 10 | 10 | 1 |
+
diff --git a/example/test/counter/counter.bdd_scenarios.dart b/example/test/counter/counter.bdd_scenarios.dart
new file mode 100644
index 0000000..4f9dead
--- /dev/null
+++ b/example/test/counter/counter.bdd_scenarios.dart
@@ -0,0 +1,27 @@
+import 'package:flutter_test/flutter_test.dart';
+
+class CounterBackground {
+ Future iHaveACounterWithValue0() async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+}
+
+class IncrementScenario {
+ Future iIncrementTheCounterByValue(WidgetTester tester, String value) async {
+ // TODO: Implement When I increment the counter by
+ }
+
+ Future theCounterShouldHaveValueExpectedValue(WidgetTester tester, String expectedValue) async {
+ // TODO: Implement Then the counter should have value
+ }
+}
+
+class DecrementScenario {
+ Future iDecrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I decrement the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value -1
+ }
+}
diff --git a/example/test/counter/counter.bdd_scenarios.g.dart b/example/test/counter/counter.bdd_scenarios.g.dart
deleted file mode 100644
index eaeabd3..0000000
--- a/example/test/counter/counter.bdd_scenarios.g.dart
+++ /dev/null
@@ -1,20 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-
-class CounterBackground {
- static Future iHaveACounterWithValue0() async {
- // TODO: Implement Given I have a counter with value 0
- }
-
-}
-
-class IncrementScenario {
- static Future iIncrementTheCounterBy(WidgetTester tester, String value) async {
- // TODO: Implement When I increment the counter by
- }
-
- static Future theCounterShouldHaveValue(WidgetTester tester, String expectedvalue) async {
- // TODO: Implement Then the counter should have value
- }
-
-}
-
diff --git a/example/test/counter/counter.bdd_test.dart b/example/test/counter/counter.bdd_test.dart
new file mode 100644
index 0000000..2aeebbb
--- /dev/null
+++ b/example/test/counter/counter.bdd_test.dart
@@ -0,0 +1,36 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'counter.bdd_scenarios.dart';
+
+void main() {
+ group('Counter', () {
+ testWidgets('Increment', (tester) async {
+ final scenario = IncrementScenario();
+ final background = CounterBackground();
+ //Background: I have a counter with value 0
+ await background.iHaveACounterWithValue0();
+ //Scenario: Increment
+ final examples = [
+ {'value': '1', 'expectedValue': '1'},
+ {'value': '2', 'expectedValue': '2'},
+ {'value': '3', 'expectedValue': '3'},
+ ];
+ for (var example in examples) {
+ // When I increment the counter by
+ await scenario.iIncrementTheCounterByValue(tester, example['value']!);
+ // Then the counter should have value
+ await scenario.theCounterShouldHaveValueExpectedValue(tester, example['expectedValue']!);
+ }
+ });
+ testWidgets('Decrement', (tester) async {
+ final scenario = DecrementScenario();
+ final background = CounterBackground();
+ //Background: I have a counter with value 0
+ await background.iHaveACounterWithValue0();
+ //Scenario: Decrement
+ // When I decrement the counter by 1
+ await scenario.iDecrementTheCounterBy1(tester);
+ // Then the counter should have value -1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ });
+}
diff --git a/example/test/counter/counter.bdd_test.g.dart b/example/test/counter/counter.bdd_test.g.dart
deleted file mode 100644
index 8b1940c..0000000
--- a/example/test/counter/counter.bdd_test.g.dart
+++ /dev/null
@@ -1,23 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'counter.bdd_scenarios.g.dart';
-
-void main() {
- group('Counter', () {
- //Background: I have a counter with value 0
- CounterBackground.iHaveACounterWithValue0();
- testWidgets('Increment', (tester) async {
- //Scenario: Increment
- final examples = [
- {'value': '1','expectedvalue': '1',},
- {'value': '2','expectedvalue': '2',},
- {'value': '3','expectedvalue': '3',},
- ];
- for (var example in examples) {
- // When I increment the counter by
- await IncrementScenario.iIncrementTheCounterBy(tester, example['value']!);
- // Then the counter should have value
- await IncrementScenario.theCounterShouldHaveValue(tester, example['expected_value']!);
- }
- });
- });
-}
diff --git a/example/test/counter/counter.feature b/example/test/counter/counter.feature
index 20bb57d..4d0658c 100644
--- a/example/test/counter/counter.feature
+++ b/example/test/counter/counter.feature
@@ -9,4 +9,8 @@ Feature: Counter
| value | expected_value |
| 1 | 1 |
| 2 | 2 |
- | 3 | 3 |
\ No newline at end of file
+ | 3 | 3 |
+
+ Scenario: Decrement
+ When I decrement the counter by 1
+ Then the counter should have value -1
\ No newline at end of file
diff --git a/example/test/features/feature1.bdd_scenarios.dart b/example/test/features/feature1.bdd_scenarios.dart
new file mode 100644
index 0000000..aba6d79
--- /dev/null
+++ b/example/test/features/feature1.bdd_scenarios.dart
@@ -0,0 +1,71 @@
+import 'package:flutter_test/flutter_test.dart';
+
+class Scenario1Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
+
+class Scenario2Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
+
+class Scenario3Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
+
+class Scenario4Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
+
+class Scenario5Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
diff --git a/example/test/features/feature1.bdd_test.dart b/example/test/features/feature1.bdd_test.dart
new file mode 100644
index 0000000..079c613
--- /dev/null
+++ b/example/test/features/feature1.bdd_test.dart
@@ -0,0 +1,57 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'feature1.bdd_scenarios.dart';
+
+void main() {
+ group('Feature 1', () {
+ testWidgets('Scenario 1', (tester) async {
+ final scenario = Scenario1Scenario();
+ //Scenario: Scenario 1
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ testWidgets('Scenario 2', (tester) async {
+ final scenario = Scenario2Scenario();
+ //Scenario: Scenario 2
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ testWidgets('Scenario 3', (tester) async {
+ final scenario = Scenario3Scenario();
+ //Scenario: Scenario 3
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ testWidgets('Scenario 4', (tester) async {
+ final scenario = Scenario4Scenario();
+ //Scenario: Scenario 4
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ testWidgets('Scenario 5', (tester) async {
+ final scenario = Scenario5Scenario();
+ //Scenario: Scenario 5
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ });
+}
diff --git a/example/test/features/feature1.feature b/example/test/features/feature1.feature
new file mode 100644
index 0000000..68adb7f
--- /dev/null
+++ b/example/test/features/feature1.feature
@@ -0,0 +1,27 @@
+Feature: Feature 1
+
+ Scenario: Scenario 1
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
+
+ Scenario: Scenario 2
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
+
+ Scenario: Scenario 3
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
+
+ Scenario: Scenario 4
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
+
+
+ Scenario: Scenario 5
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
\ No newline at end of file
diff --git a/example/test/features/feature2.bdd_scenarios.dart b/example/test/features/feature2.bdd_scenarios.dart
new file mode 100644
index 0000000..361ac4a
--- /dev/null
+++ b/example/test/features/feature2.bdd_scenarios.dart
@@ -0,0 +1,29 @@
+import 'package:flutter_test/flutter_test.dart';
+
+class Scenario2Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
+
+class Scenario3Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
diff --git a/example/test/features/feature2.bdd_test.dart b/example/test/features/feature2.bdd_test.dart
new file mode 100644
index 0000000..066027d
--- /dev/null
+++ b/example/test/features/feature2.bdd_test.dart
@@ -0,0 +1,27 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'feature2.bdd_scenarios.dart';
+
+void main() {
+ group('Feature 2', () {
+ testWidgets('Scenario 2', (tester) async {
+ final scenario = Scenario2Scenario();
+ //Scenario: Scenario 2
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ testWidgets('Scenario 3', (tester) async {
+ final scenario = Scenario3Scenario();
+ //Scenario: Scenario 3
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ });
+}
diff --git a/example/test/features/feature2.feature b/example/test/features/feature2.feature
new file mode 100644
index 0000000..7d0456a
--- /dev/null
+++ b/example/test/features/feature2.feature
@@ -0,0 +1,12 @@
+Feature: Feature 2
+
+ Scenario: Scenario 2
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
+
+ Scenario: Scenario 3
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
+
diff --git a/example/test/features/feature3.bdd_scenarios.dart b/example/test/features/feature3.bdd_scenarios.dart
new file mode 100644
index 0000000..658320b
--- /dev/null
+++ b/example/test/features/feature3.bdd_scenarios.dart
@@ -0,0 +1,15 @@
+import 'package:flutter_test/flutter_test.dart';
+
+class Scenario3Scenario {
+ Future iHaveACounterWithValue0(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter with value 0
+ }
+
+ Future iIncrementTheCounterBy1(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter by 1
+ }
+
+ Future theCounterShouldHaveValue1(WidgetTester tester) async {
+ // TODO: Implement Then the counter should have value 1
+ }
+}
diff --git a/example/test/features/feature3.bdd_test.dart b/example/test/features/feature3.bdd_test.dart
new file mode 100644
index 0000000..52a30dd
--- /dev/null
+++ b/example/test/features/feature3.bdd_test.dart
@@ -0,0 +1,17 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'feature3.bdd_scenarios.dart';
+
+void main() {
+ group('Feature 3', () {
+ testWidgets('Scenario 3', (tester) async {
+ final scenario = Scenario3Scenario();
+ //Scenario: Scenario 3
+ // Given I have a counter with value 0
+ await scenario.iHaveACounterWithValue0(tester);
+ // When I increment the counter by 1
+ await scenario.iIncrementTheCounterBy1(tester);
+ // Then the counter should have value 1
+ await scenario.theCounterShouldHaveValue1(tester);
+ });
+ });
+}
diff --git a/example/test/features/feature3.feature b/example/test/features/feature3.feature
new file mode 100644
index 0000000..2202938
--- /dev/null
+++ b/example/test/features/feature3.feature
@@ -0,0 +1,6 @@
+Feature: Feature 3
+
+ Scenario: Scenario 3
+ Given I have a counter with value 0
+ When I increment the counter by 1
+ Then the counter should have value 1
diff --git a/example/test/login/login.bdd_scenarios.dart b/example/test/login/login.bdd_scenarios.dart
new file mode 100644
index 0000000..80898e0
--- /dev/null
+++ b/example/test/login/login.bdd_scenarios.dart
@@ -0,0 +1,137 @@
+import 'package:example/src/auth_provider.dart';
+import 'package:example/src/auth_repository.dart';
+import 'package:example/src/login_screen.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:provider/provider.dart';
+
+class MockAuthRepository extends Mock implements AuthRepository {}
+
+class SuccessfulLoginScenario {
+ late MockAuthRepository mockAuthRepo;
+ late AuthProvider authProvider;
+
+ Future iHaveAMockAuthRepository(WidgetTester tester) async {
+ mockAuthRepo = MockAuthRepository();
+ authProvider = AuthProvider(mockAuthRepo);
+ }
+
+ Future theMockReturnsASuccessfulLoginForTestTestCom(WidgetTester tester) async {
+ when(
+ () => mockAuthRepo.login('test@test.com', 'password'),
+ ).thenAnswer((_) async => User(name: 'Test User', email: 'test@test.com'));
+ }
+
+ Future iPumpTheLoginScreenWithProviders(WidgetTester tester) async {
+ await tester.pumpWidget(
+ ChangeNotifierProvider.value(value: authProvider, child: const MaterialApp(home: LoginScreen())),
+ );
+ }
+
+ Future iEnterTestTestComInTheEmailField(WidgetTester tester) async {
+ await tester.enterText(find.byType(TextField).first, 'test@test.com');
+ }
+
+ Future iEnterPasswordInThePasswordField(WidgetTester tester) async {
+ await tester.enterText(find.byType(TextField).last, 'password');
+ }
+
+ Future iTapTheLoginButton(WidgetTester tester) async {
+ await tester.tap(find.widgetWithText(ElevatedButton, 'Login'));
+ await tester.pumpAndSettle();
+ }
+
+ Future iShouldSeeWelcomeTestUser(WidgetTester tester) async {
+ expect(find.text('Welcome, Test User!'), findsOneWidget);
+ }
+}
+
+class FailedLoginShowsErrorScenario {
+ late MockAuthRepository mockAuthRepo;
+ late AuthProvider authProvider;
+
+ Future iHaveAMockAuthRepository(WidgetTester tester) async {
+ mockAuthRepo = MockAuthRepository();
+ authProvider = AuthProvider(mockAuthRepo);
+ }
+
+ Future theMockThrowsAnErrorForLogin(WidgetTester tester) async {
+ when(() => mockAuthRepo.login(any(), any())).thenThrow(Exception('Invalid credentials'));
+ }
+
+ Future iPumpTheLoginScreenWithProviders(WidgetTester tester) async {
+ await tester.pumpWidget(
+ ChangeNotifierProvider.value(value: authProvider, child: const MaterialApp(home: LoginScreen())),
+ );
+ }
+
+ Future iEnterWrongTestComInTheEmailField(WidgetTester tester) async {
+ await tester.enterText(find.byType(TextField).first, 'wrong@test.com');
+ }
+
+ Future iEnterWrongInThePasswordField(WidgetTester tester) async {
+ await tester.enterText(find.byType(TextField).last, 'wrong');
+ }
+
+ Future iTapTheLoginButton(WidgetTester tester) async {
+ await tester.tap(find.widgetWithText(ElevatedButton, 'Login'));
+ await tester.pumpAndSettle();
+ }
+
+ Future iShouldSeeInvalidCredentials(WidgetTester tester) async {
+ expect(find.textContaining('Invalid credentials'), findsOneWidget);
+ }
+}
+
+class LogoutAfterLoginScenario {
+ late MockAuthRepository mockAuthRepo;
+ late AuthProvider authProvider;
+
+ Future iHaveAMockAuthRepository(WidgetTester tester) async {
+ mockAuthRepo = MockAuthRepository();
+ authProvider = AuthProvider(mockAuthRepo);
+ }
+
+ Future theMockReturnsASuccessfulLoginForTestTestCom(WidgetTester tester) async {
+ when(
+ () => mockAuthRepo.login('test@test.com', 'password'),
+ ).thenAnswer((_) async => User(name: 'Test User', email: 'test@test.com'));
+ }
+
+ Future theMockAllowsLogout(WidgetTester tester) async {
+ when(() => mockAuthRepo.logout()).thenAnswer((_) async {});
+ }
+
+ Future iPumpTheLoginScreenWithProviders(WidgetTester tester) async {
+ await tester.pumpWidget(
+ ChangeNotifierProvider.value(value: authProvider, child: const MaterialApp(home: LoginScreen())),
+ );
+ }
+
+ Future iEnterTestTestComInTheEmailField(WidgetTester tester) async {
+ await tester.enterText(find.byType(TextField).first, 'test@test.com');
+ }
+
+ Future iEnterPasswordInThePasswordField(WidgetTester tester) async {
+ await tester.enterText(find.byType(TextField).last, 'password');
+ }
+
+ Future iTapTheLoginButton(WidgetTester tester) async {
+ await tester.tap(find.widgetWithText(ElevatedButton, 'Login'));
+ await tester.pumpAndSettle();
+ }
+
+ Future iShouldSeeWelcomeTestUser(WidgetTester tester) async {
+ expect(find.text('Welcome, Test User!'), findsOneWidget);
+ }
+
+ Future iTapTheLogoutButton(WidgetTester tester) async {
+ await tester.tap(find.text('Logout'));
+ await tester.pumpAndSettle();
+ }
+
+ Future iShouldSeeLogin(WidgetTester tester) async {
+ expect(find.widgetWithText(ElevatedButton, 'Login'), findsOneWidget);
+ }
+}
diff --git a/example/test/login/login.bdd_test.dart b/example/test/login/login.bdd_test.dart
new file mode 100644
index 0000000..f037f7e
--- /dev/null
+++ b/example/test/login/login.bdd_test.dart
@@ -0,0 +1,67 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'login.bdd_scenarios.dart';
+
+void main() {
+ group('Login', () {
+ testWidgets('Successful login', (tester) async {
+ final scenario = SuccessfulLoginScenario();
+ //Scenario: Successful login
+ // Given I have a mock auth repository
+ await scenario.iHaveAMockAuthRepository(tester);
+ // And the mock returns a successful login for "test@test.com"
+ await scenario.theMockReturnsASuccessfulLoginForTestTestCom(tester);
+ // When I pump the login screen with providers
+ await scenario.iPumpTheLoginScreenWithProviders(tester);
+ // And I enter "test@test.com" in the email field
+ await scenario.iEnterTestTestComInTheEmailField(tester);
+ // And I enter "password" in the password field
+ await scenario.iEnterPasswordInThePasswordField(tester);
+ // And I tap the login button
+ await scenario.iTapTheLoginButton(tester);
+ // Then I should see "Welcome, Test User!"
+ await scenario.iShouldSeeWelcomeTestUser(tester);
+ });
+ testWidgets('Failed login shows error', (tester) async {
+ final scenario = FailedLoginShowsErrorScenario();
+ //Scenario: Failed login shows error
+ // Given I have a mock auth repository
+ await scenario.iHaveAMockAuthRepository(tester);
+ // And the mock throws an error for login
+ await scenario.theMockThrowsAnErrorForLogin(tester);
+ // When I pump the login screen with providers
+ await scenario.iPumpTheLoginScreenWithProviders(tester);
+ // And I enter "wrong@test.com" in the email field
+ await scenario.iEnterWrongTestComInTheEmailField(tester);
+ // And I enter "wrong" in the password field
+ await scenario.iEnterWrongInThePasswordField(tester);
+ // And I tap the login button
+ await scenario.iTapTheLoginButton(tester);
+ // Then I should see "Invalid credentials"
+ await scenario.iShouldSeeInvalidCredentials(tester);
+ });
+ testWidgets('Logout after login', (tester) async {
+ final scenario = LogoutAfterLoginScenario();
+ //Scenario: Logout after login
+ // Given I have a mock auth repository
+ await scenario.iHaveAMockAuthRepository(tester);
+ // And the mock returns a successful login for "test@test.com"
+ await scenario.theMockReturnsASuccessfulLoginForTestTestCom(tester);
+ // And the mock allows logout
+ await scenario.theMockAllowsLogout(tester);
+ // When I pump the login screen with providers
+ await scenario.iPumpTheLoginScreenWithProviders(tester);
+ // And I enter "test@test.com" in the email field
+ await scenario.iEnterTestTestComInTheEmailField(tester);
+ // And I enter "password" in the password field
+ await scenario.iEnterPasswordInThePasswordField(tester);
+ // And I tap the login button
+ await scenario.iTapTheLoginButton(tester);
+ // Then I should see "Welcome, Test User!"
+ await scenario.iShouldSeeWelcomeTestUser(tester);
+ // When I tap the logout button
+ await scenario.iTapTheLogoutButton(tester);
+ // Then I should see "Login"
+ await scenario.iShouldSeeLogin(tester);
+ });
+ });
+}
diff --git a/example/test/login/login.feature b/example/test/login/login.feature
new file mode 100644
index 0000000..4418c40
--- /dev/null
+++ b/example/test/login/login.feature
@@ -0,0 +1,31 @@
+Feature: Login
+
+ Scenario: Successful login
+ Given I have a mock auth repository
+ And the mock returns a successful login for "test@test.com"
+ When I pump the login screen with providers
+ And I enter "test@test.com" in the email field
+ And I enter "password" in the password field
+ And I tap the login button
+ Then I should see "Welcome, Test User!"
+
+ Scenario: Failed login shows error
+ Given I have a mock auth repository
+ And the mock throws an error for login
+ When I pump the login screen with providers
+ And I enter "wrong@test.com" in the email field
+ And I enter "wrong" in the password field
+ And I tap the login button
+ Then I should see "Invalid credentials"
+
+ Scenario: Logout after login
+ Given I have a mock auth repository
+ And the mock returns a successful login for "test@test.com"
+ And the mock allows logout
+ When I pump the login screen with providers
+ And I enter "test@test.com" in the email field
+ And I enter "password" in the password field
+ And I tap the login button
+ Then I should see "Welcome, Test User!"
+ When I tap the logout button
+ Then I should see "Login"
diff --git a/example/test/sample/sample.bdd_scenarios.dart b/example/test/sample/sample.bdd_scenarios.dart
new file mode 100644
index 0000000..c9d2f7c
--- /dev/null
+++ b/example/test/sample/sample.bdd_scenarios.dart
@@ -0,0 +1,71 @@
+import 'package:flutter_test/flutter_test.dart';
+
+class SampleScenario {
+ Future iHaveASampleFeature(WidgetTester tester) async {
+ // TODO: Implement Given I have a sample feature
+ }
+
+ Future iRunTheSampleFeature(WidgetTester tester) async {
+ // TODO: Implement When I run the sample feature
+ }
+
+ Future iShouldSeeTheSampleFeature(WidgetTester tester) async {
+ // TODO: Implement Then I should see the sample feature
+ }
+}
+
+class CounterScenario {
+ Future iHaveACounter(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter
+ }
+
+ Future iIncrementTheCounter(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter
+ }
+
+ Future iShouldSeeTheCounterIncremented(WidgetTester tester) async {
+ // TODO: Implement Then I should see the counter incremented
+ }
+}
+
+class CounterWithExamplesScenario {
+ Future iHaveACounter() async {
+ // TODO: Implement Given I have a counter
+ }
+
+ Future iIncrementTheCounter(String counter) async {
+ // TODO: Implement When I increment the
+ }
+
+ Future iShouldSeeTheCounterIncremented() async {
+ // TODO: Implement Then I should see the counter incremented
+ }
+}
+
+class CounterWithParametersScenario {
+ Future iHaveACounter() async {
+ // TODO: Implement Given I have a counter
+ }
+
+ Future iIncrementTheCounterCounter(String counter) async {
+ // TODO: Implement When I increment the counter
+ }
+
+ Future iShouldSeeTheResultResult(String result) async {
+ // TODO: Implement Then I should see the result
+ }
+}
+
+class CounterWithWidgetTestScenario {
+ Future iHaveACounter(WidgetTester tester) async {
+ // TODO: Implement Given I have a counter
+ }
+
+ Future iIncrementTheCounter(WidgetTester tester) async {
+ // TODO: Implement When I increment the counter
+ }
+
+ Future iShouldSeeTheCounterIncremented(WidgetTester tester) async {
+ // TODO: Implement Then I should see the counter incremented
+ }
+}
diff --git a/example/test/sample/sample.bdd_scenarios.g.dart b/example/test/sample/sample.bdd_scenarios.g.dart
deleted file mode 100644
index 2ea77d9..0000000
--- a/example/test/sample/sample.bdd_scenarios.g.dart
+++ /dev/null
@@ -1,62 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-
-class SampleScenario {
- static Future iHaveASampleFeature(WidgetTester tester) async {
- // TODO: Implement Given I have a sample feature
- }
-
- static Future iRunTheSampleFeature(WidgetTester tester) async {
- // TODO: Implement When I run the sample feature
- }
-
- static Future iShouldSeeTheSampleFeature(WidgetTester tester) async {
- // TODO: Implement Then I should see the sample feature
- }
-
-}
-
-class CounterCustomName {
- static Future iHaveACounter(WidgetTester tester) async {
- // TODO: Implement Given I have a counter
- }
-
- static Future iIncrementTheCounter(WidgetTester tester) async {
- // TODO: Implement When I increment the counter
- }
-
- static Future iShouldSeeTheCounterIncremented(WidgetTester tester) async {
- // TODO: Implement Then I should see the counter incremented
- }
-
-}
-
-class CounterWithExamplesScenario {
- static Future iHaveACounter() async {
- // TODO: Implement Given I have a counter
- }
-
- static Future iIncrementThe(String counter) async {
- // TODO: Implement When I increment the
- }
-
- static Future iShouldSeeTheCounterIncremented() async {
- // TODO: Implement Then I should see the counter incremented
- }
-
-}
-
-class CounterWithParametersScenario {
- static Future iHaveACounter() async {
- // TODO: Implement Given I have a counter
- }
-
- static Future iIncrementTheCounter(String counter) async {
- // TODO: Implement When I increment the counter
- }
-
- static Future iShouldSeeTheResult(String result) async {
- // TODO: Implement Then I should see the result
- }
-
-}
-
diff --git a/example/test/sample/sample.bdd_test.dart b/example/test/sample/sample.bdd_test.dart
new file mode 100644
index 0000000..b12a403
--- /dev/null
+++ b/example/test/sample/sample.bdd_test.dart
@@ -0,0 +1,71 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'sample.bdd_scenarios.dart';
+
+void main() {
+ group('Sample', () {
+ testWidgets('Sample', (tester) async {
+ final scenario = SampleScenario();
+ //Scenario: Sample
+ // Given I have a sample feature
+ await scenario.iHaveASampleFeature(tester);
+ // When I run the sample feature
+ await scenario.iRunTheSampleFeature(tester);
+ // Then I should see the sample feature
+ await scenario.iShouldSeeTheSampleFeature(tester);
+ });
+ testWidgets('Counter', (tester) async {
+ final scenario = CounterScenario();
+ //Scenario: Counter
+ // Given I have a counter
+ await scenario.iHaveACounter(tester);
+ // When I increment the counter
+ await scenario.iIncrementTheCounter(tester);
+ // Then I should see the counter incremented
+ await scenario.iShouldSeeTheCounterIncremented(tester);
+ });
+ test('Counter with examples', () async {
+ final scenario = CounterWithExamplesScenario();
+ //Scenario: Counter with examples
+ final examples = [
+ {'counter': '1'},
+ {'counter': '2'},
+ {'counter': '3'},
+ ];
+ for (var example in examples) {
+ // Given I have a counter
+ await scenario.iHaveACounter();
+ // When I increment the
+ await scenario.iIncrementTheCounter(example['counter']!);
+ // Then I should see the counter incremented
+ await scenario.iShouldSeeTheCounterIncremented();
+ }
+ });
+ test('Counter with parameters', () async {
+ final scenario = CounterWithParametersScenario();
+ //Scenario: Counter with parameters
+ final examples = [
+ {'counter': '1', 'result': '2'},
+ {'counter': '2', 'result': '3'},
+ {'counter': '3', 'result': '4'},
+ ];
+ for (var example in examples) {
+ // Given I have a counter
+ await scenario.iHaveACounter();
+ // When I increment the counter
+ await scenario.iIncrementTheCounterCounter(example['counter']!);
+ // Then I should see the result
+ await scenario.iShouldSeeTheResultResult(example['result']!);
+ }
+ });
+ testWidgets('Counter with widget test', (tester) async {
+ final scenario = CounterWithWidgetTestScenario();
+ //Scenario: Counter with widget test
+ // Given I have a counter
+ await scenario.iHaveACounter(tester);
+ // When I increment the counter
+ await scenario.iIncrementTheCounter(tester);
+ // Then I should see the counter incremented
+ await scenario.iShouldSeeTheCounterIncremented(tester);
+ });
+ });
+}
diff --git a/example/test/sample/sample.bdd_test.g.dart b/example/test/sample/sample.bdd_test.g.dart
deleted file mode 100644
index 2e514fa..0000000
--- a/example/test/sample/sample.bdd_test.g.dart
+++ /dev/null
@@ -1,57 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'sample.bdd_scenarios.g.dart';
-
-void main() {
- group('Sample', () {
- testWidgets('Sample', (tester) async {
- //Scenario: Sample
- // Given I have a sample feature
- await SampleScenario.iHaveASampleFeature(tester);
- // When I run the sample feature
- await SampleScenario.iRunTheSampleFeature(tester);
- // Then I should see the sample feature
- await SampleScenario.iShouldSeeTheSampleFeature(tester);
- });
- testWidgets('Counter', (tester) async {
- //Scenario: Counter
- // Given I have a counter
- await CounterCustomName.iHaveACounter(tester);
- // When I increment the counter
- await CounterCustomName.iIncrementTheCounter(tester);
- // Then I should see the counter incremented
- await CounterCustomName.iShouldSeeTheCounterIncremented(tester);
- });
- test('Counter with examples', () async {
- //Scenario: Counter with examples
- final examples = [
- {'counter': '1',},
- {'counter': '2',},
- {'counter': '3',},
- ];
- for (var example in examples) {
- // Given I have a counter
- await CounterWithExamplesScenario.iHaveACounter();
- // When I increment the
- await CounterWithExamplesScenario.iIncrementThe( example['counter']!);
- // Then I should see the counter incremented
- await CounterWithExamplesScenario.iShouldSeeTheCounterIncremented();
- }
- });
- test('Counter with parameters', () async {
- //Scenario: Counter with parameters
- final examples = [
- {'counter': '1','result': '2',},
- {'counter': '2','result': '3',},
- {'counter': '3','result': '4',},
- ];
- for (var example in examples) {
- // Given I have a counter
- await CounterWithParametersScenario.iHaveACounter();
- // When I increment the counter
- await CounterWithParametersScenario.iIncrementTheCounter( example['counter']!);
- // Then I should see the result
- await CounterWithParametersScenario.iShouldSeeTheResult( example['result']!);
- }
- });
- });
-}
diff --git a/example/test/sample/sample.feature b/example/test/sample/sample.feature
index 621458a..687727b 100644
--- a/example/test/sample/sample.feature
+++ b/example/test/sample/sample.feature
@@ -31,3 +31,8 @@ Feature: Sample
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
+
+ Scenario: Counter with widget test
+ Given I have a counter
+ When I increment the counter
+ Then I should see the counter incremented
diff --git a/lib/bdd_flutter.dart b/lib/bdd_flutter.dart
index 0eaff16..17b27d2 100644
--- a/lib/bdd_flutter.dart
+++ b/lib/bdd_flutter.dart
@@ -1,70 +1,33 @@
-export 'src/feature/report/test_reporter.dart' show BDDTestReporter;
-export 'src/builder.dart' show bddTestBuilder;
-
/// A Flutter package for Behavior-Driven Development (BDD) testing.
///
-/// This package allows you to write tests in a BDD style using feature files
-/// and automatically generates the corresponding Dart test files.
+/// BDD Flutter generates Dart test files from Gherkin `.feature` files.
+/// Write scenarios in plain English using Given/When/Then syntax, then run:
///
-/// ## Features
-///
-/// * Parse Gherkin-style feature files
-/// * Generate test files automatically
-/// * Support for both widget and non-widget tests
-/// * Customizable test generation options
-/// * Ignore specific generated files using bdd_ignore.yaml
-///
-/// ## Getting Started
-///
-/// 1. Add the package to your `pubspec.yaml`:
-/// ```yaml
-/// dev_dependencies:
-/// bdd_flutter: ^1.0.0
+/// ```bash
+/// dart run bdd_flutter build
/// ```
///
-/// 2. Create a feature file (e.g., `test/features/login.feature`):
-/// ```gherkin
-/// Feature: Login
-/// As a user
-/// I want to log in to the application
-/// So that I can access my account
+/// This generates `.bdd_scenarios.dart` (step stubs) and `.bdd_test.dart`
+/// (test orchestration) files alongside each `.feature` file.
///
-/// Scenario: Successful login
-/// Given I am on the login screen
-/// When I enter valid credentials
-/// And I tap the login button
-/// Then I should see the home screen
-/// ```
+/// ## Quick Start
///
-/// 3. Run the build_runner to generate test files:
-/// ```bash
-/// flutter pub run build_runner build
-/// ```
+/// 1. Create a `.feature` file in your test folder
+/// 2. Run `dart run bdd_flutter build` to generate test files
+/// 3. Implement the step methods in the generated scenario classes
+/// 4. Run `dart run bdd_flutter test` for a BDD-formatted test report
///
/// ## Configuration
///
-/// You can configure the builder in your `build.yaml`:
-/// ```yaml
-/// targets:
-/// $default:
-/// builders:
-/// bdd_flutter|bdd_test_builder:
-/// options:
-/// generate_widget_tests: true
-/// ```
-///
-/// ## Ignoring Generated Files
+/// Configure via `.bdd_flutter/config.yaml`:
///
-/// If you need to modify a generated test file, you can prevent it from being
-/// overwritten by adding it to `bdd_ignore.yaml`:
/// ```yaml
-/// ignore_files:
-/// - test/features/login_test.dart
-/// - test/features/registration_test.dart
+/// test_dir: "test/"
+/// generate_widget_tests: true
+/// ignore_features:
+/// - test/features/login.feature
+/// additional_imports:
+/// - "package:mocktail/mocktail.dart"
+/// scenario_suffix: "Scenario"
/// ```
-///
-/// Files listed in `bdd_ignore.yaml` will be skipped during build_runner execution.
-///
-/// ## Additional Information
-///
-/// For more information, visit the [documentation](https://example.com/bdd_flutter).
+library bdd_flutter;
diff --git a/lib/src/builder.dart b/lib/src/builder.dart
deleted file mode 100644
index 9b4495e..0000000
--- a/lib/src/builder.dart
+++ /dev/null
@@ -1,62 +0,0 @@
-// lib/builder.dart
-import 'dart:async';
-
-import 'package:bdd_flutter/src/feature/builder/domain/bdd_ignore.dart';
-import 'package:bdd_flutter/src/feature/builder/domain/bdd_options.dart';
-import 'package:build/build.dart';
-
-import 'feature/builder/bdd_builders/bdd_factory.dart';
-
-/// A builder that generates test files from feature files
-class BDDTestBuilder implements Builder {
- final BDDOptions options;
-
- BDDTestBuilder({required this.options});
-
- @override
- final buildExtensions = const {
- r'.feature': ['.bdd_scenarios.g.dart', '.bdd_test.g.dart'],
- };
-
- @override
- Future build(BuildStep buildStep) async {
- await BDDIgnore.initialize();
-
- // Check if the feature file should be ignored
- if (options.ignoreFeatures.contains(buildStep.inputId.path)) {
- return;
- }
-
- final factory = BDDFactory.create(options);
- final feature = await factory.featureBuilder.build(buildStep);
-
- // Check if we should ignore the generated files
- final outputId = buildStep.inputId.changeExtension('.bdd_test.g.dart');
-
- if (BDDIgnore.shouldIgnore(outputId.path)) {
- return;
- }
-
- // Skip generation if feature is empty (due to @ignore)
- if (feature.name.isEmpty) {
- return;
- }
-
- await factory.scenarioBuilder.build(buildStep, feature);
- await factory.testFileBuilder.build(buildStep, feature);
- }
-}
-
-Builder bddTestBuilder(BuilderOptions options) {
- final config = options.config;
- final generateWidgetTests = config['generate_widget_tests'] as bool? ?? true;
- final enableReporter = config['enable_reporter'] as bool? ?? false;
- final ignoreFeatures = (config['ignore_features'] as List?)?.cast() ?? [];
-
- final bddOptions = BDDOptions(
- generateWidgetTests: generateWidgetTests,
- enableReporter: enableReporter,
- ignoreFeatures: ignoreFeatures,
- );
- return BDDTestBuilder(options: bddOptions);
-}
diff --git a/lib/src/constraints/file_constraint.dart b/lib/src/constraints/file_constraint.dart
new file mode 100644
index 0000000..52a5032
--- /dev/null
+++ b/lib/src/constraints/file_constraint.dart
@@ -0,0 +1,11 @@
+/// File extension constants used by the generator.
+class FileConstraint {
+ /// Extension for Gherkin feature files.
+ static const String feature = '.feature';
+
+ /// Extension for generated test orchestration files.
+ static const String generatedTest = '.bdd_test.dart';
+
+ /// Extension for generated scenario class files.
+ static const String generatedScenarios = '.bdd_scenarios.dart';
+}
diff --git a/lib/src/domain/background.dart b/lib/src/domain/background.dart
new file mode 100644
index 0000000..b24af4e
--- /dev/null
+++ b/lib/src/domain/background.dart
@@ -0,0 +1,20 @@
+import 'step.dart';
+
+/// Represents a Background block in a Gherkin feature file.
+///
+/// Background steps run before every scenario in the feature.
+///
+/// ```gherkin
+/// Feature: Counter
+/// Background: Setup
+/// Given I have a counter with value 0
+/// ```
+class Background {
+ /// A description of the background (text after `Background:`).
+ String description;
+
+ /// The steps that make up the background setup.
+ List steps;
+
+ Background({required this.description, required this.steps});
+}
diff --git a/lib/src/domain/build_options.dart b/lib/src/domain/build_options.dart
new file mode 100644
index 0000000..c36e42f
--- /dev/null
+++ b/lib/src/domain/build_options.dart
@@ -0,0 +1,9 @@
+/// Options passed from the CLI to control build behavior.
+class BuildOptions {
+ /// When true, regenerates all files regardless of manifest state.
+ final bool force;
+
+ const BuildOptions({
+ this.force = false,
+ });
+}
diff --git a/lib/src/domain/config.dart b/lib/src/domain/config.dart
new file mode 100644
index 0000000..428d891
--- /dev/null
+++ b/lib/src/domain/config.dart
@@ -0,0 +1,34 @@
+/// Configuration loaded from `.bdd_flutter/config.yaml`.
+///
+/// Controls how the generator finds feature files, generates test code,
+/// and names scenario classes.
+class BDDConfig {
+ /// Directory to scan for `.feature` files.
+ final String testDir;
+
+ /// When true, generates `testWidgets` with `WidgetTester`.
+ /// When false, generates `test` without `WidgetTester`.
+ final bool generateWidgetTests;
+
+ /// Feature file paths to skip during generation.
+ final List ignoreFeatures;
+
+ /// Import statements added to every generated `.bdd_scenarios.dart` file.
+ ///
+ /// Useful for shared test helpers, mock packages, etc.
+ final List additionalImports;
+
+ /// Suffix appended to scenario class names.
+ ///
+ /// Default is `Scenario` (e.g., `IncrementScenario`).
+ /// Set to `Steps` for `IncrementSteps`.
+ final String scenarioSuffix;
+
+ const BDDConfig({
+ this.testDir = 'test/',
+ this.generateWidgetTests = true,
+ this.ignoreFeatures = const [],
+ this.additionalImports = const [],
+ this.scenarioSuffix = 'Scenario',
+ });
+}
diff --git a/lib/src/domain/decorator.dart b/lib/src/domain/decorator.dart
new file mode 100644
index 0000000..0d7ac1b
--- /dev/null
+++ b/lib/src/domain/decorator.dart
@@ -0,0 +1,44 @@
+/// Gherkin tag decorators that control test generation behavior.
+///
+/// Decorators are placed above `Feature:` or `Scenario:` lines in `.feature` files:
+///
+/// ```gherkin
+/// @unitTest
+/// Feature: Calculator
+/// @widgetTest
+/// Scenario: Increment
+/// ```
+///
+/// Scenario-level decorators override feature-level ones.
+enum Decorator {
+ /// Generate a unit test using `test()` without `WidgetTester`.
+ unitTest,
+
+ /// Generate a widget test using `testWidgets()` with `WidgetTester`.
+ widgetTest,
+
+ /// Unrecognized decorator tag.
+ unknown;
+
+ /// Parses a decorator string (e.g., `@unitTest`) into a [Decorator] value.
+ static Decorator fromString(String text) {
+ final trimmed = text.trim();
+ return switch (trimmed) {
+ '@unitTest' => Decorator.unitTest,
+ '@widgetTest' => Decorator.widgetTest,
+ _ => Decorator.unknown,
+ };
+ }
+}
+
+/// Convenience getters for a single [Decorator].
+extension DecoratorX on Decorator {
+ bool get isUnitTest => this == Decorator.unitTest;
+ bool get isWidgetTest => this == Decorator.widgetTest;
+}
+
+/// Convenience getters for a set of [Decorator]s.
+extension DecoratorSetX on Set {
+ bool get hasUnitTest => any((e) => e.isUnitTest);
+ bool get hasWidgetTest => any((e) => e.isWidgetTest);
+}
diff --git a/lib/src/domain/feature.dart b/lib/src/domain/feature.dart
new file mode 100644
index 0000000..c41b60f
--- /dev/null
+++ b/lib/src/domain/feature.dart
@@ -0,0 +1,52 @@
+import '../constraints/file_constraint.dart';
+
+import 'background.dart';
+import 'decorator.dart';
+import 'scenario.dart';
+
+/// Represents a parsed Gherkin feature file.
+///
+/// A feature contains a name, file path, optional background, a list of
+/// scenarios, and any decorators applied at the feature level.
+class Feature {
+ /// The feature name (text after `Feature:`).
+ String name;
+
+ /// The file path of the `.feature` file.
+ String path;
+
+ /// The scenarios defined in this feature.
+ List scenarios;
+
+ /// Optional background steps shared by all scenarios.
+ Background? background;
+
+ /// Decorators applied at the feature level (e.g., `@unitTest`).
+ Set decorators;
+
+ Feature({
+ required this.name,
+ required this.path,
+ required this.scenarios,
+ required this.decorators,
+ this.background,
+ });
+}
+
+/// Extension methods for [Feature].
+extension FeatureX on Feature {
+ /// The generated scenarios file name (e.g., `counter.bdd_scenarios.dart`).
+ String get scenariosFileName {
+ return '${fileName.replaceAll('.feature', '')}${FileConstraint.generatedScenarios}';
+ }
+
+ /// The generated test file name (e.g., `counter.bdd_test.dart`).
+ String get testFileName {
+ return '${fileName.replaceAll('.feature', '')}${FileConstraint.generatedTest}';
+ }
+
+ /// The base file name without directory path or `.feature` extension.
+ String get fileName {
+ return path.split('/').last.replaceAll('.feature', '');
+ }
+}
diff --git a/lib/src/domain/manifest.dart b/lib/src/domain/manifest.dart
new file mode 100644
index 0000000..9fc8eb4
--- /dev/null
+++ b/lib/src/domain/manifest.dart
@@ -0,0 +1,60 @@
+/// Tracks the state of generated files in `.bdd_flutter/manifest.yaml`.
+///
+/// The manifest enables incremental builds by storing hashes of each scenario.
+/// On subsequent builds, only new or changed scenarios trigger generation.
+class Manifest {
+ /// Manifest format version.
+ final String version;
+
+ /// Timestamp of the last generation run.
+ final DateTime lastGenerated;
+
+ /// List of tracked feature files and their scenarios.
+ final List features;
+
+ Manifest({
+ this.version = '1.0',
+ DateTime? lastGenerated,
+ this.features = const [],
+ }) : lastGenerated = lastGenerated ?? DateTime.now();
+}
+
+/// A feature entry in the manifest, tracking its file and scenarios.
+class ManifestFeature {
+ /// Path to the `.feature` file.
+ final String path;
+
+ /// ISO 8601 timestamp of the feature file's last modification.
+ final String lastModified;
+
+ /// Path to the generated `.bdd_test.dart` file.
+ final String testFile;
+
+ /// Tracked scenarios with their content hashes.
+ final List scenarios;
+
+ ManifestFeature({
+ required this.path,
+ required this.lastModified,
+ required this.testFile,
+ this.scenarios = const [],
+ });
+}
+
+/// A scenario entry in the manifest, identified by its content hash.
+class ManifestScenario {
+ /// The scenario name.
+ final String name;
+
+ /// MD5 hash of the scenario's content (name, steps, examples, decorators).
+ final String hash;
+
+ /// The generated test method name.
+ final String testMethod;
+
+ ManifestScenario({
+ required this.name,
+ required this.hash,
+ required this.testMethod,
+ });
+}
diff --git a/lib/src/domain/scenario.dart b/lib/src/domain/scenario.dart
new file mode 100644
index 0000000..8c3bb94
--- /dev/null
+++ b/lib/src/domain/scenario.dart
@@ -0,0 +1,69 @@
+import 'dart:convert';
+import '../extensions/string_x.dart';
+import 'package:crypto/crypto.dart';
+
+import 'decorator.dart';
+import 'step.dart';
+
+/// Represents a Gherkin scenario with its steps, examples, and decorators.
+///
+/// Each scenario generates an instance-based class with step methods.
+/// Users implement the step logic; the test file instantiates and calls them.
+class Scenario {
+ /// The scenario name (text after `Scenario:`).
+ String name;
+
+ /// The steps in this scenario (Given/When/Then/And).
+ List steps;
+
+ /// Example rows for parameterized scenarios, if any.
+ ///
+ /// Each entry maps column header to cell value.
+ List