Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions cucumber-core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,24 @@ import { PickleDocString } from '@cucumber/messages';
import { PickleStep } from '@cucumber/messages';
import { PickleTable } from '@cucumber/messages';
import { RegularExpression } from '@cucumber/cucumber-expressions';
import { Snippet } from '@cucumber/messages';
import { SourceReference } from '@cucumber/messages';
import { StepDefinition } from '@cucumber/messages';
import { TestCase } from '@cucumber/messages';
import { TestStep } from '@cucumber/messages';

// @public
export class AmbiguousError extends Error {
constructor(text: string, references: ReadonlyArray<SourceReference>);
constructor(step: AmbiguousStep);
}

// @public
export type AmbiguousStep = {
type: 'ambiguous';
pickleStep: PickleStep;
matches: ReadonlyArray<DefinedStep>;
};

// @public
export interface AssembledTestCase {
id: string;
Expand All @@ -53,7 +61,7 @@ export interface AssembledTestStep {
prefix: string;
body: string;
};
prepare(): PreparedStep;
prepare(): PreparedStep | UndefinedStep | AmbiguousStep;
sourceReference: SourceReference;
toMessage(): TestStep;
}
Expand Down Expand Up @@ -160,6 +168,7 @@ export interface NewTestRunHook {

// @public
export type PreparedStep = {
type: 'prepared';
fn: SupportCodeFunction;
args: ReadonlyArray<Argument>;
dataTable?: PickleTable;
Expand Down Expand Up @@ -212,9 +221,7 @@ export interface TestPlanOptions {

// @public
export class UndefinedError extends Error {
constructor(pickleStep: PickleStep);
// (undocumented)
readonly pickleStep: PickleStep;
constructor(step: UndefinedStep, snippets?: ReadonlyArray<Snippet>);
}

// @public
Expand All @@ -223,6 +230,12 @@ export type UndefinedParameterType = {
expression: string;
};

// @public
export type UndefinedStep = {
type: 'undefined';
pickleStep: PickleStep;
};

// (No @packageDocumentation comment for this package)

```
30 changes: 28 additions & 2 deletions src/AmbiguousError.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
import { SourceReference } from '@cucumber/messages'
import { PickleStep, SourceReference } from '@cucumber/messages'
import { expect } from 'chai'
import sinon from 'sinon'

import { AmbiguousError } from './AmbiguousError'
import { AmbiguousStep, DefinedStep } from './types'

describe('AmbiguousError', () => {
it('handles source references with and without locations', () => {
const pickleStep: PickleStep = {
id: 'step-1',
text: 'text',
astNodeIds: [],
}

const references: ReadonlyArray<SourceReference> = [
{ uri: 'steps.js', location: { line: 1, column: 2 } },
{ uri: 'steps.js', location: { line: 3, column: 4 } },
{ uri: 'mysterious.js' },
]
const error = new AmbiguousError('text', references)

const matches: ReadonlyArray<DefinedStep> = references.map((ref) => ({
id: 'def-id',
expression: {
raw: 'text',
compiled: {} as any,
},
fn: sinon.stub(),
sourceReference: ref,
toMessage: sinon.stub(),
}))

const step: AmbiguousStep = {
type: 'ambiguous',
pickleStep,
matches,
}

const error = new AmbiguousError(step)
expect(error.message).to.equal(
'Multiple matching step definitions found for text "text":\n' +
'1) steps.js:1:2\n' +
Expand Down
15 changes: 9 additions & 6 deletions src/AmbiguousError.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { SourceReference } from '@cucumber/messages'
import { AmbiguousStep } from './types'

/**
* Represents an error that occurs when multiple step definitions are found matching the text of a step
* @public
* @remarks
* Can be useful where an {@link AmbiguousStep} needs to bubble up as an error in the test framework
* and have a helpful message for the end user.
*/
export class AmbiguousError extends Error {
constructor(text: string, references: ReadonlyArray<SourceReference>) {
constructor(step: AmbiguousStep) {
super(
`Multiple matching step definitions found for text "${text}":` +
`Multiple matching step definitions found for text "${step.pickleStep.text}":` +
'\n' +
references
step.matches
.map(
(ref, index) =>
`${index + 1}) ${ref.uri}:${ref.location?.line ?? '?'}:${ref.location?.column ?? '?'}`
(def, index) =>
`${index + 1}) ${def.sourceReference.uri}:${def.sourceReference.location?.line ?? '?'}:${def.sourceReference.location?.column ?? '?'}`
)
.join('\n')
)
Expand Down
82 changes: 82 additions & 0 deletions src/UndefinedError.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { PickleStep, Snippet } from '@cucumber/messages'
import { expect } from 'chai'

import { UndefinedStep } from './types'
import { UndefinedError } from './UndefinedError'

describe('UndefinedError', () => {
it('should create an error message with the step text', () => {
const pickleStep: PickleStep = {
id: 'step-1',
text: 'I do something that is not defined',
astNodeIds: [],
}

const step: UndefinedStep = {
type: 'undefined',
pickleStep,
}

const error = new UndefinedError(step)
expect(error.message).to.eq(
'No matching step definitions found for text "I do something that is not defined"'
)
})

it('should not include snippet text when snippets array is empty', () => {
const pickleStep: PickleStep = {
id: 'step-1',
text: 'I do something that is not defined',
astNodeIds: [],
}

const step: UndefinedStep = {
type: 'undefined',
pickleStep,
}

const error = new UndefinedError(step, [])
expect(error.message).to.eq(
'No matching step definitions found for text "I do something that is not defined"'
)
})

it('should include snippets when provided', () => {
const pickleStep: PickleStep = {
id: 'step-1',
text: 'I do something that is not defined',
astNodeIds: [],
}

const step: UndefinedStep = {
type: 'undefined',
pickleStep,
}

const snippets: ReadonlyArray<Snippet> = [
{
language: 'javascript',
code: 'Given("I do something that is not defined", function () {\n // Write code here\n});',
},
{
language: 'javascript',
code: 'Given(/^I do something that is not defined$/, function () {\n // Write code here\n});',
},
]

const error = new UndefinedError(step, snippets)
expect(error.message).to.eq(
`No matching step definitions found for text "I do something that is not defined"

You can implement the step with this code:

Given("I do something that is not defined", function () {
// Write code here
});

Given(/^I do something that is not defined$/, function () {
// Write code here
});`
)
})
})
19 changes: 16 additions & 3 deletions src/UndefinedError.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { PickleStep } from '@cucumber/messages'
import { Snippet } from '@cucumber/messages'

import { UndefinedStep } from './types'

/**
* Represents an error that occurs when no step definitions are found matching the text of a step
* @public
* @remarks
* Can be useful where an {@link UndefinedStep} needs to bubble up as an error in the test framework
* and have a helpful message for the end user.
*/
export class UndefinedError extends Error {
constructor(public readonly pickleStep: PickleStep) {
super(`No matching step definitions found for text "${pickleStep.text}"`)
constructor(step: UndefinedStep, snippets?: ReadonlyArray<Snippet>) {
let message = `No matching step definitions found for text "${step.pickleStep.text}"`

if (snippets?.length) {
message +=
'\n\nYou can implement the step with this code:\n\n' +
snippets.map((snippet) => snippet.code).join('\n\n')
}

super(message)
}
}
68 changes: 44 additions & 24 deletions src/makeTestPlan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import sinon from 'sinon'
import sinonChai from 'sinon-chai'

import { parseGherkin } from '../test/parseGherkin'
import { AmbiguousError } from './AmbiguousError'
import { buildSupportCode } from './buildSupportCode'
import { makeTestPlan } from './makeTestPlan'
import { UndefinedError } from './UndefinedError'

use(sinonChai)

Expand Down Expand Up @@ -107,7 +105,7 @@ describe('makeTestPlan', () => {
})

describe('pickle steps', () => {
it('throws if a step is ambiguous', () => {
it('returns an ambiguous step when a step has multiple matches', () => {
const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId })
.step({
Expand All @@ -129,10 +127,15 @@ describe('makeTestPlan', () => {
}
)

expect(() => result.testCases[0].testSteps[0].prepare()).to.throw(AmbiguousError)
const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.type).to.eq('ambiguous')
if (prepared.type === 'ambiguous') {
expect(prepared.pickleStep).to.eq(pickles[0].steps[0])
expect(prepared.matches).to.have.lengthOf(2)
}
})

it('throws if a step is undefined', () => {
it('returns an undefined step when a step has no matches', () => {
const { gherkinDocument, pickles } = parseGherkin('minimal.feature', newId)
const supportCodeLibrary = buildSupportCode({ newId }).build()

Expand All @@ -143,11 +146,10 @@ describe('makeTestPlan', () => {
}
)

try {
result.testCases[0].testSteps[0].prepare()
} catch (err: any) {
expect(err).to.be.instanceOf(UndefinedError)
expect(err.pickleStep).to.eq(pickles[0].steps[0])
const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.type).to.eq('undefined')
if (prepared.type === 'undefined') {
expect(prepared.pickleStep).to.eq(pickles[0].steps[0])
}
})

Expand All @@ -171,8 +173,11 @@ describe('makeTestPlan', () => {
)

const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.type).to.eq('prepared')
if (prepared.type === 'prepared') {
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
}
})

it('matches and prepares a step with parameters', () => {
Expand All @@ -195,8 +200,11 @@ describe('makeTestPlan', () => {
)

const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args.map((arg) => arg.getValue(undefined))).to.deep.eq([4, 5])
expect(prepared.type).to.eq('prepared')
if (prepared.type === 'prepared') {
expect(prepared.fn).to.eq(fn)
expect(prepared.args.map((arg) => arg.getValue(undefined))).to.deep.eq([4, 5])
}
})

it('matches and prepares a step with a data table', () => {
Expand All @@ -219,9 +227,12 @@ describe('makeTestPlan', () => {
)

const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.dataTable).to.eq(pickles[0].steps[0].argument?.dataTable)
expect(prepared.type).to.eq('prepared')
if (prepared.type === 'prepared') {
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.dataTable).to.eq(pickles[0].steps[0].argument?.dataTable)
}
})

it('matches and prepares a step with a doc string', () => {
Expand All @@ -244,9 +255,12 @@ describe('makeTestPlan', () => {
)

const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.docString).to.eq(pickles[0].steps[0].argument?.docString)
expect(prepared.type).to.eq('prepared')
if (prepared.type === 'prepared') {
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.docString).to.eq(pickles[0].steps[0].argument?.docString)
}
})
})

Expand Down Expand Up @@ -442,8 +456,11 @@ describe('makeTestPlan', () => {
)

const prepared = result.testCases[0].testSteps[0].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.type).to.eq('prepared')
if (prepared.type === 'prepared') {
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
}
})

it('prepares After hooks for execution', () => {
Expand All @@ -470,8 +487,11 @@ describe('makeTestPlan', () => {
)

const prepared = result.testCases[0].testSteps[3].prepare()
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
expect(prepared.type).to.eq('prepared')
if (prepared.type === 'prepared') {
expect(prepared.fn).to.eq(fn)
expect(prepared.args).to.deep.eq([])
}
})
})

Expand Down
Loading