Skip to main content

Step Definitions

Autometa steps resemble Cucumber JS steps and should be identifiable to most Gherkin IDE plugins.

The largest difference is that Autometa step callbacks are not bound to the World object. Instead, World is a property of the App object, which will passed as the last argument to the step callback.

Because the step is not bound, it is safe to use fat arrow (=>) functions.

Defining a step

Steps can be defined using the Given, When, or Then functions. If the gherkin file includes And, But or * steps, they can be matched against any of those main 3. It is advised to define them against the step they follow.

I.e for the steps

Given a foo
And a bar

The step And a bar should be defined as a Given, however this is not enforced.

import { Given } from "@autometa/runner";

Given("a foo", () => {
// ...
});

Given("a bar", () => {
// ...
});

If no matching steps are found, a report will be generated from a fuzzy search, helping to identify typos.

App

The App object is passed as the last argument to the step callback. If there are no tables or Cucumber Expressions, it will be the only argument. Apps type must be defined by you (see Getting Started), or else it will be an empty object containing an empty world at the type level.

import { Given } from "@autometa/runner";

Given("a foo", (app) => {
app.webdriver.clickFoo();
});

Async

Step callbacks can be synchronous or asynchronous. If you do not want to store the result and define the function as async you can simply return the promise of the underlying action:

import { Given } from "@autometa/runner";

Given("a foo", (app) => {
return app.http.createFoo({ foo: "test-bar" });
});

However if, for example, you want to store the result on the World, you can await the promise and then store the result:

import { Given } from "@autometa/runner";

Given("a foo", async (app) => {
const foo = await app.http.createFoo({ foo: "test-bar" });
app.world.foo = foo;
});

Tables

Autometa tables are inspired by the Java implementation which allows tables to be parsed differently according to step.

See Data Tables for more information on tables.

To use a table in a step, simply append the table type as the last argument to the step definition, which can be accessed as the first or second to last argument before App in the definition callback.

Given a foo
| foo | bar | baz |
| 1 | true | bobby |

This represents a 'Horizontal Table' or HTable, where the first row represents the headers and the rest the data. This can be accessed as follows:

import { Given, HTable } from "@autometa/runner";

Given(
"a foo",
(table: HTable, app) => {
const fooNum = table.get("foo", 0) as number;
const fooBool = table.get("bar", 0) as boolean;
const fooStr = table.get("baz", 0) as string;
const fooNumString = table.get("foo", 0, true) as string;
},
HTable
);
tip

Datatable values will be converted to their javascript type where possible

i.e strings that represent numbers will be converted to a number, 'true' and 'false' will be treated as bools while the rest will be parsed as strings.

The original raw value can be accessed by passing true as the third argument to table.get

const fooNumString = table.get("foo", 0, true) as string;

Jest Cucumber Style

It's also possible to use a nested jest-cucumber style to write definitions. These can still access globally defined steps but also feature or scenario specific steps.

import { Feature, Scenario, ScenarioOutline, Rule Given, HTable } from "@autometa/runner";

Feature("A feature", () => {
Given('Feature shared step', ()=>{
// ...
})
Scenario("A scenario", () => {
Given("a foo", (app) => {
app.world.foo = "foo";
});

Then("the foo should be 'foo'", (app) => {
expect(app.world.foo).toBe("foo");
});
});

ScenarioOutline("A scenario outline", () => {
Given("a foo with <foo>", (table: HTable, app) => {
app.world.foo = table.get("foo", 0) as string;
});

Then("the foo should be <foo>", (table: HTable, app) => {
expect(app.world.foo).toBe(table.get("foo", 0) as string);
});
});

Rule("A rule", () => {
Given("a foo", (app) => {
app.world.foo = "foo";
});

Then("the foo should be 'foo'", (app) => {
expect(app.world.foo).toBe("foo");
});

Scenario("A scenario", () => {
Given("a foo", (app) => {
app.world.foo = "foo";
});

Then("the foo should be 'foo'", (app) => {
expect(app.world.foo).toBe("foo");
});
});
});
}, 'path/to/feature.feature');

The path argument at the end of the Feature function can be relative, root-relative, absolute, or be relative to a featuresRoot based on the following rules:

  • If the path starts with a slash, it is absolutel
  • If the path starts with ./ or ../ it is relative
  • If the path starts with neither a / or . it is relative to the root of the project
  • If the path starts with ~ and a featuresRoot is defined in autometa.config.ts, it is relative to that root.
tip

Step definitions defined in this style have specificity. I.e. if a step is defined globally and within a feature, the feature specific step will be used. This allows for more granular control over step definitions. Likewise, if a step is defined globally, within a feature, and within a scenario, the scenario specific step will be used.