Skip to content

Commit ee83c68

Browse files
authored
Emit messages for suggestions (#2703)
1 parent 1e0bc64 commit ee83c68

File tree

17 files changed

+251
-36
lines changed

17 files changed

+251
-36
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber
1010
## [Unreleased]
1111
### Added
1212
- Support named BeforeAll/AfterAll hooks ([#2661](https://github.com/cucumber/cucumber-js/pull/2661))
13+
- Emit messages for suggestions ([#2703](https://github.com/cucumber/cucumber-js/pull/2703))
1314

1415
### Changed
1516
- Render a more test case-centric HTML report ([react-components/#396](https://github.com/cucumber/react-components/pull/396))

compatibility/cck_spec.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,6 @@ const UNSUPPORTED = [
2323
'global-hooks-afterall-error',
2424
// not a test sample
2525
'test-run-exception',
26-
// suggestions not implemented yet
27-
'examples-tables-undefined',
28-
'hooks-undefined',
29-
'retry-undefined',
30-
'undefined',
31-
'unknown-parameter-type',
3226
]
3327

3428
config.truncateThreshold = 100

features/step_definition_snippets_custom_syntax.feature

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,19 @@ Feature: step definition snippets custom syntax
88
"""
99
Feature: a feature
1010
Scenario: a scenario
11+
Given a step
12+
13+
Scenario: another scenario
1114
Given an undefined step
1215
"""
16+
And a file named "features/steps.js" with:
17+
"""
18+
const {Given} = require('@cucumber/cucumber')
19+
20+
Given('a step', function() {
21+
// noop
22+
})
23+
"""
1324
And a file named "coffeescript_syntax.js" with:
1425
"""
1526
function CoffeeScriptSyntax(snippetInterface) {
@@ -61,3 +72,13 @@ Feature: step definition snippets custom syntax
6172
| promise | -> | 'pending' |
6273
| async-await | -> | 'pending' |
6374
| synchronous | -> | 'pending' |
75+
76+
Scenario: Custom snippet syntax works in parallel runtime
77+
When I run cucumber-js with `--parallel 2 --format-options '{"snippetInterface": "async-await", "snippetSyntax": "./coffeescript_syntax.js"}'`
78+
Then it fails
79+
And the output contains the text:
80+
"""
81+
@Given 'an undefined step', ->
82+
# Write code here that turns the phrase above into concrete actions
83+
'pending'
84+
"""

features/support/formatter_output_helpers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,7 @@ export const ignorableKeys = [
121121
// errors
122122
'message',
123123
'stackTrace',
124+
// snippets
125+
'language',
126+
'code',
124127
]

src/api/run_cucumber.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ Running from: ${__dirname}
153153
newId,
154154
supportCodeLibrary,
155155
options: options.runtime,
156+
snippetOptions: options.formats.options,
156157
})
157158
const success = await runtime.run()
158159
await pluginManager.cleanup()

src/formatter/step_definition_snippet_builder/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ export default class StepDefinitionSnippetBuilder {
4747
})
4848
}
4949

50+
buildMultiple({ keywordType, pickleStep }: IBuildRequest): string[] {
51+
const comment =
52+
'Write code here that turns the phrase above into concrete actions'
53+
const functionName = this.getFunctionName(keywordType)
54+
const generatedExpressions =
55+
this.cucumberExpressionGenerator.generateExpressions(pickleStep.text)
56+
const stepParameterNames = this.getStepParameterNames(pickleStep)
57+
return generatedExpressions.map((generatedExpression) => {
58+
return this.snippetSyntax.build({
59+
comment,
60+
functionName,
61+
generatedExpressions: [generatedExpression],
62+
stepParameterNames,
63+
})
64+
})
65+
}
66+
5067
getFunctionName(keywordType: KeywordType): string {
5168
switch (keywordType) {
5269
case KeywordType.Event:

src/runtime/make_runtime.ts

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { EventEmitter } from 'node:events'
22
import { IdGenerator } from '@cucumber/messages'
33
import { IRunOptionsRuntime } from '../api'
4-
import { ILogger } from '../environment'
4+
import { ILogger, IRunEnvironment } from '../environment'
55
import { SourcedPickle } from '../assemble'
66
import { SupportCodeLibrary } from '../support_code_library_builder/types'
7-
import { IRunEnvironment } from '../environment'
8-
import { Runtime, RuntimeAdapter } from './types'
7+
import FormatterBuilder from '../formatter/builder'
8+
import { FormatOptions } from '../formatter'
9+
import { Runtime } from './types'
910
import { ChildProcessAdapter } from './parallel/adapter'
1011
import { InProcessAdapter } from './serial/adapter'
1112
import { Coordinator } from './coordinator'
@@ -18,6 +19,7 @@ export async function makeRuntime({
1819
newId,
1920
supportCodeLibrary,
2021
options,
22+
snippetOptions,
2123
}: {
2224
environment: IRunEnvironment
2325
logger: ILogger
@@ -26,25 +28,19 @@ export async function makeRuntime({
2628
sourcedPickles: ReadonlyArray<SourcedPickle>
2729
supportCodeLibrary: SupportCodeLibrary
2830
options: IRunOptionsRuntime
31+
snippetOptions: Pick<FormatOptions, 'snippetInterface' | 'snippetSyntax'>
2932
}): Promise<Runtime> {
3033
const testRunStartedId = newId()
31-
const adapter: RuntimeAdapter =
32-
options.parallel > 0
33-
? new ChildProcessAdapter(
34-
testRunStartedId,
35-
environment,
36-
logger,
37-
eventBroadcaster,
38-
options,
39-
supportCodeLibrary
40-
)
41-
: new InProcessAdapter(
42-
testRunStartedId,
43-
eventBroadcaster,
44-
newId,
45-
options,
46-
supportCodeLibrary
47-
)
34+
const adapter = await makeAdapter(
35+
options,
36+
snippetOptions,
37+
testRunStartedId,
38+
environment,
39+
logger,
40+
eventBroadcaster,
41+
supportCodeLibrary,
42+
newId
43+
)
4844
return new Coordinator(
4945
testRunStartedId,
5046
eventBroadcaster,
@@ -54,3 +50,42 @@ export async function makeRuntime({
5450
adapter
5551
)
5652
}
53+
54+
async function makeAdapter(
55+
options: IRunOptionsRuntime,
56+
snippetOptions: Pick<FormatOptions, 'snippetInterface' | 'snippetSyntax'>,
57+
testRunStartedId: string,
58+
environment: IRunEnvironment,
59+
logger: ILogger,
60+
eventBroadcaster: EventEmitter,
61+
supportCodeLibrary: SupportCodeLibrary,
62+
newId: () => string
63+
) {
64+
if (options.parallel > 0) {
65+
return new ChildProcessAdapter(
66+
testRunStartedId,
67+
environment,
68+
logger,
69+
eventBroadcaster,
70+
options,
71+
snippetOptions,
72+
supportCodeLibrary
73+
)
74+
}
75+
const snippetBuilder = await FormatterBuilder.getStepDefinitionSnippetBuilder(
76+
{
77+
cwd: environment.cwd,
78+
snippetInterface: snippetOptions.snippetInterface,
79+
snippetSyntax: snippetOptions.snippetSyntax,
80+
supportCodeLibrary,
81+
}
82+
)
83+
return new InProcessAdapter(
84+
testRunStartedId,
85+
eventBroadcaster,
86+
newId,
87+
options,
88+
supportCodeLibrary,
89+
snippetBuilder
90+
)
91+
}

src/runtime/make_suggestion.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import {
2+
IdGenerator,
3+
PickleStep,
4+
PickleStepType,
5+
Suggestion,
6+
} from '@cucumber/messages'
7+
import StepDefinitionSnippetBuilder from '../formatter/step_definition_snippet_builder'
8+
import { KeywordType } from '../formatter/helpers'
9+
10+
function mapPickleStepTypeToKeywordType(type?: PickleStepType): KeywordType {
11+
switch (type) {
12+
case PickleStepType.CONTEXT:
13+
return KeywordType.Precondition
14+
case PickleStepType.ACTION:
15+
return KeywordType.Event
16+
case PickleStepType.OUTCOME:
17+
return KeywordType.Outcome
18+
default:
19+
return KeywordType.Precondition
20+
}
21+
}
22+
23+
export function makeSuggestion({
24+
newId,
25+
snippetBuilder,
26+
pickleStep,
27+
}: {
28+
newId: IdGenerator.NewId
29+
snippetBuilder: StepDefinitionSnippetBuilder
30+
pickleStep: PickleStep
31+
}): Suggestion {
32+
const keywordType = mapPickleStepTypeToKeywordType(pickleStep.type)
33+
const codes = snippetBuilder.buildMultiple({ keywordType, pickleStep })
34+
const snippets = codes.map((code) => ({
35+
code,
36+
language: 'javascript',
37+
}))
38+
39+
return {
40+
id: newId(),
41+
pickleStepId: pickleStep.id,
42+
snippets,
43+
}
44+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { describe, it } from 'mocha'
2+
import { expect } from 'chai'
3+
import { IdGenerator } from '@cucumber/messages'
4+
import * as messages from '@cucumber/messages'
5+
import { buildSupportCodeLibrary } from '../../test/runtime_helpers'
6+
import FormatterBuilder from '../formatter/builder'
7+
import { makeSuggestion } from './make_suggestion'
8+
9+
describe('makeSuggestion', () => {
10+
it('generates multiple snippets for expressions with numeric parameters', async () => {
11+
const supportCodeLibrary = buildSupportCodeLibrary()
12+
const snippetBuilder =
13+
await FormatterBuilder.getStepDefinitionSnippetBuilder({
14+
cwd: process.cwd(),
15+
supportCodeLibrary,
16+
})
17+
const newId = IdGenerator.incrementing()
18+
const pickleStep: messages.PickleStep = {
19+
id: '1',
20+
text: 'I have 5 apples',
21+
type: messages.PickleStepType.CONTEXT,
22+
astNodeIds: [],
23+
}
24+
25+
const suggestion = makeSuggestion({
26+
newId,
27+
snippetBuilder,
28+
pickleStep,
29+
})
30+
31+
expect(suggestion).to.deep.equal({
32+
id: '0',
33+
pickleStepId: '1',
34+
snippets: [
35+
{
36+
code: "Given('I have {int} apples', function (int) {\n // Write code here that turns the phrase above into concrete actions\n return 'pending';\n});",
37+
language: 'javascript',
38+
},
39+
{
40+
code: "Given('I have {float} apples', function (float) {\n // Write code here that turns the phrase above into concrete actions\n return 'pending';\n});",
41+
language: 'javascript',
42+
},
43+
],
44+
})
45+
})
46+
})

src/runtime/parallel/adapter.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AssembledTestCase } from '../../assemble'
66
import { ILogger, IRunEnvironment } from '../../environment'
77
import { RuntimeAdapter } from '../types'
88
import { IRunOptionsRuntime } from '../../api'
9+
import { FormatOptions } from '../../formatter'
910
import {
1011
FinalizeCommand,
1112
InitializeCommand,
@@ -47,6 +48,10 @@ export class ChildProcessAdapter implements RuntimeAdapter {
4748
private readonly logger: ILogger,
4849
private readonly eventBroadcaster: EventEmitter,
4950
private readonly options: IRunOptionsRuntime,
51+
private readonly snippetOptions: Pick<
52+
FormatOptions,
53+
'snippetInterface' | 'snippetSyntax'
54+
>,
5055
private readonly supportCodeLibrary: SupportCodeLibrary
5156
) {}
5257

@@ -131,6 +136,7 @@ export class ChildProcessAdapter implements RuntimeAdapter {
131136
this.supportCodeLibrary.afterTestRunHookDefinitions.map((h) => h.id),
132137
},
133138
options: this.options,
139+
snippetOptions: this.snippetOptions,
134140
} satisfies InitializeCommand)
135141
}
136142

0 commit comments

Comments
 (0)