Cucumber Expressions
Cucumber Expressions are a templating format to create dynamic step definitions, without the burden of Regular Expressions.
A Cucumber Expression is essentially a string which matches a step
defined in a gherkin .feature file.
They could match literally:
- Gherkin
- Expression
Given I have navigated to my profile
import { Given } from "@autometa/runner";
Given("I have navigated to my profile", () => {
// ...
});
Or they could match against an expression variable:
- Gherkin
- Expression
Given I have 4 dogs
import { Given } from "@autometa/runner";
Given("I have {int} dogs", (dogCount) => {
// ...
});
When a step definition contains an expression variable (or several), the corresponding value is extracted from the Gherkin step, and passed as an argument to the step definition function.
In this example, dogCount will be 4. For the following standard expression
formats, the type of the variable will be inferred and an explicit type annotdation (dogCount: number) is not required:
{int}{float}{string}{word}
Additionally, the following custom types are supported out of the box:
{number}- accepts integer or float numbers
{boolean}- accepts
trueorfalse. Does not require quotes around the value
- accepts
{bool}- alias for
{boolean}
- alias for
{date}- Converts a literate date string, or human readable date string like
'1 hour from now'or'tomorrow'to aDateobject with that value - Requires the value to be wrapped in quotes
- Converts a literate date string, or human readable date string like
{any}- wildcard
{unknown}- type enforced wildcard
{primitive}- Attempts to convert the value to a primitive type
- string, number, boolean, or date
Defining Custom Types
Custom types can be defined with the defineParameterType function. This function
accepts a collection of ParameterType objects with the following structure:
{
name: string;
regex: RegExp;
transformer?: (arg: string) => any;
useForSnippets?: boolean;
preferForRegexpMatch?: boolean;
pattern?: string;
typeName?: string;
}
Which represent the following details:
name- the name of the type. This is used to reference the type in the expressionregexp- a regular expression to match against the value in the expressionprimitive- the primitive value the result represents, if any. I.e for a primitiveNumberand a string'1', the result would be the parsed number1type- the type Constructor the result represents, if any. I.e for a typeDateand a string'1 hour from now', the result would be aDateobject with the value of 1 hour from now- Accepts any class constructor as long as it accepts a single string or primitive (see below) argument
- If a
primitiveis also defined, the value will first be converted to the specified primitive type before being passed to the constructor
transform- a function to transform the value from the expression to the desired type- If a
primitiveis also defined, the value will first be converted to the specified primitive type before being passed to the transformer - If a
typeis also defined, the value will first be converted to the specified type before being passed to the transformer - If both a
primitiveandtypeare defined, the value will first be converted to the specified primitive type, then passed to the constructor and finally the constructed object will be passed to the transformer
- If a
Example
With transform
import { defineParameterType } from "@autometa/runner";
import { getUserByUsername } from "./user-service";
defineParameterType({
name: "user",
regex: /@([a-zA-Z]+)/,
transformer: (username) => {
return getUserByUsername(username);
}
});
With primitive
import { defineParameterType } from "@autometa/runner";
defineParameterType({
name: "int",
regex: /[+-]?\d+/,
primitive: Number
});
With type
- Defining Parameters
- dtos.ts
import { defineParameterType } from "@autometa/runner";
import { MyDto } from "./dtos";
defineParameterType({
name: "myDto",
regex: /@([a-zA-Z]+)/,
type: MyDto
});
// dtos.ts
export class MyDto {
constructor(public name: string) {}
age: number;
}
Declaring Custom Types
By default, any custom types you define cannot be inferred in step definitions and will be typed
as unknown, which must be handled inside the step.
Given("I have a {myDto}", (myDto) => {
myDtoFunction(myDto); // Error, expected MyDto but was unknown
// myDto is typed as unknown
const casted = myDto as MyDto;
myDtoFunction(casted)
}
However it is possible to override the @autometa/scopes module in a declaration file with your own
custom types, which will be used to infer the type of the step definition.
For example, if you have a custom type MyDto defined in dtos.ts:
// typings/app.d.ts
import type { MyDto } from "./dtos";
declare module "@autometa/scopes" {
export interface CustomTypes {
myDto: MyDto;
}
}
Next, tell TypeScript about your custom types by adding the following to your tsconfig.json:
{
"compilerOptions": {
"types": ["typings/app.d.ts"]
}
}
Then in your step definition, the type of the myDto parameter will be inferred as MyDto:
import { Given } from "@autometa/runner";
Given("I have a {myDto}", (myDto) => {
// myDto is inferred as MyDto
});
Accessing the App In Expressions
It is possible to access the running tests App context from expressions,
in the transformer. The app will be passed as the last-most value pased to the transformer
arguments.
For example an expression can be configured to create a builder, assign it to the world, and in future steps retrieve it.
- Cucumber Expression
- Gherkin Step
- Step Definition
defineParameterType({
name: "builder:widget",
regex: [/"([^"]*)"/, /'([^']*)'/],
transform: (value: string, app: App) => {
const key = `$builder_${value}`;
if(!app.world[key]){
app.world[key]= new FooBuilder().name(value);
}
return app.world[key]
}
})
Scenario: my foo scenario
Given a widget 'User Creator'
* the 'User Creator' widget domain is 'Admin'
* the 'User Creator' widget cache is 'Dynamic'
When I execute the 'User Creator' widget
Given('a widget {builder:widget}', Pass)
Given(
'the {builder:widget} widget {word} is {string}',
(widgetBuilder, propertyName, propertyValue) => {
widgetBuilder.assign(propertyName, propertyValue)
}
)
When(
'I execute the {builder:widget} widget',
(widgetBuilder, app) => {
const widget = widgetBuilder.build();
await app.widgetClient.post(widget);
}
)