cukecooker 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.rdoc +24 -0
  2. data/bin/cukecooker +581 -0
  3. metadata +67 -0
data/README.rdoc ADDED
@@ -0,0 +1,24 @@
1
+ == Cukecooker
2
+
3
+ Cukecooker aids you in writing Cucumber scenarios.
4
+
5
+ It generates an HTML with your project's step definitions and lets you
6
+ use them to write scenarios in a comfortable way, with autocomplete
7
+ and text inputs for group matches.
8
+
9
+ === Usage
10
+
11
+ In your project root, which should contain a features folder, run this
12
+ in a terminal:
13
+
14
+ cukecooker
15
+
16
+ Or:
17
+
18
+ cukecooker path/to/your/project
19
+
20
+ If everything went well you should see
21
+
22
+ Done! Now open cukecooker-project.html in a browser.
23
+
24
+ Go ahead, try it!
data/bin/cukecooker ADDED
@@ -0,0 +1,581 @@
1
+ StepRegex = %r(\s*(?:When|Given|Then).*/\^(.+)\$\/\s*do(\s*\|.*\|)?)
2
+
3
+ steps = ""
4
+
5
+ # Checks that dir exists and is a directory
6
+ def check_dir(dir)
7
+ unless File.exists?(dir)
8
+ puts "Error: #{dir} does not exist"
9
+ exit
10
+ end
11
+
12
+ unless File.directory?(dir)
13
+ puts "Error: #{dir} is not a directory"
14
+ exit
15
+ end
16
+ end
17
+
18
+ # Get command line directory or default to current
19
+ dir = ARGV[0] || "."
20
+ dir = File.expand_path(dir)
21
+ check_dir dir
22
+
23
+ # Asume project name is the name of the given directory.
24
+ # If it's src, go one level up
25
+ name = File.basename dir
26
+ if name == 'src'
27
+ name = File.basename File.dirname(dir)
28
+ end
29
+
30
+ dir = "#{dir}/features/step_definitions"
31
+ check_dir dir
32
+
33
+ step_files = Dir["#{dir}/**/*.rb"]
34
+ step_files.each do |step_file|
35
+ lines = File.readlines step_file
36
+ lines.each do |line|
37
+ next unless match = StepRegex.match(line)
38
+
39
+ regexp = match[1]
40
+ next if regexp.empty?
41
+
42
+ steps << "," unless steps.empty?
43
+ if match[2]
44
+ parameters = match[2].strip[1 ... -1].split(',').map!{|x| "'#{x.strip.gsub('_', ' ')}'"}.join(',') if match[2]
45
+ steps << "[/^#{regexp}$/, [#{parameters}]]"
46
+ else
47
+ steps << "[/^#{regexp}$/]"
48
+ end
49
+ end
50
+ end
51
+
52
+ if steps.empty?
53
+ puts "Error: no step definitions found in #{dir}/features/step_definitions"
54
+ puts " Be sure to run cukecooker in a directory which contains a features/step_definitions directory"
55
+ puts " or give cukecooker the path to that directory."
56
+ exit
57
+ end
58
+
59
+ OrRegexp = '/\(\?\:.*?\|.*?\)/'
60
+ OptionalRegexp = '/.\?/'
61
+ OptionalWithParenRegexp = '/\(\?\:.*?\)\?/'
62
+ CaptureRegepx = '/\(.+?\)/'
63
+ CaptureWithQuotesRegepx = '/"\(.+?\)"/'
64
+ RegexpRegexp = '/\\\\\//'
65
+
66
+ File.open("cukecooker-#{name}.html", "w") do |file|
67
+ file.write <<EOF
68
+ <html>
69
+ <head>
70
+ <title>cukecooker - #{name}</title>
71
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js" type="text/javascript"></script>
72
+ <script type="text/javascript">
73
+ // The original step definitions, extracted from the step definition files.
74
+ // Each element in the array is an array whose first element is the regular expression
75
+ // and the next element is an (optional) array containing the parameters given in the do block.
76
+ originalSteps = [#{steps}];
77
+
78
+ // The expanded steps (splitted by ors or optional regexps).
79
+ // Each element in the array is a hash with the following properties:
80
+ // regexp: the regular expression to deal with
81
+ // params: an array of parameters that were given in the do block
82
+ // regexpReplaced: the regular expression with groups replaced with params
83
+ steps = [];
84
+
85
+ // The index of the selected step.
86
+ selectedStepIndex = 0;
87
+
88
+ // The indices of the li's that are shown.
89
+ liIndices = [];
90
+
91
+ // The selected step once the user pressed enter.
92
+ buildingStep = null;
93
+
94
+ // The current action entered by the user
95
+ currentAction = "Given ";
96
+
97
+ // The last action entered by the user
98
+ lastAction = '';
99
+
100
+ // Is the current action different than the last action?
101
+ lastActionChanged = false;
102
+
103
+ // The text that was in #stepMatch before a keyup event was fired
104
+ lastStepMatchText = '';
105
+
106
+ // Is the user creating the first step in the scenario?
107
+ firstStepInScenario = true;
108
+
109
+ allLiIndices = [];
110
+
111
+ // HACK: keyup event if fired twice... why? :'-(
112
+ justBuiltAStep = false;
113
+
114
+ $(function() {
115
+ $steps = $("#steps");
116
+ $stepBuilder = $("#stepBuilder");
117
+ $scenario = $("#scenario");
118
+ $stepMatch = $("#stepMatch");
119
+ $stepMatch_static = $("#stepMatch_static");
120
+ $explanationStep = $("#explanationStep");
121
+ $explanationStepBuilder = $("#explanationStepBuilder");
122
+
123
+ // Push all elements into array
124
+ function pushAll(array, elements) {
125
+ for(var i = 0; i < elements.length; i++)
126
+ array.push(elements[i]);
127
+ }
128
+
129
+ // Split the regexp into their expansions.
130
+ // For example:
131
+ // "(?:a|b) c" --> ["a c", "b c"]
132
+ // "(?:a )?b" --> ["a b", "b"]
133
+ // "(?:a )?(?:b|c) d" --> ["a b d", "a c d", "b d", "c d"]
134
+ function splitRegexp(regexp) {
135
+ var results = [];
136
+
137
+ // Find an OR expression
138
+ var match = regexp.match(#{OrRegexp});
139
+ if (match) {
140
+ match = match[0];
141
+ var idx = regexp.indexOf(match);
142
+ // Remove (?: ... )
143
+ match = match.substring(3, match.length - 1);
144
+ var pieces = match.split("\|");
145
+ for(var i = 0; i < pieces.length; i++) {
146
+ var before = regexp.substring(0, idx);
147
+ var after = regexp.substring(idx + match.length + 4);
148
+ var newRegexp = before + pieces[i] + after;
149
+ pushAll(results, splitRegexp(newRegexp));
150
+ }
151
+ return results;
152
+ }
153
+
154
+ // Find optional expression with parenthesis
155
+ match = regexp.match(#{OptionalWithParenRegexp});
156
+ if (match) {
157
+ match = match[0];
158
+ var idx = regexp.indexOf(match);
159
+ var before = regexp.substring(0, idx);
160
+ var after = regexp.substring(idx + match.length);
161
+ // This is without the surrounding (?: ... )?
162
+ var middle = match.substring(3, match.length - 2);
163
+ var regexpWithout = before + after;
164
+ var regexpWith = before + middle + after;
165
+ pushAll(results, splitRegexp(regexpWithout));
166
+ pushAll(results, splitRegexp(regexpWith));
167
+ return results;
168
+ }
169
+
170
+ // Find optional expression without parenthesis
171
+ match = regexp.match(#{OptionalRegexp});
172
+ if (match) {
173
+ match = match[0];
174
+ var idx = regexp.indexOf(match);
175
+ var before = regexp.substring(0, idx);
176
+ var after = regexp.substring(idx + match.length);
177
+ // This is without the ?
178
+ var middle = match.substring(0, match.length - 1);
179
+ var regexpWithout = before + after;
180
+ var regexpWith = before + middle + after;
181
+ pushAll(results, splitRegexp(regexpWithout));
182
+ pushAll(results, splitRegexp(regexpWith));
183
+ return results;
184
+ }
185
+
186
+ // Replace \/ with /
187
+ while(regexp.indexOf("\\\\/") >= 0) {
188
+ regexp = regexp.replace(#{RegexpRegexp}, "/");
189
+ }
190
+
191
+ return [regexp];
192
+ }
193
+
194
+ // Processes each group in the step regexp with the given callback. The callback
195
+ // receives the index of the matched group and must return a replacement
196
+ // for it.
197
+ function processGroups(step, callback) {
198
+ paramIdx = 0;
199
+ regexp = step.regexp;
200
+ while(true) {
201
+ m = regexp.match(#{CaptureRegepx});
202
+ if (!m) break;
203
+
204
+ m = m[0];
205
+ idx = regexp.indexOf(m);
206
+ regexp = regexp.substring(0, idx) + callback(paramIdx) + regexp.substring(idx + m.length);
207
+ paramIdx++;
208
+ }
209
+ return regexp;
210
+ }
211
+
212
+ // Transforms 'foo "(...)" bar' into 'foo (...) bar'
213
+ function removeQuotedGroups(step) {
214
+ regexp = step.regexp;
215
+ while(true) {
216
+ m = regexp.match(#{CaptureWithQuotesRegepx});
217
+ if (!m) break;
218
+
219
+ m = m[0];
220
+ idx = regexp.indexOf(m);
221
+ regexp = regexp.substring(0, idx) + m.substring(1, m.length -1) + regexp.substring(idx + m.length);
222
+ }
223
+ return regexp;
224
+ }
225
+
226
+ // Appends the step just built to the scenario div.
227
+ function appendToScenario() {
228
+ replacement = processGroups(buildingStep, function(idx) {
229
+ return '<span class="param">' + $("#p" + idx).val() + '</span>';
230
+ });
231
+ replacement = "<strong>" + currentAction + "</strong>" + replacement;
232
+ if (currentAction == "And ") {
233
+ replacement = "&nbsp;&nbsp;" + replacement;
234
+ } else if (lastActionChanged && !firstStepInScenario) {
235
+ replacement = "<br/>" + replacement;
236
+ }
237
+ regexp = buildingStep.regexp;
238
+ if (regexp[regexp.length - 1] == ':') {
239
+ if (currentAction == "And ") {
240
+ replacement += '<br/>&nbsp;&nbsp;"""<br/>&nbsp;&nbsp;TODO: text or table goes here&nbsp;&nbsp;<br/>&nbsp;&nbsp;"""';
241
+ } else {
242
+ replacement += '<br/>"""<br/>TODO: text or table goes here<br/>"""';
243
+ }
244
+ }
245
+ firstStepInScenario = false;
246
+ justBuiltAStep = true;
247
+ $scenario.append(replacement + "<br/>");
248
+ }
249
+
250
+ // Prepares the page for building steps (input texts for parameters)
251
+ function prepareBuildStep() {
252
+ buildingStep = steps[currentLiIndex()];
253
+
254
+ // Set the current action and see if it changed from the last one
255
+ if (lastAction == currentAction && currentAction != "And ") {
256
+ currentAction = "And ";
257
+ lastActionChanged = false;
258
+ } else {
259
+ lastActionChanged = true;
260
+ }
261
+ lastAction = currentAction;
262
+
263
+ groupsCount = 0;
264
+ processGroups(buildingStep, function(idx) {
265
+ groupsCount++;
266
+ return '';
267
+ });
268
+
269
+ if (groupsCount == 0) {
270
+ appendToScenario();
271
+ searchAgain();
272
+ } else {
273
+ $explanationStep.hide();
274
+ $explanationStepBuilder.show();
275
+ $stepMatch.attr('readonly', 'readonly');
276
+ $stepMatch.val(currentAction + buildingStep.regexpReplaced);
277
+ $steps.hide();
278
+ $stepBuilder.show();
279
+
280
+ regexpWithoutQuotes = removeQuotedGroups(buildingStep);
281
+ originalRegexp = buildingStep.regexp;
282
+ buildingStep.regexp = regexpWithoutQuotes;
283
+
284
+ html = '<table><tr><td><nobr><strong>' + currentAction + "</strong>";
285
+ html += processGroups(buildingStep, function(idx) {
286
+ return '</nobr></td><td width="100"><input type="text" id="p' + idx + '" class="complete"></td><td><nobr>';
287
+ });
288
+ html += '</td></tr><tr align="center"><td>';
289
+ for(var i = 0; i < groupsCount; i++) {
290
+ html += '</td><td class="param">' + buildingStep.params[i] + "</td><td>";
291
+ }
292
+ html += "</td></tr></table>";
293
+ $stepBuilder.html(html);
294
+
295
+ $("#p0").focus();
296
+
297
+ buildingStep.regexp = originalRegexp;
298
+ }
299
+ }
300
+
301
+ // Resets everything except the scenario div to search a new step.
302
+ function searchAgain() {
303
+ selectedStepIndex = 0;
304
+ liIndices = allLiIndices;
305
+ $explanationStep.show();
306
+ $explanationStepBuilder.hide();
307
+ $stepMatch.attr('readonly', '');
308
+ $steps.show();
309
+ $stepBuilder.hide();
310
+ $stepsLi.show();
311
+ $stepsLi.removeClass("selected");
312
+ stepLi(0).addClass("selected");
313
+ $stepMatch.val("");
314
+ $stepMatch.focus();
315
+ $steps.scrollTop(0);
316
+ }
317
+
318
+ // Returns the index of the currently selected <li>
319
+ function currentLiIndex() {
320
+ return liIndices[selectedStepIndex];
321
+ }
322
+
323
+ // Returns the selected <li> as a jQuery object
324
+ function currentLi() {
325
+ return $($stepsLi[currentLiIndex()]);
326
+ }
327
+
328
+ // Returns a step <li> as a jQuery object
329
+ function stepLi(idx) {
330
+ return $($stepsLi[idx]);
331
+ }
332
+
333
+ // Split original steps and create the steps array
334
+ for(var i = 0; i < originalSteps.length; i++) {
335
+ originalStep = originalSteps[i];
336
+ step = originalStep[0].toString();
337
+ // Remove /^ and $/
338
+ s = step.substring(2, step.length - 2);
339
+ splits = splitRegexp(s);
340
+ for(var j = 0; j < splits.length; j++) {
341
+ split = splits[j];
342
+
343
+ newStep = {}
344
+ newStep.regexp = split
345
+ newStep.params = originalStep.length == 1 ? [] : originalStep[1]
346
+ newStep.regexpReplaced = processGroups(newStep, function(idx) {
347
+ return newStep.params[idx];
348
+ });
349
+
350
+ steps.push(newStep);
351
+
352
+ allLiIndices.push(allLiIndices.length);
353
+ }
354
+ }
355
+
356
+ liIndices = allLiIndices;
357
+
358
+ // Sort steps according to regexps, alphabetically
359
+ steps.sort(function(a, b) {
360
+ x = a.regexp.toLowerCase();
361
+ y = b.regexp.toLowerCase();
362
+ return x < y ? -1 : (x > y ? 1 : 0);
363
+ });
364
+
365
+ // Write the steps in the <li>s
366
+ for(var i = 0; i < steps.length; i++) {
367
+ step = steps[i];
368
+ replacement = processGroups(step, function(idx) {
369
+ return '<span class="param">' + step.params[idx] + '</span>';
370
+ });
371
+ $steps.append("<li>" + replacement + "</li>");
372
+ }
373
+
374
+ $stepsLi = $steps.find("li");
375
+
376
+ // Highlight the first selected step
377
+ stepLi(0).addClass("selected");
378
+
379
+ // Focus the text input to write the match
380
+ $stepMatch.focus();
381
+
382
+ // When pressing a key in the step match input, filter the steps
383
+ $stepMatch.keyup(function(ev) {
384
+ if (justBuiltAStep) {
385
+ justBuiltAStep = false;
386
+ return;
387
+ }
388
+
389
+ text = $stepMatch.val();
390
+
391
+ // If the text didn't change and it's not enter, do nothing
392
+ if (text == lastStepMatchText && ev.keyCode != 13)
393
+ return true;
394
+
395
+ // These are the arrow keys, home, end, etc. We can ignore them.
396
+ if (ev.keyCode >= 33 && ev.keyCode <= 40) {
397
+ return true;
398
+ }
399
+
400
+ lastStepMatchText = text;
401
+
402
+ // If the user pressed enter, selected the step
403
+ if (ev.keyCode == 13) {
404
+ prepareBuildStep();
405
+ return false;
406
+ }
407
+
408
+ // Unselect the current step
409
+ currentLi().removeClass("selected");
410
+
411
+ // See if we the text starts with an action and remove it
412
+ foundMatch = false;
413
+ if (text.match(/^when /i)) {
414
+ text = text.substring(5);
415
+ currentAction = "When ";
416
+ }
417
+ if (text.match(/^then /i)) {
418
+ text = text.substring(5);
419
+ currentAction = "Then ";
420
+ }
421
+ if (text.match(/^given /i)) {
422
+ text = text.substring(6);
423
+ currentAction = "Given ";
424
+ }
425
+ if (text.match(/^and /i)) {
426
+ text = text.substring(4);
427
+ currentAction = "And ";
428
+ }
429
+
430
+ // Do the filtering
431
+ text = eval("/^" + text + "/i");
432
+ liIndices = [];
433
+
434
+ selectedStepIndex = -1;
435
+ for(var i = 0; i < steps.length; i++) {
436
+ step = steps[i];
437
+ $stepLi = stepLi(i);
438
+ if (step.regexpReplaced.match(text)) {
439
+ $stepLi.show();
440
+ if (selectedStepIndex == -1) {
441
+ $stepLi.addClass("selected");
442
+ selectedStepIndex = 0;
443
+ }
444
+ liIndices.push(i);
445
+ } else {
446
+ $stepLi.hide();
447
+ }
448
+ }
449
+ });
450
+
451
+ // When pressing up or down on the stepMatch input,
452
+ // change the selected step
453
+ $stepMatch.keydown(function(ev) {
454
+ if (selectedStepIndex == -1) return;
455
+
456
+ // Up, Down, PageUp or PageDown
457
+ if (ev.keyCode == 38 || ev.keyCode == 40 || ev.keyCode == 33 || ev.keyCode == 34) {
458
+ currentLi().removeClass("selected");
459
+
460
+ var increment = 0;
461
+ switch(ev.keyCode) {
462
+ case 33: increment = -10; break;
463
+ case 34: increment = 10; break;
464
+ case 38: increment = -1; break;
465
+ case 40: increment = 1; break;
466
+ }
467
+
468
+ selectedStepIndex += increment;
469
+
470
+ if (selectedStepIndex < 0) selectedStepIndex = 0;
471
+ if (selectedStepIndex > liIndices.length - 1) selectedStepIndex = liIndices.length - 1;
472
+
473
+ current = currentLi();
474
+ current.addClass("selected");
475
+ $steps.scrollTop(0);
476
+ $steps.scrollTop(current.position().top - 90);
477
+ return false;
478
+ }
479
+
480
+ return true;
481
+ });
482
+
483
+ // When hovering an <li>, highglight it
484
+ $stepsLi.mouseenter(function(ev) {
485
+ currentLi().removeClass("selected");
486
+ selectedStepIndex = $stepsLi.index(this);
487
+ currentLi().addClass("selected");
488
+ });
489
+
490
+ // When clicking an <li>, build it
491
+ $stepsLi.click(function(ev) {
492
+ selectedStepIndex = $stepsLi.index(this);
493
+ prepareBuildStep();
494
+ });
495
+
496
+ // When pressing enter or tab in the step building inputs, go
497
+ // to the next one or write to scenario if it's the last one
498
+ $(".complete").live("keyup", function(ev) {
499
+ // When pressing ESC, go back to search
500
+ if (ev.keyCode == 27) {
501
+ searchAgain();
502
+ return false;
503
+ }
504
+
505
+ // Enter
506
+ if (ev.keyCode == 13) {
507
+ $this = $(this);
508
+ value = $this.val();
509
+ if (value == '')
510
+ return false;
511
+
512
+ id = $this.attr("id").substring(1);
513
+ id = parseInt(id) + 1;
514
+ $next = $("#p" + id);
515
+ if ($next.length > 0) {
516
+ $next.focus();
517
+ } else {
518
+ appendToScenario();
519
+ searchAgain();
520
+ }
521
+ return false;
522
+ }
523
+ return true;
524
+ });
525
+
526
+ // Clear the scenario
527
+ $("#clear").click(function() {
528
+ $scenario.html("");
529
+ searchAgain();
530
+ firstStepInScenario = true;
531
+ currentAction = "Given ";
532
+ });
533
+ });
534
+ </script>
535
+ <style>
536
+ body { font-family: "Helvetica Neue",Arial,Helvetica,sans-serif; font-size:85%; height:80% }
537
+ table { font-size:100%; }
538
+ ul { list-style-type:none; margin:0px; padding:0px;}
539
+ li { padding: 4px; cursor: pointer;}
540
+ h3 { padding: 0px; margin:0px; }
541
+ #steps, #stepBuilder { height: 40%; position: relative; overflow: auto; border: 1px solid black; background-color: #DFDFEF; margin-top: 20px;}
542
+ #scenarioContainer { margin-top:20px; border: 1px solid black; padding:10px; background-color: #EFEF99; }
543
+ #scenario { font-size: 105%; padding-left: 20px; margin-top: 10px;}
544
+ #container { min-height: 100%; position: relative; }
545
+ #separator { height: 5%; }
546
+ #logo { position: absolute; bottom: 0px; right: 4px; height: 20px; width: 100%; text-align: right; font-weight:bold;}
547
+ .param { font-weight: bold; color: blue;}
548
+ .selected { background-color: #BBE; padding:4px;}
549
+ .complete { width:100%; }
550
+ </style>
551
+ </head>
552
+ <body>
553
+ <div id="container">
554
+ <h3 id="explanationStep">
555
+ Write a step as you would write it in cucumber (including given, when, then, and) and press enter when you have selected one.
556
+ </h3>
557
+ <h3 id="explanationStepBuilder" style="display:none">
558
+ Now fill in the fields for the step (press tab or enter to change between fields, or escape to search another step).
559
+ </h3>
560
+ <input type="text" id="stepMatch" size="100" />
561
+ <ul id="steps">
562
+ </ul>
563
+ <div id="stepBuilder" style="display:none">
564
+ </div>
565
+ <div id="scenarioContainer">
566
+ <h3>Scenario <a href="javascript:void(0)" id="clear" style="margin-left:10px">clear</a></h3>
567
+ <div id="scenario">
568
+ </div>
569
+ </div>
570
+ </div>
571
+ <div id="separator">
572
+ </div>
573
+ <div id="logo">
574
+ #{name} - cukecooker :)
575
+ </div>
576
+ </body>
577
+ </html>
578
+ EOF
579
+ end
580
+
581
+ puts "Done! Now open cukecooker-#{name}.html in a browser."
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cukecooker
3
+ version: !ruby/object:Gem::Version
4
+ hash: 9
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 1
9
+ version: "0.1"
10
+ platform: ruby
11
+ authors:
12
+ - Ary Borenszweig
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-12-08 00:00:00 -03:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description:
22
+ email: aborenszweig@manas.com.ar
23
+ executables:
24
+ - cukecooker
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README.rdoc
29
+ files:
30
+ - bin/cukecooker
31
+ - README.rdoc
32
+ has_rdoc: true
33
+ homepage: https://github.com/asterite/cukecooker
34
+ licenses: []
35
+
36
+ post_install_message:
37
+ rdoc_options: []
38
+
39
+ require_paths:
40
+ - .
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ required_rubygems_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.7
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Write cucumber scenarios with aid
66
+ test_files: []
67
+