Skip to main content
Version: 2x

Custom Step Library

When you have step definitions that are reused across multiple projects, packaging them as a dedicated npm package avoids duplication and lets teams share tested automation building blocks.

Package Structure

@myorg/steps-myservice/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # entry point — registers all steps
│ ├── steps/
│ │ ├── action.ts
│ │ └── assertion.ts
│ └── parameterTypes.ts # custom parameter types (optional)
└── README.md

Setting Up the Package

package.json

{
"name": "@myorg/steps-myservice",
"version": "1.0.0",
"main": "src/index.js",
"types": "src/index.d.ts",
"peerDependencies": {
"@qavajs/core": ">=2.0.0"
},
"devDependencies": {
"@qavajs/core": "^2.0.0",
"typescript": "^5.0.0"
}
}

Declare @qavajs/core as a peer dependency so the host project provides it rather than the package bundling its own copy.

tsconfig.json

{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"declaration": true,
"outDir": "./src",
"rootDir": "./src",
"strict": true
}
}

Writing Steps

Import Given, When, Then from @qavajs/core. The full set of qavajs parameter types ({value}, {validation}, {playwrightLocator}, etc.) is available automatically to consumers who load your package after @qavajs/steps-playwright or @qavajs/steps-wdio.

// src/steps/action.ts
import { When } from '@qavajs/core';
import type { MemoryValue } from '@qavajs/core';

When('I call myservice endpoint {value} and save response as {string}',
async function(endpointValue: MemoryValue, key: string) {
const url = await endpointValue.value();
const response = await fetch(url);
this.setValue(key, await response.json());
}
);
// src/steps/assertion.ts
import { Then } from '@qavajs/core';
import type { MemoryValue, Validation } from '@qavajs/core';

Then('I expect myservice response {value} {validation} {value}',
async function(actual: MemoryValue, validate: Validation, expected: MemoryValue) {
validate(await actual.value(), await expected.value());
}
);

Custom Parameter Types

Register parameter types in a dedicated file and call it from the entry point:

// src/parameterTypes.ts
import { defineParameterType } from '@qavajs/core';

defineParameterType({
name: 'httpMethod',
regexp: /GET|POST|PUT|PATCH|DELETE/,
transformer: (method: string) => method
});
// src/steps/action.ts
import { When } from '@qavajs/core';

When('I send {httpMethod} request to {value}',
async function(method: string, urlValue) {
const url = await urlValue.value();
await fetch(url, { method });
}
);

Entry Point

The entry point file simply imports all step and parameter type modules so that requiring it in a project registers everything:

// src/index.ts
import './parameterTypes';
import './steps/action';
import './steps/assertion';

Consuming the Library

In the host project's config, add the library to require before any project-specific step definitions:

export default {
require: [
'node_modules/@qavajs/steps-playwright/index.js',
'node_modules/@myorg/steps-myservice/src/index.js',
'step_definitions/**/*.ts'
],
// ...
}

Overriding Steps from a Library

If a library step does not match your project's exact needs, use Override to replace the implementation without triggering an ambiguous step error:

import { Override } from '@qavajs/core';

Override('I call myservice endpoint {value} and save response as {string}',
async function(endpointValue, key: string) {
// custom implementation for this project
const url = await endpointValue.value();
const response = await myCustomClient.get(url);
this.setValue(key, response.data);
}
);

Publishing

Publish to the npm registry in the normal way:

npm publish --access public

For a private organization registry:

npm publish --registry https://registry.mycompany.com

Once published, install in any qavajs project:

npm install @myorg/steps-myservice