Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 39 additions & 5 deletions lib/src/data_table_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ bool hasBddDataTable(List<BddLine> lines) {
lines[index].type == LineType.step ||
lines[index].type == LineType.dataTableStep;
final isNextLineTable = isTable(lines: lines, index: index + 1);
final isExamplesFormatted = hasExamplesFormat(bddLine: lines[index]);
if (isStep && isNextLineTable && !isExamplesFormatted) {
if (isStep &&
isNextLineTable &&
!isDataTableExamples(lines: lines, index: index)) {
return true;
}
}
Expand All @@ -24,9 +25,9 @@ Iterable<BddLine> replaceDataTables(List<BddLine> lines) sync* {
final isNextLineTable = isTable(lines: lines, index: index + 1);
if (isStep && isNextLineTable) {
final table =
!hasExamplesFormat(bddLine: lines[index])
? _createCucumberDataTable(lines: lines, index: index)
: _createDataTableFromExamples(lines: lines, index: index);
isDataTableExamples(lines: lines, index: index)
? _createDataTableFromExamples(lines: lines, index: index)
: _createCucumberDataTable(lines: lines, index: index);
yield* table;
// skip the parsed table
while (isTable(lines: lines, index: index + 1)) {
Expand All @@ -46,6 +47,39 @@ bool isTable({
bool hasExamplesFormat({required BddLine bddLine}) =>
examplesRegExp.firstMatch(bddLine.rawLine) != null;

/// Determines if the data table following the step at [index]
/// is intended to be used as Examples (parameter expansion) rather than
/// as a cucumber data table argument.
///
/// Heuristic: if the step contains placeholders like `<name>` and the first
/// row of the following table contains headers that include ALL of those
/// placeholder names, then treat the table as Examples; otherwise, treat it
/// as a cucumber data table.
bool isDataTableExamples({
required List<BddLine> lines,
required int index,
}) {
final step = lines[index];
if (!hasExamplesFormat(bddLine: step)) return false;
final nextIsTable = isTable(lines: lines, index: index + 1);
if (!nextIsTable) return false;

final placeholders =
examplesRegExp
.allMatches(step.rawLine)
.map((m) => m.group(1)!.trim())
.where((e) => e.isNotEmpty)
.toSet();
if (placeholders.isEmpty) return false;

// Use the first table row as headers
final headers = _createRow(bddLine: lines[index + 1]).toSet();
if (headers.isEmpty) return false;

// Only treat as examples when all placeholders are present in headers
return placeholders.every(headers.contains);
}

List<String> _createRow({
required BddLine bddLine,
}) =>
Expand Down
28 changes: 15 additions & 13 deletions lib/src/feature_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -92,25 +92,27 @@ class FeatureFile {
List<StepFile> getStepFiles() => _stepFiles;

static List<BddLine> _prepareLines(Iterable<BddLine> input) {
final lines = input
final inputList = input.toList(growable: false);
final lines = inputList
.mapIndexed(
(index, bddLine) {
final isStep = bddLine.type == LineType.step;
final hasExamplesFormat = data_table_parser.hasExamplesFormat(
bddLine: bddLine,
);
final isNextTable = data_table_parser.isTable(
lines: input.toList(),
lines: inputList,
index: index + 1,
);
if (isStep && !hasExamplesFormat && isNextTable) {
return BddLine.fromRawValue(
LineType.dataTableStep,
bddLine.rawLine,
);
} else {
return bddLine;
}
final isExamplesTable =
isNextTable &&
data_table_parser.isDataTableExamples(
lines: inputList,
index: index,
);
return isStep && isNextTable && !isExamplesTable
? BddLine.fromRawValue(
LineType.dataTableStep,
bddLine.rawLine,
)
: bddLine;
},
)
.toList(growable: false);
Expand Down
22 changes: 20 additions & 2 deletions lib/src/scenario_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,28 @@ String _replacePlaceholders(
bool isDataTableStep,
Map<String, String> example,
) {
// For data table steps, we want placeholders in the step text
// to become parameters (wrapped with {}), but placeholders inside the
// DataTable argument should be inlined as raw values.
if (isDataTableStep) {
const marker = '{const bdd.DataTable(';
final dataTableIndex = line.indexOf(marker);
if (dataTableIndex != -1) {
final head = line.substring(0, dataTableIndex);
final tail = line.substring(dataTableIndex);
var headReplaced = head;
var tailReplaced = tail;
for (final e in example.keys) {
headReplaced = headReplaced.replaceAll('<$e>', '{${example[e]}}');
tailReplaced = tailReplaced.replaceAll('<$e>', '${example[e]}');
}
return headReplaced + tailReplaced;
}
}
// Default behaviour: placeholders become parameters
var replaced = line;
for (final e in example.keys) {
final value = isDataTableStep ? '${example[e]}' : '{${example[e]}}';
replaced = replaced.replaceAll('<$e>', value);
replaced = replaced.replaceAll('<$e>', '{${example[e]}}');
}
return replaced;
}
87 changes: 64 additions & 23 deletions test/data_tables_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,19 @@ void main() {
Feature: Testing feature
Scenario: Testing scenario
Given the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
And the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
But the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
When the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
And I wait
Expand All @@ -87,10 +87,10 @@ import './step/i_wait.dart';
void main() {
group(\'\'\'Testing feature\'\'\', () {
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await iWait(tester);
await theFollowingSongs(tester, const bdd.DataTable([['The Beatles', 'Let It Be']]));
});
Expand All @@ -111,7 +111,7 @@ void main() {
Feature: Testing feature
Scenario: Testing scenario
Given the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
And I wait
Expand All @@ -136,7 +136,7 @@ import './step/the_following_users_exist.dart';
void main() {
group(\'\'\'Testing feature\'\'\', () {
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await iWait(tester);
await theFollowingUsersExist(tester, 'Oleksandr', '@olexale');
await theFollowingUsersExist(tester, 'Flutter', '@FlutterDev');
Expand Down Expand Up @@ -164,7 +164,7 @@ Feature: Testing feature
| 'Flutter' | '@FlutterDev' |
And I wait
And the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
''';
Expand All @@ -187,7 +187,7 @@ void main() {
await theFollowingUsersExist(tester, 'Oleksandr', '@olexale');
await theFollowingUsersExist(tester, 'Flutter', '@FlutterDev');
await iWait(tester);
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
});
});
}
Expand All @@ -213,7 +213,7 @@ Feature: Testing feature
And band <band> is on tour
And I wait
And the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
Examples:
Expand Down Expand Up @@ -241,7 +241,7 @@ void main() {
await theFollowingUsersExist(tester, 'Flutter', '@FlutterDev');
await bandIsOnTour(tester, 'Camel');
await iWait(tester);
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
});
});
}
Expand All @@ -261,7 +261,7 @@ void main() {
Feature: Testing feature
Scenario Outline: Testing scenario
Given the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
And band <band> is on tour
Expand Down Expand Up @@ -293,14 +293,14 @@ import './step/the_following_users_exist.dart';
void main() {
group(\'\'\'Testing feature\'\'\', () {
testWidgets(\'\'\'Outline: Testing scenario ('Camel')\'\'\', (tester) async {
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await bandIsOnTour(tester, 'Camel');
await iWait(tester);
await theFollowingUsersExist(tester, 'Oleksandr', '@olexale');
await theFollowingUsersExist(tester, 'Flutter', '@FlutterDev');
});
testWidgets(\'\'\'Outline: Testing scenario ('Pearl Jam')\'\'\', (tester) async {
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await bandIsOnTour(tester, 'Pearl Jam');
await iWait(tester);
await theFollowingUsersExist(tester, 'Oleksandr', '@olexale');
Expand All @@ -326,7 +326,7 @@ Feature: Testing feature
Background:
Given I wait
And the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
And I wait
Expand Down Expand Up @@ -355,7 +355,7 @@ void main() {
group(\'\'\'Testing feature\'\'\', () {
Future<void> bddSetUp(WidgetTester tester) async {
await iWait(tester);
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await iWait(tester);
}
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
Expand Down Expand Up @@ -391,7 +391,7 @@ Feature: Testing feature

Scenario: Testing scenario
Given the following songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |

Expand Down Expand Up @@ -419,7 +419,7 @@ void main() {
}
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
try {
await theFollowingSongs(tester, const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
} finally {
await bddTearDown(tester);
}
Expand All @@ -441,7 +441,7 @@ void main() {
Feature: Testing feature
Scenario: Testing scenario
Given the following {'Good'} songs
| artist | title |
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
''';
Expand All @@ -459,7 +459,7 @@ import './step/the_following_songs.dart';
void main() {
group(\'\'\'Testing feature\'\'\', () {
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
await theFollowingSongs(tester, 'Good', const bdd.DataTable([[artist, title], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
await theFollowingSongs(tester, 'Good', const bdd.DataTable([['artist', 'title'], ['The Beatles', 'Let It Be'], ['Camel', 'Slow yourself down']]));
});
});
}
Expand Down Expand Up @@ -527,6 +527,47 @@ void main() {
});
});
}
''';

final feature = FeatureFile(
featureDir: 'test.feature',
package: 'test',
input: featureFile,
);
expect(feature.dartContent, expectedFeatureDart);
});
test('Scenario Outline with data table variables in examples', () {
const featureFile = '''
Feature: Testing feature
Scenario Outline: Testing visibility of data table in examples
Given I load the splash screen
Then I verify welcome messages with <authStatus>
| text |
| 'Welcome' |
Examples:
| authStatus |
| 'initial' |
''';

const expectedFeatureDart = '''
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning

import 'package:bdd_widget_test/data_table.dart' as bdd;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import './step/i_load_the_splash_screen.dart';
import './step/i_verify_welcome_messages_with.dart';

void main() {
group(\'\'\'Testing feature\'\'\', () {
testWidgets(\'\'\'Outline: Testing visibility of data table in examples ('initial')\'\'\', (tester) async {
await iLoadTheSplashScreen(tester);
await iVerifyWelcomeMessagesWith(tester, 'initial', const bdd.DataTable([[text], ['Welcome']]));
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable text is not quoted as a string literal. Based on the pattern in the rest of the file, this should be 'text' to match the expected string format for data table headers.

Suggested change
await iVerifyWelcomeMessagesWith(tester, 'initial', const bdd.DataTable([[text], ['Welcome']]));
await iVerifyWelcomeMessagesWith(tester, 'initial', const bdd.DataTable([['text'], ['Welcome']]));

Copilot uses AI. Check for mistakes.
});
});
}
''';

final feature = FeatureFile(
Expand Down