@@ -8,7 +8,13 @@ import { assertAndReturn } from "./assertions";
88
99import 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
1319import { collectTagNames , traverseGherkinDocument } from "./ast-helpers" ;
1420
@@ -26,6 +32,7 @@ import { getTags } from "./environment-helpers";
2632import { notNull } from "./type-guards" ;
2733
2834import { looksLikeOptions , tagToCypressOptions } from "./tag-parser" ;
35+ import { Context } from "mocha" ;
2936
3037declare 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+
137171function 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+ }
0 commit comments