Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cucumber data table #56

Merged
merged 21 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,4 @@ pubspec.lock
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
coverage/lcov.info
/coverage/
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
7 changes: 7 additions & 0 deletions example/test/songs.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Feature: Songs

Scenario: Available songs
Given the following songs
| 'artist' | 'title' |
| 'The Beatles' | 'Let It Be' |
| 'Camel' | 'Slow yourself down' |
16 changes: 16 additions & 0 deletions example/test/songs_test.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions example/test/step/the_following_songs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:bdd_widget_test/src/data_table.dart' as bdd;
import 'package:flutter_test/flutter_test.dart';

/// Usage: the following songs
Future<void> theFollowingSongs(WidgetTester tester, bdd.DataTable dataTable) async {
throw UnimplementedError();
}
4 changes: 4 additions & 0 deletions lib/src/bdd_line.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class BddLine {
final LineType type;
}

class DataTableBddLine extends BddLine {
Copy link
Owner

Choose a reason for hiding this comment

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

Types for all other lines from features files are described in LineType enum. Are there any reason why the data table line can not follow the same pattern?

Copy link
Contributor Author

@ron-brosh ron-brosh Jan 21, 2024

Choose a reason for hiding this comment

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

Besides _lineTypeFromString which needs the next lines in order to determine the dataTable type, the
step enum is used in the generation logic which fails the tests when I add the new type.
It also adds a lot of boilerplate (will have to be repeated in multiple places):

if(type = LineType.step || type == LineType.dataTable)

LineType enum describes unique lines that can be map to those types.
A cucumber data table is determined by the combination of a step and examples.
I'll be happy to explore any proposal that might simplify and keep the existing convention though.

Copy link
Contributor Author

@ron-brosh ron-brosh Jan 22, 2024

Choose a reason for hiding this comment

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

Added a new enum value dataTableStep and modified the StepFile.create to accept the domain model BDD line instead of its internal implementation details.

DataTableBddLine(super.rawLine);
}

enum LineType {
feature,
background,
Expand Down
23 changes: 23 additions & 0 deletions lib/src/data_table.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:collection/collection.dart';

class DataTable {
const DataTable(this._data);

final List<List<dynamic>> _data;

List<List<dynamic>> asLists({bool ignoreFirstRow = false}) =>
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
_data.sublist(ignoreFirstRow ? 1 : 0, _data.length);
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved

List<Map<dynamic, dynamic>> asMaps() {
final result = <Map<dynamic, dynamic>>[];
final headers = _data.first;
_data.skip(1).forEach((row) {
final map = <dynamic, dynamic>{};
headers.forEachIndexed((index, header) {
map[header] = row[index];
});
result.add(map);
});
return result;
}
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
}
100 changes: 71 additions & 29 deletions lib/src/data_table_parser.dart
Original file line number Diff line number Diff line change
@@ -1,37 +1,79 @@
import 'package:bdd_widget_test/src/bdd_line.dart';
import 'package:bdd_widget_test/src/regex.dart';
import 'package:bdd_widget_test/src/scenario_generator.dart';

Iterable<BddLine> replaceDataTables(List<BddLine> lines) sync* {
var reachedExamples = false;
var i = 0;
while (i < lines.length) {
if (lines[i].type == LineType.exampleTitle) {
reachedExamples = true;
}
if (reachedExamples) {
yield lines[i++];
continue;
abstract class DataTableParser {
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
static bool hasBddDataTable(List<BddLine> lines) {
for (var index = 0; index < lines.length; index++) {
final isStep = lines[index].type == LineType.step;
final isNextLineTable = isNextTable(lines: lines, index: index + 1);
final isExamplesFormatted = hasExamplesFormat(bddLine: lines[index]);
if (isStep && isNextLineTable && !isExamplesFormatted) {
return true;
}
}
if (i + 1 < lines.length && _foundDataTable(lines, i)) {
final dataTable = [
lines[i],
...lines.skip(i + 1).takeWhile((l) => l.type == LineType.examples),
];
final data = generateScenariosFromScenaioOutline([
// pretend to be an Example section to re-use some logic
BddLine.fromValue(LineType.exampleTitle, ''),
...dataTable,
]);
for (final item in data) {
yield item[1];
return false;
}

static Iterable<BddLine> replaceDataTables(List<BddLine> lines) sync* {
for (var index = 0; index < lines.length; index++) {
final isStep = lines[index].type == LineType.step;
final isNextLineTable = isNextTable(lines: lines, index: index + 1);
if (isStep && isNextLineTable) {
if (!hasExamplesFormat(bddLine: lines[index])) {
// Cucumber data table
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
final text = lines[index].value;
final table = List<List<String>>.empty(growable: true);
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
do {
table.add(_createRow(bddLine: lines[++index]));
} while (isNextTable(lines: lines, index: index + 1));
yield BddLine.fromValue(
LineType.step,
'$text {const bdd.DataTable($table)}',
);
} else {
// Data table formatted as examples
final dataTable = [lines[index]];
do {
dataTable.add(lines[++index]);
} while (index + 1 < lines.length &&
lines[index + 1].type == LineType.examples);
final data = generateScenariosFromScenaioOutline([
// pretend to be an Example section to re-use some logic
BddLine.fromValue(LineType.exampleTitle, ''),
...dataTable,
]);

for (final item in data) {
yield item[1];
}
}
} else {
yield lines[index];
}
i += dataTable.length;
continue;
}
yield lines[i++];
}
}

bool _foundDataTable(List<BddLine> lines, int index) =>
lines[index].type == LineType.step &&
lines[index + 1].type == LineType.examples;
static bool isNextTable({
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
required List<BddLine> lines,
required int index,
}) =>
index < lines.length && _isTable(bddLine: lines[index]);

static bool _isTable({required BddLine bddLine}) =>
bddLine.type == LineType.examples;
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved

static bool hasExamplesFormat({required BddLine bddLine}) =>
examplesRegExp.allMatches(bddLine.rawLine).isNotEmpty;
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved

static List<String> _createRow({
required BddLine bddLine,
}) =>
List<String>.unmodifiable(
bddLine.value
.split('|')
.map((example) => example.trim())
.takeWhile((value) => value.isNotEmpty)
.toList(),
);
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
}
27 changes: 24 additions & 3 deletions lib/src/feature_file.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:bdd_widget_test/src/bdd_line.dart';
import 'package:bdd_widget_test/src/data_table_parser.dart';
import 'package:bdd_widget_test/src/feature_generator.dart';
import 'package:bdd_widget_test/src/generator_options.dart';
import 'package:bdd_widget_test/src/step_file.dart';
import 'package:bdd_widget_test/src/util/common.dart';
import 'package:bdd_widget_test/src/util/constants.dart';
import 'package:collection/collection.dart';

class FeatureFile {
FeatureFile({
Expand Down Expand Up @@ -39,6 +41,7 @@ class FeatureFile {
generatorOptions,
_testerType,
_testerName,
e is DataTableBddLine,
),
)
.toList();
Expand Down Expand Up @@ -67,10 +70,28 @@ class FeatureFile {
List<StepFile> getStepFiles() => _stepFiles;

static List<BddLine> _prepareLines(Iterable<BddLine> input) {
final headers = input.takeWhile((value) => value.type == LineType.unknown);
final lines = input
final lines = input.mapIndexed(
(index, bddLine) {
final isStep = bddLine.type == LineType.step;
final hasExamplesFormat = DataTableParser.hasExamplesFormat(
bddLine: bddLine,
);
final isNextTable = DataTableParser.isNextTable(
lines: input.toList(),
index: index + 1,
);
if (isStep && !hasExamplesFormat && isNextTable) {
return DataTableBddLine(bddLine.rawLine);
} else {
return bddLine;
}
},
);

final headers = lines.takeWhile((value) => value.type == LineType.unknown);
final steps = lines
.skip(headers.length)
.where((value) => value.type != LineType.unknown);
return [...headers, ...lines];
return [...headers, ...steps];
}
}
5 changes: 4 additions & 1 deletion lib/src/feature_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ String generateFeatureDart(
if (tags.isNotEmpty) {
sb.writeln("@Tags(['${tags.join("', '")}'])");
}
if (DataTableParser.hasBddDataTable(lines)) {
sb.writeln("import 'package:bdd_widget_test/src/data_table.dart' as bdd;");
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
}
sb.writeln("import 'package:flutter/material.dart';");
sb.writeln("import 'package:flutter_test/flutter_test.dart';");
if (isIntegrationTest) {
Expand Down Expand Up @@ -175,7 +178,7 @@ void _parseFeature(
scenarioParamsTag,
);

final flattenDataTables = replaceDataTables(
final flattenDataTables = DataTableParser.replaceDataTables(
scenario.skipWhile((line) => line.type == LineType.tag).toList(),
).toList();
final scenariosToParse = flattenDataTables.first.type == LineType.scenario
Expand Down
19 changes: 16 additions & 3 deletions lib/src/step/generic_step.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,37 @@ class GenericStep implements BddStep {
this.rawLine,
this.testerType,
this.customTesterName,
this.hadDataTable,
);

final String rawLine;
final String methodName;
final String testerType;
final String customTesterName;
final bool hadDataTable;

@override
String get content => '''
String get content {
var result = '';
if (hadDataTable) {
result +=
"import 'package:bdd_widget_test/src/data_table.dart' as bdd;\n";
}
return result += '''
ron-brosh marked this conversation as resolved.
Show resolved Hide resolved
import 'package:flutter_test/flutter_test.dart';

/// Usage: $rawLine
Future<void> $methodName($testerType $customTesterName${_getMethodParameters(rawLine)}) async {
Future<void> $methodName($testerType $customTesterName${_getMethodParameters(rawLine, hadDataTable)}) async {
throw UnimplementedError();
}
''';
}

String _getMethodParameters(String stepLine, bool hadDataTable) {
if (hadDataTable) {
return ', bdd.DataTable dataTable';
}

String _getMethodParameters(String stepLine) {
final params = parseRawStepLine(stepLine).skip(1);
if (params.isNotEmpty) {
return params
Expand Down
15 changes: 12 additions & 3 deletions lib/src/step_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ abstract class StepFile {
GeneratorOptions generatorOptions,
String testerTypeTagValue,
String testerNameTagValue,
bool hadDataTable,
) {
final file = '${getStepFilename(line)}.dart';

Expand Down Expand Up @@ -42,6 +43,7 @@ abstract class StepFile {
line,
testerTypeTagValue,
testerNameTagValue,
hadDataTable,
);
}

Expand All @@ -57,6 +59,7 @@ abstract class StepFile {
line,
testerTypeTagValue,
testerNameTagValue,
hadDataTable,
);
}
}
Expand All @@ -69,16 +72,22 @@ class NewStepFile extends StepFile {
this.line,
this.testerType,
this.testerName,
this.isDataTable,
) : super._();

final String package;
final String line;
final String filename;
final String testerType;
final String testerName;

String get dartContent =>
generateStepDart(package, line, testerType, testerName);
final bool isDataTable;
String get dartContent => generateStepDart(
package,
line,
testerType,
testerName,
isDataTable,
);
}

class ExistingStepFile extends StepFile {
Expand Down
20 changes: 17 additions & 3 deletions lib/src/step_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,18 @@ String generateStepDart(
String line,
String testerType,
String customTesterName,
bool hadDataTable,
) {
final methodName = getStepMethodName(line);

final bddStep =
_getStep(methodName, package, line, testerType, customTesterName);
final bddStep = _getStep(
methodName,
package,
line,
testerType,
customTesterName,
hadDataTable,
);
return bddStep.content;
}

Expand All @@ -65,10 +72,17 @@ BddStep _getStep(
String line,
String testerType,
String testerName,
bool hadDataTable,
) {
//for now, predefined steps don't support testerType
final factory = predefinedSteps[methodName] ??
(_, __) => GenericStep(methodName, line, testerType, testerName);
(_, __) => GenericStep(
methodName,
line,
testerType,
testerName,
hadDataTable,
);
return factory(package, line);
}

Expand Down
Loading
Loading