Skip to content

Commit f15b23e

Browse files
committed
Improvde error messages upon missing step definitions
My hope is that this will give greater understanding into the resolvement process and give the end-user some hints as to what might be wrong. Fixes #763.
1 parent d593bbd commit f15b23e

File tree

8 files changed

+295
-22
lines changed

8 files changed

+295
-22
lines changed

features/step_definitions/cli_steps.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import util from "util";
22
import { Given, When, Then } from "@cucumber/cucumber";
33
import assert from "assert";
44
import childProcess from "child_process";
5+
import { isPost10, isPre10 } from "../support/helpers";
56

67
function execAsync(
78
command: string
@@ -138,6 +139,18 @@ Then("the output should contain", function (content) {
138139
assert.match(this.lastRun.stdout, new RegExp(rescape(content)));
139140
});
140141

142+
Then("if pre-v10, the output should contain", function (content) {
143+
if (isPre10()) {
144+
assert.match(this.lastRun.stdout, new RegExp(rescape(content)));
145+
}
146+
});
147+
148+
Then("if post-v10, the output should contain", function (content) {
149+
if (isPost10()) {
150+
assert.match(this.lastRun.stdout, new RegExp(rescape(content)));
151+
}
152+
});
153+
141154
Then("the output should match", function (content) {
142155
assert.match(this.lastRun.stdout, new RegExp(content));
143156
});

features/undefined_step.feature

Lines changed: 94 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Feature: undefined Steps
22

3-
Scenario:
3+
Scenario: no files containing step definitions
44
Given a file named "cypress/e2e/a.feature" with:
55
"""
66
Feature: a feature name
@@ -9,7 +9,98 @@ Feature: undefined Steps
99
"""
1010
When I run cypress
1111
Then it fails
12-
And the output should contain
12+
And if pre-v10, the output should contain
1313
"""
14-
Step implementation missing for: an undefined step
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+
And if post-v10, the output should contain
31+
"""
32+
Step implementation missing for "an undefined step".
33+
34+
We tried searching for files containing step definitions using the following search pattern templates:
35+
36+
- [filepath]/**/*.{js,mjs,ts,tsx}
37+
- [filepath].{js,mjs,ts,tsx}
38+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
39+
40+
These templates resolved to the following search patterns:
41+
42+
- cypress/e2e/a/**/*.{js,mjs,ts,tsx}
43+
- cypress/e2e/a.{js,mjs,ts,tsx}
44+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
45+
46+
These patterns matched **no files** containing step definitions. This almost certainly means that you have misconfigured `stepDefinitions`.
47+
"""
48+
49+
Scenario: step definitions exist, but none matching
50+
Given a file named "cypress/e2e/a.feature" with:
51+
"""
52+
Feature: a feature name
53+
Scenario: a scenario name
54+
Given an undefined step
55+
"""
56+
And a file named "cypress/support/step_definitions/steps.js" with:
57+
"""
58+
const { When } = require("@badeball/cypress-cucumber-preprocessor");
59+
When("unused step definition", function() {});
60+
"""
61+
When I run cypress
62+
Then it fails
63+
And if pre-v10, the output should contain
64+
"""
65+
Step implementation missing for "an undefined step".
66+
67+
We tried searching for files containing step definitions using the following search pattern templates:
68+
69+
- cypress/e2e/[filepath]/**/*.{js,mjs,ts,tsx}
70+
- cypress/e2e/[filepath].{js,mjs,ts,tsx}
71+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
72+
73+
These templates resolved to the following search patterns:
74+
75+
- cypress/e2e/a/**/*.{js,mjs,ts,tsx}
76+
- cypress/e2e/a.{js,mjs,ts,tsx}
77+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
78+
79+
These patterns matched the following files:
80+
81+
- cypress/support/step_definitions/steps.js
82+
83+
However, none of these files contained a step definition matching "an undefined step".
84+
"""
85+
And if post-v10, the output should contain
86+
"""
87+
Step implementation missing for "an undefined step".
88+
89+
We tried searching for files containing step definitions using the following search pattern templates:
90+
91+
- [filepath]/**/*.{js,mjs,ts,tsx}
92+
- [filepath].{js,mjs,ts,tsx}
93+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
94+
95+
These templates resolved to the following search patterns:
96+
97+
- cypress/e2e/a/**/*.{js,mjs,ts,tsx}
98+
- cypress/e2e/a.{js,mjs,ts,tsx}
99+
- cypress/support/step_definitions/**/*.{js,mjs,ts,tsx}
100+
101+
These patterns matched the following files:
102+
103+
- cypress/support/step_definitions/steps.js
104+
105+
However, none of these files contained a step definition matching "an undefined step".
15106
"""

lib/add-cucumber-preprocessor-plugin.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ import { getTags } from "./environment-helpers";
3434

3535
import { ensureIsAbsolute } from "./helpers";
3636

37+
/**
38+
* Work-around for the fact that some Cypress versions pre v10 were missing this property in their types.
39+
*/
40+
declare global {
41+
namespace Cypress {
42+
interface PluginConfigOptions {
43+
testFiles: string[];
44+
}
45+
}
46+
}
47+
3748
function memoize<T extends (...args: any[]) => any>(
3849
fn: T
3950
): (...args: Parameters<T>) => ReturnType<T> {

lib/create-tests.ts

Lines changed: 139 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import { assertAndReturn } from "./assertions";
88

99
import DataTable from "./data_table";
1010

11-
import { assignRegistry, freeRegistry, IHook, Registry } from "./registry";
11+
import {
12+
assignRegistry,
13+
freeRegistry,
14+
IHook,
15+
MissingDefinitionError,
16+
Registry,
17+
} from "./registry";
1218

1319
import { collectTagNames, traverseGherkinDocument } from "./ast-helpers";
1420

@@ -26,6 +32,7 @@ import { getTags } from "./environment-helpers";
2632
import { notNull } from "./type-guards";
2733

2834
import { looksLikeOptions, tagToCypressOptions } from "./tag-parser";
35+
import { Context } from "mocha";
2936

3037
declare global {
3138
namespace globalThis {
@@ -45,6 +52,11 @@ interface CompositionContext {
4552
enabled: boolean;
4653
stack: messages.IEnvelope[];
4754
};
55+
stepDefinitionHints: {
56+
stepDefinitions: string[];
57+
stepDefinitionPatterns: string[];
58+
stepDefinitionPaths: string[];
59+
};
4860
}
4961

5062
/**
@@ -134,6 +146,28 @@ function duration(
134146
};
135147
}
136148

149+
function minIndent(content: string) {
150+
const match = content.match(/^[ \t]*(?=\S)/gm);
151+
152+
if (!match) {
153+
return 0;
154+
}
155+
156+
return match.reduce((r, a) => Math.min(r, a.length), Infinity);
157+
}
158+
159+
function stripIndent(content: string) {
160+
const indent = minIndent(content);
161+
162+
if (indent === 0) {
163+
return content;
164+
}
165+
166+
const regex = new RegExp(`^[ \\t]{${indent}}`, "gm");
167+
168+
return content.replace(regex, "");
169+
}
170+
137171
function createFeature(
138172
context: CompositionContext,
139173
feature: messages.GherkinDocument.IFeature
@@ -481,14 +515,24 @@ function createPickle(
481515
const ensureChain = (value: any): Cypress.Chainable<any> =>
482516
Cypress.isCy(value) ? value : cy.wrap(value, { log: false });
483517

484-
return ensureChain(
485-
registry.runStepDefininition(this, text, argument)
486-
).then((result: any) => {
487-
return {
488-
start,
489-
result,
490-
};
491-
});
518+
try {
519+
return ensureChain(
520+
registry.runStepDefininition(this, text, argument)
521+
).then((result: any) => {
522+
return {
523+
start,
524+
result,
525+
};
526+
});
527+
} catch (e) {
528+
if (e instanceof MissingDefinitionError) {
529+
throw new Error(
530+
createMissingStepDefinitionMessage(context, text)
531+
);
532+
} else {
533+
throw e;
534+
}
535+
}
492536
})
493537
.then(({ start, result }) => {
494538
const end = createTimestamp();
@@ -578,7 +622,12 @@ export default function createTests(
578622
gherkinDocument: messages.IGherkinDocument,
579623
pickles: messages.IPickle[],
580624
messagesEnabled: boolean,
581-
omitFiltered: boolean
625+
omitFiltered: boolean,
626+
stepDefinitionHints: {
627+
stepDefinitions: string[];
628+
stepDefinitionPatterns: string[];
629+
stepDefinitionPaths: string[];
630+
}
582631
) {
583632
const noopNode = { evaluate: () => true };
584633
const environmentTags = getTags(Cypress.env());
@@ -630,6 +679,7 @@ export default function createTests(
630679
enabled: messagesEnabled,
631680
stack: messages,
632681
},
682+
stepDefinitionHints,
633683
},
634684
gherkinDocument.feature
635685
);
@@ -744,3 +794,82 @@ export default function createTests(
744794
}
745795
});
746796
}
797+
798+
function strictIsInteractive(): boolean {
799+
const isInteractive = Cypress.config(
800+
"isInteractive" as keyof Cypress.ConfigOptions
801+
);
802+
803+
if (typeof isInteractive === "boolean") {
804+
return isInteractive;
805+
}
806+
807+
throw new Error(
808+
"Expected to find a Cypress configuration property `isInteractive`, but didn't"
809+
);
810+
}
811+
812+
function createMissingStepDefinitionMessage(
813+
context: CompositionContext,
814+
text: string
815+
) {
816+
const noStepDefinitionPathsTemplate = `
817+
Step implementation missing for "<text>".
818+
819+
We tried searching for files containing step definitions using the following search pattern templates:
820+
821+
<step-definitions>
822+
823+
These templates resolved to the following search patterns:
824+
825+
<step-definition-patterns>
826+
827+
These patterns matched **no files** containing step definitions. This almost certainly means that you have misconfigured \`stepDefinitions\`.
828+
`;
829+
830+
const someStepDefinitionPathsTemplate = `
831+
Step implementation missing for "<text>".
832+
833+
We tried searching for files containing step definitions using the following search pattern templates:
834+
835+
<step-definitions>
836+
837+
These templates resolved to the following search patterns:
838+
839+
<step-definition-patterns>
840+
841+
These patterns matched the following files:
842+
843+
<step-definition-paths>
844+
845+
However, none of these files contained a step definition matching "<text>".
846+
`;
847+
848+
const { stepDefinitionHints } = context;
849+
850+
const template =
851+
stepDefinitionHints.stepDefinitionPaths.length > 0
852+
? someStepDefinitionPathsTemplate
853+
: noStepDefinitionPathsTemplate;
854+
855+
const maybeEscape = (string: string) =>
856+
strictIsInteractive() ? string.replace("*", "\\*") : string;
857+
858+
const prettyPrintList = (items: string[]) =>
859+
items.map((item) => " - " + maybeEscape(item)).join("\n");
860+
861+
return stripIndent(template)
862+
.replaceAll("<text>", text)
863+
.replaceAll(
864+
"<step-definitions>",
865+
prettyPrintList(stepDefinitionHints.stepDefinitions)
866+
)
867+
.replaceAll(
868+
"<step-definition-patterns>",
869+
prettyPrintList(stepDefinitionHints.stepDefinitionPatterns)
870+
)
871+
.replaceAll(
872+
"<step-definition-paths>",
873+
prettyPrintList(stepDefinitionHints.stepDefinitionPaths)
874+
);
875+
}

lib/step-definitions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ export async function getStepDefinitionPaths(
2929
cypress: ICypressConfiguration;
3030
preprocessor: IPreprocessorConfiguration;
3131
},
32-
filepath: string
32+
stepDefinitionPatterns: string[]
3333
): Promise<string[]> {
3434
const files = (
3535
await Promise.all(
36-
getStepDefinitionPatterns(configuration, filepath).map((pattern) =>
36+
stepDefinitionPatterns.map((pattern) =>
3737
util.promisify(glob)(pattern, { nodir: true })
3838
)
3939
)

0 commit comments

Comments
 (0)