Skip to content

Commit 6cf8a9f

Browse files
committed
Output snippets upon missing step definitions
This fixes #799.
1 parent c6f03ed commit 6cf8a9f

File tree

5 files changed

+200
-92
lines changed

5 files changed

+200
-92
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.
88

99
- Remove `And` and `But` from the public API, fixes [#821](https://github.com/badeball/cypress-cucumber-preprocessor/issues/821).
1010

11+
- Output snippet suggestions upon missing step definition, fixes [#799](https://github.com/badeball/cypress-cucumber-preprocessor/issues/799).
12+
1113
## v12.2.0
1214

1315
- Total execution time is correctly shown in HTML reports, fixes [#813](https://github.com/badeball/cypress-cucumber-preprocessor/issues/813).

features/undefined_step.feature

+127-64
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,129 @@
11
Feature: undefined Steps
22

3-
Scenario: no files containing step definitions
4-
Given a file named "cypress/e2e/a.feature" with:
5-
"""
6-
Feature: a feature name
7-
Scenario: a scenario name
8-
Given an undefined step
9-
"""
10-
When I run cypress
11-
Then it fails
12-
And the output should contain
13-
"""
14-
Step implementation missing for "an undefined step".
15-
16-
We tried searching for files containing step definitions using the following search pattern templates:
17-
18-
- cypress/e2e/[filepath]/**/*.{js,mjs,ts,tsx}
19-
- cypress/e2e/[filepath].{js,mjs,ts,tsx}
20-
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
21-
22-
These templates resolved to the following search patterns:
23-
24-
- cypress/e2e/a/**/*.{js,mjs,ts,tsx}
25-
- cypress/e2e/a.{js,mjs,ts,tsx}
26-
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
27-
28-
These patterns matched **no files** containing step definitions. This almost certainly means that you have misconfigured `stepDefinitions`.
29-
"""
30-
31-
Scenario: step definitions exist, but none matching
32-
Given a file named "cypress/e2e/a.feature" with:
33-
"""
34-
Feature: a feature name
35-
Scenario: a scenario name
36-
Given an undefined step
37-
"""
38-
And a file named "cypress/support/step_definitions/steps.js" with:
39-
"""
40-
const { When } = require("@badeball/cypress-cucumber-preprocessor");
41-
When("unused step definition", function() {});
42-
"""
43-
When I run cypress
44-
Then it fails
45-
And the output should contain
46-
"""
47-
Step implementation missing for "an undefined step".
48-
49-
We tried searching for files containing step definitions using the following search pattern templates:
50-
51-
- cypress/e2e/[filepath]/**/*.{js,mjs,ts,tsx}
52-
- cypress/e2e/[filepath].{js,mjs,ts,tsx}
53-
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
54-
55-
These templates resolved to the following search patterns:
56-
57-
- cypress/e2e/a/**/*.{js,mjs,ts,tsx}
58-
- cypress/e2e/a.{js,mjs,ts,tsx}
59-
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
60-
61-
These patterns matched the following files:
62-
63-
- cypress/support/step_definitions/steps.js
64-
65-
However, none of these files contained a step definition matching "an undefined step".
66-
"""
3+
Rule: it should output appropriate path info
4+
5+
Scenario: no files containing step definitions
6+
Given a file named "cypress/e2e/a.feature" with:
7+
"""
8+
Feature: a feature name
9+
Scenario: a scenario name
10+
Given an undefined step
11+
"""
12+
When I run cypress
13+
Then it fails
14+
And the output should contain
15+
"""
16+
Step implementation missing for "an undefined step".
17+
18+
We tried searching for files containing step definitions using the following search pattern templates:
19+
20+
- cypress/e2e/[filepath]/**/*.{js,mjs,ts,tsx}
21+
- cypress/e2e/[filepath].{js,mjs,ts,tsx}
22+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
23+
24+
These templates resolved to the following search patterns:
25+
26+
- cypress/e2e/a/**/*.{js,mjs,ts,tsx}
27+
- cypress/e2e/a.{js,mjs,ts,tsx}
28+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
29+
30+
These patterns matched **no files** containing step definitions. This almost certainly means that you have misconfigured `stepDefinitions`.
31+
"""
32+
33+
Scenario: step definitions exist, but none matching
34+
Given a file named "cypress/e2e/a.feature" with:
35+
"""
36+
Feature: a feature name
37+
Scenario: a scenario name
38+
Given an undefined step
39+
"""
40+
And a file named "cypress/support/step_definitions/steps.js" with:
41+
"""
42+
const { When } = require("@badeball/cypress-cucumber-preprocessor");
43+
When("unused step definition", function() {});
44+
"""
45+
When I run cypress
46+
Then it fails
47+
And the output should contain
48+
"""
49+
Step implementation missing for "an undefined step".
50+
51+
We tried searching for files containing step definitions using the following search pattern templates:
52+
53+
- cypress/e2e/[filepath]/**/*.{js,mjs,ts,tsx}
54+
- cypress/e2e/[filepath].{js,mjs,ts,tsx}
55+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
56+
57+
These templates resolved to the following search patterns:
58+
59+
- cypress/e2e/a/**/*.{js,mjs,ts,tsx}
60+
- cypress/e2e/a.{js,mjs,ts,tsx}
61+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
62+
63+
These patterns matched the following files:
64+
65+
- cypress/support/step_definitions/steps.js
66+
67+
However, none of these files contained a step definition matching "an undefined step".
68+
"""
69+
70+
Rule: it should output snippet suggestions
71+
72+
Scenario: undefined step without args
73+
Given a file named "cypress/e2e/a.feature" with:
74+
"""
75+
Feature: a feature name
76+
Scenario: a scenario name
77+
Given an undefined step
78+
"""
79+
When I run cypress
80+
Then it fails
81+
And the output should contain
82+
"""
83+
You can implement it using the suggestion(s) below.
84+
85+
Given("an undefined step", function () {
86+
return "pending";
87+
});
88+
"""
89+
90+
Scenario: undefined step with doc string
91+
Given a file named "cypress/e2e/a.feature" with:
92+
"""
93+
Feature: a feature name
94+
Scenario: a scenario name
95+
Given an undefined step
96+
\"\"\"
97+
foo
98+
\"\"\"
99+
"""
100+
When I run cypress
101+
Then it fails
102+
And the output should contain
103+
"""
104+
You can implement it using the suggestion(s) below.
105+
106+
Given("an undefined step", function (docString) {
107+
return "pending";
108+
});
109+
"""
110+
111+
Scenario: undefined step with data table
112+
Given a file named "cypress/e2e/a.feature" with:
113+
"""
114+
Feature: a feature name
115+
Scenario: a scenario name
116+
Given an undefined step
117+
| foo |
118+
| bar |
119+
"""
120+
When I run cypress
121+
Then it fails
122+
And the output should contain
123+
"""
124+
You can implement it using the suggestion(s) below.
125+
126+
Given("an undefined step", function (dataTable) {
127+
return "pending";
128+
});
129+
"""

lib/create-tests.ts

+42-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import messages from "@cucumber/messages";
22

33
import parse from "@cucumber/tag-expressions";
44

5+
import {
6+
CucumberExpressionGenerator,
7+
ParameterTypeRegistry,
8+
} from "@cucumber/cucumber-expressions";
9+
510
import { v4 as uuid } from "uuid";
611

712
import { assertAndReturn, fail } from "./assertions";
@@ -41,7 +46,9 @@ import { createTimestamp, duration } from "./messages-helpers";
4146

4247
import { createWeakCache } from "./helpers/maps";
4348

44-
import { stripIndent } from "./helpers/strings";
49+
import { indent, stripIndent } from "./helpers/strings";
50+
51+
import { generateSnippet } from "./snippets";
4552

4653
declare global {
4754
namespace globalThis {
@@ -467,7 +474,11 @@ function createPickle(
467474
} catch (e) {
468475
if (e instanceof MissingDefinitionError) {
469476
throw new Error(
470-
createMissingStepDefinitionMessage(context, text)
477+
createMissingStepDefinitionMessage(
478+
context,
479+
pickleStep,
480+
context.registry.parameterTypeRegistry
481+
)
471482
);
472483
} else {
473484
throw e;
@@ -798,7 +809,8 @@ function strictIsInteractive(): boolean {
798809

799810
function createMissingStepDefinitionMessage(
800811
context: CompositionContext,
801-
text: string
812+
pickleStep: messages.PickleStep,
813+
parameterTypeRegistry: ParameterTypeRegistry
802814
) {
803815
const noStepDefinitionPathsTemplate = `
804816
Step implementation missing for "<text>".
@@ -812,6 +824,10 @@ function createMissingStepDefinitionMessage(
812824
<step-definition-patterns>
813825
814826
These patterns matched **no files** containing step definitions. This almost certainly means that you have misconfigured \`stepDefinitions\`.
827+
828+
You can implement it using the suggestion(s) below.
829+
830+
<snippets>
815831
`;
816832

817833
const someStepDefinitionPathsTemplate = `
@@ -830,6 +846,10 @@ function createMissingStepDefinitionMessage(
830846
<step-definition-paths>
831847
832848
However, none of these files contained a step definition matching "<text>".
849+
850+
You can implement it using the suggestion(s) below.
851+
852+
<snippets>
833853
`;
834854

835855
const { stepDefinitionHints } = context;
@@ -845,8 +865,24 @@ function createMissingStepDefinitionMessage(
845865
const prettyPrintList = (items: string[]) =>
846866
items.map((item) => " - " + maybeEscape(item)).join("\n");
847867

868+
let parameter: "dataTable" | "docString" | null = null;
869+
870+
if (pickleStep.argument?.dataTable) {
871+
parameter = "dataTable";
872+
} else if (pickleStep.argument?.docString) {
873+
parameter = "docString";
874+
}
875+
876+
const snippets = new CucumberExpressionGenerator(
877+
() => parameterTypeRegistry.parameterTypes
878+
)
879+
.generateExpressions(pickleStep.text)
880+
.map((expression) => generateSnippet(expression, parameter))
881+
.map((snippet) => indent(snippet, { count: 2 }))
882+
.join("\n\n");
883+
848884
return stripIndent(template)
849-
.replaceAll("<text>", text)
885+
.replaceAll("<text>", pickleStep.text)
850886
.replaceAll(
851887
"<step-definitions>",
852888
prettyPrintList([stepDefinitionHints.stepDefinitions].flat())
@@ -858,5 +894,6 @@ function createMissingStepDefinitionMessage(
858894
.replaceAll(
859895
"<step-definition-paths>",
860896
prettyPrintList(stepDefinitionHints.stepDefinitionPaths)
861-
);
897+
)
898+
.replaceAll("<snippets>", snippets);
862899
}

lib/diagnostics/index.ts

+4-23
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
UnmatchedStep,
2727
} from "./diagnose";
2828
import { assertAndReturn } from "../assertions";
29+
import { generateSnippet } from "../snippets";
2930

3031
const TEMPLATE = `
3132
Given("[expression]", function ([arguments]) {
@@ -281,33 +282,13 @@ export function createUnmatchedStep(
281282
const generatedExpressions =
282283
cucumberExpressionGenerator.generateExpressions(unmatch.step.text);
283284

284-
const stepParameterNames = [];
285-
286-
if (unmatch.argument === "dataTable") {
287-
stepParameterNames.push("dataTable");
288-
} else if (unmatch.argument === "docString") {
289-
stepParameterNames.push("docString");
290-
}
291-
292285
for (const generatedExpression of generatedExpressions) {
293-
const expression = generatedExpression.source
294-
.replace(/\\/g, "\\\\")
295-
.replace(/"/g, '\\"');
296-
297-
const args = generatedExpression.parameterNames
298-
.concat(stepParameterNames)
299-
.join(", ");
300-
301286
append("");
302287

303288
append(
304-
indent(
305-
TEMPLATE.replace("[expression]", expression).replace(
306-
"[arguments]",
307-
args
308-
),
309-
{ count: 2 }
310-
)
289+
indent(generateSnippet(generatedExpression, unmatch.argument), {
290+
count: 2,
291+
})
311292
);
312293
}
313294
});

lib/snippets.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { GeneratedExpression } from "@cucumber/cucumber-expressions";
2+
3+
const TEMPLATE = `
4+
Given("[definition]", function ([arguments]) {
5+
return "pending";
6+
});
7+
`.trim();
8+
9+
export function generateSnippet(
10+
expression: GeneratedExpression,
11+
parameter: "dataTable" | "docString" | null
12+
) {
13+
const definition = expression.source
14+
.replace(/\\/g, "\\\\")
15+
.replace(/"/g, '\\"');
16+
17+
const stepParameterNames = parameter ? [parameter] : [];
18+
19+
const args = expression.parameterNames.concat(stepParameterNames).join(", ");
20+
21+
return TEMPLATE.replace("[definition]", definition).replace(
22+
"[arguments]",
23+
args
24+
);
25+
}

0 commit comments

Comments
 (0)