Repository: timjroberts/cucumber-js-tsflow
Branch: master
Commit: 87da1c24d45a
Files: 54
Total size: 155.9 KB
Directory structure:
gitextract_67pl99xn/
├── .build/
│ └── setPackageVersion.js
├── .github/
│ └── workflows/
│ ├── ci.yml
│ ├── release.yml
│ └── stale.yml
├── .gitignore
├── .npm-upgrade.json
├── .run/
│ ├── All Tests.run.xml
│ └── Template Cucumber.js.run.xml
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── CONTRIBUTE.md
├── LICENSE
├── README.md
├── cucumber-tsflow/
│ ├── .npmignore
│ ├── package.json
│ ├── src/
│ │ ├── binding-decorator.ts
│ │ ├── binding-registry.ts
│ │ ├── hook-decorators.ts
│ │ ├── index.ts
│ │ ├── logger.ts
│ │ ├── managed-scenario-context.ts
│ │ ├── our-callsite.ts
│ │ ├── provided-context.ts
│ │ ├── scenario-context.ts
│ │ ├── scenario-info.ts
│ │ ├── step-binding-flags.ts
│ │ ├── step-binding.ts
│ │ ├── step-definition-decorators.ts
│ │ ├── tag-normalization.ts
│ │ └── types.ts
│ └── tsconfig.json
├── cucumber-tsflow-specs/
│ ├── features/
│ │ ├── basic-test.feature
│ │ ├── cucumber-context-objects.feature
│ │ ├── custom-context-objects.feature
│ │ ├── external-context-extraction.feature
│ │ ├── global-hooks.feature
│ │ ├── hooks.feature
│ │ └── tag-parameters.feature
│ ├── package.json
│ ├── src/
│ │ ├── step_definitions/
│ │ │ ├── cucumber_steps.ts
│ │ │ ├── file_steps.ts
│ │ │ ├── prepare.ts
│ │ │ └── scenario_steps.ts
│ │ └── support/
│ │ ├── formatter_output_helpers.ts
│ │ ├── helpers.ts
│ │ ├── runner.ts
│ │ └── testDir.ts
│ └── tsconfig.json
├── cucumber.js
├── lerna.json
├── package.json
├── tsconfig.json
├── tslint.json
└── version.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .build/setPackageVersion.js
================================================
const nbgv = require("nerdbank-gitversioning");
const setPackageVersionAndBuildNumber = (versionInfo) => {
// Set a build output value representing the NPM package version
console.log(
"::set-output name=package_version::" + versionInfo.npmPackageVersion,
);
nbgv.setPackageVersion("cucumber-tsflow");
nbgv.setPackageVersion("cucumber-tsflow-specs");
};
const handleError = (err) =>
console.error(
"Failed to update the package version number. nerdbank-gitversion failed: " +
err,
);
nbgv.getVersion().then(setPackageVersionAndBuildNumber).catch(handleError);
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
branches: [master, release/**]
pull_request:
branches: [master, release/**]
jobs:
# Build and Test the 'cucumber-tsflow' package
build:
name: Build and Test
runs-on: ubuntu-latest
strategy:
matrix:
cucumberVersion: ["^10", "^11", "^12"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install npm packages
run: |-
npm ci
npm install @cucumber/cucumber@${{ matrix.cucumberVersion }}
- name: Build
run: npm run build
- name: Run specification tests
run: npm test
================================================
FILE: .github/workflows/release.yml
================================================
#
# This workflow creates a release from a specified branch. The Package version is managed
# by Nerdbank Gitversioning based on configuration held in 'version.json' file.
#
name: Release
on:
workflow_dispatch:
jobs:
# Build, Test and Pack the 'cucumber-tsflow' package
build:
name: Build and Test
runs-on: ubuntu-latest
outputs:
version: ${{ steps.set_package_version.outputs.NpmPackageVersion }}
releaseTag: ${{ steps.tagInfo.outputs.releaseTag }}
steps:
- uses: actions/checkout@v3
with:
# avoid shallow clone (required by Nerbank GitVersioning)
fetch-depth: 0
- uses: actions/setup-node@v3
with:
node-version: 22
- name: Install npm packages
run: npm ci
- name: Update package version
id: set_package_version
uses: dotnet/nbgv@master
with:
stamp: cucumber-tsflow/package.json
- name: Build
run: npm run build
- name: Create npm package
run: npm pack ./cucumber-tsflow
- name: Read tag info
id: tagInfo
run: |-
echo "releaseTag=$(jq '.releaseTag // "latest"' version.json)" | tee -a $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: npm-package
path: |
cucumber-tsflow-${{ steps.set_package_version.outputs.NpmPackageVersion }}.tgz
# Publish the 'cucumber-tsflow' package to npm
publish:
name: Publish to npm
runs-on: ubuntu-latest
needs: build
permissions:
contents: write
steps:
- uses: actions/setup-node@v3
with:
node-version: 22
registry-url: "https://registry.npmjs.org"
- uses: actions/download-artifact@v4
name: Download npm package
with:
name: npm-package
- name: Publish npm package
run: |-
npm publish \
cucumber-tsflow-${{ needs.build.outputs.version }}.tgz \
--tag ${{ needs.build.outputs.releaseTag }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish GitHub release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.build.outputs.version }}
commit: ${{ github.sha }}
artifacts: cucumber-tsflow-${{ needs.build.outputs.version }}.tgz
generateReleaseNotes: true
================================================
FILE: .github/workflows/stale.yml
================================================
name: "Stale issue handler"
on:
workflow_dispatch:
schedule:
- cron: "0 0 * * *"
permissions:
contents: write # only for delete-branch option
issues: write
pull-requests: write
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v6
id: stale
with:
days-before-stale: 60
days-before-close: 7
stale-issue-message: "This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days."
close-issue-message: "There hasn't been any activity on this issue for 67 days. Closing it as Spoiled."
stale-issue-label: stale
close-issue-label: spoiled
exempt-issue-labels: "blocked,discussion,good first issue"
stale-pr-message: "This PR is stale because it has been 60 days with no activity. Remove stale lable or comment or this will be closed in 7 days."
close-pr-message: "There hasn't been any activity on this PR for 67 days. Closing it as Spoiled."
stale-pr-label: stale
close-pr-label: spoiled
exempt-pr-labels: "blocked,discussion"
- name: Print outputs
run: echo ${{ join(steps.stale.outputs.*, ',') }}
================================================
FILE: .gitignore
================================================
node_modules
dist
tmp/
.idea/
tsconfig.tsbuildinfo
================================================
FILE: .npm-upgrade.json
================================================
{
"ignore": {
"@cucumber/cucumber": {
"versions": "^8",
"reason": "Mantain compatibility with cucumber 7 and 8"
}
}
}
================================================
FILE: .run/All Tests.run.xml
================================================
================================================
FILE: .run/Template Cucumber.js.run.xml
================================================
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"name": "Specs",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/cucumber/bin/cucumber-js",
"stopOnEntry": true,
"args": ["--require", "./cucumber-tsflow-specs/dist"],
"cwd": "${workspaceRoot}",
"runtimeExecutable": null,
"runtimeArgs": ["--nolazy"],
"env": {
"NODE_ENV": "development"
},
"externalConsole": false,
"sourceMaps": true,
"outDir": null
},
{
"name": "Attach",
"type": "node",
"request": "attach",
"port": 5858,
"sourceMaps": false,
"outDir": null,
"localRoot": "${workspaceRoot}",
"remoteRoot": null
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}
================================================
FILE: CONTRIBUTE.md
================================================
The project should set-up all of its inner links and bindings when you first install it.
Run the tests locally to ensure everything is properly configured.
```terminal
> git clone https://github.com/timjroberts/cucumber-js-tsflow.git
> cd cucumber-js-tsflow
> npm install
> npm test
```
## Setting up Run/Debug in IDE
For IntelliJ, a run configuration is stored in `.run/cucumber-js.run.xml` to run/debug the tests.
For other IDE, using the following runtime config for node:
- working dir: `cucumber-tsflow-spec`
- node-parameters: `--require ts-node/register `
- js script to run: `node_modules/@cucumber/cucumber/bin/cucumber-js`
- application parameters: `features/**/*.feature --require "src/step_definitions/**/*.ts" `
An example command line runner:
```shell script
"C:\Program Files\nodejs\node.exe" --require ts-node/register C:\Users\wudon\repo\cucumber-js-tsflow\node_modules\@cucumber\cucumber\bin\cucumber-js features/**/*.feature --require src/step_definitions/**/*.ts
```
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018-2020 Tim Roberts and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# cucumber-tsflow

Provides 'SpecFlow' like bindings for CucumberJS in TypeScript 1.7+.
## Table of content
See that menu icon to the left of "README.md"?
Did you know that every markdown file in GitHub with more than two headings
have that icon as a Table of Content linking to every heading?
## Quick Start
cucumber-tsflow uses TypeScript Decorators to create SpecFlow like bindings for
TypeScript classes and methods that allow those classes and methods to be used
in your CucumberJS support files. As such, cucumber-tsflow has a peer dependency
on CucumberJS, and you still run your specifications using the cucumber
command line tool.
### Install cucumber and cucumber-tsflow
```bash
npm install @cucumber/cucumber cucumber-tsflow
```
### Create .feature files to describe your specifications
By default, CucumberJS looks for .feature files in a folder called 'features',
so create that folder and then create a new file called `my_feature.feature`:
```gherkin
# features/my_feature.feature
Feature: Example Feature
This is an example feature
Scenario: Adding two numbers
Given I enter '2' and '8'
Then I receive the result '10'
```
### Create the Support Files to support the Feature
CucumberJS requires Support Files defining what each step in the Feature files mean.
By default, CucumberJS looks for Support Files beneath the 'features' folder.
We need to write step definitions to support the two steps that we created above.
Create a new `ArithmeticSteps.ts` file:
```ts
// features/ArithmeticSteps.ts
import { binding, given, then } from "cucumber-tsflow";
@binding()
class ArithmeticSteps {
private computedResult: number;
@given(/I enter '(\d*)' and '(\d*)'/)
public givenTwoNumbers(num1: string, num2: string): void {
this.computedResult = parseInt(num1) + parseInt(num2);
}
@then(/I receive the result '(\d*)'/)
public thenResultReceived(expectedResult: string): void {
if (parseInt(expectedResult) !== this.computedResult) {
throw new Error("Arithmetic Error");
}
}
}
export = ArithmeticSteps;
```
Note how the cucumber-tsflow Decorators are being used to bind the methods in
the class. During runtime, these Decorators simply call the Cucumber code on
your behalf in order to register callbacks with Given(), When(), Then(), etc.
The callbacks that are being registered with Cucumber are wrappers around your
bound class. This allows you to maintain a state between each step on the same
class by using instance properties.
In this quick example, the entire test state is encapsulated directly in the class.
As your test suite grows larger and step definitions get shared between
multiple classes, you can use 'Context Injection' to share state between
running step definitions (see below).
### Compiling your TypeScript Support Code
To use `cucumber-tsflow` with TypeScript, you'll also need a `tsconfig.json` file
with these options:
```json
{
"compilerOptions": {
"moduleResolution": "node",
"experimentalDecorators": true
}
}
```
> Hint: You can add that to `features/tsconfig.json` to have it applied only for
> your integration tests.
With the TS config in place, CucumberJS should automatically compile your code
before running it.
## Reference
### Bindings
Bindings provide the automation that connects a specification step in a Gherkin
feature file to some code that executes for that step.
When using Cucumber with TypeScript you can define this automation using the
`binding` decorator on top of a class:
```ts
import { binding } from "cucumber-tsflow";
@binding()
class MySteps {
// ...
}
export = MySteps;
```
Through this reference, classes decorated with the `binding` decorator are
referred "binding classes".
_Note_: You must use the `export = ;` due to how Cucumber interprets
the exported items of a Support File.
### Step Definitions
Step definitions can be bound to automation code in a binding class by decorating
a public function with a 'given', 'when' or 'then' binding decorator:
```ts
import { binding, given, when, then } from "cucumber-tsflow";
@binding()
class MySteps {
@given(/I perform a search using the value "([^"]*)"/)
public givenAValueBasedSearch(searchValue: string): void {
// ...
}
}
export = MySteps;
```
The methods have the same requirements and guarantees of functions you would normally
supply to Cucumber, which means that the methods may be:
- Synchronous by returning `void`
- Asynchronous by receiving and using a callback as the last parameter\
The callback has signature `() => void`
- Asynchronous by returning a `Promise`
The step definition functions must always receive a pattern as the first argument,
which can be either a string or a regular expression.
Additionally, a step definition may receive additional options in the format:
```ts
@binding()
class MySteps {
@given("pattern", {
tag: "not @expensive",
timeout: 1000,
wrapperOptions: {},
})
public givenAValueBasedSearch(searchValue: string): void {
// ...
}
}
```
For backward compatibility, the `tag` and `timeout` options can also be passed
as direct arguments:
```ts
@binding()
class MySteps {
@given("pattern", "not @expensive", 1000)
public givenAValueBasedSearch(searchValue: string): void {
// ...
}
}
```
### Hooks
Hooks can be used to add logic that happens before or after each scenario execution.
They are configured in the same way as the [Step Definitions](#step-definitions).
```typescript
import { binding, before, beforeAll, after, afterAll } from "cucumber-tsflow";
@binding()
class MySteps {
@beforeAll()
public static beforeAllScenarios(): void {
// ...
}
@afterAll()
public static beforeAllScenarios(): void {
// ...
}
@before()
public beforeAllScenarios(): void {
// ...
}
@after()
public afterAllScenarios(): void {
// ...
}
}
export = MySteps;
```
Contrary to the Step Definitions, Hooks don't need a pattern since they don't
run for some particular step, but once for each scenario.
Hooks can receive aditional options just like the Step Definitions:
```ts
@binding()
class MySteps {
// Runs before each scenarios with tag `@requireTempDir` with 2 seconds of timeout
@before({ tag: "@requireTempDir", timeout: 2000 })
public async beforeAllScenariosRequiringTempDirectory(): Promise {
let tempDirInfo = await this.createTemporaryDirectory();
// ...
}
// Runs after each scenarios with tag `@requireTempDir` with 2 seconds of timeout
@after({ tag: "@requireTempDir", timeout: 2000 })
public async afterAllScenariosRequiringTempDirectory(): void {
await this.deleteTemporaryDirectory();
// ...
}
}
```
For backward compatibility, the `tag` option can also be passes as a direct argument:
```ts
@binding()
class MySteps {
@before('@local')
public async runForLocalOnly(): Promise {
...
}
}
```
### Step and hook options
#### Tag filters
Both Step Definitions and Hooks can receive a `tag` option. This option defines
a filter such that the binding will only be considered for scenarios matching
the filter.
The syntax of the tag filter is
a ["Tag expression"](https://cucumber.io/docs/cucumber/api/?lang=javascript#tag-expressions)
specified by Cucumber.
**Note**: The tag might be set for the `Feature` or for the `Scenario`, and there
is no distinction between them. This is
called ["Tag Inheritance"](https://cucumber.io/docs/cucumber/api/?lang=javascript#tag-inheritance).
For backward compatibility, setting a tag to a single word is treated the same
as a filter for that word as a tag:
```ts
// This backward compatible format
@given({ tag: 'foo' })
// Is transformed into this
@given({ tag: '@foo' })
```
#### Timeout
Both Step Definition and Hooks can receive a `timeout` option. This option defines
the maximum runtime allowed for the binding before it is flagged as failed.
`cucumber-tsflow` currently doesn't have a way to define a global default step timeout,
but it can be easily done through CucumberJS' `setDefaultTimeout` function.
#### Passing WrapOptions
In step definition, we can passing additional wrapper options to CucumberJS.
For example:
```typescript
@given(/I perform a search using the value "([^"]*)"/, { wrapperOptions: { retry: 2 } })
public
givenAValueBasedSearch(searchValue
:
string
):
void {
...
}
```
The type of `wrapperOptions` is defined by the function given to `setDefinitionFunctionWrapper`.
**Note**: `wrapperOptions` and `setDefinitionFunctionWrapper` were deprecated in
[CucumberJS 7.3.1](https://github.com/cucumber/cucumber-js/blob/8900158748a3f36c4b2fa5d172fe27013b39ab17/CHANGELOG.md#731---2021-07-20)
and are kept here for backward compatibility only while this library supports
CucumberJS 7.
### Sharing Data between Bindings
#### Context Injection
Like 'SpecFlow', `cucumber-tsflow` supports a simple dependency injection
framework that will instantitate and inject class instances into binding classes
for each executing scenario.
To use context injection:
- Create simple classes representing the shared data and/or behavior.\
These classes **must** have public constructors with no arguments (default constructors).
Defining a class with no constructor at all also works.
- Define a constructor on the binding classes that receives an instance of
the class defined above as an parameter.
- Update the `@binding()` decorator to indicate the types of context objects
that are required by the binding class
```ts
// Workspace.ts
export class Workspace {
public folder: string = "default folder";
public updateFolder(folder: string) {
this.folder = folder;
}
}
// my-steps.ts
import { binding, before, after } from "cucumber-tsflow";
import { Workspace } from "./Workspace";
@binding([Workspace])
class MySteps {
public constructor(protected workspace: Workspace) {}
@before("requireTempDir")
public async beforeAllScenariosRequiringTempDirectory(): Promise {
let tempDirInfo = await this.createTemporaryDirectory();
this.workspace.updateFolder(tempDirInfo);
}
}
export = MySteps;
```
#### Provided Context Types
This library provides 3 Context Types to interact with CucumberJS' World object.
- `WorldParameters`, which expose value passed to the `worldParameters` configuration
or the `--world-parameters` CLI option.
- `CucumberLog`, which exposes the `log` method of the `World` object.
- `CucumberAttachments`, which exposes the `attach` method of the `World` object.
- `ScenarioInfo`, which exposes information about the running scenario and allows
changing the behavior of steps and hooks based on tags easier.
================================================
FILE: cucumber-tsflow/.npmignore
================================================
*.ts
tsconfig.json
typings.json
typings
.npmignore
*.tsbuildinfo
!dist/**/*.d.ts
================================================
FILE: cucumber-tsflow/package.json
================================================
{
"name": "cucumber-tsflow",
"description": "Provides 'specflow' like bindings for CucumberJS 7.0.0+ in TypeScript 1.7+.",
"version": "5.0.0",
"author": "Tim Roberts ",
"maintainers": [
{
"name": "Luiz Ferraz",
"email": "luiz@lferraz.com",
"url": "https://github.com/Fryuni"
}
],
"license": "MIT",
"main": "./dist",
"keywords": [
"testing",
"bdd",
"cucumber",
"gherkin",
"tests",
"typescript",
"specflow"
],
"repository": {
"type": "git",
"url": "https://github.com/timjroberts/cucumber-js-tsflow.git"
},
"dependencies": {
"log4js": "^6.9.1",
"source-map-support": "^0.5.21",
"underscore": "^1.13.8"
},
"peerDependencies": {
"@cucumber/cucumber": "^10 || ^11 || ^12"
}
}
================================================
FILE: cucumber-tsflow/src/binding-decorator.ts
================================================
import {
After,
AfterAll,
AfterStep,
Before,
BeforeAll,
BeforeStep,
Given,
Then,
When,
World,
} from "@cucumber/cucumber";
import {
IDefineStepOptions,
IDefineTestStepHookOptions,
} from "@cucumber/cucumber/lib/support_code_library_builder/types";
import { PickleTag } from "@cucumber/messages";
import * as _ from "underscore";
import { BindingRegistry, DEFAULT_TAG } from "./binding-registry";
import logger from "./logger";
import {
ManagedScenarioContext,
ScenarioContext,
ScenarioInfo,
} from "./managed-scenario-context";
import {
CucumberAttachments,
CucumberLog,
WorldParameters,
} from "./provided-context";
import { StepBinding, StepBindingFlags } from "./step-binding";
import { ContextType, StepPattern, TypeDecorator } from "./types";
interface WritableWorld extends World {
[key: string]: any;
}
/**
* The property name of the current scenario context that will be attached to the Cucumber
* world object.
*/
const SCENARIO_CONTEXT_SLOTNAME: string = "__SCENARIO_CONTEXT";
/**
* A set of step patterns that have been registered with Cucumber.
*
* In order to support scoped (or tagged) step definitions, we must ensure that any step binding is
* only registered with Cucumber once. The binding function for that step pattern then becomes
* responsible for looking up and execuing the step binding based on the context that is in scope at
* the point of invocation.
*/
const stepPatternRegistrations = new Map();
// tslint:disable:no-bitwise
function ensureNoCyclicDependencies(target: any, currentPath: any[] = []) {
const dependencies = BindingRegistry.instance.getContextTypesForTarget(
target.prototype,
);
if (dependencies.length === 0) {
return;
}
for (const dependency of dependencies) {
if (dependency === undefined) {
throw new Error(
`Undefined dependency detected in ${target.name}. You possibly have an import cycle.\n` +
"See https://nodejs.org/api/modules.html#modules_cycles",
);
}
if (currentPath.includes(dependency)) {
throw new Error(
`Cyclic dependency detected: ${dependency.name} -> ${target.name} -> ${currentPath.map((t) => t.name).join(" -> ")}`,
);
}
ensureNoCyclicDependencies(dependency, [...currentPath, target]);
}
}
/**
* A class decorator that marks the associated class as a CucumberJS binding.
*
* @param requiredContextTypes An optional array of Types that will be created and passed into the created
* object for each scenario.
*
* An instance of the decorated class will be created for each scenario.
*/
export function binding(requiredContextTypes?: ContextType[]): TypeDecorator {
return (target: new (...args: any[]) => T) => {
ensureSystemBindings();
const bindingRegistry = BindingRegistry.instance;
bindingRegistry.registerContextTypesForTarget(
target.prototype,
requiredContextTypes,
);
ensureNoCyclicDependencies(target);
const allBindings: StepBinding[] = [
...bindingRegistry.getStepBindingsForTarget(target),
...bindingRegistry.getStepBindingsForTarget(target.prototype),
];
for (const stepBinding of allBindings) {
if (stepBinding.bindingType & StepBindingFlags.StepDefinitions) {
let stepBindingFlags = stepPatternRegistrations.get(
stepBinding.stepPattern.toString(),
);
if (stepBindingFlags === undefined) {
stepBindingFlags = StepBindingFlags.none;
}
if (stepBindingFlags & stepBinding.bindingType) {
return;
}
const bound = bindStepDefinition(stepBinding);
if (bound) {
stepPatternRegistrations.set(
stepBinding.stepPattern.toString(),
stepBindingFlags | stepBinding.bindingType,
);
}
} else if (stepBinding.bindingType & StepBindingFlags.Hooks) {
bindHook(stepBinding);
} else {
logger.trace("Ignored binding", stepBinding);
}
}
};
}
function getContextFromWorld(world: World): ScenarioContext {
const context: unknown = (world as Record)[
SCENARIO_CONTEXT_SLOTNAME
];
if (context instanceof ManagedScenarioContext) {
return context;
}
throw new Error(
"Scenario context have not been initialized in the provided World object.",
);
}
export function getBindingFromWorld(
world: World,
contextType: T,
): InstanceType {
const context = getContextFromWorld(world);
return context.getContextInstance(contextType);
}
export function ensureWorldIsInitialized() {
ensureSystemBindings();
}
/**
* Ensures that the 'cucumber-tsflow' hooks are bound to Cucumber.
*
* @param cucumber The cucumber object.
*
* The hooks will only be registered with Cucumber once regardless of which binding invokes the
* function.
*/
const ensureSystemBindings = _.once(() => {
Before(function (this: WritableWorld, scenario) {
logger.trace(
"Setting up scenario context for scenario:",
JSON.stringify(scenario),
);
const scenarioInfo = new ScenarioInfo(
scenario.pickle.name!,
_.map(scenario.pickle.tags!, (tag: PickleTag) => tag.name!),
);
const scenarioContext = new ManagedScenarioContext(scenarioInfo);
this[SCENARIO_CONTEXT_SLOTNAME] = scenarioContext;
scenarioContext.addExternalObject(scenarioInfo);
scenarioContext.addExternalObject(new WorldParameters(this.parameters));
scenarioContext.addExternalObject(new CucumberLog(this.log.bind(this)));
scenarioContext.addExternalObject(
new CucumberAttachments(this.attach.bind(this)),
);
});
After(function (this: WritableWorld) {
const scenarioContext = this[
SCENARIO_CONTEXT_SLOTNAME
] as ManagedScenarioContext;
if (scenarioContext) {
scenarioContext.dispose();
}
});
try {
const stackFilter = require("@cucumber/cucumber/lib/filter_stack_trace");
const path = require("path");
const originalFileNameFilter = stackFilter.isFileNameInCucumber;
if (originalFileNameFilter !== undefined) {
const projectRootPath = path.join(__dirname, "..") + "/";
Object.defineProperty(stackFilter, "isFileNameInCucumber", {
value: (fileName: string) =>
originalFileNameFilter(fileName) ||
fileName.startsWith(projectRootPath) ||
fileName.includes("node_modules"),
configurable: true,
enumerable: true,
});
}
} catch {
// Ignore errors, proper stack filtering is not officially supported
// so we override on a best effor basis only
}
// Decorate the Cucumber step definition snippet builder so that it uses our syntax
// let currentSnippetBuilder = cucumberSys.SupportCode.StepDefinitionSnippetBuilder;
// cucumberSys.SupportCode.StepDefinitionSnippetBuilder = function (step, syntax) {
// return currentSnippetBuilder(step, {
// build: function (functionName: string, pattern, parameters, comment) {
// let callbackName = parameters[parameters.length - 1];
// return `@${functionName.toLowerCase()}(${pattern})\n` +
// `public ${functionName}XXX (${parameters.join(", ")}): void {\n` +
// ` // ${comment}\n` +
// ` ${callbackName}.pending();\n` +
// `}\n`;
// }
// });
// }
});
/**
* Binds a step definition to Cucumber.
*
* @param stepBinding The [[StepBinding]] that represents a 'given', 'when', or 'then' step definition.
*/
function bindStepDefinition(stepBinding: StepBinding): boolean {
const bindingFunc = function (this: WritableWorld): any {
const bindingRegistry = BindingRegistry.instance;
const scenarioContext = this[
SCENARIO_CONTEXT_SLOTNAME
] as ManagedScenarioContext;
const matchingStepBindings = bindingRegistry.getStepBindings(
stepBinding.stepPattern.toString(),
);
const contextTypes = bindingRegistry.getContextTypesForTarget(
matchingStepBindings[0].targetPrototype,
);
const bindingObject = scenarioContext.getOrActivateBindingClass(
matchingStepBindings[0].targetPrototype,
contextTypes,
);
return (
bindingObject[matchingStepBindings[0].targetPropertyKey] as () => void
).apply(bindingObject, arguments as any);
};
Object.defineProperty(bindingFunc, "length", {
value: stepBinding.argsLength,
});
logger.trace("Binding step:", stepBinding);
const bindingOptions: IDefineStepOptions & IDefineTestStepHookOptions = {
timeout: stepBinding.timeout,
wrapperOptions: stepBinding.wrapperOption,
tags: stepBinding.tag === DEFAULT_TAG ? undefined : stepBinding.tag,
};
if (stepBinding.bindingType & StepBindingFlags.given) {
Given(stepBinding.stepPattern, bindingOptions, bindingFunc);
} else if (stepBinding.bindingType & StepBindingFlags.when) {
When(stepBinding.stepPattern, bindingOptions, bindingFunc);
} else if (stepBinding.bindingType & StepBindingFlags.then) {
Then(stepBinding.stepPattern, bindingOptions, bindingFunc);
} else {
return false;
}
return true;
}
/**
* Binds a hook to Cucumber.
*
* @param cucumber The cucumber object.
* @param stepBinding The [[StepBinding]] that represents a 'before', or 'after', step definition.
*/
function bindHook(stepBinding: StepBinding): void {
const bindingFunc = function (this: any): any {
const scenarioContext = this[
SCENARIO_CONTEXT_SLOTNAME
] as ManagedScenarioContext;
const contextTypes = BindingRegistry.instance.getContextTypesForTarget(
stepBinding.targetPrototype,
);
const bindingObject = scenarioContext.getOrActivateBindingClass(
stepBinding.targetPrototype,
contextTypes,
);
return (bindingObject[stepBinding.targetPropertyKey] as () => void).apply(
bindingObject,
arguments as any,
);
};
const globalBindFunc = () => {
const targetPrototype = stepBinding.targetPrototype;
const targetPrototypeKey = stepBinding.targetPropertyKey;
return targetPrototype[targetPrototypeKey].apply(targetPrototype);
};
Object.defineProperty(bindingFunc, "length", {
value: stepBinding.argsLength,
});
const bindingOptions: IDefineTestStepHookOptions = {
timeout: stepBinding.timeout,
tags: stepBinding.tag === DEFAULT_TAG ? undefined : stepBinding.tag,
...(stepBinding.hookOptions ?? {}),
};
logger.trace("Binding hook:", stepBinding);
switch (stepBinding.bindingType) {
case StepBindingFlags.before:
Before(bindingOptions, bindingFunc);
break;
case StepBindingFlags.after:
After(bindingOptions, bindingFunc);
break;
case StepBindingFlags.beforeAll:
BeforeAll(globalBindFunc);
break;
case StepBindingFlags.beforeStep:
BeforeStep(bindingFunc);
break;
case StepBindingFlags.afterStep:
AfterStep(bindingFunc);
break;
case StepBindingFlags.afterAll:
AfterAll(globalBindFunc);
break;
}
}
================================================
FILE: cucumber-tsflow/src/binding-registry.ts
================================================
import logger from "./logger";
import { StepBinding } from "./step-binding";
import { ContextType, StepPattern } from "./types";
/**
* Describes the binding metadata that is associated with a binding class.
*/
interface TargetBinding {
/**
* A reference to the step bindings that are associated with the binding class.
*/
stepBindings: StepBinding[];
/**
* The context types that are to be injected into the binding class during execution.
*/
contextTypes: ContextType[];
}
/**
* Represents the default step pattern.
*/
export const DEFAULT_STEP_PATTERN: string = "/.*/";
/**
* Represents the default tag.
*/
export const DEFAULT_TAG: string = "*";
/**
* A metadata registry that captures information about bindings and their bound step bindings.
*/
export class BindingRegistry {
private _bindings = new Map();
private _targetBindings = new Map();
/**
* Gets the binding registry singleton.
*
* @returns A [[BindingRegistry]].
*/
public static get instance(): BindingRegistry {
const BINDING_REGISTRY_SLOTNAME: string =
"__CUCUMBER_TSFLOW_BINDINGREGISTRY";
const registry = (global as any)[BINDING_REGISTRY_SLOTNAME];
if (!registry) {
(global as any)[BINDING_REGISTRY_SLOTNAME] = new BindingRegistry();
}
return registry || (global as any)[BINDING_REGISTRY_SLOTNAME];
}
/**
* Updates the binding registry with information about the context types required by a
* binding class.
*
* @param targetPrototype The class representing the binding (constructor function).
* @param contextTypes An array of [[ContextType]] that define the types of objects that
* should be injected into the binding class during a scenario execution.
*/
public registerContextTypesForTarget(
targetPrototype: any,
contextTypes?: ContextType[],
): void {
if (!contextTypes) {
return;
}
let targetDecorations = this._targetBindings.get(targetPrototype);
if (!targetDecorations) {
targetDecorations = {
stepBindings: [],
contextTypes: [],
};
this._targetBindings.set(targetPrototype, targetDecorations);
}
targetDecorations.contextTypes = contextTypes;
}
/**
* Retrieves the context types that have been registered for a given binding class.
*
* @param targetPrototype The class representing the binding (constructor function).
*
* @returns An array of [[ContextType]] that have been registered for the specified
* binding class.
*/
public getContextTypesForTarget(targetPrototype: any): ContextType[] {
const targetBinding = this._targetBindings.get(targetPrototype);
if (!targetBinding) {
return [];
}
return targetBinding.contextTypes;
}
/**
* Updates the binding registry indexes with a step binding.
*
* @param stepBinding The step binding that is to be registered with the binding registry.
*/
public registerStepBinding(stepBinding: StepBinding): void {
if (!stepBinding.tag) {
stepBinding.tag = DEFAULT_TAG;
}
const stepPattern: StepPattern = stepBinding.stepPattern
? stepBinding.stepPattern.toString()
: DEFAULT_STEP_PATTERN;
let stepBindings = this._bindings.get(stepPattern);
if (!stepBindings) {
stepBindings = [];
this._bindings.set(stepPattern, stepBindings);
}
logger.trace("Attempting to register step binding", stepBinding);
if (!stepBindings.some((b) => isSameStepBinding(stepBinding, b))) {
logger.trace("Saving new step binding.");
stepBindings.push(stepBinding);
}
// Index the step binding for the target
let targetBinding = this._targetBindings.get(stepBinding.targetPrototype);
if (!targetBinding) {
targetBinding = {
stepBindings: [],
contextTypes: [],
};
this._targetBindings.set(stepBinding.targetPrototype, targetBinding);
}
if (
!targetBinding.stepBindings.some((b) => isSameStepBinding(stepBinding, b))
) {
logger.trace("Saving new step binding to target.");
targetBinding.stepBindings.push(stepBinding);
}
logger.trace(
"All target step bindings",
targetBinding.stepBindings.map(
(binding) => `${binding.stepPattern} ${binding.tag}`,
),
);
function isSameStepBinding(a: StepBinding, b: StepBinding) {
return (
a.callsite.filename === b.callsite.filename &&
a.callsite.lineNumber === b.callsite.lineNumber &&
String(a.stepPattern) === String(b.stepPattern) &&
a.targetPropertyKey === b.targetPropertyKey
);
}
}
/**
* Retrieves the step bindings that have been registered for a given binding class.
*
* @param targetPrototype The class representing the binding (constructor function).
*
* @returns An array of [[StepBinding]] objects that have been registered for the specified
* binding class.
*/
public getStepBindingsForTarget(targetPrototype: any): StepBinding[] {
const targetBinding = this._targetBindings.get(targetPrototype);
if (!targetBinding) {
return [];
}
return targetBinding.stepBindings;
}
/**
* Retrieves the step bindings for a given step pattern and collection of tag names.
*
* @param stepPattern The step pattern to search.
*
* @returns An array of [[StepBinding]] that map to the given step pattern and set of tag names.
*/
public getStepBindings(stepPattern: StepPattern): StepBinding[] {
return this._bindings.get(stepPattern) ?? [];
}
}
================================================
FILE: cucumber-tsflow/src/hook-decorators.ts
================================================
import {
IDefineTestCaseHookOptions,
IDefineTestRunHookOptions,
IDefineTestStepHookOptions,
} from "@cucumber/cucumber/lib/support_code_library_builder/types";
import { BindingRegistry } from "./binding-registry";
import { Callsite } from "./our-callsite";
import { StepBinding, StepBindingFlags } from "./step-binding";
import { normalizeTag } from "./tag-normalization";
// Replace `tags` with `tag` for backwards compatibility
type HookOptions = Omit & {
tag?: string;
};
function overloadedOption(tag?: string | HookOptions): HookOptions {
if (tag === undefined || typeof tag === "string") {
return { tag };
}
return tag;
}
function createHookDecorator(
flag: StepBindingFlags,
tagOrOption?: string | HookOptions,
): MethodDecorator {
const callsite = Callsite.capture(2);
const { tag, timeout, ...hookOptions } = overloadedOption(tagOrOption);
return (
target: any,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor,
) => {
const stepBinding: StepBinding = {
stepPattern: "",
bindingType: flag,
targetPrototype: target,
targetPropertyKey: propertyKey,
argsLength: target[propertyKey].length,
tag: normalizeTag(tag),
callsite: callsite,
timeout: timeout,
hookOptions: hookOptions,
};
BindingRegistry.instance.registerStepBinding(stepBinding);
return descriptor;
};
}
/**
* A method decorator that marks the associated function as a 'Before Scenario' step. The function is
* executed before each scenario.
*
* @param tagOrOption An optional tag or hook options object.
*/
export function before(tagOrOption?: string | HookOptions): MethodDecorator {
return createHookDecorator(StepBindingFlags.before, tagOrOption);
}
/**
* A method decorator that marks the associated function as an 'After Scenario' step. The function is
* executed after each scenario.
*
* @param tagOrOption An optional tag or hook options object.
*/
export function after(tagOrOption?: string | HookOptions): MethodDecorator {
return createHookDecorator(StepBindingFlags.after, tagOrOption);
}
/**
* A method decorator that marks the associated function as a 'Before Scenario' step. The function is
* executed before each scenario.
*
* @param options Optional hook options object.
*/
export function beforeAll(
options?: IDefineTestRunHookOptions,
): MethodDecorator {
return createHookDecorator(StepBindingFlags.beforeAll, options);
}
/**
* A method decorator that marks the associated function as an 'After Scenario' step. The function is
* executed after each scenario.
*
* @param options Optional hook options object.
*/
export function afterAll(options?: IDefineTestRunHookOptions): MethodDecorator {
return createHookDecorator(StepBindingFlags.afterAll, options);
}
/**
* A method decorator that marks the associated function as a 'Before Step' step. The function is
* executed before each step.
*
* @param options Optional hook options object.
*/
export function beforeStep(
options?: IDefineTestStepHookOptions,
): MethodDecorator {
return createHookDecorator(StepBindingFlags.beforeStep, options);
}
/**
* A method decorator that marks the associated function as an 'After Step' step. The function is
* executed after each step.
*
* @param options Optional hook options object.
*/
export function afterStep(
options?: IDefineTestStepHookOptions,
): MethodDecorator {
return createHookDecorator(StepBindingFlags.afterStep, options);
}
================================================
FILE: cucumber-tsflow/src/index.ts
================================================
export * from "./binding-decorator";
export * from "./hook-decorators";
export * from "./step-definition-decorators";
export { ScenarioContext, ScenarioInfo } from "./scenario-context";
export * from "./provided-context";
================================================
FILE: cucumber-tsflow/src/logger.ts
================================================
import * as log4js from "log4js";
const logger = log4js.getLogger("cucumber-js.tsflow");
export default logger;
================================================
FILE: cucumber-tsflow/src/managed-scenario-context.ts
================================================
import * as _ from "underscore";
import { BindingRegistry } from "./binding-registry";
import { ScenarioContext } from "./scenario-context";
import { ScenarioInfo } from "./scenario-info";
import { ContextType, isProvidedContextType } from "./types";
/**
* Represents a [[ScenarioContext]] implementation that manages a collection of context objects that
* are created and used by binding classes during a running Cucumber scenario.
*/
export class ManagedScenarioContext implements ScenarioContext {
private _activeObjects = new Map();
constructor(private readonly _scenarioInfo: ScenarioInfo) {}
/**
* Gets information about the scenario.
*/
public get scenarioInfo(): ScenarioInfo {
return this._scenarioInfo;
}
public getOrActivateBindingClass(
targetPrototype: any,
contextTypes: ContextType[],
): any {
return this.getOrActivateObject(targetPrototype, () => {
return this.activateBindingClass(targetPrototype, contextTypes);
});
}
public dispose(): void {
this._activeObjects.forEach((value: any) => {
if (typeof value.dispose === "function") {
value.dispose();
}
});
}
/**
* @internal
*/
public getContextInstance(contextType: ContextType) {
return this.getOrActivateObject(contextType.prototype, () => {
if (isProvidedContextType(contextType)) {
throw new Error(
`The requested type "${contextType.name}" should be provided by cucumber-tsflow, but was not registered. Please report a bug.`,
);
}
return new contextType();
});
}
/**
* @internal
*/
public addExternalObject(value: unknown) {
if (value == null) {
return;
}
const proto = value.constructor.prototype;
const existingObject = this._activeObjects.get(proto);
if (existingObject !== undefined) {
throw new Error(
`Conflicting objects of type "${proto.name}" registered.`,
);
}
this._activeObjects.set(proto, value);
}
private activateBindingClass(
targetPrototype: any,
contextTypes: ContextType[],
): any {
const invokeBindingConstructor = (args: any[]): any => {
return new (targetPrototype.constructor as any)(...args);
};
const contextObjects = _.map(contextTypes, (contextType) => {
return this.getOrActivateBindingClass(
contextType.prototype,
BindingRegistry.instance.getContextTypesForTarget(
contextType.prototype,
),
);
});
return invokeBindingConstructor(contextObjects);
}
private getOrActivateObject(
targetPrototype: any,
activatorFunc: () => any,
): any {
let activeObject = this._activeObjects.get(targetPrototype);
if (activeObject) {
return activeObject;
}
activeObject = activatorFunc();
this._activeObjects.set(targetPrototype, activeObject);
return activeObject;
}
}
export * from "./scenario-context";
================================================
FILE: cucumber-tsflow/src/our-callsite.ts
================================================
// @ts-ignore
import * as sourceMapSupport from "source-map-support";
/**
* Represents a callsite of where a step binding is being applied.
*/
export class Callsite {
/**
* Initializes a new [[Callsite]].
*
* @param filename The filename of the callsite.
* @param lineNumber The line number of the callsite.
*/
constructor(
public filename: string,
public lineNumber: number,
) {}
/**
* Captures the current [[Callsite]] object.
*/
public static capture(up = 1): Callsite {
const stack = callsites()[up + 1];
const tsStack = sourceMapSupport.wrapCallSite(stack);
return new Callsite(
tsStack.getFileName() || "",
tsStack.getLineNumber() || -1,
);
}
/**
* Returns a string representation of the callsite.
*
* @returns A string representing the callsite formatted with the filename and line
* number.
*/
public toString(): string {
return `${this.filename}:${this.lineNumber}`;
}
}
function callsites() {
const _prepareStackTrace = Error.prepareStackTrace;
try {
let result: NodeJS.CallSite[] = [];
Error.prepareStackTrace = (_, callSites) => {
const callSitesWithoutCurrent = callSites.slice(1);
result = callSitesWithoutCurrent;
return callSitesWithoutCurrent;
};
new Error().stack; // eslint-disable-line unicorn/error-message, no-unused-expressions
return result;
} finally {
Error.prepareStackTrace = _prepareStackTrace;
}
}
================================================
FILE: cucumber-tsflow/src/provided-context.ts
================================================
/* tslint:disable:max-classes-per-file */
import {
ICreateAttachment,
ICreateLog,
} from "@cucumber/cucumber/lib/runtime/attachment_manager";
import { Readable } from "stream";
export class WorldParameters {
public constructor(public readonly value: T) {}
}
export class CucumberLog {
public constructor(private readonly target: ICreateLog) {}
public log(text: string): void | Promise {
return this.target(text);
}
}
export class CucumberAttachments {
public constructor(private readonly target: ICreateAttachment) {}
public attach(data: string, mediaType?: string): void;
public attach(data: Buffer, mediaType: string): void;
public attach(data: Readable, mediaType: string): Promise;
public attach(data: Readable, mediaType: string, callback: () => void): void;
public attach(...args: any): void | Promise {
return this.target.apply(this, args);
}
}
================================================
FILE: cucumber-tsflow/src/scenario-context.ts
================================================
import { ScenarioInfo } from "./scenario-info";
/**
* Provides context for the currently running Cucumber scenario.
*/
export interface ScenarioContext {
/**
* Gets information about the scenario.
*
*/
scenarioInfo: ScenarioInfo;
/**
* Gets or sets an arbitary object within the running scenario.
*/
[key: string]: any;
}
export * from "./scenario-info";
================================================
FILE: cucumber-tsflow/src/scenario-info.ts
================================================
import logger from "./logger";
import { TagName } from "./types";
/**
* Provides information about a running Cucumber scenario.
*/
export class ScenarioInfo {
private _attributeTags?: Map;
private _optionTags?: Map;
private _flagTags?: Set;
/**
* Initializes the [[ScenarioInfo]] object.
*
* @param scenarioTitle The string title of the currently running Cucumber scenario.
* @param tags An array of [[TagName]] representing the tags that are in scope for the currently
* running Cucumber scenario.
*/
constructor(
public scenarioTitle: string,
public tags: TagName[],
) {}
private static parseAttributeTags(tags: TagName[]): Map {
const RGX = /^@?(?[\w-]+)\((?.+?)\)$/s;
const result = new Map();
for (const tag of tags) {
const match = tag.match(RGX)?.groups;
if (match !== undefined) {
const { attributeName, value } = match;
result.set(attributeName, JSON.parse(value));
}
}
logger.trace("Parsed attribute tags", { fromTags: tags, options: result });
return result;
}
private static parseOptionTags(tags: TagName[]): Map {
const RGX = /^@?(?