cucumber-expressions
Version:
Cucumber Expressions - a simpler alternative to Regular Expressions
332 lines (290 loc) • 10.3 kB
text/typescript
import assert from 'assert'
import CucumberExpressionGenerator from '../src/CucumberExpressionGenerator'
import CucumberExpression from '../src/CucumberExpression'
import ParameterType from '../src/ParameterType'
import ParameterTypeRegistry from '../src/ParameterTypeRegistry'
class Currency {
constructor(public readonly s: string) {}
}
describe('CucumberExpressionGenerator', () => {
let parameterTypeRegistry: ParameterTypeRegistry
let generator: CucumberExpressionGenerator
function assertExpression(
expectedExpression: string,
expectedArgumentNames: string[],
text: string
) {
const generatedExpression = generator.generateExpressions(text)[0]
assert.deepStrictEqual(
generatedExpression.parameterNames,
expectedArgumentNames
)
assert.strictEqual(generatedExpression.source, expectedExpression)
const cucumberExpression = new CucumberExpression(
generatedExpression.source,
parameterTypeRegistry
)
const match = cucumberExpression.match(text)
if (match === null) {
assert.fail(
`Expected text '${text}' to match generated expression '${generatedExpression.source}'`
)
}
assert.strictEqual(match.length, expectedArgumentNames.length)
}
beforeEach(() => {
parameterTypeRegistry = new ParameterTypeRegistry()
generator = new CucumberExpressionGenerator(parameterTypeRegistry)
})
it('documents expression generation', () => {
parameterTypeRegistry = new ParameterTypeRegistry()
generator = new CucumberExpressionGenerator(parameterTypeRegistry)
const undefinedStepText = 'I have 2 cucumbers and 1.5 tomato'
const generatedExpression = generator.generateExpressions(
undefinedStepText
)[0]
assert.strictEqual(
generatedExpression.source,
'I have {int} cucumbers and {float} tomato'
)
assert.strictEqual(generatedExpression.parameterNames[0], 'int')
assert.strictEqual(generatedExpression.parameterTypes[1].name, 'float')
})
it('generates expression for no args', () => {
assertExpression('hello', [], 'hello')
})
it('generates expression with escaped left parenthesis', () => {
assertExpression('\\(iii)', [], '(iii)')
})
it('generates expression with escaped left curly brace', () => {
assertExpression('\\{iii}', [], '{iii}')
})
it('generates expression with escaped slashes', () => {
assertExpression(
'The {int}\\/{int}\\/{int} hey',
['int', 'int2', 'int3'],
'The 1814/05/17 hey'
)
})
it('generates expression for int float arg', () => {
assertExpression(
'I have {int} cukes and {float} euro',
['int', 'float'],
'I have 2 cukes and 1.5 euro'
)
})
it('generates expression for strings', () => {
assertExpression(
'I like {string} and {string}',
['string', 'string2'],
'I like "bangers" and \'mash\''
)
})
it('generates expression with % sign', () => {
assertExpression('I am {int}%% foobar', ['int'], 'I am 20%% foobar')
})
it('generates expression for just int', () => {
assertExpression('{int}', ['int'], '99999')
})
it('numbers only second argument when builtin type is not reserved keyword', () => {
assertExpression(
'I have {float} cukes and {float} euro',
['float', 'float2'],
'I have 2.5 cukes and 1.5 euro'
)
})
it('generates expression for custom type', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType(
'currency',
/[A-Z]{3}/,
Currency,
s => new Currency(s),
true,
false
)
)
assertExpression(
'I have a {currency} account',
['currency'],
'I have a EUR account'
)
})
it('prefers leftmost match when there is overlap', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType<Currency>(
'currency',
/c d/,
Currency,
s => new Currency(s),
true,
false
)
)
parameterTypeRegistry.defineParameterType(
new ParameterType('date', /b c/, Date, s => new Date(s), true, false)
)
assertExpression('a {date} d e f g', ['date'], 'a b c d e f g')
})
// TODO: prefers widest match
it('generates all combinations of expressions when several parameter types match', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType(
'currency',
/x/,
null,
s => new Currency(s),
true,
false
)
)
parameterTypeRegistry.defineParameterType(
new ParameterType('date', /x/, null, s => new Date(s), true, false)
)
const generatedExpressions = generator.generateExpressions(
'I have x and x and another x'
)
const expressions = generatedExpressions.map(e => e.source)
assert.deepStrictEqual(expressions, [
'I have {currency} and {currency} and another {currency}',
'I have {currency} and {currency} and another {date}',
'I have {currency} and {date} and another {currency}',
'I have {currency} and {date} and another {date}',
'I have {date} and {currency} and another {currency}',
'I have {date} and {currency} and another {date}',
'I have {date} and {date} and another {currency}',
'I have {date} and {date} and another {date}',
])
})
it('exposes parameter type names in generated expression', () => {
const expression = generator.generateExpressions(
'I have 2 cukes and 1.5 euro'
)[0]
const typeNames = expression.parameterTypes.map(parameter => parameter.name)
assert.deepStrictEqual(typeNames, ['int', 'float'])
})
it('matches parameter types with optional capture groups', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType(
'optional-flight',
/(1st flight)?/,
null,
s => s,
true,
false
)
)
parameterTypeRegistry.defineParameterType(
new ParameterType(
'optional-hotel',
/(1 hotel)?/,
null,
s => s,
true,
false
)
)
const expression = generator.generateExpressions(
'I reach Stage 4: 1st flight -1 hotel'
)[0]
// While you would expect this to be `I reach Stage {int}: {optional-flight} -{optional-hotel}` the `-1` causes
// {int} to match just before {optional-hotel}.
assert.strictEqual(
expression.source,
'I reach Stage {int}: {optional-flight} {int} hotel'
)
})
it('generates at most 256 expressions', () => {
for (let i = 0; i < 4; i++) {
parameterTypeRegistry.defineParameterType(
new ParameterType(
'my-type-' + i,
/([a-z] )*?[a-z]/,
null,
s => s,
true,
false
)
)
}
// This would otherwise generate 4^11=419430 expressions and consume just shy of 1.5GB.
const expressions = generator.generateExpressions('a s i m p l e s t e p')
assert.strictEqual(expressions.length, 256)
})
it('prefers expression with longest non empty match', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType('zero-or-more', /[a-z]*/, null, s => s, true, false)
)
parameterTypeRegistry.defineParameterType(
new ParameterType('exactly-one', /[a-z]/, null, s => s, true, false)
)
const expressions = generator.generateExpressions('a simple step')
assert.strictEqual(expressions.length, 2)
assert.strictEqual(
expressions[0].source,
'{exactly-one} {zero-or-more} {zero-or-more}'
)
assert.strictEqual(
expressions[1].source,
'{zero-or-more} {zero-or-more} {zero-or-more}'
)
})
it('does not suggest parameter included at the beginning of a word', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType('direction', /(up|down)/, null, s => s, true, false)
)
const expressions = generator.generateExpressions('I download a picture')
assert.strictEqual(expressions.length, 1)
assert.notEqual(expressions[0].source, 'I {direction}load a picture')
assert.strictEqual(expressions[0].source, 'I download a picture')
})
it('does not suggest parameter included inside a word', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType('direction', /(up|down)/, null, s => s, true, false)
)
const expressions = generator.generateExpressions('I watch the muppet show')
assert.strictEqual(expressions.length, 1)
assert.notEqual(expressions[0].source, 'I watch the m{direction}pet show')
assert.strictEqual(expressions[0].source, 'I watch the muppet show')
})
it('does not suggest parameter at the end of a word', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType('direction', /(up|down)/, null, s => s, true, false)
)
const expressions = generator.generateExpressions('I create a group')
assert.strictEqual(expressions.length, 1)
assert.notEqual(expressions[0].source, 'I create a gro{direction}')
assert.strictEqual(expressions[0].source, 'I create a group')
})
it('does suggest parameter that are a full word', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType('direction', /(up|down)/, null, s => s, true, false)
)
assert.strictEqual(
generator.generateExpressions('When I go down the road')[0].source,
'When I go {direction} the road'
)
assert.strictEqual(
generator.generateExpressions('When I walk up the hill')[0].source,
'When I walk {direction} the hill'
)
assert.strictEqual(
generator.generateExpressions('up the hill, the road goes down')[0]
.source,
'{direction} the hill, the road goes {direction}'
)
})
it('does not consider punctuation as being part of a word', () => {
parameterTypeRegistry.defineParameterType(
new ParameterType('direction', /(up|down)/, null, s => s, true, false)
)
assert.strictEqual(
generator.generateExpressions('direction is:down')[0].source,
'direction is:{direction}'
)
assert.strictEqual(
generator.generateExpressions('direction is down.')[0].source,
'direction is {direction}.'
)
})
})