Skip to content

Commit 3d086cd

Browse files
authored
Merge pull request #118 from olexale/bugfix/scenario_outlines_arrays
Fix Scenario Outline Variables in Array and Named Parameter Variable Wrapped in Invalid Curly Braces
2 parents 2d445e8 + a0c0c0e commit 3d086cd

6 files changed

Lines changed: 671 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [2.1.3] - Data table special characters fix
2+
3+
* Make placeholder replacement function more context-aware to avoid replacing special characters inside data table cells
4+
15
## [2.1.2] - Gherkin comments support
26

37
* Add support for Gherkin comments (lines starting with `#`)

lib/src/scenario_generator.dart

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,59 @@ String _replacePlaceholders(
152152
return headReplaced + tailReplaced;
153153
}
154154
}
155-
// Default behaviour: placeholders become parameters
156-
var replaced = line;
157-
for (final e in example.keys) {
158-
replaced = replaced.replaceAll('<$e>', '{${example[e]}}');
155+
156+
return _replacePlaceholdersWithContext(line, example);
157+
}
158+
159+
// Placeholders inside {} blocks become raw values,
160+
// Placeholders outside {} blocks become parameters (wrapped with {})
161+
String _replacePlaceholdersWithContext(
162+
String line,
163+
Map<String, String> example,
164+
) {
165+
final result = StringBuffer();
166+
var i = 0;
167+
var braceDepth = 0;
168+
169+
while (i < line.length) {
170+
// Track brace depth to know if we're inside a parameter block
171+
if (line[i] == '{') {
172+
braceDepth++;
173+
result.write('{');
174+
i++;
175+
} else if (line[i] == '}') {
176+
braceDepth--;
177+
result.write('}');
178+
i++;
179+
} else if (line[i] == '<') {
180+
// Check if this is a placeholder
181+
var foundPlaceholder = false;
182+
for (final key in example.keys) {
183+
final placeholder = '<$key>';
184+
if (i + placeholder.length <= line.length &&
185+
line.substring(i, i + placeholder.length) == placeholder) {
186+
// Found a placeholder
187+
if (braceDepth > 0) {
188+
// Inside a parameter block - use raw value
189+
result.write(example[key]);
190+
} else {
191+
// Outside parameter blocks - wrap with {}
192+
result.write('{${example[key]}}');
193+
}
194+
i += placeholder.length;
195+
foundPlaceholder = true;
196+
break;
197+
}
198+
}
199+
if (!foundPlaceholder) {
200+
result.write(line[i]);
201+
i++;
202+
}
203+
} else {
204+
result.write(line[i]);
205+
i++;
206+
}
159207
}
160-
return replaced;
208+
209+
return result.toString();
161210
}

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: bdd_widget_test
22
description: A BDD-style widget testing library. Generates Flutter widget tests from *.feature files.
3-
version: 2.1.2
3+
version: 2.1.3
44
repository: https://github.com/olexale/bdd_widget_test
55
issue_tracker: https://github.com/olexale/bdd_widget_test/issues
66

test/data_tables_test.dart

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,118 @@ void main() {
568568
});
569569
});
570570
}
571+
''';
572+
573+
final feature = FeatureFile(
574+
featureDir: 'test.feature',
575+
package: 'test',
576+
input: featureFile,
577+
);
578+
expect(feature.dartContent, expectedFeatureDart);
579+
});
580+
581+
test('Data table with single row (headers only)', () {
582+
const featureFile = '''
583+
Feature: Testing feature
584+
Scenario: Testing scenario
585+
Given the following songs
586+
| 'artist' | 'title' |
587+
''';
588+
589+
const expectedFeatureDart = '''
590+
// GENERATED CODE - DO NOT MODIFY BY HAND
591+
// ignore_for_file: type=lint, type=warning
592+
593+
import 'package:bdd_widget_test/data_table.dart' as bdd;
594+
import 'package:flutter/material.dart';
595+
import 'package:flutter_test/flutter_test.dart';
596+
597+
import './step/the_following_songs.dart';
598+
599+
void main() {
600+
group(\'\'\'Testing feature\'\'\', () {
601+
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
602+
await theFollowingSongs(tester, const bdd.DataTable([['artist', 'title']]));
603+
});
604+
});
605+
}
606+
''';
607+
608+
final feature = FeatureFile(
609+
featureDir: 'test.feature',
610+
package: 'test',
611+
input: featureFile,
612+
);
613+
expect(feature.dartContent, expectedFeatureDart);
614+
});
615+
616+
test('Data table with special characters in cells', () {
617+
const featureFile = '''
618+
Feature: Testing feature
619+
Scenario: Testing scenario
620+
Given the following data
621+
| 'name' | 'description' |
622+
| 'Test "One"' | 'Has quotes' |
623+
| 'Test <Two>' | 'Has angle brackets' |
624+
| 'Test {3}' | 'Has braces' |
625+
| 'Test, Four' | 'Has comma' |
626+
''';
627+
628+
const expectedFeatureDart = '''
629+
// GENERATED CODE - DO NOT MODIFY BY HAND
630+
// ignore_for_file: type=lint, type=warning
631+
632+
import 'package:bdd_widget_test/data_table.dart' as bdd;
633+
import 'package:flutter/material.dart';
634+
import 'package:flutter_test/flutter_test.dart';
635+
636+
import './step/the_following_data.dart';
637+
638+
void main() {
639+
group(\'\'\'Testing feature\'\'\', () {
640+
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
641+
await theFollowingData(tester, const bdd.DataTable([['name', 'description'], ['Test "One"', 'Has quotes'], ['Test <Two>', 'Has angle brackets'], ['Test {3}', 'Has braces'], ['Test, Four', 'Has comma']]));
642+
});
643+
});
644+
}
645+
''';
646+
647+
final feature = FeatureFile(
648+
featureDir: 'test.feature',
649+
package: 'test',
650+
input: featureFile,
651+
);
652+
expect(feature.dartContent, expectedFeatureDart);
653+
});
654+
655+
test('Data table with unicode and emoji', () {
656+
const featureFile = '''
657+
Feature: Testing feature
658+
Scenario: Testing scenario
659+
Given the following items
660+
| 'emoji' | 'description' |
661+
| '🚀' | 'Rocket' |
662+
| '💯' | 'Perfect' |
663+
| 'Ñoño' | 'Spanish' |
664+
''';
665+
666+
const expectedFeatureDart = '''
667+
// GENERATED CODE - DO NOT MODIFY BY HAND
668+
// ignore_for_file: type=lint, type=warning
669+
670+
import 'package:bdd_widget_test/data_table.dart' as bdd;
671+
import 'package:flutter/material.dart';
672+
import 'package:flutter_test/flutter_test.dart';
673+
674+
import './step/the_following_items.dart';
675+
676+
void main() {
677+
group(\'\'\'Testing feature\'\'\', () {
678+
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
679+
await theFollowingItems(tester, const bdd.DataTable([['emoji', 'description'], ['🚀', 'Rocket'], ['💯', 'Perfect'], ['Ñoño', 'Spanish']]));
680+
});
681+
});
682+
}
571683
''';
572684

573685
final feature = FeatureFile(

test/feature_test.dart

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import 'package:bdd_widget_test/src/feature_file.dart';
2-
import 'package:bdd_widget_test/src/generator_options.dart';
32
import 'package:test/test.dart';
43

54
import 'util/testing_data.dart';
@@ -108,18 +107,49 @@ void main() {
108107
);
109108
expect(feature.dartContent, expectedFeatureDart);
110109
});
111-
test('custom headers replace default imports in feature file', () async {
110+
111+
test('Feature with special characters in names', () {
112112
const expectedFeatureDart = '''
113-
${expectedComment}import 'package:patrol/patrol.dart';
114-
// Import flutter_test for compatibility
115-
import 'package:flutter_test/flutter_test.dart';
113+
$expectedComment// some comment
114+
115+
${expectedImports}import './step/the_app_is_running.dart';
116+
import './step/i_see_text.dart';
117+
118+
void main() {
119+
group(\'\'\'"Testing" <Special> {Characters}\'\'\', () {
120+
testWidgets(\'\'\'Test's "special" characters\'\'\', (tester) async {
121+
await theAppIsRunning(tester);
122+
await iSeeText(tester, 'test');
123+
});
124+
});
125+
}
126+
''';
116127

117-
import './step/the_app_is_running.dart';
128+
final feature = FeatureFile(
129+
featureDir: 'test.feature',
130+
package: 'test',
131+
input: '''
132+
// some comment
133+
134+
Feature: "Testing" <Special> {Characters}
135+
Scenario: Test's "special" characters
136+
Given the app is running
137+
Then I see {'test'} text
138+
''',
139+
);
140+
expect(feature.dartContent, expectedFeatureDart);
141+
});
142+
143+
test('Feature with very long step description', () {
144+
const expectedFeatureDart = '''
145+
${expectedHeader}import './step/the_app_is_running.dart';
146+
import './step/i_verify_that_this_is_a_very_long_step_description_that_tests_whether_the_framework_can_handle_extremely_long_step_names_without_issues.dart';
118147
119148
void main() {
120149
group(\'\'\'Testing feature\'\'\', () {
121150
testWidgets(\'\'\'Testing scenario\'\'\', (tester) async {
122151
await theAppIsRunning(tester);
152+
await iVerifyThatThisIsAVeryLongStepDescriptionThatTestsWhetherTheFrameworkCanHandleExtremelyLongStepNamesWithoutIssues(tester);
123153
});
124154
});
125155
}
@@ -128,14 +158,55 @@ void main() {
128158
final feature = FeatureFile(
129159
featureDir: 'test.feature',
130160
package: 'test',
131-
input: minimalFeatureFile,
132-
generatorOptions: const GeneratorOptions(
133-
customHeaders: [
134-
"import 'package:patrol/patrol.dart';",
135-
'// Import flutter_test for compatibility',
136-
"import 'package:flutter_test/flutter_test.dart';",
137-
],
138-
),
161+
input: '''
162+
Feature: Testing feature
163+
Scenario: Testing scenario
164+
Given the app is running
165+
Then I verify that this is a very long step description that tests whether the framework can handle extremely long step names without issues
166+
''',
167+
);
168+
expect(feature.dartContent, expectedFeatureDart);
169+
});
170+
171+
test('Multiple scenarios in single feature', () {
172+
const expectedFeatureDart = '''
173+
${expectedHeader}import './step/the_app_is_running.dart';
174+
import './step/i_see_text.dart';
175+
import './step/i_tap_icon.dart';
176+
177+
void main() {
178+
group(\'\'\'Login feature\'\'\', () {
179+
testWidgets(\'\'\'Successful login\'\'\', (tester) async {
180+
await theAppIsRunning(tester);
181+
await iSeeText(tester, 'Login');
182+
});
183+
testWidgets(\'\'\'Failed login\'\'\', (tester) async {
184+
await theAppIsRunning(tester);
185+
await iSeeText(tester, 'Error');
186+
});
187+
testWidgets(\'\'\'Logout\'\'\', (tester) async {
188+
await iTapIcon(tester, Icons.logout);
189+
});
190+
});
191+
}
192+
''';
193+
194+
final feature = FeatureFile(
195+
featureDir: 'test.feature',
196+
package: 'test',
197+
input: '''
198+
Feature: Login feature
199+
Scenario: Successful login
200+
Given the app is running
201+
Then I see {'Login'} text
202+
203+
Scenario: Failed login
204+
Given the app is running
205+
Then I see {'Error'} text
206+
207+
Scenario: Logout
208+
When I tap {Icons.logout} icon
209+
''',
139210
);
140211
expect(feature.dartContent, expectedFeatureDart);
141212
});

0 commit comments

Comments
 (0)