master 87da1c24d45a cached
54 files
155.9 KB
36.7k tokens
152 symbols
1 requests
Download .txt
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
================================================
<component name="ProjectRunConfigurationManager">
  <configuration default="false" name="All Tests" type="cucumber.js" factoryName="Cucumber.js">
    <option name="myFilePath" value="$PROJECT_DIR$/cucumber-tsflow-specs/features" />
    <option name="myNameFilter" value="" />
    <option name="cucumberJsArguments" value="" />
    <option name="workingDirectory" value="$PROJECT_DIR$" />
    <method v="2">
      <option name="NpmBeforeRunTask" enabled="true">
        <package-json value="$PROJECT_DIR$/package.json" />
        <command value="run" />
        <scripts>
          <script value="build" />
        </scripts>
        <node-interpreter value="project" />
        <envs />
      </option>
    </method>
  </configuration>
</component>

================================================
FILE: .run/Template Cucumber.js.run.xml
================================================
<component name="ProjectRunConfigurationManager">
  <configuration default="true" type="cucumber.js" factoryName="Cucumber.js">
    <option name="myFilePath" value="" />
    <option name="myNameFilter" value="" />
    <option name="cucumberJsArguments" value="" />
    <option name="workingDirectory" value="$PROJECT_DIR$" />
    <method v="2">
      <option name="NpmBeforeRunTask" enabled="true">
        <package-json value="$PROJECT_DIR$/package.json" />
        <command value="run" />
        <scripts>
          <script value="build" />
        </scripts>
        <node-interpreter value="project" />
        <envs />
      </option>
    </method>
  </configuration>
</component>

================================================
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

![CI](https://github.com/timjroberts/cucumber-js-tsflow/workflows/CI/badge.svg)

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 = <class>;` 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<void>`

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<void> {
    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<void> {
  ...
  }
}
```

### 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<void> {
    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 <tim@timjroberts.com>",
  "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<StepPattern, StepBindingFlags>();

// 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 <T>(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<string, any>)[
    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<T extends ContextType>(
  world: World,
  contextType: T,
): InstanceType<T> {
  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<StepPattern, StepBinding[]>();

  private _targetBindings = new Map<any, TargetBinding>();

  /**
   * 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<IDefineTestCaseHookOptions, "tags"> & {
  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 <T>(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<T>,
  ) => {
    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<any, any>();

  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<T = any> {
  public constructor(public readonly value: T) {}
}

export class CucumberLog {
  public constructor(private readonly target: ICreateLog) {}

  public log(text: string): void | Promise<void> {
    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<void>;
  public attach(data: Readable, mediaType: string, callback: () => void): void;
  public attach(...args: any): void | Promise<void> {
    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<string, unknown>;

  private _optionTags?: Map<string, string[]>;

  private _flagTags?: Set<string>;

  /**
   * 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<string, unknown> {
    const RGX = /^@?(?<attributeName>[\w-]+)\((?<value>.+?)\)$/s;

    const result = new Map<string, unknown>();

    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<string, string[]> {
    const RGX = /^@?(?<option>[\w-]+)\((?<value>.+?)\)$/s;

    const result = new Map<string, string[]>();

    for (const tag of tags) {
      const match = tag.match(RGX)?.groups;

      if (match !== undefined) {
        const { option, value } = match;

        const list = result.get(option);
        if (list === undefined) {
          result.set(option, [value]);
        } else {
          list.push(value);
        }
      }
    }

    logger.trace("Parsed options", { fromTags: tags, options: result });

    return result;
  }

  private static parseFlagTags(tags: TagName[]): Set<string> {
    const RGX = /^@?(?<flag>[\w-]+)$/s;

    const result = new Set<string>();

    for (const tag of tags) {
      const flag = tag.match(RGX)?.groups?.flag;

      if (flag !== undefined) {
        result.add(flag);
      }
    }

    logger.trace("Parsed flags", { fromTags: tags, flags: result });

    return result;
  }

  public getAttributeTag(name: string): unknown | undefined {
    if (this._attributeTags === undefined) {
      this._attributeTags = ScenarioInfo.parseAttributeTags(this.tags);
    }

    return this._attributeTags.get(name);
  }

  public getOptionTag(name: string): string | undefined {
    if (this._optionTags === undefined) {
      this._optionTags = ScenarioInfo.parseOptionTags(this.tags);
    }

    return this._optionTags.get(name)?.at(-1);
  }

  public getMultiOptionTag(name: string): string[] | undefined {
    if (this._optionTags === undefined) {
      this._optionTags = ScenarioInfo.parseOptionTags(this.tags);
    }

    return this._optionTags.get(name) ?? [];
  }

  public getFlag(name: string): boolean {
    if (this._flagTags === undefined) {
      this._flagTags = ScenarioInfo.parseFlagTags(this.tags);
    }

    return this._flagTags.has(name);
  }
}


================================================
FILE: cucumber-tsflow/src/step-binding-flags.ts
================================================
// tslint:disable:no-bitwise
/**
 * The CucumberJS step binding types.
 */
export enum StepBindingFlags {
  /**
   * No bindings.
   */
  none = 0,

  /**
   * A 'Given' step definition binding.
   */
  given = 1 << 0,

  /**
   * A 'When' step definition binding.
   */
  when = 1 << 1,

  /**
   * A 'Then' step definition binding.
   */
  then = 1 << 2,

  /**
   * A 'Before' hook binding.
   */
  before = 1 << 3,

  /**
   * An 'After' hook binding.
   */
  after = 1 << 4,

  /**
   * A 'Before All' hook binding.
   */
  beforeAll = 1 << 5,

  /**
   * An 'After All' hook binding.
   */
  afterAll = 1 << 6,

  /**
   * A 'Before Step' hook binding.
   */
  beforeStep = 1 << 7,

  /**
   * An 'After Step' hook binding.
   */
  afterStep = 1 << 8,

  /**
   * All step definition bindings.
   */
  StepDefinitions = StepBindingFlags.given |
    StepBindingFlags.when |
    StepBindingFlags.then,

  /**
   * All hook bindings.
   */
  Hooks = StepBindingFlags.before |
    StepBindingFlags.after |
    StepBindingFlags.beforeAll |
    StepBindingFlags.afterAll |
    StepBindingFlags.beforeStep |
    StepBindingFlags.afterStep,
}


================================================
FILE: cucumber-tsflow/src/step-binding.ts
================================================
import type { IDefineTestStepHookOptions } from "@cucumber/cucumber/lib/support_code_library_builder/types";
import { Callsite } from "./our-callsite";
import { StepBindingFlags } from "./step-binding-flags";

/**
 * Encapsulates data about a step binding.
 */
export interface StepBinding {
  /**
   * The step pattern.
   */
  stepPattern: RegExp | string;

  /**
   * The step binding type.
   */
  bindingType: StepBindingFlags;

  /**
   * The type that is associated with the current step binding.
   */
  targetPrototype: any;

  /**
   * The function name that is associated with the current step binding.
   */
  targetPropertyKey: string | symbol;

  /**
   * The count of arguments that have been specified on the [[StepBindingDescriptor.targetPropertyKey]].
   */
  argsLength: number;

  /**
   * The optional tag that is associated with the current step binding.
   */
  tag?: string;

  /**
   * The optiomal timeout that is associated with the current step binding.
   */
  timeout?: number;

  /**
   * The wrapper Option passing to cucumber
   */
  wrapperOption?: any;

  hookOptions?: Omit<IDefineTestStepHookOptions, "tags" | "timeout">;

  /**
   * The callsite of the step binding.
   */
  callsite: Callsite;
}

export * from "./step-binding-flags";


================================================
FILE: cucumber-tsflow/src/step-definition-decorators.ts
================================================
import { BindingRegistry } from "./binding-registry";
import logger from "./logger";
import { Callsite } from "./our-callsite";
import { StepBinding, StepBindingFlags } from "./step-binding";
import { normalizeTag } from "./tag-normalization";

interface StepOptions {
  tag?: string;

  timeout?: number;

  wrapperOption?: any;
}

function overloadedOptions(
  tag?: string | StepOptions,
  timeout?: number,
): StepOptions {
  if (tag === undefined || typeof tag === "string") {
    return { tag, timeout };
  }

  if (timeout !== undefined) {
    throw new Error(
      "Cannot specify a separate timeout argument when an options object is given.",
    );
  }

  return tag;
}

/**
 * A method decorator that marks the associated function as a 'Given' step.
 *
 * @param stepPattern The regular expression that will be used to match steps.
 * @param tag An optional tag or an options object.
 * @param timeout An optional timeout.
 */
export function given(
  stepPattern: RegExp | string,
  tagOrOption?: string | StepOptions,
  timeout?: number,
): MethodDecorator {
  const callsite = Callsite.capture();

  const options = overloadedOptions(tagOrOption, timeout);

  return <T>(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<T>,
  ) => {
    const stepBinding: StepBinding = {
      stepPattern: stepPattern,
      bindingType: StepBindingFlags.given,
      targetPrototype: target,
      targetPropertyKey: propertyKey,
      argsLength: target[propertyKey].length,
      callsite: callsite,
      tag: normalizeTag(options.tag),
      timeout: options.timeout,
      wrapperOption: options.wrapperOption,
    };

    logger.trace("Registering step definition:", stepBinding);

    BindingRegistry.instance.registerStepBinding(stepBinding);

    return descriptor;
  };
}

/**
 * A method decorator that marks the associated function as a 'When' step.
 *
 * @param stepPattern The regular expression that will be used to match steps.
 * @param tag An optional tag.
 * @param timeout An optional timeout.
 */
export function when(
  stepPattern: RegExp | string,
  tagOrOption?: string | StepOptions,
  timeout?: number,
): MethodDecorator {
  const callsite = Callsite.capture();

  const options = overloadedOptions(tagOrOption, timeout);

  return <T>(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<T>,
  ) => {
    const stepBinding: StepBinding = {
      stepPattern: stepPattern,
      bindingType: StepBindingFlags.when,
      targetPrototype: target,
      targetPropertyKey: propertyKey,
      argsLength: target[propertyKey].length,
      callsite: callsite,
      tag: normalizeTag(options.tag),
      timeout: options.timeout,
      wrapperOption: options.wrapperOption,
    };

    BindingRegistry.instance.registerStepBinding(stepBinding);

    return descriptor;
  };
}

/**
 * A method decorator that marks the associated function as a 'Then' step.
 *
 * @param stepPattern The regular expression that will be used to match steps.
 * @param tag An optional tag.
 * @param timeout An optional timeout.
 */
export function then(
  stepPattern: RegExp | string,
  tagOrOption?: string | StepOptions,
  timeout?: number,
): MethodDecorator {
  const callsite = Callsite.capture();

  const options = overloadedOptions(tagOrOption, timeout);

  return <T>(
    target: any,
    propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<T>,
  ) => {
    const stepBinding: StepBinding = {
      stepPattern: stepPattern,
      bindingType: StepBindingFlags.then,
      targetPrototype: target,
      targetPropertyKey: propertyKey,
      argsLength: target[propertyKey].length,
      callsite: callsite,
      tag: normalizeTag(options.tag),
      timeout: options.timeout,
      wrapperOption: options.wrapperOption,
    };

    BindingRegistry.instance.registerStepBinding(stepBinding);

    return descriptor;
  };
}


================================================
FILE: cucumber-tsflow/src/tag-normalization.ts
================================================
export function normalizeTag(tag?: string): string | undefined {
  // Tag is not provided or already includes a @
  if (tag === undefined || tag.includes("@")) {
    return tag;
  }

  // If a tag doesn't include any @, for compatibility, prefix it with a @
  return `@${tag}`;
}


================================================
FILE: cucumber-tsflow/src/types.ts
================================================
import {
  CucumberAttachments,
  CucumberLog,
  WorldParameters,
} from "./provided-context";
import { ScenarioInfo } from "./scenario-info";

/**
 * A string representation of a [[RegExp]] that defines a Cucumber step pattern.
 */
export type StepPattern = string;

/**
 * A Cucumber tag name.
 */
export type TagName = string;

/**
 * Represents a class that will be injected into a binding class to provide context
 * during the execution of a Cucumber scenario.
 */
export type CustomContextType = new (...args: any[]) => any;

export type ProvidedContextType =
  | typeof ScenarioInfo
  | typeof WorldParameters
  | typeof CucumberLog
  | typeof CucumberAttachments;

export type ContextType = ProvidedContextType | CustomContextType;

const providedPrototypes: ProvidedContextType[] = [
  WorldParameters,
  CucumberLog,
  CucumberAttachments,
  ScenarioInfo,
];

export function isProvidedContextType(
  typ: ContextType,
): typ is ProvidedContextType {
  return providedPrototypes.some((proto) => Object.is(typ, proto));
}

export type TypeDecorator = <T>(target: new (...args: any[]) => T) => void;


================================================
FILE: cucumber-tsflow/tsconfig.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "module": "umd",
    "moduleResolution": "node",
    "target": "es2022",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "removeComments": false,
    "experimentalDecorators": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["./src/**/*.ts"]
}


================================================
FILE: cucumber-tsflow-specs/features/basic-test.feature
================================================
Feature: Binding steps

    Scenario Outline: Bind steps with <Bind Mode>
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given step one
                When step two
                Then step three
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, when, then} from 'cucumber-tsflow';

            @binding()
            class Steps {
                @given(<Step 1>)
                public given() {
                    console.log("Step one executed");
                }

                @when(<Step 2>)
                public when() {
                    console.log("Step two executed");
                }

                @then(<Step 3>)
                public then() {
                    console.log("Step three executed");
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains "Step one executed"
        And the output contains "Step two executed"
        And the output contains "Step three executed"

        Examples:
            | Bind Mode | Step 1       | Step 2       | Step 3         |
            | names     | "step one"   | "step two"   | "step three"   |
            | regex     | /^step one$/ | /^step two$/ | /^step three$/ |

    Scenario: Failing test
        Given a file named "features/a.feature" with:
            """feature
            Feature: Some feature
              Scenario: example
                Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given} from 'cucumber-tsflow';

            @binding()
            class Step {
                @given("a step")
                public step() {
                    throw new Error("Inner error message.");
                }
            }

            export = Step;
            """
        When I run cucumber-js
        Then it fails
        And the output contains "Error: Inner error message."

    Scenario: Missing step definition
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given missing step
            """
        When I run cucumber-js
        Then it fails
        # TODO: https://github.com/timjroberts/cucumber-js-tsflow/issues/97
        And the output contains "Implement with the following snippet:"


================================================
FILE: cucumber-tsflow-specs/features/cucumber-context-objects.feature
================================================
Feature: Cucumber context objects

    Scenario: Using the cucumber logger
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              Scenario: example
                Given a step
                And another step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, CucumberLog} from 'cucumber-tsflow';

            @binding([CucumberLog])
            class Steps {
                public constructor(private readonly logger: CucumberLog) {}

                @given("a step")
                public one() {
                    this.logger.log("logged value");
                }

                @given("another step")
                public noop() {}
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And scenario "example" step "Given a step" has the logs:
            | logged value |
        And scenario "example" step "And another step" has no attachments

    Scenario: Using the cucumber attachments
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              Scenario: example
                Given a step
                And another step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, CucumberAttachments} from 'cucumber-tsflow';

            @binding([CucumberAttachments])
            class Steps {
                public constructor(private readonly att: CucumberAttachments) {}

                @given("a step")
                public one() {
                    this.att.attach("my string", "text/plain+custom");
                }

                @given("another step")
                public noop() {}
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And scenario "example" step "Given a step" has the attachments:
            | DATA      | MEDIA TYPE        | MEDIA ENCODING |
            | my string | text/plain+custom | IDENTITY       |
        And scenario "example" step "And another step" has no attachments

    Scenario: Using the cucumber attachments in hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              Scenario: example
                Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, before, after, given, CucumberAttachments} from 'cucumber-tsflow';

            @binding([CucumberAttachments])
            class Steps {
                public constructor(private readonly att: CucumberAttachments) {}

                @before()
                public before() {
                    this.att.attach("my first string", "text/plain+custom");
                }

                @given("a step")
                public one() {}

                @after()
                public after() {
                    this.att.attach("my second string", "text/plain+custom");
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And scenario "example" "Before" hook has the attachments:
            | DATA            | MEDIA TYPE        | MEDIA ENCODING |
            | my first string | text/plain+custom | IDENTITY       |
        And scenario "example" "After" hook has the attachments:
            | DATA             | MEDIA TYPE        | MEDIA ENCODING |
            | my second string | text/plain+custom | IDENTITY       |
        And scenario "example" step "Given a step" has no attachments

    Scenario: Using world parameters
        Given a file named "cucumber.js" with:
            """js
            const cucumberPkg = require("@cucumber/cucumber/package.json");

            module.exports = cucumberPkg.version.startsWith("7.")
              ? {
                default: [
                  "--world-parameters '{\"name\":\"Earth\"}'"
                ].join(" ")
              }
              : {
                default: {
                  worldParameters: {
                    name: 'Earth'
                  }
                }
              };
            """
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              Scenario: example
                Then the world name is "Earth"
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, then, WorldParameters} from 'cucumber-tsflow';
            import * as assert from 'node:assert';

            @binding([WorldParameters])
            class Steps {
                public constructor(private readonly world: WorldParameters) {}

                @then("the world name is {string}")
                public checkWorldName(name: string) {
                    assert.deepStrictEqual(this.world.value, {name})
                }
            }

            export = Steps;
            """

        When I run cucumber-js
        Then it passes

    Scenario: Reading the scenario information
        Given a file named "features/a.feature" with:
            """feature
            @foo
            Feature: Feature
              @bar
              Scenario: example
                Then the scenario title is "example"
                And  the tags are [ "@foo", "@bar" ]
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, then, ScenarioInfo} from 'cucumber-tsflow';
            import * as assert from 'node:assert';

            @binding([ScenarioInfo])
            class Steps {
                public constructor(private readonly scenario: ScenarioInfo) {}

                @then("the scenario title is {string}")
                public checkScenarioName(name: string) {
                    assert.strictEqual(this.scenario.scenarioTitle, name);
                }

                @then("the tags are {}")
                public checkTags(tags: string) {
                    assert.deepStrictEqual(this.scenario.tags, JSON.parse(tags));
                }
            }

            export = Steps;
            """

        When I run cucumber-js
        Then it passes


================================================
FILE: cucumber-tsflow-specs/features/custom-context-objects.feature
================================================
Feature: Custom context objects

    Scenario: Using custom context objects to share state
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given the state is "initial value"
                When I set the state to "step value"
                Then the state is "step value"
            """
        And a file named "support/state.ts" with:
            """ts
            export class State {
                public value: string = "initial value";
            }
            """
        And a file named "step_definitions/one.ts" with:
            """ts
            import {State} from '../support/state';
            import {binding, when} from 'cucumber-tsflow';

            @binding([State])
            class Steps {
                public constructor(private readonly state: State) {}

                @when("I set the state to {string}")
                public setState(newValue: string) {
                    this.state.value = newValue;
                }
            }

            export = Steps;
            """
        And a file named "step_definitions/two.ts" with:
            """ts
            import {State} from '../support/state';
            import {binding, then} from 'cucumber-tsflow';
            import * as assert from 'node:assert';

            @binding([State])
            class Steps {
                public constructor(private readonly state: State) {}

                @then("the state is {string}")
                public checkValue(value: string) {
                    console.log(`The state is '${this.state.value}'`);
                    assert.equal(this.state.value, value, "State value does not match");
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains "The state is 'initial value'"
        And the output contains "The state is 'step value'"

    Scenario: Custom context objects can depend on other custom context objects two levels deep
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given the state is "initial value"
                When I set the state to "step value"
                Then the state is "step value"
            """
        And a file named "support/level-one-state.ts" with:
            """ts
            import {binding} from 'cucumber-tsflow';
            import {LevelTwoState} from './level-two-state';

            @binding([LevelTwoState])
            export class LevelOneState {
                constructor(public levelTwoState: LevelTwoState) {
                }
            }
            """
        And a file named "support/level-two-state.ts" with:
            """ts
            export class LevelTwoState {
                public value: string = "initial value";
            }
            """
        And a file named "step_definitions/one.ts" with:
            """ts
            import {LevelTwoState} from '../support/level-two-state';
            import {binding, when} from 'cucumber-tsflow';

            @binding([LevelTwoState])
            class Steps {
                public constructor(private readonly levelTwoState: LevelTwoState) {
                }

                @when("I set the state to {string}")
                public setState(newValue: string) {
                    this.levelTwoState.value = newValue;
                }
            }

            export = Steps;
            """
        And a file named "step_definitions/two.ts" with:
            """ts
            import {LevelOneState} from '../support/level-one-state';
            import {binding, then} from 'cucumber-tsflow';
            import * as assert from 'node:assert';

            @binding([LevelOneState])
            class Steps {
                public constructor(private readonly levelOneState: LevelOneState) {}

                @then("the state is {string}")
                public checkValue(value: string) {
                    console.log(`The state is '${this.levelOneState.levelTwoState.value}'`);
                    assert.equal(this.levelOneState.levelTwoState.value, value, "State value does not match");
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains "The state is 'initial value'"
        And the output contains "The state is 'step value'"

    Scenario: Custom context objects can depend on other custom context objects three levels deep
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given the state is "initial value"
                When I set the state to "step value"
                Then the state is "step value"
            """
        And a file named "support/level-one-state.ts" with:
            """ts
            import {binding} from 'cucumber-tsflow';
            import {LevelTwoState} from './level-two-state';

            @binding([LevelTwoState])
            export class LevelOneState {
                constructor(public levelTwoState: LevelTwoState) {
                }
            }
            """
        And a file named "support/level-two-state.ts" with:
            """ts
            import {binding} from 'cucumber-tsflow';
            import {LevelThreeState} from './level-three-state';

            @binding([LevelThreeState])
            export class LevelTwoState {
                constructor(public levelThreeState: LevelThreeState) {
                }
            }
            """
        And a file named "support/level-three-state.ts" with:
            """ts
            export class LevelThreeState {
                public value: string = "initial value";
            }
            """
        And a file named "step_definitions/one.ts" with:
            """ts
            import {LevelThreeState} from '../support/level-three-state';
            import {binding, when} from 'cucumber-tsflow';

            @binding([LevelThreeState])
            class Steps {
                public constructor(private readonly levelThreeState: LevelThreeState) {
                }

                @when("I set the state to {string}")
                public setState(newValue: string) {
                    this.levelThreeState.value = newValue;
                }
            }

            export = Steps;
            """
        And a file named "step_definitions/two.ts" with:
            """ts
            import {LevelOneState} from '../support/level-one-state';
            import {binding, then} from 'cucumber-tsflow';
            import * as assert from 'node:assert';

            @binding([LevelOneState])
            class Steps {
                public constructor(private readonly levelOneState: LevelOneState) {}

                @then("the state is {string}")
                public checkValue(value: string) {
                    console.log(`The state is '${this.levelOneState.levelTwoState.levelThreeState.value}'`);
                    assert.equal(this.levelOneState.levelTwoState.levelThreeState.value, value, "State value does not match");
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains "The state is 'initial value'"
        And the output contains "The state is 'step value'"

    Scenario: Cyclic imports are detected and communicated to the developer
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given the state is "initial value"
                When I set the state to "step value"
                Then the state is "step value"
            """
        And a file named "support/state-one.ts" with:
            """ts
            import {binding} from 'cucumber-tsflow';
            import {StateTwo} from './state-two';

            @binding([StateTwo])
            export class StateOne {
                constructor(public stateTwo: StateTwo) {
                }
            }
            """
        And a file named "support/state-two.ts" with:
            """ts
            import {StateOne} from './state-one';
            import {binding} from 'cucumber-tsflow';

            @binding([StateOne])
            export class StateTwo {
                public value: string = "initial value";
                constructor(public stateOne: StateOne) {
                }
            }
            """
        And a file named "step_definitions/one.ts" with:
            """ts
            import {StateTwo} from '../support/state-two';
            import {binding, when} from 'cucumber-tsflow';

            @binding([StateTwo])
            class Steps {
                public constructor(private readonly stateTwo: StateTwo) {
                }

                @when("I set the state to {string}")
                public setState(newValue: string) {
                    this.stateTwo.value = newValue;
                }
            }

            export = Steps;
            """
        And a file named "step_definitions/two.ts" with:
            """ts
            import {StateOne} from '../support/state-one';
            import {binding, then} from 'cucumber-tsflow';
            import * as assert from 'node:assert';

            @binding([StateOne])
            class Steps {
                public constructor(private readonly stateOne: StateOne) {}

                @then("the state is {string}")
                public checkValue(value: string) {
                    console.log(`The state is '${this.stateOne.stateTwo.value}'`);
                    assert.equal(this.stateOne.stateTwo.value, value, "State value does not match");
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it fails
        And the error output contains text:
            """
            Error: Undefined dependency detected in StateOne. You possibly have an import cycle.
            See https://nodejs.org/api/modules.html#modules_cycles
            """

    Scenario: Cyclic state dependencies are detected and communicated to the developer
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given the state is "initial value"
                When I set the state to "step value"
                Then the state is "step value"
            """
        And a file named "support/state.ts" with:
            """ts
            import {binding} from 'cucumber-tsflow';

            export class StateOne {
                constructor(public stateTwo: StateTwo) { }
            }

            @binding([StateOne])
            export class StateTwo {
                public value: string = "initial value";
                constructor(public stateOne: StateOne) { }
            }

            exports.StateOne = binding([StateTwo])(StateOne);
            """
        And a file named "step_definitions/one.ts" with:
            """ts
            import {StateTwo} from '../support/state';
            import {binding, when} from 'cucumber-tsflow';

            @binding([StateTwo])
            class StepsOne {
                public constructor(private readonly stateTwo: StateTwo) {
                }

                @when("I set the state to {string}")
                public setState(newValue: string) {
                    this.stateTwo.value = newValue;
                }
            }

            export = StepsOne;
            """
        And a file named "step_definitions/two.ts" with:
            """ts
            import {StateOne} from '../support/state';
            import {binding, then} from 'cucumber-tsflow';
            import * as assert from 'node:assert';

            @binding([StateOne])
            class StepsTwo {
                public constructor(private readonly stateOne: StateOne) {}

                @then("the state is {string}")
                public checkValue(value: string) {
                    console.log(`The state is '${this.stateOne.stateTwo.value}'`);
                    assert.equal(this.stateOne.stateTwo.value, value, "State value does not match");
                }
            }

            export = StepsTwo;
            """
        When I run cucumber-js
        Then it fails
        And the error output contains text:
            """
            Error: Cyclic dependency detected: StateOne -> StateTwo -> StateOne
            """

    Scenario: Cyclic single-file state dependencies are detected and communicated to the developer
        Given a file named "features/a.feature" with:
        """feature
        Feature: some feature
          Scenario: scenario a
            Given the state is "initial value"
            When I set the state to "step value"
            Then the state is "step value"
        """
        And a file named "support/circular.ts" with:
        """ts
        import {binding} from 'cucumber-tsflow';

        export class StateOne {
            constructor(public stateTwo: StateTwo) { }
        }

        @binding([StateOne])
        export class StateTwo {
            public value: string = "initial value";
            constructor(public stateOne: StateOne) { }
        }

        exports.StateOne = binding([StateTwo])(StateOne);
        """
        And a file named "step_definitions/one.ts" with:
        """ts
        import {StateTwo} from '../support/circular';
        import * as assert from 'node:assert';
        import {binding, when, then} from 'cucumber-tsflow';

        @binding([StateTwo])
        class Steps {
            public constructor(private readonly stateTwo: StateTwo) {
            }

            @when("I set the state to {string}")
            public setState(newValue: string) {
                this.stateTwo.value = newValue;
            }

            @then("the state is {string}")
            public checkValue(value: string) {
                console.log(`The state is '${this.stateTwo.value}'`);
                assert.equal(this.stateTwo.value, value, "State value does not match");
            }
        }

        export = Steps;
        """
        When I run cucumber-js
        Then it fails
        And the error output contains text:
            """
            Error: Cyclic dependency detected: StateOne -> StateTwo -> StateOne
            """


================================================
FILE: cucumber-tsflow-specs/features/external-context-extraction.feature
================================================
Feature: Extracing context objects from World externally

    Scenario: Failing to retrieve state from a non-initialized World object
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given a step
            """
        And a file named "support/state.ts" with:
            """ts
            export class State {
                public constructor() {
                  console.log('State has been initialized');
                }

                public value: string = "initial value";
            }
            """
        And a file named "step_definitions/a.ts" with:
            """ts
            import {State} from '../support/state';
            import {Before, Given} from '@cucumber/cucumber';
            import {getBindingFromWorld} from 'cucumber-tsflow';

            Before(function() {
              const state = getBindingFromWorld(this, State);

              console.log(`Cucumber-style before. State is "${state.value}"`);

              state.value = 'cucumber-style before';
            });

            Given('a step', function() {
              const state = getBindingFromWorld(this, State);

              console.log(`Cucumber-style step. State is "${state.value}"`);

              state.value = 'cucumber-style step';
            });
            """
        When I run cucumber-js
        Then it fails
        And the output contains text:
            """
            Before # step_definitions/a.ts:5
                   Error: Scenario context have not been initialized in the provided World object.
            """

    Scenario: Sharing a state between native Cucumber and Decorator-style steps
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario a
                Given a cucumber-style step is called
                And a decorator-style step is called
            """
        And a file named "support/state.ts" with:
            """ts
            export class State {
                public constructor() {
                  console.log('State has been initialized');
                }

                public value: string = "initial value";
            }
            """
        And a file named "step_definitions/a.ts" with:
            """ts
            import {State} from '../support/state';
            import {Before, Given} from '@cucumber/cucumber';
            import {ensureWorldIsInitialized,getBindingFromWorld} from 'cucumber-tsflow';

            ensureWorldIsInitialized();

            Before(function() {
              const state = getBindingFromWorld(this, State);

              console.log(`Cucumber-style before. State is "${state.value}"`);

              state.value = 'cucumber-style before';
            });

            Given('a cucumber-style step is called', function() {
              const state = getBindingFromWorld(this, State);

              console.log(`Cucumber-style step. State is "${state.value}"`);

              state.value = 'cucumber-style step';
            });
            """
        And a file named "step_definitions/b.ts" with:
            """ts
            import {State} from '../support/state';
            import {binding, before, given} from 'cucumber-tsflow';

            @binding([State])
            class Steps {
                public constructor(private readonly state: State) {}

                @before()
                public before() {
                    console.log(`Decorator-style before. State is "${this.state.value}"`);

                    this.state.value = 'decorator-style before';
                }

                @given('a decorator-style step is called')
                public step() {
                    console.log(`Decorator-style step. State is "${this.state.value}"`);

                    this.state.value = 'decorator-style step';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text:
            """
            .State has been initialized
            Cucumber-style before. State is "initial value"
            .Decorator-style before. State is "cucumber-style before"
            .Cucumber-style step. State is "decorator-style before"
            .Decorator-style step. State is "cucumber-style step"
            """

    Scenario: Share state between underlying cucumber functionality and TSFlow functionality
        Given a file named "features/a.feature" with:
            """feature
            Feature: some feature
              Scenario: scenario before year 2k
                Given a step for 1999-03-04T21:43:54Z
              @y2k
              Scenario: scenario after year 2k
                Given a step for 2023-09-13T12:34:56.789Z
            """
        And a file named "support/state.ts" with:
            """ts
            export class State {
                public maxDate = new Date('2000-01-01');
            }
            """
        And a file named "step_definitions/a.ts" with:
            """ts
            import {State} from '../support/state';
            import {defineParameterType} from '@cucumber/cucumber';
            import {ensureWorldIsInitialized,getBindingFromWorld} from 'cucumber-tsflow';

            ensureWorldIsInitialized();

            defineParameterType({
              name: 'datetime',
              regexp: /[+-]?\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z/,
              preferForRegexpMatch: true,
              useForSnippets: true,
              transformer: function (datetime): Date {
                const state = getBindingFromWorld(this, State);

                const date = new Date(datetime);

                console.log(`Parsing date up to ${state.maxDate.toISOString()}`);

                if (state.maxDate.valueOf() < date.valueOf()) {
                    throw new Error('Date after maximum date.');
                }

                return new Date(datetime)
              }
            });
            """
        And a file named "step_definitions/b.ts" with:
            """ts
            import {State} from '../support/state';
            import {binding, before, given} from 'cucumber-tsflow';

            @binding([State])
            class Steps {
                public constructor(private readonly state: State) {}

                @before({tag: '@y2k'})
                public before() {
                    this.state.maxDate = new Date('3000-01-01');
                }

                @given('a step for {datetime}')
                public step(datetime: Date) {
                    console.log(`Step received a ${datetime.constructor.name}: ${datetime.toISOString()}`);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text:
            """
            .Parsing date up to 2000-01-01T00:00:00.000Z
            Step received a Date: 1999-03-04T21:43:54.000Z
            ....Parsing date up to 3000-01-01T00:00:00.000Z
            Step received a Date: 2023-09-13T12:34:56.789Z
            """


================================================
FILE: cucumber-tsflow-specs/features/global-hooks.feature
================================================
Feature: Support for Cucumber hooks

    Scenario: Binding a beforeAll hook
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example one
                    Given a step

                Scenario: example two
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeAll} from 'cucumber-tsflow';

            @binding()
            class Steps {
                @beforeAll()
                public static hook() {
                    console.log('hook exec')
                }

                @given("a step")
                public given() {
                    console.log('step exec');
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text once:
            """
            hook exec
            .step exec
            ...step exec
            ..
            """

    Scenario: Binding a afterAll hook
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example one
                    Given a step

                Scenario: example two
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, afterAll} from 'cucumber-tsflow';

            @binding()
            class Steps {
                @afterAll()
                public static hook() {
                    console.log('hook exec')
                }

                @given("a step")
                public given() {
                    console.log('step exec');
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text once:
            """
            .step exec
            ...step exec
            ..hook exec
            """

    Scenario: Binding beforeAll and afterAll hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example one
                    Given a step

                Scenario: example two
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeAll, afterAll} from 'cucumber-tsflow';

            @binding()
            class Steps {
                @beforeAll()
                public static before() {
                    console.log('before exec')
                }

                @afterAll()
                public static after() {
                    console.log('after exec')
                }

                @given("a step")
                public given() {
                    console.log('step exec');
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text once:
            """
            before exec
            .step exec
            ...step exec
            ..after exec
            """

    Scenario: Binding multiple beforeAll hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example one
                    Given a step

                Scenario: example two
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeAll} from 'cucumber-tsflow';

            @binding()
            class Steps {
                @beforeAll()
                public static one() {
                    console.log('one')
                }

                @beforeAll()
                public static two() {
                    console.log('two')
                }

                @given("a step")
                public given() {
                    console.log('step');
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text once:
            """
            one
            two
            .step
            ...step
            ..
            """

    Scenario: Binding multiple global hooks on the same line
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example one
                    Given a step

                Scenario: example two
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeAll} from 'cucumber-tsflow';

            @binding()
            class Steps {
                @beforeAll()public static one() { console.log('one') }@beforeAll()public static two() { console.log('two') }

                @given("a step")
                public given() {
                    console.log('step');
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text once:
            """
            one
            two
            .step
            ...step
            ..
            """

    Scenario: Binding multiple afterAll hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example one
                    Given a step

                Scenario: example two
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, afterAll} from 'cucumber-tsflow';

            @binding()
            class Steps {
                @afterAll()
                public static one() {
                    console.log('one')
                }

                @afterAll()
                public static two() {
                    console.log('two')
                }

                @given("a step")
                public given() {
                    console.log('step');
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output contains text once:
            """
            .step
            ...step
            ..two
            one
            """


================================================
FILE: cucumber-tsflow-specs/features/hooks.feature
================================================
Feature: Support for Cucumber hooks

    Scenario: Binding a before hook
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, before} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'hook has not executed';

                @before()
                public hook() {
                    this.state = 'hook has executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "hook has not executed"
        And the output contains "hook has executed"

    Scenario: Binding an after hook
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, after} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'step has not executed';

                @after()
                public hook() {
                    console.log(this.state);
                }

                @given("a step")
                public given() {
                    this.state = 'step has executed';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains "step has executed"

    Scenario: Binding before and after hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, before, after} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'hook has not executed';

                @before()
                public before() {
                    this.state = 'before hook executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                    this.state = 'step has executed';
                }

                @after()
                public after() {
                    console.log(this.state);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
            """
            .before hook executed
            .step has executed
            """

    Scenario: Binding multiple before hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, before} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'no hook executed';

                @before()
                public hookOne() {
                    console.log(this.state)
                    this.state = 'hook one has executed';
                }

                @before()
                public hookTwo() {
                    console.log(this.state)
                    this.state = 'hook two has executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                    this.state = 'step has executed';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
            """
            .no hook executed
            .hook one has executed
            .hook two has executed
            """


    Scenario: Binding multiple hooks same line
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, before} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'no hook executed';

                @before()public hookOne() {console.log(this.state);this.state = 'hook one has executed';}@before()public hookTwo() {console.log(this.state);this.state = 'hook two has executed';}

                @given("a step")
                public given() {
                    console.log(this.state);
                    this.state = 'step has executed';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
            """
            .no hook executed
            .hook one has executed
            .hook two has executed
            """

    Scenario: Binding multiple after hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, after} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'no hook executed';

                @given("a step")
                public given() {
                    console.log(this.state)
                    this.state = 'step has executed';
                }

                @after()
                public hookOne() {
                    console.log(this.state)
                    this.state = 'hook one has executed';
                }

                @after()
                public hookTwo() {
                    console.log(this.state);
                    this.state = 'hook two has executed';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
            """
            .no hook executed
            .step has executed
            .hook two has executed
            """

    @oldApis
    Scenario: Attempting to bind named hooks with old cucumber
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, before, after} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'hook has not executed';

                @before({name: 'setup environment'})
                public before() {
                    this.state = 'before hook executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                    this.state = 'step has executed';
                }

                @after({name: 'tear down environment'})
                public after() {
                    console.log(this.state);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it fails
        And the error output contains text:
            """
            Object literal may only specify known properties, and 'name' does not exist in type 'HookOptions'.
            """

    @newApis
    Scenario: Binding named hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, before, after} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'hook has not executed';

                @before({name: 'setup environment'})
                public before() {
                    this.state = 'before hook executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                    this.state = 'step has executed';
                }

                @after({name: 'tear down environment'})
                public after() {
                    console.log(this.state);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the hook "setup environment" was executed on scenario "example"
        And the hook "tear down environment" was executed on scenario "example"

    Scenario: Binding a before step hook
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeStep} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'hook has not executed';

                @beforeStep()
                public hook() {
                    this.state = 'hook has executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "hook has not executed"
        And the output contains "hook has executed"

    Scenario: Binding a before step hook with multiple steps
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
                    And another step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeStep, when} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private counter = 0;

                @beforeStep()
                public hook() {
                    console.log(`${this.counter++} execution: beforeStep`);
                }

                @given("a step")
                public given() {
                    console.log(`${this.counter++} execution: given`);
                }

                @when("another step")
                public when() {
                    console.log(`${this.counter++} execution: when`);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "hook has not executed"
        And the output contains text:
            """
            .0 execution: beforeStep
            1 execution: given
            .2 execution: beforeStep
            3 execution: when
            """

    Scenario: Binding multiple before step hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeStep} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'no hook executed';

                @beforeStep()
                public hookOne() {
                    console.log(this.state)
                    this.state = 'hook one has executed';
                }

                @beforeStep()
                public hookTwo() {
                    console.log(this.state)
                    this.state = 'hook two has executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                    this.state = 'step has executed';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
            """
            .no hook executed
            hook one has executed
            hook two has executed
            """

    Scenario: Binding an after step hook
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, afterStep} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'step has not executed';

                @afterStep()
                public hook() {
                    console.log(this.state);
                }

                @given("a step")
                public given() {
                    this.state = 'step has executed';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains "step has executed"

    Scenario: Binding an after step hook with multiple steps
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
                    And another step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, afterStep, when} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private counter = 0;

                @afterStep()
                public hook() {
                    console.log(`${this.counter++} execution: afterStep`);
                }

                @given("a step")
                public given() {
                    console.log(`${this.counter++} execution: given`);
                }

                @when("another step")
                public when() {
                    console.log(`${this.counter++} execution: when`);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
        """
        .0 execution: given
        1 execution: afterStep
        .2 execution: when
        3 execution: afterStep
        """

    Scenario: Binding multiple after step hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, afterStep} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'no hook executed';

                @given("a step")
                public given() {
                    console.log(this.state)
                    this.state = 'step has executed';
                }

                @afterStep()
                public hookOne() {
                    console.log(this.state)
                    this.state = 'hook one has executed';
                }

                @afterStep()
                public hookTwo() {
                    console.log(this.state);
                    this.state = 'hook two has executed';
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
            """
            .no hook executed
            step has executed
            hook two has executed
            """

    Scenario: Binding before and after step hooks
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
                Scenario: example
                    Given a step
            """
        And a file named "step_definitions/steps.ts" with:
            """ts
            import {binding, given, beforeStep, afterStep} from 'cucumber-tsflow';

            @binding()
            class Steps {
                private state = 'hook has not executed';

                @beforeStep()
                public beforeStep() {
                    this.state = 'before hook executed';
                }

                @given("a step")
                public given() {
                    console.log(this.state);
                    this.state = 'step has executed';
                }

                @afterStep()
                public afterStep() {
                    console.log(this.state);
                }
            }

            export = Steps;
            """
        When I run cucumber-js
        Then it passes
        And the output does not contain "step has not executed"
        And the output contains text:
            """
            .before hook executed
            step has executed
            """


================================================
FILE: cucumber-tsflow-specs/features/tag-parameters.feature
================================================
Feature: Tag parameters

    Background:
        Given a file named "step_definitions/steps.ts" with:
            """ts
            import * as assert from 'assert';
            import {binding, then, ScenarioInfo} from 'cucumber-tsflow';

            @binding([ScenarioInfo])
            class Steps {
                public constructor(private readonly scenario: ScenarioInfo) {}

                @then("the flag {string} is enabled")
                public checkEnabled(name: string) {
                    assert.ok(this.scenario.getFlag(name))
                }

                @then("the flag {string} is disabled")
                public checkDisabled(name: string) {
                    assert.ok(!this.scenario.getFlag(name))
                }

                @then("the option tag {string} is set to {string}")
                public checkOption(name: string, value: string) {
                    assert.strictEqual(this.scenario.getOptionTag(name), value);
                }

                @then("the option tag {string} is unset")
                public checkOptionUnset(name: string) {
                    assert.strictEqual(this.scenario.getOptionTag(name), undefined);
                }

                @then("the multi option tag {string} is set to {}")
                public checkMultiOption(name: string, value: string) {
                    assert.deepStrictEqual(this.scenario.getMultiOptionTag(name), JSON.parse(value));
                }

                @then("the attribute tag {string} is set to {}")
                @then("the attribute tag {string} is set to:")
                public checkAttributes(name: string, values: string) {
                    assert.deepStrictEqual(this.scenario.getAttributeTag(name), JSON.parse(values));
                }

                @then("the attribute tag {string} is unset")
                public checkAttributesUnset(name: string) {
                    assert.deepStrictEqual(this.scenario.getAttributeTag(name), undefined);
                }
            }

            export = Steps;
            """

    Scenario: Checking for an absent flag
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              Scenario: example
                Then the flag "enableFoo" is disabled
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for a flag on the feature
        Given a file named "features/a.feature" with:
            """feature
            @enableFoo
            Feature: Feature
              Scenario: One
                Then the flag "enableFoo" is enabled
              Scenario: Two
                Then the flag "enableFoo" is enabled
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for a flag on the scenario
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              @enableFoo
              Scenario: One
                Then the flag "enableFoo" is enabled
                Then the flag "enableBar" is disabled
              @enableBar
              Scenario: Two
                Then the flag "enableFoo" is disabled
                Then the flag "enableBar" is enabled
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an absent option
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              Scenario: example
                Then the option tag "foo" is unset
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an option on the feature
        Given a file named "features/a.feature" with:
            """feature
            @foo(bar)
            Feature: Feature
              Scenario: One
                Then the option tag "foo" is set to "bar"
              Scenario: Two
                Then the option tag "foo" is set to "bar"
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an option on the scenario
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              @foo(bar)
              Scenario: One
                Then the option tag "foo" is set to "bar"
              @foo(baz)
              Scenario: Two
                Then the option tag "foo" is set to "baz"
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an option on the scenario overriding one on the feature
        Given a file named "features/a.feature" with:
            """feature
            @foo(bar)
            Feature: Feature
              Scenario: One
                Then the option tag "foo" is set to "bar"
              @foo(baz)
              Scenario: Two
                Then the option tag "foo" is set to "baz"
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an absent attribute tag
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              Scenario: example
                Then the attribute tag "foo" is unset
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for multi options on the feature
        Given a file named "features/a.feature" with:
            """feature
            @foo(bar)
            @foo(baz)
            Feature: Feature
              Scenario: One
                Then the multi option tag "foo" is set to ["bar", "baz"]
              Scenario: Two
                Then the multi option tag "foo" is set to ["bar", "baz"]
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for multi options on the scenario
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              @foo(bar)
              @foo(baz)
              Scenario: One
                Then the multi option tag "foo" is set to ["bar", "baz"]
              @foo(qux)
              @foo(zzz)
              Scenario: Two
                Then the multi option tag "foo" is set to ["qux", "zzz"]
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for multi options on the scenario combining with multi options on the feature
        Given a file named "features/a.feature" with:
            """feature
            @foo(bar)
            Feature: Feature
              Scenario: One
                Then the multi option tag "foo" is set to ["bar"]
              @foo(baz)
              Scenario: Two
                Then the multi option tag "foo" is set to ["bar", "baz"]
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an attribute tag on the feature
        Given a file named "features/a.feature" with:
            """feature
            @foo({"bar":1})
            Feature: Feature
              Scenario: One
                Then the attribute tag "foo" is set to { "bar": 1 }
              Scenario: Two
                Then the attribute tag "foo" is set to { "bar": 1 }
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an attribute tag on the scenario
        Given a file named "features/a.feature" with:
            """feature
            Feature: Feature
              @foo({"bar":1})
              Scenario: One
                Then the attribute tag "foo" is set to { "bar": 1 }
              @foo({"bar":2})
              Scenario: Two
                Then the attribute tag "foo" is set to { "bar": 2 }
            """
        When I run cucumber-js
        Then it passes

    Scenario: Checking for an attribute tag on the scenario overriding one on the feature
        Given a file named "features/a.feature" with:
            """feature
            @foo({"bar":1})
            Feature: Feature
              Scenario: One
                Then the attribute tag "foo" is set to { "bar": 1 }
              @foo({"not-bar":2})
              Scenario: Two
                Then the attribute tag "foo" is set to { "not-bar": 2 }
            """
        When I run cucumber-js
        Then it passes


================================================
FILE: cucumber-tsflow-specs/package.json
================================================
{
  "name": "cucumber-tsflow-specs",
  "version": "5.0.0",
  "private": true,
  "description": "Specification for 'cucumber-tsflow'.",
  "license": "MIT",
  "author": "Tim Roberts <tim@timjroberts.com>",
  "main": "./index.js",
  "typings": "./dist/index.d.ts",
  "dependencies": {
    "cucumber-tsflow": "file:../cucumber-tsflow",
    "expect": "^30.2.0",
    "fs-extra": "^11.3.3",
    "verror": "^1.10.1"
  },
  "devDependencies": {
    "@types/fs-extra": "^11.0.4",
    "@types/verror": "^1.10.11"
  }
}


================================================
FILE: cucumber-tsflow-specs/src/step_definitions/cucumber_steps.ts
================================================
import { binding, then, when } from "cucumber-tsflow";
import expect from "expect";
import { parseEnvString } from "../support/helpers";
import { TestRunner } from "../support/runner";

@binding([TestRunner])
class CucumberSteps {
  public constructor(private readonly runner: TestRunner) {}

  @when("my env includes {string}")
  public setEnvironment(envString: string) {
    this.runner.sharedEnv = parseEnvString(envString);
  }

  @when("I run cucumber-js with env `{}`")
  public async runCucumberWithEnv(envString: string) {
    await this.runner.run(parseEnvString(envString));
  }

  @when("I run cucumber-js")
  public async runCucumber() {
    await this.runner.run();
  }

  @then("it passes")
  public checkPassed() {
    const { lastRun } = this.runner;

    if (lastRun?.error != null) {
      throw new Error(
        `Last run errored unexpectedly. Output:\n\n${lastRun.output}\n\n` +
          `Error Output:\n\n${lastRun.errorOutput}`,
      );
    }
  }

  @then("it fails")
  public ensureFailure() {
    const exitCode = this.runner.lastRun.error?.code ?? 0;
    expect(exitCode).not.toBe(0);

    this.runner.verifiedLastRunError = true;
  }

  @then("the output contains {string}")
  @then("the output contains text:")
  public checkStdoutContains(text: string) {
    expect(this.runner.lastRun.output).toContain(text);
  }

  @then("the output contains {string} once")
  @then("the output contains text once:")
  public checkStdoutContainsOnce(text: string) {
    const { output } = this.runner.lastRun;

    expect(output).toContain(text);

    const firstOccurrence = output.indexOf(text);
    const remaining = output.substring(firstOccurrence + 1);

    expect(remaining).not.toContain(text);
  }

  @then("the output does not contain {string}")
  @then("the output does not contain text:")
  public checkStdoutDoesNotContains(text: string) {
    expect(this.runner.lastRun.output).not.toContain(text);
  }

  @then("the error output contains {string}")
  @then("the error output contains text:")
  public checkStderrContains(text: string) {
    expect(this.runner.lastRun.errorOutput).toContain(text);
  }

  @then("the error output does not contain {string}")
  @then("the error output does not contain text:")
  public checkStderrDoesNotContains(text: string) {
    expect(this.runner.lastRun.errorOutput).not.toContain(text);
  }
}

export = CucumberSteps;

// Then(
//   "the output contains these types and quantities of message:",
//   function(this: World, expectedMessages: DataTable) {
//     const envelopes = this.lastRun.output
//       .split("\n")
//       .filter((line) => !!line)
//       .map((line) => JSON.parse(line));
//     expectedMessages.rows().forEach(([type, count]) => {
//       expect(envelopes.filter((envelope) => !!envelope[type])).to.have.length(
//         Number(count),
//         `Didn't find expected number of ${type} messages`
//       );
//     });
//   }
// );


================================================
FILE: cucumber-tsflow-specs/src/step_definitions/file_steps.ts
================================================
import { binding, given } from "cucumber-tsflow";
import { TestRunner } from "../support/runner";

@binding([TestRunner])
class FileSteps {
  public constructor(private readonly runner: TestRunner) {}

  @given("a file named {string} with:")
  public newFile(filePath: string, fileContent: string) {
    this.runner.dir.writeFile(filePath, fileContent);
  }

  @given("an empty file named {string}")
  public newEmptyFile(filePath: string) {
    return this.newFile(filePath, "");
  }

  @given("a directory named {string}")
  public async newDir(filePath: string) {
    this.runner.dir.mkdir(filePath);
  }
}

export = FileSteps;


================================================
FILE: cucumber-tsflow-specs/src/step_definitions/prepare.ts
================================================
import { formatterHelpers, ITestCaseHookParameter } from "@cucumber/cucumber";
import { after, before, binding } from "cucumber-tsflow";
import fsExtra from "fs-extra";
import path from "path";
import { TestRunner } from "../support/runner";

const projectPath = path.join(__dirname, "..", "..", "..");
const projectNodeModulePath = path.join(projectPath, "node_modules");
const cucumberPath = path.join(projectNodeModulePath, "@cucumber", "cucumber");
const tsNodePath = path.join(projectNodeModulePath, "ts-node");
const projectLibPath = path.join(projectPath, "cucumber-tsflow");
const log4jsPath = path.join(projectLibPath, "node_modules", "log4js");

@binding([TestRunner])
class Prepare {
  public constructor(private readonly runner: TestRunner) {}

  @before()
  public setupTestDir({ gherkinDocument, pickle }: ITestCaseHookParameter) {
    const { line } = formatterHelpers.PickleParser.getPickleLocation({
      gherkinDocument,
      pickle,
    });

    const tmpDir = path.join(
      projectPath,
      "tmp",
      `${path.basename(pickle.uri)}_${line.toString()}`,
    );

    fsExtra.removeSync(tmpDir);

    this.runner.dir.path = tmpDir;

    this.setupNodeModules();

    const tags = [
      ...pickle.tags.map((tag) => tag.name),
      ...(gherkinDocument.feature?.tags.map((tag) => tag.name) ?? []),
    ];

    this.writeDefaultFiles(tags);
  }

  @after()
  public tearDownTestDir() {
    const { lastRun } = this.runner;

    if (lastRun?.error != null && !this.runner.verifiedLastRunError) {
      throw new Error(
        `Last run errored unexpectedly. Output:\n\n${lastRun.output}\n\n` +
          `Error Output:\n\n${lastRun.errorOutput}`,
      );
    }
  }

  private setupNodeModules() {
    const tmpDirNodeModulesPath = this.runner.dir.mkdir("node_modules");

    fsExtra.ensureSymlinkSync(
      cucumberPath,
      path.join(tmpDirNodeModulesPath, "@cucumber", "cucumber"),
    );
    fsExtra.ensureSymlinkSync(
      tsNodePath,
      path.join(tmpDirNodeModulesPath, "ts-node"),
    );
    fsExtra.ensureSymlinkSync(
      projectLibPath,
      path.join(tmpDirNodeModulesPath, "cucumber-tsflow"),
    );
    fsExtra.ensureSymlinkSync(
      log4jsPath,
      path.join(tmpDirNodeModulesPath, "log4js"),
    );
  }

  private writeDefaultFiles(tags: string[]) {
    if (!tags.includes("custom-tsconfig")) {
      this.runner.dir.writeFile(
        "tsconfig.json",
        JSON.stringify({
          compilerOptions: {
            experimentalDecorators: true,
          },
        }),
      );
    }

    if (!tags.includes("no-logging")) {
      this.runner.dir.writeFile(
        "a-logging.ts",
        `
import * as log4js from 'log4js';

log4js.configure({
  appenders: {
    logfile: {
      type: "file",
      filename: "output.log",
    }
  },
  categories: {
    default: { appenders: ["logfile"], level: "trace" },
  }
});
`,
      );
    }
  }
}

export = Prepare;


================================================
FILE: cucumber-tsflow-specs/src/step_definitions/scenario_steps.ts
================================================
import { DataTable } from "@cucumber/cucumber";
import * as messages from "@cucumber/messages";
import assert from "assert";
import { binding, then } from "cucumber-tsflow";
import expect from "expect";
import { Extractor } from "../support/helpers";
import { TestRunner } from "../support/runner";

const ENCODING_MAP: { [key: string]: messages.AttachmentContentEncoding } = {
  IDENTITY: messages.AttachmentContentEncoding.IDENTITY,
  BASE64: messages.AttachmentContentEncoding.BASE64,
};

@binding([TestRunner])
class ScenarioSteps {
  public constructor(private readonly runner: TestRunner) {}

  @then("it runs {int} scenarios")
  public checkScenarioCount(expectedCount: number) {
    const startedTestCases = this.runner.lastRun.envelopes.reduce(
      (acc, e) => (e.testCaseStarted == null ? acc : acc + 1),
      0,
    );

    expect(startedTestCases).toBe(expectedCount);
  }

  @then("it runs the scenario {string}")
  public checkRunSingleScenario(scenario: string) {
    const actualNames =
      this.runner.extractor.getPickleNamesInOrderOfExecution();
    expect(actualNames).toEqual([scenario]);
  }

  @then("it runs the scenarios:")
  public checkScenarios(table: DataTable) {
    const expectedNames = table.rows().map((row) => row[0]);
    const actualNames =
      this.runner.extractor.getPickleNamesInOrderOfExecution();
    expect(actualNames).toEqual(expectedNames);
  }

  @then("scenario {string} has status {string}")
  public checkScenarioStatus(name: string, status: string) {
    const result = this.runner.extractor.getTestCaseResult(name);

    expect(result.status).toEqual(status.toUpperCase());
  }

  @then("scenario {string} step {string} has the attachments:")
  public checkStepAttachment(
    pickleName: string,
    stepText: string,
    table: DataTable,
  ) {
    const expectedAttachments = table.hashes().map((x) => {
      return {
        body: x.DATA,
        mediaType: x["MEDIA TYPE"],
        contentEncoding: ENCODING_MAP[x["MEDIA ENCODING"]],
      };
    });

    const actualAttachments = this.runner.extractor
      .getAttachmentsForStep(pickleName, stepText)
      .map(Extractor.simplifyAttachment);

    expect(actualAttachments).toEqual(expectedAttachments);
  }

  @then("scenario {string} {string} hook has the attachments:")
  public checkHookAttachment(
    pickleName: string,
    hookKeyword: string,
    table: DataTable,
  ) {
    const expectedAttachments: messages.Attachment[] = table
      .hashes()
      .map((x) => {
        return {
          body: x.DATA,
          mediaType: x["MEDIA TYPE"],
          contentEncoding: ENCODING_MAP[x["MEDIA ENCODING"]],
        };
      });

    const actualAttachments = this.runner.extractor
      .getAttachmentsForHook(pickleName, hookKeyword === "Before")
      .map(Extractor.simplifyAttachment);

    expect(actualAttachments).toEqual(expectedAttachments);
  }

  @then("scenario {string} step {string} has the logs:")
  public checkStepLogs(pickleName: string, stepName: string, logs: DataTable) {
    const expectedLogs = logs.raw().map((row) => row[0]);
    const actualLogs = Extractor.logsFromAttachments(
      this.runner.extractor.getAttachmentsForStep(pickleName, stepName),
    );

    expect(actualLogs).toStrictEqual(expectedLogs);
  }

  @then("scenario {string} step {string} has no attachments")
  public checkNoStepLogs(pickleName: string, stepName: string) {
    const attachments = this.runner.extractor.getAttachmentsForStep(
      pickleName,
      stepName,
    );

    expect(attachments).toStrictEqual([]);
  }

  @then("the hook {string} was executed on scenario {string}")
  public checkNamedHookExecution(hookName: string, scenarioName: string) {
    const hook = this.runner.extractor.getHookByName(hookName);
    const executions = this.runner.extractor.getHookExecutions(
      scenarioName,
      hook.id,
    );

    assert(
      executions.length === 1,
      `Hook ${hookName} executed ${executions.length} times on scenario "${scenarioName}"`,
    );
  }
}

export = ScenarioSteps;

// Then(
//   "the scenario {string} has the steps:",
//   function(this: World, name: string, table: DataTable) {
//     const actualTexts = getTestStepResults(this.lastRun.envelopes, name).map(
//       (s) => s.text
//     );
//     const expectedTexts = table.rows().map((row) => row[0]);
//     expect(actualTexts).to.eql(expectedTexts);
//   }
// );
//
// Then(
//   "scenario {string} step {string} has status {string}",
//   function(this: World, pickleName: string, stepText: string, status: string) {
//     const testStepResults = getTestStepResults(
//       this.lastRun.envelopes,
//       pickleName
//     );
//     const testStepResult = testStepResults.find((x) => x.text === stepText);
//     expect(testStepResult.result.status).to.eql(
//       status.toUpperCase() as messages.TestStepResultStatus
//     );
//   }
// );
//
// Then(
//   "scenario {string} attempt {int} step {string} has status {string}",
//   function(
//     this: World,
//     pickleName: string,
//     attempt: number,
//     stepText: string,
//     status: string
//   ) {
//     const testStepResults = getTestStepResults(
//       this.lastRun.envelopes,
//       pickleName,
//       attempt
//     );
//     const testStepResult = testStepResults.find((x) => x.text === stepText);
//     expect(testStepResult.result.status).to.eql(
//       status.toUpperCase() as messages.TestStepResultStatus
//     );
//   }
// );
//
// Then(
//   "scenario {string} {string} hook has status {string}",
//   function(
//     this: World,
//     pickleName: string,
//     hookKeyword: string,
//     status: string
//   ) {
//     const testStepResults = getTestStepResults(
//       this.lastRun.envelopes,
//       pickleName
//     );
//     const testStepResult = testStepResults.find((x) => x.text === hookKeyword);
//     expect(testStepResult.result.status).to.eql(
//       status.toUpperCase() as messages.TestStepResultStatus
//     );
//   }
// );
//
// Then(
//   "scenario {string} step {string} failed with:",
//   function(
//     this: World,
//     pickleName: string,
//     stepText: string,
//     errorMessage: string
//   ) {
//     const testStepResults = getTestStepResults(
//       this.lastRun.envelopes,
//       pickleName
//     );
//     const testStepResult = testStepResults.find((x) => x.text === stepText);
//     if (semver.satisfies(process.version, ">=14.0.0")) {
//       errorMessage = errorMessage.replace(
//         "{ member: [Circular] }",
//         "<ref *1> { member: [Circular *1] }"
//       );
//     }
//     expect(testStepResult.result.status).to.eql(
//       messages.TestStepResultStatus.FAILED
//     );
//     expect(testStepResult.result.message).to.include(errorMessage);
//   }
// );
//
// Then(
//   "scenario {string} attempt {int} step {string} failed with:",
//   function(
//     this: World,
//     pickleName: string,
//     attempt: number,
//     stepText: string,
//     errorMessage: string
//   ) {
//     const testStepResults = getTestStepResults(
//       this.lastRun.envelopes,
//       pickleName,
//       attempt
//     );
//     const testStepResult = testStepResults.find((x) => x.text === stepText);
//     expect(testStepResult.result.status).to.eql(
//       messages.TestStepResultStatus.FAILED
//     );
//     expect(testStepResult.result.message).to.include(errorMessage);
//   }
// );
//
// Then(
//   "scenario {string} step {string} has the doc string:",
//   function(
//     this: World,
//     pickleName: string,
//     stepText: string,
//     docString: string
//   ) {
//     const pickleStep = getPickleStep(
//       this.lastRun.envelopes,
//       pickleName,
//       stepText
//     );
//     expect(pickleStep.argument.docString.content).to.eql(docString);
//   }
// );
//
// Then(
//   "scenario {string} step {string} has the data table:",
//   function(
//     this: World,
//     pickleName: string,
//     stepText: string,
//     dataTable: DataTable
//   ) {
//     const pickleStep = getPickleStep(
//       this.lastRun.envelopes,
//       pickleName,
//       stepText
//     );
//     expect(new DataTable(pickleStep.argument.dataTable)).to.eql(dataTable);
//   }
// );


================================================
FILE: cucumber-tsflow-specs/src/support/formatter_output_helpers.ts
================================================
import {
  IJsonFeature,
  IJsonScenario,
  IJsonStep,
} from "@cucumber/cucumber/lib/formatter/json_formatter";
import { valueOrDefault } from "@cucumber/cucumber/lib/value_checker";
import * as messages from "@cucumber/messages";

function normalizeExceptionAndUri(exception: string, cwd: string): string {
  return exception
    .replace(cwd, "")
    .replace(/\\/g, "/")
    .replace("/features", "features")
    .split("\n")[0];
}

function normalizeMessage(obj: any, cwd: string): void {
  if (obj.uri != null) {
    obj.uri = normalizeExceptionAndUri(obj.uri, cwd);
  }
  if (obj.sourceReference?.uri != null) {
    obj.sourceReference.uri = normalizeExceptionAndUri(
      obj.sourceReference.uri,
      cwd,
    );
  }
  if (obj.testStepResult != null) {
    if (obj.testStepResult.duration != null) {
      obj.testStepResult.duration.nanos = 0;
    }
    if (obj.testStepResult.message != null) {
      obj.testStepResult.message = normalizeExceptionAndUri(
        obj.testStepResult.message,
        cwd,
      );
    }
  }
}

export function normalizeMessageOutput(
  envelopeObjects: messages.Envelope[],
  cwd: string,
): messages.Envelope[] {
  envelopeObjects.forEach((e: any) => {
    for (const key of Object.keys(e)) {
      normalizeMessage(e[key], cwd);
    }
  });
  return envelopeObjects;
}

export function stripMetaMessages(
  envelopeObjects: messages.Envelope[],
): messages.Envelope[] {
  return envelopeObjects.filter((e: any) => {
    // filter off meta objects, almost none of it predictable/useful for testing
    return e.meta == null;
  });
}

export function normalizeJsonOutput(str: string, cwd: string): IJsonFeature[] {
  const json: IJsonFeature[] = JSON.parse(valueOrDefault(str, "[]"));
  json.forEach((feature: IJsonFeature) => {
    if (feature.uri != null) {
      feature.uri = normalizeExceptionAndUri(feature.uri, cwd);
    }
    feature.elements.forEach((element: IJsonScenario) => {
      element.steps.forEach((step: IJsonStep) => {
        if (step.match != null && step.match.location != null) {
          step.match.location = normalizeExceptionAndUri(
            step.match.location,
            cwd,
          );
        }
        if (step.result != null) {
          if (step.result.duration != null) {
            step.result.duration = 0;
          }
          if (step.result.error_message != null) {
            step.result.error_message = normalizeExceptionAndUri(
              step.result.error_message,
              cwd,
            );
          }
        }
      });
    });
  });
  return json;
}

export const ignorableKeys = [
  "meta",
  // sources
  "uri",
  "line",
  // ids
  "astNodeId",
  "astNodeIds",
  "hookId",
  "id",
  "pickleId",
  "pickleStepId",
  "stepDefinitionIds",
  "testCaseId",
  "testCaseStartedId",
  "testStepId",
  // time
  "nanos",
  "seconds",
  // errors
  "message",
];


================================================
FILE: cucumber-tsflow-specs/src/support/helpers.ts
================================================
// Adapted from:
// https://github.com/cucumber/cucumber-js/blob/6505e61abce385787767f270b6ce2077eb3d7c1c/features/support/message_helpers.ts
import { getGherkinStepMap } from "@cucumber/cucumber/lib/formatter/helpers/gherkin_document_parser";
import {
  getPickleStepMap,
  getStepKeyword,
} from "@cucumber/cucumber/lib/formatter/helpers/pickle_parser";
import { doesHaveValue } from "@cucumber/cucumber/lib/value_checker";
import * as messages from "@cucumber/messages";
import * as assert from "node:assert";
import util, { inspect } from "util";

export function parseEnvString(str: string): NodeJS.ProcessEnv {
  const result: NodeJS.ProcessEnv = {};
  if (doesHaveValue(str)) {
    try {
      Object.assign(result, JSON.parse(str));
    } catch {
      str
        .split(/\s+/)
        .map((keyValue) => keyValue.split("="))
        .forEach((pair) => (result[pair[0]] = pair[1]));
    }
  }
  return result;
}

export function dump(val: unknown): string {
  return inspect(val, { depth: null });
}

export interface IStepTextAndResult {
  text: string;

  result: messages.TestStepResult;
}

export type SimpleAttachment = Pick<
  messages.Attachment,
  "body" | "mediaType" | "contentEncoding"
>;

export class Extractor {
  public constructor(private readonly envelopes: messages.Envelope[]) {}

  public static logsFromAttachments(
    attachments: messages.Attachment[],
  ): string[] {
    return attachments
      .filter(
        (att) =>
          att.contentEncoding === messages.AttachmentContentEncoding.IDENTITY &&
          att.mediaType === "text/x.cucumber.log+plain",
      )
      .map((att) => att.body);
  }

  public static simplifyAttachment(
    attachment: messages.Attachment,
  ): SimpleAttachment {
    return {
      body: attachment.body,
      mediaType: attachment.mediaType,
      contentEncoding: attachment.contentEncoding,
    };
  }

  public getPickleNamesInOrderOfExecution(): string[] {
    const pickleNameMap: Record<string, string> = {};
    const testCaseToPickleNameMap: Record<string, string> = {};
    const result: string[] = [];
    this.envelopes.forEach((e) => {
      if (e.pickle != null) {
        pickleNameMap[e.pickle.id] = e.pickle.name;
      } else if (e.testCase != null) {
        testCaseToPickleNameMap[e.testCase.id] =
          pickleNameMap[e.testCase.pickleId];
      } else if (e.testCaseStarted != null) {
        result.push(testCaseToPickleNameMap[e.testCaseStarted.testCaseId]);
      }
    });
    return result;
  }

  public getPickleStep(
    pickleName: string,
    stepText: string,
  ): messages.PickleStep {
    const pickle = this.getPickle(pickleName);
    const gherkinDocument = this.getGherkinDocument(pickle.uri);
    return this.getPickleStepByStepText(pickle, gherkinDocument, stepText);
  }

  public getHookByName(hookName: string): messages.Hook {
    const hookEnvelope = this.envelopes.find(
      ({ hook }) => hook?.name === hookName,
    );

    assert.ok(hookEnvelope, `Unknown hook ${hookName}`);

    return hookEnvelope.hook!;
  }

  public getHookExecutions(
    pickleName: string,
    hookId: string,
  ): messages.TestStep[] {
    const pickle = this.getPickle(pickleName);
    const testCase = this.getTestCase(pickle.id);

    return testCase.testSteps.filter((step) => step.hookId === hookId);
  }

  public getTestCaseResult(pickleName: string): messages.TestStepResult {
    const pickle = this.getPickle(pickleName);
    const testCase = this.getTestCase(pickle.id);
    const testCaseStarted = this.getTestCaseStarted(testCase.id);
    const testStepResults: messages.TestStepResult[] = [];
    this.envelopes.forEach((e) => {
      if (
        e.testStepFinished != null &&
        e.testStepFinished.testCaseStartedId === testCaseStarted.id
      ) {
        testStepResults.push(e.testStepFinished.testStepResult);
      }
    });
    return messages.getWorstTestStepResult(testStepResults);
  }

  public getTestStepResults(
    pickleName: string,
    attempt = 0,
  ): IStepTextAndResult[] {
    const pickle = this.getPickle(pickleName);
    const gherkinDocument = this.getGherkinDocument(pickle.uri);
    const testCase = this.getTestCase(pickle.id);
    const testCaseStarted = this.getTestCaseStarted(testCase.id, attempt);
    const testStepIdToResultMap: Record<string, messages.TestStepResult> = {};
    this.envelopes.forEach((e) => {
      if (
        e.testStepFinished != null &&
        e.testStepFinished.testCaseStartedId === testCaseStarted.id
      ) {
        testStepIdToResultMap[e.testStepFinished.testStepId] =
          e.testStepFinished.testStepResult;
      }
    });
    const gherkinStepMap = getGherkinStepMap(gherkinDocument);
    const pickleStepMap = getPickleStepMap(pickle);
    let isBeforeHook = true;
    return testCase.testSteps.map((testStep) => {
      let text;
      if (testStep.pickleStepId == null) {
        text = isBeforeHook ? "Before" : "After";
      } else {
        isBeforeHook = false;
        const pickleStep = pickleStepMap[testStep.pickleStepId];
        const keyword = getStepKeyword({ pickleStep, gherkinStepMap });
        text = `${keyword}${pickleStep.text}`;
      }
      return { text, result: testStepIdToResultMap[testStep.id] };
    });
  }

  public getAttachmentsForStep(
    pickleName: string,
    stepText: string,
  ): messages.Attachment[] {
    const pickle = this.getPickle(pickleName);
    const gherkinDocument = this.getGherkinDocument(pickle.uri);
    const testCase = this.getTestCase(pickle.id);
    const pickleStep = this.getPickleStepByStepText(
      pickle,
      gherkinDocument,
      stepText,
    );
    assert.ok(
      pickleStep,
      `Step "${stepText}" not found in pickle ${dump(pickle)}`,
    );

    const testStep = testCase.testSteps.find(
      (s) => s.pickleStepId === pickleStep.id,
    )!;
    const testCaseStarted = this.getTestCaseStarted(testCase.id);
    return this.getTestStepAttachments(testCaseStarted.id, testStep.id);
  }

  public getAttachmentsForHook(
    pickleName: string,
    isBeforeHook: boolean,
  ): messages.Attachment[] {
    const pickle = this.getPickle(pickleName);
    const testCase = this.getTestCase(pickle.id);
    // Ignore the first Before hook and the last After hook
    // Those are used to set up and tear down the tsflow harness
    const testStepIndex = isBeforeHook ? 1 : testCase.testSteps.length - 2;
    const testStep = testCase.testSteps[testStepIndex];
    const testCaseStarted = this.getTestCaseStarted(testCase.id);
    return this.getTestStepAttachments(testCaseStarted.id, testStep.id);
  }

  private getPickle(pickleName: string): messages.Pickle {
    const pickleEnvelope = this.envelopes.find(
      (e) => e.pickle != null && e.pickle.name === pickleName,
    );
    if (pickleEnvelope == null) {
      throw new Error(
        `No pickle with name "${pickleName}" in this.envelopes:\n ${util.inspect(
          this.envelopes,
        )}`,
      );
    }
    return pickleEnvelope.pickle!;
  }

  private getGherkinDocument(uri: string): messages.GherkinDocument {
    const gherkinDocumentEnvelope = this.envelopes.find(
      (e) => e.gherkinDocument != null && e.gherkinDocument.uri === uri,
    );
    if (gherkinDocumentEnvelope == null) {
      throw new Error(
        `No gherkinDocument with uri "${uri}" in this.envelopes:\n ${util.inspect(
          this.envelopes,
        )}`,
      );
    }
    return gherkinDocumentEnvelope.gherkinDocument!;
  }

  private getTestCase(pickleId: string): messages.TestCase {
    const testCaseEnvelope = this.envelopes.find(
      (e) => e.testCase != null && e.testCase.pickleId === pickleId,
    );
    if (testCaseEnvelope == null) {
      throw new Error(
        `No testCase with pickleId "${pickleId}" in this.envelopes:\n ${util.inspect(
          this.envelopes,
        )}`,
      );
    }
    return testCaseEnvelope.testCase!;
  }

  private getTestCaseStarted(
    testCaseId: string,
    attempt = 0,
  ): messages.TestCaseStarted {
    const testCaseStartedEnvelope = this.envelopes.find(
      (e) =>
        e.testCaseStarted != null &&
        e.testCaseStarted.testCaseId === testCaseId &&
        e.testCaseStarted.attempt === attempt,
    );
    if (testCaseStartedEnvelope == null) {
      throw new Error(
        `No testCaseStarted with testCaseId "${testCaseId}" in this.envelopes:\n ${util.inspect(
          this.envelopes,
        )}`,
      );
    }
    return testCaseStartedEnvelope.testCaseStarted!;
  }

  private getPickleStepByStepText(
    pickle: messages.Pickle,
    gherkinDocument: messages.GherkinDocument,
    stepText: string,
  ): messages.PickleStep {
    const gherkinStepMap = getGherkinStepMap(gherkinDocument);
    return pickle.steps.find((s) => {
      const keyword = getStepKeyword({ pickleStep: s, gherkinStepMap });
      return `${keyword}${s.text}` === stepText;
    })!;
  }

  private getTestStepAttachments(
    testCaseStartedId: string,
    testStepId: string,
  ): messages.Attachment[] {
    return this.envelopes
      .filter(
        (e) =>
          e.attachment != null &&
          e.attachment.testCaseStartedId === testCaseStartedId &&
          e.attachment.testStepId === testStepId,
      )
      .map((e) => e.attachment!);
  }
}


================================================
FILE: cucumber-tsflow-specs/src/support/runner.ts
================================================
// Adapted from:
// https://github.com/cucumber/cucumber-js/blob/6505e61abce385787767f270b6ce2077eb3d7c1c/features/support/world.ts
import * as messageStreams from "@cucumber/message-streams";
import * as messages from "@cucumber/messages";
import assert from "assert";
import { execFile } from "child_process";
import expect from "expect";
import path from "path";
import { Writable } from "stream";
import { finished } from "stream/promises";
import stripAnsi from "strip-ansi";
import VError from "verror";
import { Extractor } from "./helpers";
import { TestDir } from "./testDir";

const projectPath = path.join(__dirname, "..", "..", "..");
const cucumberBinPath = path.join(
  projectPath,
  "node_modules",
  ".bin",
  "cucumber-js",
);

interface IRunResult {
  error: any;

  stderr: string;

  stdout: string;
}

interface ILastRun {
  error: any;

  errorOutput: string;

  envelopes: messages.Envelope[];

  output: string;
}

export class TestRunner {
  public readonly dir = new TestDir();

  public sharedEnv?: NodeJS.ProcessEnv;

  public spawn: boolean = false;

  public debug: boolean = false;

  public worldParameters?: any;

  public verifiedLastRunError!: boolean;

  private _lastRun?: ILastRun;

  public get lastRun(): ILastRun {
    assert(this._lastRun, "Cucumber has not executed yet.");
    return this._lastRun;
  }

  public get extractor(): Extractor {
    return new Extractor(this.lastRun.envelopes);
  }

  public async run(
    envOverride: NodeJS.ProcessEnv | null = null,
  ): Promise<void> {
    const messageFilename = "message.ndjson";
    const env = { ...process.env, ...this.sharedEnv, ...envOverride };

    const result = await new Promise<IRunResult>((resolve) => {
      execFile(
        "node",
        [
          cucumberBinPath,
          "--format",
          `message:${messageFilename}`,
          "--require-module",
          "ts-node/register",
          "--require",
          "a-logging.ts",
          "--require",
          "step_definitions/**/*.ts",
        ],
        { cwd: this.dir.path, env },
        (error, stdout, stderr) => {
          resolve({ error, stdout, stderr });
        },
      );
    });

    const stderrSuffix =
      result.error != null ? VError.fullStack(result.error) : "";

    const envelopes: messages.Envelope[] = [];
    const messageOutputStream = this.dir
      .readFileStream(messageFilename)
      ?.pipe(new messageStreams.NdjsonToMessageStream())
      .pipe(
        new Writable({
          objectMode: true,
          write(envelope: messages.Envelope, _: BufferEncoding, callback) {
            envelopes.push(envelope);
            callback();
          },
        }),
      );

    if (messageOutputStream !== undefined) {
      await finished(messageOutputStream);
    }

    this._lastRun = {
      error: result.error,
      errorOutput: result.stderr + stderrSuffix,
      envelopes,
      output: stripAnsi(result.stdout),
    };
    this.verifiedLastRunError = false;

    expect(this._lastRun.output).not.toContain("Unhandled rejection.");
  }
}


================================================
FILE: cucumber-tsflow-specs/src/support/testDir.ts
================================================
import assert from "assert";
import fs, { ReadStream, WriteStream } from "fs";
import path from "path";

export class TestDir {
  private _path?: string;

  public get path(): string {
    assert(this._path, "Test directory not configured");
    return this._path;
  }

  public set path(newPath: string) {
    this._path = newPath;
  }

  public readFileStream(
    ...pathParts: string[] | [string[]]
  ): ReadStream | null {
    const filePath = this.getPath(...pathParts);

    if (fs.existsSync(filePath)) {
      return fs.createReadStream(filePath, { encoding: "utf-8" });
    }

    return null;
  }

  public writeFileStream(...pathParts: string[] | [string[]]): WriteStream {
    const filePath = this.getPath(...pathParts);

    return fs.createWriteStream(filePath, { encoding: "utf-8" });
  }

  public readFile(...pathParts: string[] | [string[]]): string | null {
    const filePath = this.getPath(...pathParts);

    if (fs.existsSync(filePath)) {
      return fs.readFileSync(filePath, { encoding: "utf-8" });
    }

    return null;
  }

  public writeFile(pathParts: string | string[], data: string): void {
    const pathArgs = (
      typeof pathParts === "string" ? [pathParts] : pathParts
    ).flatMap((part) => part.split("/"));

    if (pathArgs.length > 1) {
      this.mkdir(pathArgs.slice(0, pathArgs.length - 1));
    }

    const filePath = this.getPath(pathArgs);

    fs.writeFileSync(filePath, data, { encoding: "utf-8" });
  }

  public mkdir(...pathParts: string[] | [string[]]): string {
    const dirPath = this.getPath(...pathParts);
    fs.mkdirSync(dirPath, { recursive: true });
    return dirPath;
  }

  private getPath(...pathParts: string[] | [string[]]): string {
    return path.join(
      this.path,
      ...(pathParts.length === 1 && Array.isArray(pathParts[0])
        ? pathParts[0]
        : (pathParts as string[])),
    );
  }
}


================================================
FILE: cucumber-tsflow-specs/tsconfig.json
================================================
{
  "compilerOptions": {
    "composite": true,
    "moduleResolution": "node",
    "module": "umd",
    "target": "es2018",
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "strict": true,
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "removeComments": false,
    "esModuleInterop": true,
    "noEmit": true,
    "experimentalDecorators": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["./src/**/*.ts"],
  "references": [
    {
      "path": "../cucumber-tsflow"
    }
  ]
}


================================================
FILE: cucumber.js
================================================
const cucumberPkg = require("@cucumber/cucumber/package.json");

module.exports = cucumberPkg.version.startsWith("7.")
  ? {
      default: [
        "--publish-quiet",
        "--require-module ts-node/register",
        "--require cucumber-tsflow-specs/src/**/*.ts",
        "--tags 'not @newApis'",
        '--world-parameters \'{"foo":"bar"}\'',
      ].join(" "),
    }
  : {
      default: {
        requireModule: ["ts-node/register"],
        require: ["cucumber-tsflow-specs/src/**/*.ts"],
        tags: "not @oldApis",
        worldParameters: {
          foo: "bar",
        },
      },
    };


================================================
FILE: lerna.json
================================================
{
  "packages": ["*"],
  "version": "5.0.0"
}


================================================
FILE: package.json
================================================
{
  "name": "cucumber-tsflow-workspace",
  "version": "2.0.0",
  "private": true,
  "description": "Workspace for cucumber-tsflow",
  "repository": {
    "type": "git",
    "url": "https://github.com/timjroberts/cucumber-js-tsflow.git"
  },
  "license": "MIT",
  "scripts": {
    "clean": "rm -rf tmp tsconfig.tsbuildinfo cucumber-tsflow/tsconfig.tsbuildinfo cucumber-tsflow/dist cucumber-tsflow-specs/tsconfig.tsbuildinfo",
    "build": "tsc -p cucumber-tsflow",
    "build:watch": "tsc --build --watch",
    "preinstall": "cd cucumber-tsflow && npm install && cd ../cucumber-tsflow-specs && npm install",
    "postinstall": "npm run fix-peer-cucumber",
    "fix-peer-cucumber": "rm -rf cucumber-tsflow/node_modules/@cucumber/cucumber && ln -s ../../node_modules/@cucumber cucumber-tsflow/node_modules/@cucumber/cucumber && mkdir -p cucumber-tsflow-specs/node_modules/@cucumber && rm -rf cucumber-tsflow-specs/node_modules/@cucumber/cucumber && ln -s ../../node_modules/@cucumber/cucumber cucumber-tsflow-specs/node_modules/@cucumber/cucumber",
    "precommit": "lint-staged && npm test",
    "set-packageversion": "node .build/setPackageVersion.js",
    "pretest": "npm run build",
    "test": "cucumber-js -p default cucumber-tsflow-specs/features/**/*.feature",
    "upgrade": "npm-check -u; cd cucumber-tsflow && npm-check -u; cd ../cucumber-tsflow-specs && npm-check -u; cd .. && npm install"
  },
  "lint-staged": {
    "*.ts": [
      "prettier --write",
      "tslint --fix",
      "git add"
    ],
    "*.json": [
      "prettier --write",
      "git add"
    ]
  },
  "devDependencies": {
    "@cucumber/cucumber": "^12.7.0",
    "@types/node": "^25.3.3",
    "@types/underscore": "^1.13.0",
    "lerna": "^9.0.5",
    "lint-staged": "^16.3.1",
    "nerdbank-gitversioning": "^3.9.50",
    "npm-check": "^6.0.1",
    "prettier": "^3.8.1",
    "ts-node": "^10.9.2",
    "tslint": "^6.1.3",
    "tslint-config-prettier": "^1.18.0",
    "typescript": "^5.9.3"
  },
  "overrides": {
    "semver": "^7.5.3"
  }
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "esModuleInterop": true
  },
  "files": [],
  "include": [],
  "references": [
    {
      "path": "./cucumber-tsflow"
    },
    {
      "path": "./cucumber-tsflow-specs"
    }
  ]
}


================================================
FILE: tslint.json
================================================
{
  "defaultSeverity": "error",
  "extends": ["tslint:recommended", "tslint-config-prettier"],
  "jsRules": {},
  "rules": {
    "variable-name": [
      true,
      "allow-leading-underscore",
      "allow-trailing-underscore",
      "allow-pascal-case"
    ],
    "interface-name": [false],
    "object-literal-sort-keys": false,
    "member-ordering": [false],
    "object-literal-shorthand": false,
    "trailing-comma": ["multiline"]
  }
}


================================================
FILE: version.json
================================================
{
  "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
  "version": "5.0",
  "publicReleaseRefSpec": ["^refs/heads/master$", "^refs/heads/release/"]
}
Download .txt
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
Download .txt
SYMBOL INDEX (152 symbols across 21 files)

FILE: cucumber-tsflow-specs/src/step_definitions/cucumber_steps.ts
  class CucumberSteps (line 6) | @binding([TestRunner])
    method constructor (line 8) | public constructor(private readonly runner: TestRunner) {}
    method setEnvironment (line 11) | public setEnvironment(envString: string) {
    method runCucumberWithEnv (line 16) | public async runCucumberWithEnv(envString: string) {
    method runCucumber (line 21) | public async runCucumber() {
    method checkPassed (line 26) | public checkPassed() {
    method ensureFailure (line 38) | public ensureFailure() {
    method checkStdoutContains (line 47) | public checkStdoutContains(text: string) {
    method checkStdoutContainsOnce (line 53) | public checkStdoutContainsOnce(text: string) {
    method checkStdoutDoesNotContains (line 66) | public checkStdoutDoesNotContains(text: string) {
    method checkStderrContains (line 72) | public checkStderrContains(text: string) {
    method checkStderrDoesNotContains (line 78) | public checkStderrDoesNotContains(text: string) {

FILE: cucumber-tsflow-specs/src/step_definitions/file_steps.ts
  class FileSteps (line 4) | @binding([TestRunner])
    method constructor (line 6) | public constructor(private readonly runner: TestRunner) {}
    method newFile (line 9) | public newFile(filePath: string, fileContent: string) {
    method newEmptyFile (line 14) | public newEmptyFile(filePath: string) {
    method newDir (line 19) | public async newDir(filePath: string) {

FILE: cucumber-tsflow-specs/src/step_definitions/prepare.ts
  class Prepare (line 14) | @binding([TestRunner])
    method constructor (line 16) | public constructor(private readonly runner: TestRunner) {}
    method setupTestDir (line 19) | public setupTestDir({ gherkinDocument, pickle }: ITestCaseHookParamete...
    method tearDownTestDir (line 46) | public tearDownTestDir() {
    method setupNodeModules (line 57) | private setupNodeModules() {
    method writeDefaultFiles (line 78) | private writeDefaultFiles(tags: string[]) {

FILE: cucumber-tsflow-specs/src/step_definitions/scenario_steps.ts
  constant ENCODING_MAP (line 9) | const ENCODING_MAP: { [key: string]: messages.AttachmentContentEncoding ...
  class ScenarioSteps (line 14) | @binding([TestRunner])
    method constructor (line 16) | public constructor(private readonly runner: TestRunner) {}
    method checkScenarioCount (line 19) | public checkScenarioCount(expectedCount: number) {
    method checkRunSingleScenario (line 29) | public checkRunSingleScenario(scenario: string) {
    method checkScenarios (line 36) | public checkScenarios(table: DataTable) {
    method checkScenarioStatus (line 44) | public checkScenarioStatus(name: string, status: string) {
    method checkStepAttachment (line 51) | public checkStepAttachment(
    method checkHookAttachment (line 72) | public checkHookAttachment(
    method checkStepLogs (line 95) | public checkStepLogs(pickleName: string, stepName: string, logs: DataT...
    method checkNoStepLogs (line 105) | public checkNoStepLogs(pickleName: string, stepName: string) {
    method checkNamedHookExecution (line 115) | public checkNamedHookExecution(hookName: string, scenarioName: string) {

FILE: cucumber-tsflow-specs/src/support/formatter_output_helpers.ts
  function normalizeExceptionAndUri (line 9) | function normalizeExceptionAndUri(exception: string, cwd: string): string {
  function normalizeMessage (line 17) | function normalizeMessage(obj: any, cwd: string): void {
  function normalizeMessageOutput (line 40) | function normalizeMessageOutput(
  function stripMetaMessages (line 52) | function stripMetaMessages(
  function normalizeJsonOutput (line 61) | function normalizeJsonOutput(str: string, cwd: string): IJsonFeature[] {

FILE: cucumber-tsflow-specs/src/support/helpers.ts
  function parseEnvString (line 13) | function parseEnvString(str: string): NodeJS.ProcessEnv {
  function dump (line 28) | function dump(val: unknown): string {
  type IStepTextAndResult (line 32) | interface IStepTextAndResult {
  type SimpleAttachment (line 38) | type SimpleAttachment = Pick<
  class Extractor (line 43) | class Extractor {
    method constructor (line 44) | public constructor(private readonly envelopes: messages.Envelope[]) {}
    method logsFromAttachments (line 46) | public static logsFromAttachments(
    method simplifyAttachment (line 58) | public static simplifyAttachment(
    method getPickleNamesInOrderOfExecution (line 68) | public getPickleNamesInOrderOfExecution(): string[] {
    method getPickleStep (line 85) | public getPickleStep(
    method getHookByName (line 94) | public getHookByName(hookName: string): messages.Hook {
    method getHookExecutions (line 104) | public getHookExecutions(
    method getTestCaseResult (line 114) | public getTestCaseResult(pickleName: string): messages.TestStepResult {
    method getTestStepResults (line 130) | public getTestStepResults(
    method getAttachmentsForStep (line 165) | public getAttachmentsForStep(
    method getAttachmentsForHook (line 189) | public getAttachmentsForHook(
    method getPickle (line 203) | private getPickle(pickleName: string): messages.Pickle {
    method getGherkinDocument (line 217) | private getGherkinDocument(uri: string): messages.GherkinDocument {
    method getTestCase (line 231) | private getTestCase(pickleId: string): messages.TestCase {
    method getTestCaseStarted (line 245) | private getTestCaseStarted(
    method getPickleStepByStepText (line 265) | private getPickleStepByStepText(
    method getTestStepAttachments (line 277) | private getTestStepAttachments(

FILE: cucumber-tsflow-specs/src/support/runner.ts
  type IRunResult (line 24) | interface IRunResult {
  type ILastRun (line 32) | interface ILastRun {
  class TestRunner (line 42) | class TestRunner {
    method lastRun (line 57) | public get lastRun(): ILastRun {
    method extractor (line 62) | public get extractor(): Extractor {
    method run (line 66) | public async run(

FILE: cucumber-tsflow-specs/src/support/testDir.ts
  class TestDir (line 5) | class TestDir {
    method path (line 8) | public get path(): string {
    method path (line 13) | public set path(newPath: string) {
    method readFileStream (line 17) | public readFileStream(
    method writeFileStream (line 29) | public writeFileStream(...pathParts: string[] | [string[]]): WriteStre...
    method readFile (line 35) | public readFile(...pathParts: string[] | [string[]]): string | null {
    method writeFile (line 45) | public writeFile(pathParts: string | string[], data: string): void {
    method mkdir (line 59) | public mkdir(...pathParts: string[] | [string[]]): string {
    method getPath (line 65) | private getPath(...pathParts: string[] | [string[]]): string {

FILE: cucumber-tsflow/src/binding-decorator.ts
  type WritableWorld (line 34) | interface WritableWorld extends World {
  constant SCENARIO_CONTEXT_SLOTNAME (line 42) | const SCENARIO_CONTEXT_SLOTNAME: string = "__SCENARIO_CONTEXT";
  function ensureNoCyclicDependencies (line 56) | function ensureNoCyclicDependencies(target: any, currentPath: any[] = []) {
  function binding (line 91) | function binding(requiredContextTypes?: ContextType[]): TypeDecorator {
  function getContextFromWorld (line 138) | function getContextFromWorld(world: World): ScenarioContext {
  function getBindingFromWorld (line 152) | function getBindingFromWorld<T extends ContextType>(
  function ensureWorldIsInitialized (line 161) | function ensureWorldIsInitialized() {
  function bindStepDefinition (line 254) | function bindStepDefinition(stepBinding: StepBinding): boolean {
  function bindHook (line 310) | function bindHook(stepBinding: StepBinding): void {

FILE: cucumber-tsflow/src/binding-registry.ts
  type TargetBinding (line 9) | interface TargetBinding {
  constant DEFAULT_STEP_PATTERN (line 24) | const DEFAULT_STEP_PATTERN: string = "/.*/";
  constant DEFAULT_TAG (line 29) | const DEFAULT_TAG: string = "*";
  class BindingRegistry (line 34) | class BindingRegistry {
    method instance (line 44) | public static get instance(): BindingRegistry {
    method registerContextTypesForTarget (line 65) | public registerContextTypesForTarget(
    method getContextTypesForTarget (line 95) | public getContextTypesForTarget(targetPrototype: any): ContextType[] {
    method registerStepBinding (line 110) | public registerStepBinding(stepBinding: StepBinding): void {
    method getStepBindingsForTarget (line 179) | public getStepBindingsForTarget(targetPrototype: any): StepBinding[] {
    method getStepBindings (line 196) | public getStepBindings(stepPattern: StepPattern): StepBinding[] {

FILE: cucumber-tsflow/src/hook-decorators.ts
  type HookOptions (line 12) | type HookOptions = Omit<IDefineTestCaseHookOptions, "tags"> & {
  function overloadedOption (line 16) | function overloadedOption(tag?: string | HookOptions): HookOptions {
  function createHookDecorator (line 24) | function createHookDecorator(
  function before (line 61) | function before(tagOrOption?: string | HookOptions): MethodDecorator {
  function after (line 71) | function after(tagOrOption?: string | HookOptions): MethodDecorator {
  function beforeAll (line 81) | function beforeAll(
  function afterAll (line 93) | function afterAll(options?: IDefineTestRunHookOptions): MethodDecorator {
  function beforeStep (line 103) | function beforeStep(
  function afterStep (line 115) | function afterStep(

FILE: cucumber-tsflow/src/managed-scenario-context.ts
  class ManagedScenarioContext (line 11) | class ManagedScenarioContext implements ScenarioContext {
    method constructor (line 14) | constructor(private readonly _scenarioInfo: ScenarioInfo) {}
    method scenarioInfo (line 19) | public get scenarioInfo(): ScenarioInfo {
    method getOrActivateBindingClass (line 23) | public getOrActivateBindingClass(
    method dispose (line 32) | public dispose(): void {
    method getContextInstance (line 43) | public getContextInstance(contextType: ContextType) {
    method addExternalObject (line 58) | public addExternalObject(value: unknown) {
    method activateBindingClass (line 76) | private activateBindingClass(
    method getOrActivateObject (line 96) | private getOrActivateObject(

FILE: cucumber-tsflow/src/our-callsite.ts
  class Callsite (line 7) | class Callsite {
    method constructor (line 14) | constructor(
    method capture (line 22) | public static capture(up = 1): Callsite {
    method toString (line 37) | public toString(): string {
  function callsites (line 42) | function callsites() {

FILE: cucumber-tsflow/src/provided-context.ts
  class WorldParameters (line 8) | class WorldParameters<T = any> {
    method constructor (line 9) | public constructor(public readonly value: T) {}
  class CucumberLog (line 12) | class CucumberLog {
    method constructor (line 13) | public constructor(private readonly target: ICreateLog) {}
    method log (line 15) | public log(text: string): void | Promise<void> {
  class CucumberAttachments (line 20) | class CucumberAttachments {
    method constructor (line 21) | public constructor(private readonly target: ICreateAttachment) {}
    method attach (line 27) | public attach(...args: any): void | Promise<void> {

FILE: cucumber-tsflow/src/scenario-context.ts
  type ScenarioContext (line 6) | interface ScenarioContext {

FILE: cucumber-tsflow/src/scenario-info.ts
  class ScenarioInfo (line 7) | class ScenarioInfo {
    method constructor (line 21) | constructor(
    method parseAttributeTags (line 26) | private static parseAttributeTags(tags: TagName[]): Map<string, unknow...
    method parseOptionTags (line 45) | private static parseOptionTags(tags: TagName[]): Map<string, string[]> {
    method parseFlagTags (line 70) | private static parseFlagTags(tags: TagName[]): Set<string> {
    method getAttributeTag (line 88) | public getAttributeTag(name: string): unknown | undefined {
    method getOptionTag (line 96) | public getOptionTag(name: string): string | undefined {
    method getMultiOptionTag (line 104) | public getMultiOptionTag(name: string): string[] | undefined {
    method getFlag (line 112) | public getFlag(name: string): boolean {

FILE: cucumber-tsflow/src/step-binding-flags.ts
  type StepBindingFlags (line 5) | enum StepBindingFlags {

FILE: cucumber-tsflow/src/step-binding.ts
  type StepBinding (line 8) | interface StepBinding {

FILE: cucumber-tsflow/src/step-definition-decorators.ts
  type StepOptions (line 7) | interface StepOptions {
  function overloadedOptions (line 15) | function overloadedOptions(
  function given (line 39) | function given(
  function when (line 80) | function when(
  function then (line 119) | function then(

FILE: cucumber-tsflow/src/tag-normalization.ts
  function normalizeTag (line 1) | function normalizeTag(tag?: string): string | undefined {

FILE: cucumber-tsflow/src/types.ts
  type StepPattern (line 11) | type StepPattern = string;
  type TagName (line 16) | type TagName = string;
  type CustomContextType (line 22) | type CustomContextType = new (...args: any[]) => any;
  type ProvidedContextType (line 24) | type ProvidedContextType =
  type ContextType (line 30) | type ContextType = ProvidedContextType | CustomContextType;
  function isProvidedContextType (line 39) | function isProvidedContextType(
  type TypeDecorator (line 45) | type TypeDecorator = <T>(target: new (...args: any[]) => T) => void;
Condensed preview — 54 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (171K chars).
[
  {
    "path": ".build/setPackageVersion.js",
    "chars": 592,
    "preview": "const nbgv = require(\"nerdbank-gitversioning\");\n\nconst setPackageVersionAndBuildNumber = (versionInfo) => {\n  // Set a b"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 666,
    "preview": "name: CI\non:\n  push:\n    branches: [master, release/**]\n  pull_request:\n    branches: [master, release/**]\njobs:\n  # Bui"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 2385,
    "preview": "#\n# This workflow creates a release from a specified branch. The Package version is managed\n# by Nerdbank Gitversioning "
  },
  {
    "path": ".github/workflows/stale.yml",
    "chars": 1265,
    "preview": "name: \"Stale issue handler\"\n\non:\n  workflow_dispatch:\n  schedule:\n    - cron: \"0 0 * * *\"\n\npermissions:\n  contents: writ"
  },
  {
    "path": ".gitignore",
    "chars": 51,
    "preview": "node_modules\ndist\ntmp/\n.idea/\ntsconfig.tsbuildinfo\n"
  },
  {
    "path": ".npm-upgrade.json",
    "chars": 142,
    "preview": "{\n  \"ignore\": {\n    \"@cucumber/cucumber\": {\n      \"versions\": \"^8\",\n      \"reason\": \"Mantain compatibility with cucumber"
  },
  {
    "path": ".run/All Tests.run.xml",
    "chars": 748,
    "preview": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"false\" name=\"All Tests\" type=\"cucumber.js\" f"
  },
  {
    "path": ".run/Template Cucumber.js.run.xml",
    "chars": 686,
    "preview": "<component name=\"ProjectRunConfigurationManager\">\n  <configuration default=\"true\" type=\"cucumber.js\" factoryName=\"Cucumb"
  },
  {
    "path": ".vscode/launch.json",
    "chars": 756,
    "preview": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Specs\",\n      \"type\": \"node\",\n      \"request\": \"launc"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 57,
    "preview": "{\n  \"typescript.tsdk\": \"node_modules\\\\typescript\\\\lib\"\n}\n"
  },
  {
    "path": "CONTRIBUTE.md",
    "chars": 995,
    "preview": "The project should set-up all of its inner links and bindings when you first install it.\n\nRun the tests locally to ensur"
  },
  {
    "path": "LICENSE",
    "chars": 1091,
    "preview": "MIT License\n\nCopyright (c) 2018-2020 Tim Roberts and contributors.\n\nPermission is hereby granted, free of charge, to any"
  },
  {
    "path": "README.md",
    "chars": 10768,
    "preview": "# cucumber-tsflow\n\n![CI](https://github.com/timjroberts/cucumber-js-tsflow/workflows/CI/badge.svg)\n\nProvides 'SpecFlow' "
  },
  {
    "path": "cucumber-tsflow/.npmignore",
    "chars": 81,
    "preview": "*.ts\ntsconfig.json\ntypings.json\ntypings\n.npmignore\n*.tsbuildinfo\n!dist/**/*.d.ts\n"
  },
  {
    "path": "cucumber-tsflow/package.json",
    "chars": 802,
    "preview": "{\n  \"name\": \"cucumber-tsflow\",\n  \"description\": \"Provides 'specflow' like bindings for CucumberJS 7.0.0+ in TypeScript 1"
  },
  {
    "path": "cucumber-tsflow/src/binding-decorator.ts",
    "chars": 11131,
    "preview": "import {\n  After,\n  AfterAll,\n  AfterStep,\n  Before,\n  BeforeAll,\n  BeforeStep,\n  Given,\n  Then,\n  When,\n  World,\n} from"
  },
  {
    "path": "cucumber-tsflow/src/binding-registry.ts",
    "chars": 5619,
    "preview": "import logger from \"./logger\";\n\nimport { StepBinding } from \"./step-binding\";\nimport { ContextType, StepPattern } from \""
  },
  {
    "path": "cucumber-tsflow/src/hook-decorators.ts",
    "chars": 3551,
    "preview": "import {\n  IDefineTestCaseHookOptions,\n  IDefineTestRunHookOptions,\n  IDefineTestStepHookOptions,\n} from \"@cucumber/cucu"
  },
  {
    "path": "cucumber-tsflow/src/index.ts",
    "chars": 222,
    "preview": "export * from \"./binding-decorator\";\nexport * from \"./hook-decorators\";\nexport * from \"./step-definition-decorators\";\nex"
  },
  {
    "path": "cucumber-tsflow/src/logger.ts",
    "chars": 114,
    "preview": "import * as log4js from \"log4js\";\n\nconst logger = log4js.getLogger(\"cucumber-js.tsflow\");\n\nexport default logger;\n"
  },
  {
    "path": "cucumber-tsflow/src/managed-scenario-context.ts",
    "chars": 2965,
    "preview": "import * as _ from \"underscore\";\nimport { BindingRegistry } from \"./binding-registry\";\nimport { ScenarioContext } from \""
  },
  {
    "path": "cucumber-tsflow/src/our-callsite.ts",
    "chars": 1479,
    "preview": "// @ts-ignore\nimport * as sourceMapSupport from \"source-map-support\";\n\n/**\n * Represents a callsite of where a step bind"
  },
  {
    "path": "cucumber-tsflow/src/provided-context.ts",
    "chars": 918,
    "preview": "/* tslint:disable:max-classes-per-file */\nimport {\n  ICreateAttachment,\n  ICreateLog,\n} from \"@cucumber/cucumber/lib/run"
  },
  {
    "path": "cucumber-tsflow/src/scenario-context.ts",
    "chars": 383,
    "preview": "import { ScenarioInfo } from \"./scenario-info\";\n\n/**\n * Provides context for the currently running Cucumber scenario.\n *"
  },
  {
    "path": "cucumber-tsflow/src/scenario-info.ts",
    "chars": 3072,
    "preview": "import logger from \"./logger\";\nimport { TagName } from \"./types\";\n\n/**\n * Provides information about a running Cucumber "
  },
  {
    "path": "cucumber-tsflow/src/step-binding-flags.ts",
    "chars": 1141,
    "preview": "// tslint:disable:no-bitwise\n/**\n * The CucumberJS step binding types.\n */\nexport enum StepBindingFlags {\n  /**\n   * No "
  },
  {
    "path": "cucumber-tsflow/src/step-binding.ts",
    "chars": 1274,
    "preview": "import type { IDefineTestStepHookOptions } from \"@cucumber/cucumber/lib/support_code_library_builder/types\";\nimport { Ca"
  },
  {
    "path": "cucumber-tsflow/src/step-definition-decorators.ts",
    "chars": 3940,
    "preview": "import { BindingRegistry } from \"./binding-registry\";\nimport logger from \"./logger\";\nimport { Callsite } from \"./our-cal"
  },
  {
    "path": "cucumber-tsflow/src/tag-normalization.ts",
    "chars": 280,
    "preview": "export function normalizeTag(tag?: string): string | undefined {\n  // Tag is not provided or already includes a @\n  if ("
  },
  {
    "path": "cucumber-tsflow/src/types.ts",
    "chars": 1109,
    "preview": "import {\n  CucumberAttachments,\n  CucumberLog,\n  WorldParameters,\n} from \"./provided-context\";\nimport { ScenarioInfo } f"
  },
  {
    "path": "cucumber-tsflow/tsconfig.json",
    "chars": 457,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"umd\",\n    \"moduleResolution\": \"node\",\n    \"target\": \"es20"
  },
  {
    "path": "cucumber-tsflow-specs/features/basic-test.feature",
    "chars": 2607,
    "preview": "Feature: Binding steps\n\n    Scenario Outline: Bind steps with <Bind Mode>\n        Given a file named \"features/a.feature"
  },
  {
    "path": "cucumber-tsflow-specs/features/cucumber-context-objects.feature",
    "chars": 6429,
    "preview": "Feature: Cucumber context objects\n\n    Scenario: Using the cucumber logger\n        Given a file named \"features/a.featur"
  },
  {
    "path": "cucumber-tsflow-specs/features/custom-context-objects.feature",
    "chars": 14705,
    "preview": "Feature: Custom context objects\n\n    Scenario: Using custom context objects to share state\n        Given a file named \"f"
  },
  {
    "path": "cucumber-tsflow-specs/features/external-context-extraction.feature",
    "chars": 7268,
    "preview": "Feature: Extracing context objects from World externally\n\n    Scenario: Failing to retrieve state from a non-initialized"
  },
  {
    "path": "cucumber-tsflow-specs/features/global-hooks.feature",
    "chars": 6655,
    "preview": "Feature: Support for Cucumber hooks\n\n    Scenario: Binding a beforeAll hook\n        Given a file named \"features/a.featu"
  },
  {
    "path": "cucumber-tsflow-specs/features/hooks.feature",
    "chars": 18512,
    "preview": "Feature: Support for Cucumber hooks\n\n    Scenario: Binding a before hook\n        Given a file named \"features/a.feature\""
  },
  {
    "path": "cucumber-tsflow-specs/features/tag-parameters.feature",
    "chars": 8307,
    "preview": "Feature: Tag parameters\n\n    Background:\n        Given a file named \"step_definitions/steps.ts\" with:\n            \"\"\"ts\n"
  },
  {
    "path": "cucumber-tsflow-specs/package.json",
    "chars": 508,
    "preview": "{\n  \"name\": \"cucumber-tsflow-specs\",\n  \"version\": \"5.0.0\",\n  \"private\": true,\n  \"description\": \"Specification for 'cucum"
  },
  {
    "path": "cucumber-tsflow-specs/src/step_definitions/cucumber_steps.ts",
    "chars": 2935,
    "preview": "import { binding, then, when } from \"cucumber-tsflow\";\nimport expect from \"expect\";\nimport { parseEnvString } from \"../s"
  },
  {
    "path": "cucumber-tsflow-specs/src/step_definitions/file_steps.ts",
    "chars": 631,
    "preview": "import { binding, given } from \"cucumber-tsflow\";\nimport { TestRunner } from \"../support/runner\";\n\n@binding([TestRunner]"
  },
  {
    "path": "cucumber-tsflow-specs/src/step_definitions/prepare.ts",
    "chars": 2919,
    "preview": "import { formatterHelpers, ITestCaseHookParameter } from \"@cucumber/cucumber\";\nimport { after, before, binding } from \"c"
  },
  {
    "path": "cucumber-tsflow-specs/src/step_definitions/scenario_steps.ts",
    "chars": 8222,
    "preview": "import { DataTable } from \"@cucumber/cucumber\";\nimport * as messages from \"@cucumber/messages\";\nimport assert from \"asse"
  },
  {
    "path": "cucumber-tsflow-specs/src/support/formatter_output_helpers.ts",
    "chars": 2874,
    "preview": "import {\n  IJsonFeature,\n  IJsonScenario,\n  IJsonStep,\n} from \"@cucumber/cucumber/lib/formatter/json_formatter\";\nimport "
  },
  {
    "path": "cucumber-tsflow-specs/src/support/helpers.ts",
    "chars": 9256,
    "preview": "// Adapted from:\n// https://github.com/cucumber/cucumber-js/blob/6505e61abce385787767f270b6ce2077eb3d7c1c/features/suppo"
  },
  {
    "path": "cucumber-tsflow-specs/src/support/runner.ts",
    "chars": 3064,
    "preview": "// Adapted from:\n// https://github.com/cucumber/cucumber-js/blob/6505e61abce385787767f270b6ce2077eb3d7c1c/features/suppo"
  },
  {
    "path": "cucumber-tsflow-specs/src/support/testDir.ts",
    "chars": 1886,
    "preview": "import assert from \"assert\";\nimport fs, { ReadStream, WriteStream } from \"fs\";\nimport path from \"path\";\n\nexport class Te"
  },
  {
    "path": "cucumber-tsflow-specs/tsconfig.json",
    "chars": 576,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"moduleResolution\": \"node\",\n    \"module\": \"umd\",\n    \"target\": \"es20"
  },
  {
    "path": "cucumber.js",
    "chars": 605,
    "preview": "const cucumberPkg = require(\"@cucumber/cucumber/package.json\");\n\nmodule.exports = cucumberPkg.version.startsWith(\"7.\")\n "
  },
  {
    "path": "lerna.json",
    "chars": 46,
    "preview": "{\n  \"packages\": [\"*\"],\n  \"version\": \"5.0.0\"\n}\n"
  },
  {
    "path": "package.json",
    "chars": 2019,
    "preview": "{\n  \"name\": \"cucumber-tsflow-workspace\",\n  \"version\": \"2.0.0\",\n  \"private\": true,\n  \"description\": \"Workspace for cucumb"
  },
  {
    "path": "tsconfig.json",
    "chars": 249,
    "preview": "{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"esModuleInterop\": true\n  },\n  \"files\": [],\n  \"include\""
  },
  {
    "path": "tslint.json",
    "chars": 445,
    "preview": "{\n  \"defaultSeverity\": \"error\",\n  \"extends\": [\"tslint:recommended\", \"tslint-config-prettier\"],\n  \"jsRules\": {},\n  \"rules"
  },
  {
    "path": "version.json",
    "chars": 232,
    "preview": "{\n  \"$schema\": \"https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/versio"
  }
]

About this extraction

This page contains the full source code of the timjroberts/cucumber-js-tsflow GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 54 files (155.9 KB), approximately 36.7k tokens, and a symbol index with 152 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!