From 7a23637968e7f439bac9590bf2ea87a2da6fe643 Mon Sep 17 00:00:00 2001 From: Oleksandr Leushchenko Date: Tue, 23 Sep 2025 10:32:36 +0300 Subject: [PATCH 1/4] Add failing test --- test/data_tables_test.dart | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/data_tables_test.dart b/test/data_tables_test.dart index da54746..bd2cbfe 100644 --- a/test/data_tables_test.dart +++ b/test/data_tables_test.dart @@ -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 + | 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']])); + }); + }); +} '''; final feature = FeatureFile( From c1ad6b8d86010dc5aa6b84382e0bfb9a95a173f7 Mon Sep 17 00:00:00 2001 From: Oleksandr Leushchenko Date: Tue, 23 Sep 2025 19:01:13 +0300 Subject: [PATCH 2/4] Improve DataTable parser --- lib/src/data_table_parser.dart | 44 +++++++++++++++++++++++++++++---- lib/src/feature_file.dart | 28 +++++++++++---------- lib/src/scenario_generator.dart | 22 +++++++++++++++-- 3 files changed, 74 insertions(+), 20 deletions(-) diff --git a/lib/src/data_table_parser.dart b/lib/src/data_table_parser.dart index 658d806..305ad5b 100644 --- a/lib/src/data_table_parser.dart +++ b/lib/src/data_table_parser.dart @@ -8,8 +8,9 @@ bool hasBddDataTable(List 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; } } @@ -24,9 +25,9 @@ Iterable replaceDataTables(List 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)) { @@ -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 `` 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 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 _createRow({ required BddLine bddLine, }) => diff --git a/lib/src/feature_file.dart b/lib/src/feature_file.dart index a358e40..6330161 100644 --- a/lib/src/feature_file.dart +++ b/lib/src/feature_file.dart @@ -92,25 +92,27 @@ class FeatureFile { List getStepFiles() => _stepFiles; static List _prepareLines(Iterable 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); diff --git a/lib/src/scenario_generator.dart b/lib/src/scenario_generator.dart index e1da2b7..b5c9354 100644 --- a/lib/src/scenario_generator.dart +++ b/lib/src/scenario_generator.dart @@ -134,10 +134,28 @@ String _replacePlaceholders( bool isDataTableStep, Map 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; } From a5e0b43555bdb50f96844e0123052074f13f1b34 Mon Sep 17 00:00:00 2001 From: Oleksandr Leushchenko Date: Tue, 23 Sep 2025 19:01:19 +0300 Subject: [PATCH 3/4] Update tests --- test/data_tables_test.dart | 46 +++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/test/data_tables_test.dart b/test/data_tables_test.dart index bd2cbfe..e2892fa 100644 --- a/test/data_tables_test.dart +++ b/test/data_tables_test.dart @@ -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 @@ -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']])); }); @@ -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 @@ -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'); @@ -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' | '''; @@ -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']])); }); }); } @@ -213,7 +213,7 @@ Feature: Testing feature And 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: @@ -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']])); }); }); } @@ -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 is on tour @@ -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'); @@ -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 @@ -355,7 +355,7 @@ void main() { group(\'\'\'Testing feature\'\'\', () { Future 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 { @@ -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' | @@ -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); } @@ -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' | '''; @@ -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']])); }); }); } From fdcd94eb2d69e8d91016976b1629284f160d74ae Mon Sep 17 00:00:00 2001 From: Oleksandr Leushchenko Date: Tue, 23 Sep 2025 19:04:11 +0300 Subject: [PATCH 4/4] FIx DataTable in tests --- test/data_tables_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/data_tables_test.dart b/test/data_tables_test.dart index e2892fa..5d53648 100644 --- a/test/data_tables_test.dart +++ b/test/data_tables_test.dart @@ -542,7 +542,7 @@ Feature: Testing feature Scenario Outline: Testing visibility of data table in examples Given I load the splash screen Then I verify welcome messages with - | text | + | 'text' | | 'Welcome' | Examples: | authStatus | @@ -564,7 +564,7 @@ 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']])); + await iVerifyWelcomeMessagesWith(tester, 'initial', const bdd.DataTable([['text'], ['Welcome']])); }); }); }