├── .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-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-lock.json
├── 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-tsflow
├── .npmignore
├── README.md
├── package-lock.json
├── 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.js
├── lerna.json
├── package-lock.json
├── package.json
├── tsconfig.json
├── tslint.json
└── version.json
/.build/setPackageVersion.js:
--------------------------------------------------------------------------------
1 | const nbgv = require("nerdbank-gitversioning");
2 |
3 | const setPackageVersionAndBuildNumber = (versionInfo) => {
4 | // Set a build output value representing the NPM package version
5 | console.log(
6 | "::set-output name=package_version::" + versionInfo.npmPackageVersion,
7 | );
8 |
9 | nbgv.setPackageVersion("cucumber-tsflow");
10 | nbgv.setPackageVersion("cucumber-tsflow-specs");
11 | };
12 |
13 | const handleError = (err) =>
14 | console.error(
15 | "Failed to update the package version number. nerdbank-gitversion failed: " +
16 | err,
17 | );
18 |
19 | nbgv.getVersion().then(setPackageVersionAndBuildNumber).catch(handleError);
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches: [master, release/**]
5 | pull_request:
6 | branches: [master, release/**]
7 | jobs:
8 | # Build and Test the 'cucumber-tsflow' package
9 | build:
10 | name: Build and Test
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | cucumberVersion: ["^7", "^8", "^9", "^10", "^11"]
15 | steps:
16 | - uses: actions/checkout@v2
17 | - uses: actions/setup-node@v3
18 | with:
19 | node-version: 22
20 | - name: Install npm packages
21 | run: |-
22 | npm ci
23 | npm install @cucumber/cucumber@${{ matrix.cucumberVersion }}
24 | - name: Build
25 | run: npm run build
26 | - name: Run specification tests
27 | run: npm test
28 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | #
2 | # This workflow creates a release from a specified branch. The Package version is managed
3 | # by Nerdbank Gitversioning based on configuration held in 'version.json' file.
4 | #
5 | name: Release
6 | on:
7 | workflow_dispatch:
8 |
9 | jobs:
10 | # Build, Test and Pack the 'cucumber-tsflow' package
11 | build:
12 | name: Build and Test
13 | runs-on: ubuntu-latest
14 | outputs:
15 | version: ${{ steps.set_package_version.outputs.NpmPackageVersion }}
16 | releaseTag: ${{ steps.tagInfo.outputs.releaseTag }}
17 | steps:
18 | - uses: actions/checkout@v3
19 | with:
20 | # avoid shallow clone (required by Nerbank GitVersioning)
21 | fetch-depth: 0
22 | - uses: actions/setup-node@v3
23 | with:
24 | node-version: 22
25 | - name: Install npm packages
26 | run: npm ci
27 | - name: Update package version
28 | id: set_package_version
29 | uses: dotnet/nbgv@master
30 | with:
31 | stamp: cucumber-tsflow/package.json
32 | - name: Build
33 | run: npm run build
34 | - name: Create npm package
35 | run: npm pack ./cucumber-tsflow
36 | - name: Read tag info
37 | id: tagInfo
38 | run: |-
39 | echo "releaseTag=$(jq '.releaseTag // "latest"' version.json)" | tee -a $GITHUB_OUTPUT
40 | - uses: actions/upload-artifact@v4
41 | with:
42 | name: npm-package
43 | path: |
44 | cucumber-tsflow-${{ steps.set_package_version.outputs.NpmPackageVersion }}.tgz
45 |
46 | # Publish the 'cucumber-tsflow' package to npm
47 | publish:
48 | name: Publish to npm
49 | runs-on: ubuntu-latest
50 | needs: build
51 | permissions:
52 | contents: write
53 | steps:
54 | - uses: actions/setup-node@v3
55 | with:
56 | node-version: 22
57 | registry-url: "https://registry.npmjs.org"
58 | - uses: actions/download-artifact@v4
59 | name: Download npm package
60 | with:
61 | name: npm-package
62 | - name: Publish npm package
63 | run: |-
64 | npm publish \
65 | cucumber-tsflow-${{ needs.build.outputs.version }}.tgz \
66 | --tag ${{ needs.build.outputs.releaseTag }}
67 | env:
68 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
69 | - name: Publish GitHub release
70 | uses: ncipollo/release-action@v1
71 | with:
72 | tag: ${{ needs.build.outputs.version }}
73 | commit: ${{ github.sha }}
74 | artifacts: cucumber-tsflow-${{ needs.build.outputs.version }}.tgz
75 | generateReleaseNotes: true
76 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: "Stale issue handler"
2 |
3 | on:
4 | workflow_dispatch:
5 | schedule:
6 | - cron: "0 0 * * *"
7 |
8 | permissions:
9 | contents: write # only for delete-branch option
10 | issues: write
11 | pull-requests: write
12 |
13 | jobs:
14 | stale:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/stale@v6
18 | id: stale
19 | with:
20 | days-before-stale: 60
21 | days-before-close: 7
22 |
23 | 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."
24 | close-issue-message: "There hasn't been any activity on this issue for 67 days. Closing it as Spoiled."
25 | stale-issue-label: stale
26 | close-issue-label: spoiled
27 | exempt-issue-labels: "blocked,discussion,good first issue"
28 |
29 | 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."
30 | close-pr-message: "There hasn't been any activity on this PR for 67 days. Closing it as Spoiled."
31 | stale-pr-label: stale
32 | close-pr-label: spoiled
33 | exempt-pr-labels: "blocked,discussion"
34 |
35 | - name: Print outputs
36 | run: echo ${{ join(steps.stale.outputs.*, ',') }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | tmp/
4 | .idea/
5 | tsconfig.tsbuildinfo
6 |
--------------------------------------------------------------------------------
/.npm-upgrade.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignore": {
3 | "@cucumber/cucumber": {
4 | "versions": "^8",
5 | "reason": "Mantain compatibility with cucumber 7 and 8"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.run/All Tests.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.run/Template Cucumber.js.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Specs",
6 | "type": "node",
7 | "request": "launch",
8 | "program": "${workspaceRoot}/node_modules/cucumber/bin/cucumber-js",
9 | "stopOnEntry": true,
10 | "args": ["--require", "./cucumber-tsflow-specs/dist"],
11 | "cwd": "${workspaceRoot}",
12 | "runtimeExecutable": null,
13 | "runtimeArgs": ["--nolazy"],
14 | "env": {
15 | "NODE_ENV": "development"
16 | },
17 | "externalConsole": false,
18 | "sourceMaps": true,
19 | "outDir": null
20 | },
21 | {
22 | "name": "Attach",
23 | "type": "node",
24 | "request": "attach",
25 | "port": 5858,
26 | "sourceMaps": false,
27 | "outDir": null,
28 | "localRoot": "${workspaceRoot}",
29 | "remoteRoot": null
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules\\typescript\\lib"
3 | }
4 |
--------------------------------------------------------------------------------
/CONTRIBUTE.md:
--------------------------------------------------------------------------------
1 | The project should set-up all of its inner links and bindings when you first install it.
2 |
3 | Run the tests locally to ensure everything is properly configured.
4 |
5 | ```terminal
6 | > git clone https://github.com/timjroberts/cucumber-js-tsflow.git
7 | > cd cucumber-js-tsflow
8 | > npm install
9 | > npm test
10 | ```
11 |
12 | ## Setting up Run/Debug in IDE
13 |
14 | For IntelliJ, a run configuration is stored in `.run/cucumber-js.run.xml` to run/debug the tests.
15 |
16 | For other IDE, using the following runtime config for node:
17 |
18 | - working dir: `cucumber-tsflow-spec`
19 | - node-parameters: `--require ts-node/register `
20 | - js script to run: `node_modules/@cucumber/cucumber/bin/cucumber-js`
21 | - application parameters: `features/**/*.feature --require "src/step_definitions/**/*.ts" `
22 |
23 | An example command line runner:
24 |
25 | ```shell script
26 | "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
27 | ```
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018-2020 Tim Roberts and contributors.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cucumber-tsflow
2 |
3 | 
4 |
5 | Provides 'SpecFlow' like bindings for CucumberJS in TypeScript 1.7+.
6 |
7 | ## Table of content
8 |
9 | See that menu icon to the left of "README.md"?
10 |
11 | Did you know that every markdown file in GitHub with more than two headings
12 | have that icon as a Table of Content linking to every heading?
13 |
14 | ## Quick Start
15 |
16 | cucumber-tsflow uses TypeScript Decorators to create SpecFlow like bindings for
17 | TypeScript classes and methods that allow those classes and methods to be used
18 | in your CucumberJS support files. As such, cucumber-tsflow has a peer dependency
19 | on CucumberJS, and you still run your specifications using the cucumber
20 | command line tool.
21 |
22 | ### Install cucumber and cucumber-tsflow
23 |
24 | ```bash
25 | npm install @cucumber/cucumber cucumber-tsflow
26 | ```
27 |
28 | ### Create .feature files to describe your specifications
29 |
30 | By default, CucumberJS looks for .feature files in a folder called 'features',
31 | so create that folder and then create a new file called `my_feature.feature`:
32 |
33 | ```gherkin
34 | # features/my_feature.feature
35 |
36 | Feature: Example Feature
37 | This is an example feature
38 |
39 | Scenario: Adding two numbers
40 | Given I enter '2' and '8'
41 | Then I receive the result '10'
42 | ```
43 |
44 | ### Create the Support Files to support the Feature
45 |
46 | CucumberJS requires Support Files defining what each step in the Feature files mean.
47 |
48 | By default, CucumberJS looks for Support Files beneath the 'features' folder.
49 | We need to write step definitions to support the two steps that we created above.
50 |
51 | Create a new `ArithmeticSteps.ts` file:
52 |
53 | ```ts
54 | // features/ArithmeticSteps.ts
55 |
56 | import { binding, given, then } from "cucumber-tsflow";
57 |
58 | @binding()
59 | class ArithmeticSteps {
60 | private computedResult: number;
61 |
62 | @given(/I enter '(\d*)' and '(\d*)'/)
63 | public givenTwoNumbers(num1: string, num2: string): void {
64 | this.computedResult = parseInt(num1) + parseInt(num2);
65 | }
66 |
67 | @then(/I receive the result '(\d*)'/)
68 | public thenResultReceived(expectedResult: string): void {
69 | if (parseInt(expectedResult) !== this.computedResult) {
70 | throw new Error("Arithmetic Error");
71 | }
72 | }
73 | }
74 |
75 | export = ArithmeticSteps;
76 | ```
77 |
78 | Note how the cucumber-tsflow Decorators are being used to bind the methods in
79 | the class. During runtime, these Decorators simply call the Cucumber code on
80 | your behalf in order to register callbacks with Given(), When(), Then(), etc.
81 |
82 | The callbacks that are being registered with Cucumber are wrappers around your
83 | bound class. This allows you to maintain a state between each step on the same
84 | class by using instance properties.
85 |
86 | In this quick example, the entire test state is encapsulated directly in the class.
87 | As your test suite grows larger and step definitions get shared between
88 | multiple classes, you can use 'Context Injection' to share state between
89 | running step definitions (see below).
90 |
91 | ### Compiling your TypeScript Support Code
92 |
93 | To use `cucumber-tsflow` with TypeScript, you'll also need a `tsconfig.json` file
94 | with these options:
95 |
96 | ```json
97 | {
98 | "compilerOptions": {
99 | "moduleResolution": "node",
100 | "experimentalDecorators": true
101 | }
102 | }
103 | ```
104 |
105 | > Hint: You can add that to `features/tsconfig.json` to have it applied only for
106 | > your integration tests.
107 |
108 | With the TS config in place, CucumberJS should automatically compile your code
109 | before running it.
110 |
111 | ## Reference
112 |
113 | ### Bindings
114 |
115 | Bindings provide the automation that connects a specification step in a Gherkin
116 | feature file to some code that executes for that step.
117 | When using Cucumber with TypeScript you can define this automation using the
118 | `binding` decorator on top of a class:
119 |
120 | ```ts
121 | import { binding } from "cucumber-tsflow";
122 |
123 | @binding()
124 | class MySteps {
125 | // ...
126 | }
127 |
128 | export = MySteps;
129 | ```
130 |
131 | Through this reference, classes decorated with the `binding` decorator are
132 | referred "binding classes".
133 |
134 | _Note_: You must use the `export = ;` due to how Cucumber interprets
135 | the exported items of a Support File.
136 |
137 | ### Step Definitions
138 |
139 | Step definitions can be bound to automation code in a binding class by decorating
140 | a public function with a 'given', 'when' or 'then' binding decorator:
141 |
142 | ```ts
143 | import { binding, given, when, then } from "cucumber-tsflow";
144 |
145 | @binding()
146 | class MySteps {
147 | @given(/I perform a search using the value "([^"]*)"/)
148 | public givenAValueBasedSearch(searchValue: string): void {
149 | // ...
150 | }
151 | }
152 |
153 | export = MySteps;
154 | ```
155 |
156 | The methods have the same requirements and guarantees of functions you would normally
157 | supply to Cucumber, which means that the methods may be:
158 |
159 | - Synchronous by returning `void`
160 | - Asynchronous by receiving and using a callback as the last parameter\
161 | The callback has signature `() => void`
162 | - Asynchronous by returning a `Promise`
163 |
164 | The step definition functions must always receive a pattern as the first argument,
165 | which can be either a string or a regular expression.
166 |
167 | Additionally, a step definition may receive additional options in the format:
168 |
169 | ```ts
170 | @binding()
171 | class MySteps {
172 | @given("pattern", {
173 | tag: "not @expensive",
174 | timeout: 1000,
175 | wrapperOptions: {},
176 | })
177 | public givenAValueBasedSearch(searchValue: string): void {
178 | // ...
179 | }
180 | }
181 | ```
182 |
183 | For backward compatibility, the `tag` and `timeout` options can also be passed
184 | as direct arguments:
185 |
186 | ```ts
187 | @binding()
188 | class MySteps {
189 | @given("pattern", "not @expensive", 1000)
190 | public givenAValueBasedSearch(searchValue: string): void {
191 | // ...
192 | }
193 | }
194 | ```
195 |
196 | ### Hooks
197 |
198 | Hooks can be used to add logic that happens before or after each scenario execution.
199 | They are configured in the same way as the [Step Definitions](#step-definitions).
200 |
201 | ```typescript
202 | import { binding, before, beforeAll, after, afterAll } from "cucumber-tsflow";
203 |
204 | @binding()
205 | class MySteps {
206 | @beforeAll()
207 | public static beforeAllScenarios(): void {
208 | // ...
209 | }
210 |
211 | @afterAll()
212 | public static beforeAllScenarios(): void {
213 | // ...
214 | }
215 |
216 | @before()
217 | public beforeAllScenarios(): void {
218 | // ...
219 | }
220 |
221 | @after()
222 | public afterAllScenarios(): void {
223 | // ...
224 | }
225 | }
226 |
227 | export = MySteps;
228 | ```
229 |
230 | Contrary to the Step Definitions, Hooks don't need a pattern since they don't
231 | run for some particular step, but once for each scenario.
232 |
233 | Hooks can receive aditional options just like the Step Definitions:
234 |
235 | ```ts
236 | @binding()
237 | class MySteps {
238 | // Runs before each scenarios with tag `@requireTempDir` with 2 seconds of timeout
239 | @before({ tag: "@requireTempDir", timeout: 2000 })
240 | public async beforeAllScenariosRequiringTempDirectory(): Promise {
241 | let tempDirInfo = await this.createTemporaryDirectory();
242 | // ...
243 | }
244 |
245 | // Runs after each scenarios with tag `@requireTempDir` with 2 seconds of timeout
246 | @after({ tag: "@requireTempDir", timeout: 2000 })
247 | public async afterAllScenariosRequiringTempDirectory(): void {
248 | await this.deleteTemporaryDirectory();
249 | // ...
250 | }
251 | }
252 | ```
253 |
254 | For backward compatibility, the `tag` option can also be passes as a direct argument:
255 |
256 | ```ts
257 | @binding()
258 | class MySteps {
259 | @before('@local')
260 | public async runForLocalOnly(): Promise {
261 | ...
262 | }
263 | }
264 | ```
265 |
266 | ### Step and hook options
267 |
268 | #### Tag filters
269 |
270 | Both Step Definitions and Hooks can receive a `tag` option. This option defines
271 | a filter such that the binding will only be considered for scenarios matching
272 | the filter.
273 |
274 | The syntax of the tag filter is
275 | a ["Tag expression"](https://cucumber.io/docs/cucumber/api/?lang=javascript#tag-expressions)
276 | specified by Cucumber.
277 |
278 | **Note**: The tag might be set for the `Feature` or for the `Scenario`, and there
279 | is no distinction between them. This is
280 | called ["Tag Inheritance"](https://cucumber.io/docs/cucumber/api/?lang=javascript#tag-inheritance).
281 |
282 | For backward compatibility, setting a tag to a single word is treated the same
283 | as a filter for that word as a tag:
284 |
285 | ```ts
286 | // This backward compatible format
287 | @given({ tag: 'foo' })
288 |
289 | // Is transformed into this
290 | @given({ tag: '@foo' })
291 | ```
292 |
293 | #### Timeout
294 |
295 | Both Step Definition and Hooks can receive a `timeout` option. This option defines
296 | the maximum runtime allowed for the binding before it is flagged as failed.
297 |
298 | `cucumber-tsflow` currently doesn't have a way to define a global default step timeout,
299 | but it can be easily done through CucumberJS' `setDefaultTimeout` function.
300 |
301 | #### Passing WrapOptions
302 |
303 | In step definition, we can passing additional wrapper options to CucumberJS.
304 |
305 | For example:
306 |
307 | ```typescript
308 | @given(/I perform a search using the value "([^"]*)"/, { wrapperOptions: { retry: 2 } })
309 | public
310 | givenAValueBasedSearch(searchValue
311 | :
312 | string
313 | ):
314 | void {
315 | ...
316 | }
317 | ```
318 |
319 | The type of `wrapperOptions` is defined by the function given to `setDefinitionFunctionWrapper`.
320 |
321 | **Note**: `wrapperOptions` and `setDefinitionFunctionWrapper` were deprecated in
322 | [CucumberJS 7.3.1](https://github.com/cucumber/cucumber-js/blob/8900158748a3f36c4b2fa5d172fe27013b39ab17/CHANGELOG.md#731---2021-07-20)
323 | and are kept here for backward compatibility only while this library supports
324 | CucumberJS 7.
325 |
326 | ### Sharing Data between Bindings
327 |
328 | #### Context Injection
329 |
330 | Like 'SpecFlow', `cucumber-tsflow` supports a simple dependency injection
331 | framework that will instantitate and inject class instances into binding classes
332 | for each executing scenario.
333 |
334 | To use context injection:
335 |
336 | - Create simple classes representing the shared data and/or behavior.\
337 | These classes **must** have public constructors with no arguments (default constructors).
338 | Defining a class with no constructor at all also works.
339 | - Define a constructor on the binding classes that receives an instance of
340 | the class defined above as an parameter.
341 | - Update the `@binding()` decorator to indicate the types of context objects
342 | that are required by the binding class
343 |
344 | ```ts
345 | // Workspace.ts
346 |
347 | export class Workspace {
348 | public folder: string = "default folder";
349 |
350 | public updateFolder(folder: string) {
351 | this.folder = folder;
352 | }
353 | }
354 |
355 | // my-steps.ts
356 | import { binding, before, after } from "cucumber-tsflow";
357 | import { Workspace } from "./Workspace";
358 |
359 | @binding([Workspace])
360 | class MySteps {
361 | public constructor(protected workspace: Workspace) {}
362 |
363 | @before("requireTempDir")
364 | public async beforeAllScenariosRequiringTempDirectory(): Promise {
365 | let tempDirInfo = await this.createTemporaryDirectory();
366 |
367 | this.workspace.updateFolder(tempDirInfo);
368 | }
369 | }
370 |
371 | export = MySteps;
372 | ```
373 |
374 | #### Provided Context Types
375 |
376 | This library provides 3 Context Types to interact with CucumberJS' World object.
377 |
378 | - `WorldParameters`, which expose value passed to the `worldParameters` configuration
379 | or the `--world-parameters` CLI option.
380 | - `CucumberLog`, which exposes the `log` method of the `World` object.
381 | - `CucumberAttachments`, which exposes the `attach` method of the `World` object.
382 | - `ScenarioInfo`, which exposes information about the running scenario and allows
383 | changing the behavior of steps and hooks based on tags easier.
384 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/features/basic-test.feature:
--------------------------------------------------------------------------------
1 | Feature: Binding steps
2 |
3 | Scenario Outline: Bind steps with
4 | Given a file named "features/a.feature" with:
5 | """feature
6 | Feature: some feature
7 | Scenario: scenario a
8 | Given step one
9 | When step two
10 | Then step three
11 | """
12 | And a file named "step_definitions/steps.ts" with:
13 | """ts
14 | import {binding, given, when, then} from 'cucumber-tsflow';
15 |
16 | @binding()
17 | class Steps {
18 | @given()
19 | public given() {
20 | console.log("Step one executed");
21 | }
22 |
23 | @when()
24 | public when() {
25 | console.log("Step two executed");
26 | }
27 |
28 | @then()
29 | public then() {
30 | console.log("Step three executed");
31 | }
32 | }
33 |
34 | export = Steps;
35 | """
36 | When I run cucumber-js
37 | Then it passes
38 | And the output contains "Step one executed"
39 | And the output contains "Step two executed"
40 | And the output contains "Step three executed"
41 |
42 | Examples:
43 | | Bind Mode | Step 1 | Step 2 | Step 3 |
44 | | names | "step one" | "step two" | "step three" |
45 | | regex | /^step one$/ | /^step two$/ | /^step three$/ |
46 |
47 | Scenario: Failing test
48 | Given a file named "features/a.feature" with:
49 | """feature
50 | Feature: Some feature
51 | Scenario: example
52 | Given a step
53 | """
54 | And a file named "step_definitions/steps.ts" with:
55 | """ts
56 | import {binding, given} from 'cucumber-tsflow';
57 |
58 | @binding()
59 | class Step {
60 | @given("a step")
61 | public step() {
62 | throw new Error("Inner error message.");
63 | }
64 | }
65 |
66 | export = Step;
67 | """
68 | When I run cucumber-js
69 | Then it fails
70 | And the output contains "Error: Inner error message."
71 |
72 | Scenario: Missing step definition
73 | Given a file named "features/a.feature" with:
74 | """feature
75 | Feature: some feature
76 | Scenario: scenario a
77 | Given missing step
78 | """
79 | When I run cucumber-js
80 | Then it fails
81 | # TODO: https://github.com/timjroberts/cucumber-js-tsflow/issues/97
82 | And the output contains "Implement with the following snippet:"
83 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/features/cucumber-context-objects.feature:
--------------------------------------------------------------------------------
1 | Feature: Cucumber context objects
2 |
3 | Scenario: Using the cucumber logger
4 | Given a file named "features/a.feature" with:
5 | """feature
6 | Feature: Feature
7 | Scenario: example
8 | Given a step
9 | And another step
10 | """
11 | And a file named "step_definitions/steps.ts" with:
12 | """ts
13 | import {binding, given, CucumberLog} from 'cucumber-tsflow';
14 |
15 | @binding([CucumberLog])
16 | class Steps {
17 | public constructor(private readonly logger: CucumberLog) {}
18 |
19 | @given("a step")
20 | public one() {
21 | this.logger.log("logged value");
22 | }
23 |
24 | @given("another step")
25 | public noop() {}
26 | }
27 |
28 | export = Steps;
29 | """
30 | When I run cucumber-js
31 | Then it passes
32 | And scenario "example" step "Given a step" has the logs:
33 | | logged value |
34 | And scenario "example" step "And another step" has no attachments
35 |
36 | Scenario: Using the cucumber attachments
37 | Given a file named "features/a.feature" with:
38 | """feature
39 | Feature: Feature
40 | Scenario: example
41 | Given a step
42 | And another step
43 | """
44 | And a file named "step_definitions/steps.ts" with:
45 | """ts
46 | import {binding, given, CucumberAttachments} from 'cucumber-tsflow';
47 |
48 | @binding([CucumberAttachments])
49 | class Steps {
50 | public constructor(private readonly att: CucumberAttachments) {}
51 |
52 | @given("a step")
53 | public one() {
54 | this.att.attach("my string", "text/plain+custom");
55 | }
56 |
57 | @given("another step")
58 | public noop() {}
59 | }
60 |
61 | export = Steps;
62 | """
63 | When I run cucumber-js
64 | Then it passes
65 | And scenario "example" step "Given a step" has the attachments:
66 | | DATA | MEDIA TYPE | MEDIA ENCODING |
67 | | my string | text/plain+custom | IDENTITY |
68 | And scenario "example" step "And another step" has no attachments
69 |
70 | Scenario: Using the cucumber attachments in hooks
71 | Given a file named "features/a.feature" with:
72 | """feature
73 | Feature: Feature
74 | Scenario: example
75 | Given a step
76 | """
77 | And a file named "step_definitions/steps.ts" with:
78 | """ts
79 | import {binding, before, after, given, CucumberAttachments} from 'cucumber-tsflow';
80 |
81 | @binding([CucumberAttachments])
82 | class Steps {
83 | public constructor(private readonly att: CucumberAttachments) {}
84 |
85 | @before()
86 | public before() {
87 | this.att.attach("my first string", "text/plain+custom");
88 | }
89 |
90 | @given("a step")
91 | public one() {}
92 |
93 | @after()
94 | public after() {
95 | this.att.attach("my second string", "text/plain+custom");
96 | }
97 | }
98 |
99 | export = Steps;
100 | """
101 | When I run cucumber-js
102 | Then it passes
103 | And scenario "example" "Before" hook has the attachments:
104 | | DATA | MEDIA TYPE | MEDIA ENCODING |
105 | | my first string | text/plain+custom | IDENTITY |
106 | And scenario "example" "After" hook has the attachments:
107 | | DATA | MEDIA TYPE | MEDIA ENCODING |
108 | | my second string | text/plain+custom | IDENTITY |
109 | And scenario "example" step "Given a step" has no attachments
110 |
111 | Scenario: Using world parameters
112 | Given a file named "cucumber.js" with:
113 | """js
114 | const cucumberPkg = require("@cucumber/cucumber/package.json");
115 |
116 | module.exports = cucumberPkg.version.startsWith("7.")
117 | ? {
118 | default: [
119 | "--world-parameters '{\"name\":\"Earth\"}'"
120 | ].join(" ")
121 | }
122 | : {
123 | default: {
124 | worldParameters: {
125 | name: 'Earth'
126 | }
127 | }
128 | };
129 | """
130 | Given a file named "features/a.feature" with:
131 | """feature
132 | Feature: Feature
133 | Scenario: example
134 | Then the world name is "Earth"
135 | """
136 | And a file named "step_definitions/steps.ts" with:
137 | """ts
138 | import {binding, then, WorldParameters} from 'cucumber-tsflow';
139 | import * as assert from 'node:assert';
140 |
141 | @binding([WorldParameters])
142 | class Steps {
143 | public constructor(private readonly world: WorldParameters) {}
144 |
145 | @then("the world name is {string}")
146 | public checkWorldName(name: string) {
147 | assert.deepStrictEqual(this.world.value, {name})
148 | }
149 | }
150 |
151 | export = Steps;
152 | """
153 |
154 | When I run cucumber-js
155 | Then it passes
156 |
157 | Scenario: Reading the scenario information
158 | Given a file named "features/a.feature" with:
159 | """feature
160 | @foo
161 | Feature: Feature
162 | @bar
163 | Scenario: example
164 | Then the scenario title is "example"
165 | And the tags are [ "@foo", "@bar" ]
166 | """
167 | And a file named "step_definitions/steps.ts" with:
168 | """ts
169 | import {binding, then, ScenarioInfo} from 'cucumber-tsflow';
170 | import * as assert from 'node:assert';
171 |
172 | @binding([ScenarioInfo])
173 | class Steps {
174 | public constructor(private readonly scenario: ScenarioInfo) {}
175 |
176 | @then("the scenario title is {string}")
177 | public checkScenarioName(name: string) {
178 | assert.strictEqual(this.scenario.scenarioTitle, name);
179 | }
180 |
181 | @then("the tags are {}")
182 | public checkTags(tags: string) {
183 | assert.deepStrictEqual(this.scenario.tags, JSON.parse(tags));
184 | }
185 | }
186 |
187 | export = Steps;
188 | """
189 |
190 | When I run cucumber-js
191 | Then it passes
192 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/features/custom-context-objects.feature:
--------------------------------------------------------------------------------
1 | Feature: Custom context objects
2 |
3 | Scenario: Using custom context objects to share state
4 | Given a file named "features/a.feature" with:
5 | """feature
6 | Feature: some feature
7 | Scenario: scenario a
8 | Given the state is "initial value"
9 | When I set the state to "step value"
10 | Then the state is "step value"
11 | """
12 | And a file named "support/state.ts" with:
13 | """ts
14 | export class State {
15 | public value: string = "initial value";
16 | }
17 | """
18 | And a file named "step_definitions/one.ts" with:
19 | """ts
20 | import {State} from '../support/state';
21 | import {binding, when} from 'cucumber-tsflow';
22 |
23 | @binding([State])
24 | class Steps {
25 | public constructor(private readonly state: State) {}
26 |
27 | @when("I set the state to {string}")
28 | public setState(newValue: string) {
29 | this.state.value = newValue;
30 | }
31 | }
32 |
33 | export = Steps;
34 | """
35 | And a file named "step_definitions/two.ts" with:
36 | """ts
37 | import {State} from '../support/state';
38 | import {binding, then} from 'cucumber-tsflow';
39 | import * as assert from 'node:assert';
40 |
41 | @binding([State])
42 | class Steps {
43 | public constructor(private readonly state: State) {}
44 |
45 | @then("the state is {string}")
46 | public checkValue(value: string) {
47 | console.log(`The state is '${this.state.value}'`);
48 | assert.equal(this.state.value, value, "State value does not match");
49 | }
50 | }
51 |
52 | export = Steps;
53 | """
54 | When I run cucumber-js
55 | Then it passes
56 | And the output contains "The state is 'initial value'"
57 | And the output contains "The state is 'step value'"
58 |
59 | Scenario: Custom context objects can depend on other custom context objects two levels deep
60 | Given a file named "features/a.feature" with:
61 | """feature
62 | Feature: some feature
63 | Scenario: scenario a
64 | Given the state is "initial value"
65 | When I set the state to "step value"
66 | Then the state is "step value"
67 | """
68 | And a file named "support/level-one-state.ts" with:
69 | """ts
70 | import {binding} from 'cucumber-tsflow';
71 | import {LevelTwoState} from './level-two-state';
72 |
73 | @binding([LevelTwoState])
74 | export class LevelOneState {
75 | constructor(public levelTwoState: LevelTwoState) {
76 | }
77 | }
78 | """
79 | And a file named "support/level-two-state.ts" with:
80 | """ts
81 | export class LevelTwoState {
82 | public value: string = "initial value";
83 | }
84 | """
85 | And a file named "step_definitions/one.ts" with:
86 | """ts
87 | import {LevelTwoState} from '../support/level-two-state';
88 | import {binding, when} from 'cucumber-tsflow';
89 |
90 | @binding([LevelTwoState])
91 | class Steps {
92 | public constructor(private readonly levelTwoState: LevelTwoState) {
93 | }
94 |
95 | @when("I set the state to {string}")
96 | public setState(newValue: string) {
97 | this.levelTwoState.value = newValue;
98 | }
99 | }
100 |
101 | export = Steps;
102 | """
103 | And a file named "step_definitions/two.ts" with:
104 | """ts
105 | import {LevelOneState} from '../support/level-one-state';
106 | import {binding, then} from 'cucumber-tsflow';
107 | import * as assert from 'node:assert';
108 |
109 | @binding([LevelOneState])
110 | class Steps {
111 | public constructor(private readonly levelOneState: LevelOneState) {}
112 |
113 | @then("the state is {string}")
114 | public checkValue(value: string) {
115 | console.log(`The state is '${this.levelOneState.levelTwoState.value}'`);
116 | assert.equal(this.levelOneState.levelTwoState.value, value, "State value does not match");
117 | }
118 | }
119 |
120 | export = Steps;
121 | """
122 | When I run cucumber-js
123 | Then it passes
124 | And the output contains "The state is 'initial value'"
125 | And the output contains "The state is 'step value'"
126 |
127 | Scenario: Custom context objects can depend on other custom context objects three levels deep
128 | Given a file named "features/a.feature" with:
129 | """feature
130 | Feature: some feature
131 | Scenario: scenario a
132 | Given the state is "initial value"
133 | When I set the state to "step value"
134 | Then the state is "step value"
135 | """
136 | And a file named "support/level-one-state.ts" with:
137 | """ts
138 | import {binding} from 'cucumber-tsflow';
139 | import {LevelTwoState} from './level-two-state';
140 |
141 | @binding([LevelTwoState])
142 | export class LevelOneState {
143 | constructor(public levelTwoState: LevelTwoState) {
144 | }
145 | }
146 | """
147 | And a file named "support/level-two-state.ts" with:
148 | """ts
149 | import {binding} from 'cucumber-tsflow';
150 | import {LevelThreeState} from './level-three-state';
151 |
152 | @binding([LevelThreeState])
153 | export class LevelTwoState {
154 | constructor(public levelThreeState: LevelThreeState) {
155 | }
156 | }
157 | """
158 | And a file named "support/level-three-state.ts" with:
159 | """ts
160 | export class LevelThreeState {
161 | public value: string = "initial value";
162 | }
163 | """
164 | And a file named "step_definitions/one.ts" with:
165 | """ts
166 | import {LevelThreeState} from '../support/level-three-state';
167 | import {binding, when} from 'cucumber-tsflow';
168 |
169 | @binding([LevelThreeState])
170 | class Steps {
171 | public constructor(private readonly levelThreeState: LevelThreeState) {
172 | }
173 |
174 | @when("I set the state to {string}")
175 | public setState(newValue: string) {
176 | this.levelThreeState.value = newValue;
177 | }
178 | }
179 |
180 | export = Steps;
181 | """
182 | And a file named "step_definitions/two.ts" with:
183 | """ts
184 | import {LevelOneState} from '../support/level-one-state';
185 | import {binding, then} from 'cucumber-tsflow';
186 | import * as assert from 'node:assert';
187 |
188 | @binding([LevelOneState])
189 | class Steps {
190 | public constructor(private readonly levelOneState: LevelOneState) {}
191 |
192 | @then("the state is {string}")
193 | public checkValue(value: string) {
194 | console.log(`The state is '${this.levelOneState.levelTwoState.levelThreeState.value}'`);
195 | assert.equal(this.levelOneState.levelTwoState.levelThreeState.value, value, "State value does not match");
196 | }
197 | }
198 |
199 | export = Steps;
200 | """
201 | When I run cucumber-js
202 | Then it passes
203 | And the output contains "The state is 'initial value'"
204 | And the output contains "The state is 'step value'"
205 |
206 | Scenario: Cyclic imports are detected and communicated to the developer
207 | Given a file named "features/a.feature" with:
208 | """feature
209 | Feature: some feature
210 | Scenario: scenario a
211 | Given the state is "initial value"
212 | When I set the state to "step value"
213 | Then the state is "step value"
214 | """
215 | And a file named "support/state-one.ts" with:
216 | """ts
217 | import {binding} from 'cucumber-tsflow';
218 | import {StateTwo} from './state-two';
219 |
220 | @binding([StateTwo])
221 | export class StateOne {
222 | constructor(public stateTwo: StateTwo) {
223 | }
224 | }
225 | """
226 | And a file named "support/state-two.ts" with:
227 | """ts
228 | import {StateOne} from './state-one';
229 | import {binding} from 'cucumber-tsflow';
230 |
231 | @binding([StateOne])
232 | export class StateTwo {
233 | public value: string = "initial value";
234 | constructor(public stateOne: StateOne) {
235 | }
236 | }
237 | """
238 | And a file named "step_definitions/one.ts" with:
239 | """ts
240 | import {StateTwo} from '../support/state-two';
241 | import {binding, when} from 'cucumber-tsflow';
242 |
243 | @binding([StateTwo])
244 | class Steps {
245 | public constructor(private readonly stateTwo: StateTwo) {
246 | }
247 |
248 | @when("I set the state to {string}")
249 | public setState(newValue: string) {
250 | this.stateTwo.value = newValue;
251 | }
252 | }
253 |
254 | export = Steps;
255 | """
256 | And a file named "step_definitions/two.ts" with:
257 | """ts
258 | import {StateOne} from '../support/state-one';
259 | import {binding, then} from 'cucumber-tsflow';
260 | import * as assert from 'node:assert';
261 |
262 | @binding([StateOne])
263 | class Steps {
264 | public constructor(private readonly stateOne: StateOne) {}
265 |
266 | @then("the state is {string}")
267 | public checkValue(value: string) {
268 | console.log(`The state is '${this.stateOne.stateTwo.value}'`);
269 | assert.equal(this.stateOne.stateTwo.value, value, "State value does not match");
270 | }
271 | }
272 |
273 | export = Steps;
274 | """
275 | When I run cucumber-js
276 | Then it fails
277 | And the error output contains text:
278 | """
279 | Error: Undefined dependency detected in StateOne. You possibly have an import cycle.
280 | See https://nodejs.org/api/modules.html#modules_cycles
281 | """
282 |
283 | Scenario: Cyclic state dependencies are detected and communicated to the developer
284 | Given a file named "features/a.feature" with:
285 | """feature
286 | Feature: some feature
287 | Scenario: scenario a
288 | Given the state is "initial value"
289 | When I set the state to "step value"
290 | Then the state is "step value"
291 | """
292 | And a file named "support/state.ts" with:
293 | """ts
294 | import {binding} from 'cucumber-tsflow';
295 |
296 | export class StateOne {
297 | constructor(public stateTwo: StateTwo) { }
298 | }
299 |
300 | @binding([StateOne])
301 | export class StateTwo {
302 | public value: string = "initial value";
303 | constructor(public stateOne: StateOne) { }
304 | }
305 |
306 | exports.StateOne = binding([StateTwo])(StateOne);
307 | """
308 | And a file named "step_definitions/one.ts" with:
309 | """ts
310 | import {StateTwo} from '../support/state';
311 | import {binding, when} from 'cucumber-tsflow';
312 |
313 | @binding([StateTwo])
314 | class StepsOne {
315 | public constructor(private readonly stateTwo: StateTwo) {
316 | }
317 |
318 | @when("I set the state to {string}")
319 | public setState(newValue: string) {
320 | this.stateTwo.value = newValue;
321 | }
322 | }
323 |
324 | export = StepsOne;
325 | """
326 | And a file named "step_definitions/two.ts" with:
327 | """ts
328 | import {StateOne} from '../support/state';
329 | import {binding, then} from 'cucumber-tsflow';
330 | import * as assert from 'node:assert';
331 |
332 | @binding([StateOne])
333 | class StepsTwo {
334 | public constructor(private readonly stateOne: StateOne) {}
335 |
336 | @then("the state is {string}")
337 | public checkValue(value: string) {
338 | console.log(`The state is '${this.stateOne.stateTwo.value}'`);
339 | assert.equal(this.stateOne.stateTwo.value, value, "State value does not match");
340 | }
341 | }
342 |
343 | export = StepsTwo;
344 | """
345 | When I run cucumber-js
346 | Then it fails
347 | And the error output contains text:
348 | """
349 | Error: Cyclic dependency detected: StateOne -> StateTwo -> StateOne
350 | """
351 |
352 | Scenario: Cyclic single-file state dependencies are detected and communicated to the developer
353 | Given a file named "features/a.feature" with:
354 | """feature
355 | Feature: some feature
356 | Scenario: scenario a
357 | Given the state is "initial value"
358 | When I set the state to "step value"
359 | Then the state is "step value"
360 | """
361 | And a file named "support/circular.ts" with:
362 | """ts
363 | import {binding} from 'cucumber-tsflow';
364 |
365 | export class StateOne {
366 | constructor(public stateTwo: StateTwo) { }
367 | }
368 |
369 | @binding([StateOne])
370 | export class StateTwo {
371 | public value: string = "initial value";
372 | constructor(public stateOne: StateOne) { }
373 | }
374 |
375 | exports.StateOne = binding([StateTwo])(StateOne);
376 | """
377 | And a file named "step_definitions/one.ts" with:
378 | """ts
379 | import {StateTwo} from '../support/circular';
380 | import * as assert from 'node:assert';
381 | import {binding, when, then} from 'cucumber-tsflow';
382 |
383 | @binding([StateTwo])
384 | class Steps {
385 | public constructor(private readonly stateTwo: StateTwo) {
386 | }
387 |
388 | @when("I set the state to {string}")
389 | public setState(newValue: string) {
390 | this.stateTwo.value = newValue;
391 | }
392 |
393 | @then("the state is {string}")
394 | public checkValue(value: string) {
395 | console.log(`The state is '${this.stateTwo.value}'`);
396 | assert.equal(this.stateTwo.value, value, "State value does not match");
397 | }
398 | }
399 |
400 | export = Steps;
401 | """
402 | When I run cucumber-js
403 | Then it fails
404 | And the error output contains text:
405 | """
406 | Error: Cyclic dependency detected: StateOne -> StateTwo -> StateOne
407 | """
408 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/features/external-context-extraction.feature:
--------------------------------------------------------------------------------
1 | Feature: Extracing context objects from World externally
2 |
3 | Scenario: Failing to retrieve state from a non-initialized World object
4 | Given a file named "features/a.feature" with:
5 | """feature
6 | Feature: some feature
7 | Scenario: scenario a
8 | Given a step
9 | """
10 | And a file named "support/state.ts" with:
11 | """ts
12 | export class State {
13 | public constructor() {
14 | console.log('State has been initialized');
15 | }
16 |
17 | public value: string = "initial value";
18 | }
19 | """
20 | And a file named "step_definitions/a.ts" with:
21 | """ts
22 | import {State} from '../support/state';
23 | import {Before, Given} from '@cucumber/cucumber';
24 | import {getBindingFromWorld} from 'cucumber-tsflow';
25 |
26 | Before(function() {
27 | const state = getBindingFromWorld(this, State);
28 |
29 | console.log(`Cucumber-style before. State is "${state.value}"`);
30 |
31 | state.value = 'cucumber-style before';
32 | });
33 |
34 | Given('a step', function() {
35 | const state = getBindingFromWorld(this, State);
36 |
37 | console.log(`Cucumber-style step. State is "${state.value}"`);
38 |
39 | state.value = 'cucumber-style step';
40 | });
41 | """
42 | When I run cucumber-js
43 | Then it fails
44 | And the output contains text:
45 | """
46 | Before # step_definitions/a.ts:5
47 | Error: Scenario context have not been initialized in the provided World object.
48 | """
49 |
50 | Scenario: Sharing a state between native Cucumber and Decorator-style steps
51 | Given a file named "features/a.feature" with:
52 | """feature
53 | Feature: some feature
54 | Scenario: scenario a
55 | Given a cucumber-style step is called
56 | And a decorator-style step is called
57 | """
58 | And a file named "support/state.ts" with:
59 | """ts
60 | export class State {
61 | public constructor() {
62 | console.log('State has been initialized');
63 | }
64 |
65 | public value: string = "initial value";
66 | }
67 | """
68 | And a file named "step_definitions/a.ts" with:
69 | """ts
70 | import {State} from '../support/state';
71 | import {Before, Given} from '@cucumber/cucumber';
72 | import {ensureWorldIsInitialized,getBindingFromWorld} from 'cucumber-tsflow';
73 |
74 | ensureWorldIsInitialized();
75 |
76 | Before(function() {
77 | const state = getBindingFromWorld(this, State);
78 |
79 | console.log(`Cucumber-style before. State is "${state.value}"`);
80 |
81 | state.value = 'cucumber-style before';
82 | });
83 |
84 | Given('a cucumber-style step is called', function() {
85 | const state = getBindingFromWorld(this, State);
86 |
87 | console.log(`Cucumber-style step. State is "${state.value}"`);
88 |
89 | state.value = 'cucumber-style step';
90 | });
91 | """
92 | And a file named "step_definitions/b.ts" with:
93 | """ts
94 | import {State} from '../support/state';
95 | import {binding, before, given} from 'cucumber-tsflow';
96 |
97 | @binding([State])
98 | class Steps {
99 | public constructor(private readonly state: State) {}
100 |
101 | @before()
102 | public before() {
103 | console.log(`Decorator-style before. State is "${this.state.value}"`);
104 |
105 | this.state.value = 'decorator-style before';
106 | }
107 |
108 | @given('a decorator-style step is called')
109 | public step() {
110 | console.log(`Decorator-style step. State is "${this.state.value}"`);
111 |
112 | this.state.value = 'decorator-style step';
113 | }
114 | }
115 |
116 | export = Steps;
117 | """
118 | When I run cucumber-js
119 | Then it passes
120 | And the output contains text:
121 | """
122 | .State has been initialized
123 | Cucumber-style before. State is "initial value"
124 | .Decorator-style before. State is "cucumber-style before"
125 | .Cucumber-style step. State is "decorator-style before"
126 | .Decorator-style step. State is "cucumber-style step"
127 | """
128 |
129 | Scenario: Share state between underlying cucumber functionality and TSFlow functionality
130 | Given a file named "features/a.feature" with:
131 | """feature
132 | Feature: some feature
133 | Scenario: scenario before year 2k
134 | Given a step for 1999-03-04T21:43:54Z
135 | @y2k
136 | Scenario: scenario after year 2k
137 | Given a step for 2023-09-13T12:34:56.789Z
138 | """
139 | And a file named "support/state.ts" with:
140 | """ts
141 | export class State {
142 | public maxDate = new Date('2000-01-01');
143 | }
144 | """
145 | And a file named "step_definitions/a.ts" with:
146 | """ts
147 | import {State} from '../support/state';
148 | import {defineParameterType} from '@cucumber/cucumber';
149 | import {ensureWorldIsInitialized,getBindingFromWorld} from 'cucumber-tsflow';
150 |
151 | ensureWorldIsInitialized();
152 |
153 | defineParameterType({
154 | name: 'datetime',
155 | regexp: /[+-]?\d{4,}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z/,
156 | preferForRegexpMatch: true,
157 | useForSnippets: true,
158 | transformer: function (datetime): Date {
159 | const state = getBindingFromWorld(this, State);
160 |
161 | const date = new Date(datetime);
162 |
163 | console.log(`Parsing date up to ${state.maxDate.toISOString()}`);
164 |
165 | if (state.maxDate.valueOf() < date.valueOf()) {
166 | throw new Error('Date after maximum date.');
167 | }
168 |
169 | return new Date(datetime)
170 | }
171 | });
172 | """
173 | And a file named "step_definitions/b.ts" with:
174 | """ts
175 | import {State} from '../support/state';
176 | import {binding, before, given} from 'cucumber-tsflow';
177 |
178 | @binding([State])
179 | class Steps {
180 | public constructor(private readonly state: State) {}
181 |
182 | @before({tag: '@y2k'})
183 | public before() {
184 | this.state.maxDate = new Date('3000-01-01');
185 | }
186 |
187 | @given('a step for {datetime}')
188 | public step(datetime: Date) {
189 | console.log(`Step received a ${datetime.constructor.name}: ${datetime.toISOString()}`);
190 | }
191 | }
192 |
193 | export = Steps;
194 | """
195 | When I run cucumber-js
196 | Then it passes
197 | And the output contains text:
198 | """
199 | .Parsing date up to 2000-01-01T00:00:00.000Z
200 | Step received a Date: 1999-03-04T21:43:54.000Z
201 | ....Parsing date up to 3000-01-01T00:00:00.000Z
202 | Step received a Date: 2023-09-13T12:34:56.789Z
203 | """
204 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/features/global-hooks.feature:
--------------------------------------------------------------------------------
1 | Feature: Support for Cucumber hooks
2 |
3 | Scenario: Binding a beforeAll hook
4 | Given a file named "features/a.feature" with:
5 | """feature
6 | Feature: Feature
7 | Scenario: example one
8 | Given a step
9 |
10 | Scenario: example two
11 | Given a step
12 | """
13 | And a file named "step_definitions/steps.ts" with:
14 | """ts
15 | import {binding, given, beforeAll} from 'cucumber-tsflow';
16 |
17 | @binding()
18 | class Steps {
19 | @beforeAll()
20 | public static hook() {
21 | console.log('hook exec')
22 | }
23 |
24 | @given("a step")
25 | public given() {
26 | console.log('step exec');
27 | }
28 | }
29 |
30 | export = Steps;
31 | """
32 | When I run cucumber-js
33 | Then it passes
34 | And the output contains text once:
35 | """
36 | hook exec
37 | .step exec
38 | ...step exec
39 | ..
40 | """
41 |
42 | Scenario: Binding a afterAll hook
43 | Given a file named "features/a.feature" with:
44 | """feature
45 | Feature: Feature
46 | Scenario: example one
47 | Given a step
48 |
49 | Scenario: example two
50 | Given a step
51 | """
52 | And a file named "step_definitions/steps.ts" with:
53 | """ts
54 | import {binding, given, afterAll} from 'cucumber-tsflow';
55 |
56 | @binding()
57 | class Steps {
58 | @afterAll()
59 | public static hook() {
60 | console.log('hook exec')
61 | }
62 |
63 | @given("a step")
64 | public given() {
65 | console.log('step exec');
66 | }
67 | }
68 |
69 | export = Steps;
70 | """
71 | When I run cucumber-js
72 | Then it passes
73 | And the output contains text once:
74 | """
75 | .step exec
76 | ...step exec
77 | ..hook exec
78 | """
79 |
80 | Scenario: Binding beforeAll and afterAll hooks
81 | Given a file named "features/a.feature" with:
82 | """feature
83 | Feature: Feature
84 | Scenario: example one
85 | Given a step
86 |
87 | Scenario: example two
88 | Given a step
89 | """
90 | And a file named "step_definitions/steps.ts" with:
91 | """ts
92 | import {binding, given, beforeAll, afterAll} from 'cucumber-tsflow';
93 |
94 | @binding()
95 | class Steps {
96 | @beforeAll()
97 | public static before() {
98 | console.log('before exec')
99 | }
100 |
101 | @afterAll()
102 | public static after() {
103 | console.log('after exec')
104 | }
105 |
106 | @given("a step")
107 | public given() {
108 | console.log('step exec');
109 | }
110 | }
111 |
112 | export = Steps;
113 | """
114 | When I run cucumber-js
115 | Then it passes
116 | And the output contains text once:
117 | """
118 | before exec
119 | .step exec
120 | ...step exec
121 | ..after exec
122 | """
123 |
124 | Scenario: Binding multiple beforeAll hooks
125 | Given a file named "features/a.feature" with:
126 | """feature
127 | Feature: Feature
128 | Scenario: example one
129 | Given a step
130 |
131 | Scenario: example two
132 | Given a step
133 | """
134 | And a file named "step_definitions/steps.ts" with:
135 | """ts
136 | import {binding, given, beforeAll} from 'cucumber-tsflow';
137 |
138 | @binding()
139 | class Steps {
140 | @beforeAll()
141 | public static one() {
142 | console.log('one')
143 | }
144 |
145 | @beforeAll()
146 | public static two() {
147 | console.log('two')
148 | }
149 |
150 | @given("a step")
151 | public given() {
152 | console.log('step');
153 | }
154 | }
155 |
156 | export = Steps;
157 | """
158 | When I run cucumber-js
159 | Then it passes
160 | And the output contains text once:
161 | """
162 | one
163 | two
164 | .step
165 | ...step
166 | ..
167 | """
168 |
169 | Scenario: Binding multiple global hooks on the same line
170 | Given a file named "features/a.feature" with:
171 | """feature
172 | Feature: Feature
173 | Scenario: example one
174 | Given a step
175 |
176 | Scenario: example two
177 | Given a step
178 | """
179 | And a file named "step_definitions/steps.ts" with:
180 | """ts
181 | import {binding, given, beforeAll} from 'cucumber-tsflow';
182 |
183 | @binding()
184 | class Steps {
185 | @beforeAll()public static one() { console.log('one') }@beforeAll()public static two() { console.log('two') }
186 |
187 | @given("a step")
188 | public given() {
189 | console.log('step');
190 | }
191 | }
192 |
193 | export = Steps;
194 | """
195 | When I run cucumber-js
196 | Then it passes
197 | And the output contains text once:
198 | """
199 | one
200 | two
201 | .step
202 | ...step
203 | ..
204 | """
205 |
206 | Scenario: Binding multiple afterAll hooks
207 | Given a file named "features/a.feature" with:
208 | """feature
209 | Feature: Feature
210 | Scenario: example one
211 | Given a step
212 |
213 | Scenario: example two
214 | Given a step
215 | """
216 | And a file named "step_definitions/steps.ts" with:
217 | """ts
218 | import {binding, given, afterAll} from 'cucumber-tsflow';
219 |
220 | @binding()
221 | class Steps {
222 | @afterAll()
223 | public static one() {
224 | console.log('one')
225 | }
226 |
227 | @afterAll()
228 | public static two() {
229 | console.log('two')
230 | }
231 |
232 | @given("a step")
233 | public given() {
234 | console.log('step');
235 | }
236 | }
237 |
238 | export = Steps;
239 | """
240 | When I run cucumber-js
241 | Then it passes
242 | And the output contains text once:
243 | """
244 | .step
245 | ...step
246 | ..two
247 | one
248 | """
249 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/features/hooks.feature:
--------------------------------------------------------------------------------
1 | Feature: Support for Cucumber hooks
2 |
3 | Scenario: Binding a before hook
4 | Given a file named "features/a.feature" with:
5 | """feature
6 | Feature: Feature
7 | Scenario: example
8 | Given a step
9 | """
10 | And a file named "step_definitions/steps.ts" with:
11 | """ts
12 | import {binding, given, before} from 'cucumber-tsflow';
13 |
14 | @binding()
15 | class Steps {
16 | private state = 'hook has not executed';
17 |
18 | @before()
19 | public hook() {
20 | this.state = 'hook has executed';
21 | }
22 |
23 | @given("a step")
24 | public given() {
25 | console.log(this.state);
26 | }
27 | }
28 |
29 | export = Steps;
30 | """
31 | When I run cucumber-js
32 | Then it passes
33 | And the output does not contain "hook has not executed"
34 | And the output contains "hook has executed"
35 |
36 | Scenario: Binding an after hook
37 | Given a file named "features/a.feature" with:
38 | """feature
39 | Feature: Feature
40 | Scenario: example
41 | Given a step
42 | """
43 | And a file named "step_definitions/steps.ts" with:
44 | """ts
45 | import {binding, given, after} from 'cucumber-tsflow';
46 |
47 | @binding()
48 | class Steps {
49 | private state = 'step has not executed';
50 |
51 | @after()
52 | public hook() {
53 | console.log(this.state);
54 | }
55 |
56 | @given("a step")
57 | public given() {
58 | this.state = 'step has executed';
59 | }
60 | }
61 |
62 | export = Steps;
63 | """
64 | When I run cucumber-js
65 | Then it passes
66 | And the output does not contain "step has not executed"
67 | And the output contains "step has executed"
68 |
69 | Scenario: Binding before and after hooks
70 | Given a file named "features/a.feature" with:
71 | """feature
72 | Feature: Feature
73 | Scenario: example
74 | Given a step
75 | """
76 | And a file named "step_definitions/steps.ts" with:
77 | """ts
78 | import {binding, given, before, after} from 'cucumber-tsflow';
79 |
80 | @binding()
81 | class Steps {
82 | private state = 'hook has not executed';
83 |
84 | @before()
85 | public before() {
86 | this.state = 'before hook executed';
87 | }
88 |
89 | @given("a step")
90 | public given() {
91 | console.log(this.state);
92 | this.state = 'step has executed';
93 | }
94 |
95 | @after()
96 | public after() {
97 | console.log(this.state);
98 | }
99 | }
100 |
101 | export = Steps;
102 | """
103 | When I run cucumber-js
104 | Then it passes
105 | And the output does not contain "step has not executed"
106 | And the output contains text:
107 | """
108 | .before hook executed
109 | .step has executed
110 | """
111 |
112 | Scenario: Binding multiple before hooks
113 | Given a file named "features/a.feature" with:
114 | """feature
115 | Feature: Feature
116 | Scenario: example
117 | Given a step
118 | """
119 | And a file named "step_definitions/steps.ts" with:
120 | """ts
121 | import {binding, given, before} from 'cucumber-tsflow';
122 |
123 | @binding()
124 | class Steps {
125 | private state = 'no hook executed';
126 |
127 | @before()
128 | public hookOne() {
129 | console.log(this.state)
130 | this.state = 'hook one has executed';
131 | }
132 |
133 | @before()
134 | public hookTwo() {
135 | console.log(this.state)
136 | this.state = 'hook two has executed';
137 | }
138 |
139 | @given("a step")
140 | public given() {
141 | console.log(this.state);
142 | this.state = 'step has executed';
143 | }
144 | }
145 |
146 | export = Steps;
147 | """
148 | When I run cucumber-js
149 | Then it passes
150 | And the output does not contain "step has not executed"
151 | And the output contains text:
152 | """
153 | .no hook executed
154 | .hook one has executed
155 | .hook two has executed
156 | """
157 |
158 |
159 | Scenario: Binding multiple hooks same line
160 | Given a file named "features/a.feature" with:
161 | """feature
162 | Feature: Feature
163 | Scenario: example
164 | Given a step
165 | """
166 | And a file named "step_definitions/steps.ts" with:
167 | """ts
168 | import {binding, given, before} from 'cucumber-tsflow';
169 |
170 | @binding()
171 | class Steps {
172 | private state = 'no hook executed';
173 |
174 | @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';}
175 |
176 | @given("a step")
177 | public given() {
178 | console.log(this.state);
179 | this.state = 'step has executed';
180 | }
181 | }
182 |
183 | export = Steps;
184 | """
185 | When I run cucumber-js
186 | Then it passes
187 | And the output does not contain "step has not executed"
188 | And the output contains text:
189 | """
190 | .no hook executed
191 | .hook one has executed
192 | .hook two has executed
193 | """
194 |
195 | Scenario: Binding multiple after hooks
196 | Given a file named "features/a.feature" with:
197 | """feature
198 | Feature: Feature
199 | Scenario: example
200 | Given a step
201 | """
202 | And a file named "step_definitions/steps.ts" with:
203 | """ts
204 | import {binding, given, after} from 'cucumber-tsflow';
205 |
206 | @binding()
207 | class Steps {
208 | private state = 'no hook executed';
209 |
210 | @given("a step")
211 | public given() {
212 | console.log(this.state)
213 | this.state = 'step has executed';
214 | }
215 |
216 | @after()
217 | public hookOne() {
218 | console.log(this.state)
219 | this.state = 'hook one has executed';
220 | }
221 |
222 | @after()
223 | public hookTwo() {
224 | console.log(this.state);
225 | this.state = 'hook two has executed';
226 | }
227 | }
228 |
229 | export = Steps;
230 | """
231 | When I run cucumber-js
232 | Then it passes
233 | And the output does not contain "step has not executed"
234 | And the output contains text:
235 | """
236 | .no hook executed
237 | .step has executed
238 | .hook two has executed
239 | """
240 |
241 | @oldApis
242 | Scenario: Attempting to bind named hooks with old cucumber
243 | Given a file named "features/a.feature" with:
244 | """feature
245 | Feature: Feature
246 | Scenario: example
247 | Given a step
248 | """
249 | And a file named "step_definitions/steps.ts" with:
250 | """ts
251 | import {binding, given, before, after} from 'cucumber-tsflow';
252 |
253 | @binding()
254 | class Steps {
255 | private state = 'hook has not executed';
256 |
257 | @before({name: 'setup environment'})
258 | public before() {
259 | this.state = 'before hook executed';
260 | }
261 |
262 | @given("a step")
263 | public given() {
264 | console.log(this.state);
265 | this.state = 'step has executed';
266 | }
267 |
268 | @after({name: 'tear down environment'})
269 | public after() {
270 | console.log(this.state);
271 | }
272 | }
273 |
274 | export = Steps;
275 | """
276 | When I run cucumber-js
277 | Then it fails
278 | And the error output contains text:
279 | """
280 | Object literal may only specify known properties, and 'name' does not exist in type 'HookOptions'.
281 | """
282 |
283 | @newApis
284 | Scenario: Binding named hooks
285 | Given a file named "features/a.feature" with:
286 | """feature
287 | Feature: Feature
288 | Scenario: example
289 | Given a step
290 | """
291 | And a file named "step_definitions/steps.ts" with:
292 | """ts
293 | import {binding, given, before, after} from 'cucumber-tsflow';
294 |
295 | @binding()
296 | class Steps {
297 | private state = 'hook has not executed';
298 |
299 | @before({name: 'setup environment'})
300 | public before() {
301 | this.state = 'before hook executed';
302 | }
303 |
304 | @given("a step")
305 | public given() {
306 | console.log(this.state);
307 | this.state = 'step has executed';
308 | }
309 |
310 | @after({name: 'tear down environment'})
311 | public after() {
312 | console.log(this.state);
313 | }
314 | }
315 |
316 | export = Steps;
317 | """
318 | When I run cucumber-js
319 | Then it passes
320 | And the hook "setup environment" was executed on scenario "example"
321 | And the hook "tear down environment" was executed on scenario "example"
322 |
323 | Scenario: Binding a before step hook
324 | Given a file named "features/a.feature" with:
325 | """feature
326 | Feature: Feature
327 | Scenario: example
328 | Given a step
329 | """
330 | And a file named "step_definitions/steps.ts" with:
331 | """ts
332 | import {binding, given, beforeStep} from 'cucumber-tsflow';
333 |
334 | @binding()
335 | class Steps {
336 | private state = 'hook has not executed';
337 |
338 | @beforeStep()
339 | public hook() {
340 | this.state = 'hook has executed';
341 | }
342 |
343 | @given("a step")
344 | public given() {
345 | console.log(this.state);
346 | }
347 | }
348 |
349 | export = Steps;
350 | """
351 | When I run cucumber-js
352 | Then it passes
353 | And the output does not contain "hook has not executed"
354 | And the output contains "hook has executed"
355 |
356 | Scenario: Binding a before step hook with multiple steps
357 | Given a file named "features/a.feature" with:
358 | """feature
359 | Feature: Feature
360 | Scenario: example
361 | Given a step
362 | And another step
363 | """
364 | And a file named "step_definitions/steps.ts" with:
365 | """ts
366 | import {binding, given, beforeStep, when} from 'cucumber-tsflow';
367 |
368 | @binding()
369 | class Steps {
370 | private counter = 0;
371 |
372 | @beforeStep()
373 | public hook() {
374 | console.log(`${this.counter++} execution: beforeStep`);
375 | }
376 |
377 | @given("a step")
378 | public given() {
379 | console.log(`${this.counter++} execution: given`);
380 | }
381 |
382 | @when("another step")
383 | public when() {
384 | console.log(`${this.counter++} execution: when`);
385 | }
386 | }
387 |
388 | export = Steps;
389 | """
390 | When I run cucumber-js
391 | Then it passes
392 | And the output does not contain "hook has not executed"
393 | And the output contains text:
394 | """
395 | .0 execution: beforeStep
396 | 1 execution: given
397 | .2 execution: beforeStep
398 | 3 execution: when
399 | """
400 |
401 | Scenario: Binding multiple before step hooks
402 | Given a file named "features/a.feature" with:
403 | """feature
404 | Feature: Feature
405 | Scenario: example
406 | Given a step
407 | """
408 | And a file named "step_definitions/steps.ts" with:
409 | """ts
410 | import {binding, given, beforeStep} from 'cucumber-tsflow';
411 |
412 | @binding()
413 | class Steps {
414 | private state = 'no hook executed';
415 |
416 | @beforeStep()
417 | public hookOne() {
418 | console.log(this.state)
419 | this.state = 'hook one has executed';
420 | }
421 |
422 | @beforeStep()
423 | public hookTwo() {
424 | console.log(this.state)
425 | this.state = 'hook two has executed';
426 | }
427 |
428 | @given("a step")
429 | public given() {
430 | console.log(this.state);
431 | this.state = 'step has executed';
432 | }
433 | }
434 |
435 | export = Steps;
436 | """
437 | When I run cucumber-js
438 | Then it passes
439 | And the output does not contain "step has not executed"
440 | And the output contains text:
441 | """
442 | .no hook executed
443 | hook one has executed
444 | hook two has executed
445 | """
446 |
447 | Scenario: Binding an after step hook
448 | Given a file named "features/a.feature" with:
449 | """feature
450 | Feature: Feature
451 | Scenario: example
452 | Given a step
453 | """
454 | And a file named "step_definitions/steps.ts" with:
455 | """ts
456 | import {binding, given, afterStep} from 'cucumber-tsflow';
457 |
458 | @binding()
459 | class Steps {
460 | private state = 'step has not executed';
461 |
462 | @afterStep()
463 | public hook() {
464 | console.log(this.state);
465 | }
466 |
467 | @given("a step")
468 | public given() {
469 | this.state = 'step has executed';
470 | }
471 | }
472 |
473 | export = Steps;
474 | """
475 | When I run cucumber-js
476 | Then it passes
477 | And the output does not contain "step has not executed"
478 | And the output contains "step has executed"
479 |
480 | Scenario: Binding an after step hook with multiple steps
481 | Given a file named "features/a.feature" with:
482 | """feature
483 | Feature: Feature
484 | Scenario: example
485 | Given a step
486 | And another step
487 | """
488 | And a file named "step_definitions/steps.ts" with:
489 | """ts
490 | import {binding, given, afterStep, when} from 'cucumber-tsflow';
491 |
492 | @binding()
493 | class Steps {
494 | private counter = 0;
495 |
496 | @afterStep()
497 | public hook() {
498 | console.log(`${this.counter++} execution: afterStep`);
499 | }
500 |
501 | @given("a step")
502 | public given() {
503 | console.log(`${this.counter++} execution: given`);
504 | }
505 |
506 | @when("another step")
507 | public when() {
508 | console.log(`${this.counter++} execution: when`);
509 | }
510 | }
511 |
512 | export = Steps;
513 | """
514 | When I run cucumber-js
515 | Then it passes
516 | And the output does not contain "step has not executed"
517 | And the output contains text:
518 | """
519 | .0 execution: given
520 | 1 execution: afterStep
521 | .2 execution: when
522 | 3 execution: afterStep
523 | """
524 |
525 | Scenario: Binding multiple after step hooks
526 | Given a file named "features/a.feature" with:
527 | """feature
528 | Feature: Feature
529 | Scenario: example
530 | Given a step
531 | """
532 | And a file named "step_definitions/steps.ts" with:
533 | """ts
534 | import {binding, given, afterStep} from 'cucumber-tsflow';
535 |
536 | @binding()
537 | class Steps {
538 | private state = 'no hook executed';
539 |
540 | @given("a step")
541 | public given() {
542 | console.log(this.state)
543 | this.state = 'step has executed';
544 | }
545 |
546 | @afterStep()
547 | public hookOne() {
548 | console.log(this.state)
549 | this.state = 'hook one has executed';
550 | }
551 |
552 | @afterStep()
553 | public hookTwo() {
554 | console.log(this.state);
555 | this.state = 'hook two has executed';
556 | }
557 | }
558 |
559 | export = Steps;
560 | """
561 | When I run cucumber-js
562 | Then it passes
563 | And the output does not contain "step has not executed"
564 | And the output contains text:
565 | """
566 | .no hook executed
567 | step has executed
568 | hook two has executed
569 | """
570 |
571 | Scenario: Binding before and after step hooks
572 | Given a file named "features/a.feature" with:
573 | """feature
574 | Feature: Feature
575 | Scenario: example
576 | Given a step
577 | """
578 | And a file named "step_definitions/steps.ts" with:
579 | """ts
580 | import {binding, given, beforeStep, afterStep} from 'cucumber-tsflow';
581 |
582 | @binding()
583 | class Steps {
584 | private state = 'hook has not executed';
585 |
586 | @beforeStep()
587 | public beforeStep() {
588 | this.state = 'before hook executed';
589 | }
590 |
591 | @given("a step")
592 | public given() {
593 | console.log(this.state);
594 | this.state = 'step has executed';
595 | }
596 |
597 | @afterStep()
598 | public afterStep() {
599 | console.log(this.state);
600 | }
601 | }
602 |
603 | export = Steps;
604 | """
605 | When I run cucumber-js
606 | Then it passes
607 | And the output does not contain "step has not executed"
608 | And the output contains text:
609 | """
610 | .before hook executed
611 | step has executed
612 | """
613 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/features/tag-parameters.feature:
--------------------------------------------------------------------------------
1 | Feature: Tag parameters
2 |
3 | Background:
4 | Given a file named "step_definitions/steps.ts" with:
5 | """ts
6 | import * as assert from 'assert';
7 | import {binding, then, ScenarioInfo} from 'cucumber-tsflow';
8 |
9 | @binding([ScenarioInfo])
10 | class Steps {
11 | public constructor(private readonly scenario: ScenarioInfo) {}
12 |
13 | @then("the flag {string} is enabled")
14 | public checkEnabled(name: string) {
15 | assert.ok(this.scenario.getFlag(name))
16 | }
17 |
18 | @then("the flag {string} is disabled")
19 | public checkDisabled(name: string) {
20 | assert.ok(!this.scenario.getFlag(name))
21 | }
22 |
23 | @then("the option tag {string} is set to {string}")
24 | public checkOption(name: string, value: string) {
25 | assert.strictEqual(this.scenario.getOptionTag(name), value);
26 | }
27 |
28 | @then("the option tag {string} is unset")
29 | public checkOptionUnset(name: string) {
30 | assert.strictEqual(this.scenario.getOptionTag(name), undefined);
31 | }
32 |
33 | @then("the multi option tag {string} is set to {}")
34 | public checkMultiOption(name: string, value: string) {
35 | assert.deepStrictEqual(this.scenario.getMultiOptionTag(name), JSON.parse(value));
36 | }
37 |
38 | @then("the attribute tag {string} is set to {}")
39 | @then("the attribute tag {string} is set to:")
40 | public checkAttributes(name: string, values: string) {
41 | assert.deepStrictEqual(this.scenario.getAttributeTag(name), JSON.parse(values));
42 | }
43 |
44 | @then("the attribute tag {string} is unset")
45 | public checkAttributesUnset(name: string) {
46 | assert.deepStrictEqual(this.scenario.getAttributeTag(name), undefined);
47 | }
48 | }
49 |
50 | export = Steps;
51 | """
52 |
53 | Scenario: Checking for an absent flag
54 | Given a file named "features/a.feature" with:
55 | """feature
56 | Feature: Feature
57 | Scenario: example
58 | Then the flag "enableFoo" is disabled
59 | """
60 | When I run cucumber-js
61 | Then it passes
62 |
63 | Scenario: Checking for a flag on the feature
64 | Given a file named "features/a.feature" with:
65 | """feature
66 | @enableFoo
67 | Feature: Feature
68 | Scenario: One
69 | Then the flag "enableFoo" is enabled
70 | Scenario: Two
71 | Then the flag "enableFoo" is enabled
72 | """
73 | When I run cucumber-js
74 | Then it passes
75 |
76 | Scenario: Checking for a flag on the scenario
77 | Given a file named "features/a.feature" with:
78 | """feature
79 | Feature: Feature
80 | @enableFoo
81 | Scenario: One
82 | Then the flag "enableFoo" is enabled
83 | Then the flag "enableBar" is disabled
84 | @enableBar
85 | Scenario: Two
86 | Then the flag "enableFoo" is disabled
87 | Then the flag "enableBar" is enabled
88 | """
89 | When I run cucumber-js
90 | Then it passes
91 |
92 | Scenario: Checking for an absent option
93 | Given a file named "features/a.feature" with:
94 | """feature
95 | Feature: Feature
96 | Scenario: example
97 | Then the option tag "foo" is unset
98 | """
99 | When I run cucumber-js
100 | Then it passes
101 |
102 | Scenario: Checking for an option on the feature
103 | Given a file named "features/a.feature" with:
104 | """feature
105 | @foo(bar)
106 | Feature: Feature
107 | Scenario: One
108 | Then the option tag "foo" is set to "bar"
109 | Scenario: Two
110 | Then the option tag "foo" is set to "bar"
111 | """
112 | When I run cucumber-js
113 | Then it passes
114 |
115 | Scenario: Checking for an option on the scenario
116 | Given a file named "features/a.feature" with:
117 | """feature
118 | Feature: Feature
119 | @foo(bar)
120 | Scenario: One
121 | Then the option tag "foo" is set to "bar"
122 | @foo(baz)
123 | Scenario: Two
124 | Then the option tag "foo" is set to "baz"
125 | """
126 | When I run cucumber-js
127 | Then it passes
128 |
129 | Scenario: Checking for an option on the scenario overriding one on the feature
130 | Given a file named "features/a.feature" with:
131 | """feature
132 | @foo(bar)
133 | Feature: Feature
134 | Scenario: One
135 | Then the option tag "foo" is set to "bar"
136 | @foo(baz)
137 | Scenario: Two
138 | Then the option tag "foo" is set to "baz"
139 | """
140 | When I run cucumber-js
141 | Then it passes
142 |
143 | Scenario: Checking for an absent attribute tag
144 | Given a file named "features/a.feature" with:
145 | """feature
146 | Feature: Feature
147 | Scenario: example
148 | Then the attribute tag "foo" is unset
149 | """
150 | When I run cucumber-js
151 | Then it passes
152 |
153 | Scenario: Checking for multi options on the feature
154 | Given a file named "features/a.feature" with:
155 | """feature
156 | @foo(bar)
157 | @foo(baz)
158 | Feature: Feature
159 | Scenario: One
160 | Then the multi option tag "foo" is set to ["bar", "baz"]
161 | Scenario: Two
162 | Then the multi option tag "foo" is set to ["bar", "baz"]
163 | """
164 | When I run cucumber-js
165 | Then it passes
166 |
167 | Scenario: Checking for multi options on the scenario
168 | Given a file named "features/a.feature" with:
169 | """feature
170 | Feature: Feature
171 | @foo(bar)
172 | @foo(baz)
173 | Scenario: One
174 | Then the multi option tag "foo" is set to ["bar", "baz"]
175 | @foo(qux)
176 | @foo(zzz)
177 | Scenario: Two
178 | Then the multi option tag "foo" is set to ["qux", "zzz"]
179 | """
180 | When I run cucumber-js
181 | Then it passes
182 |
183 | Scenario: Checking for multi options on the scenario combining with multi options on the feature
184 | Given a file named "features/a.feature" with:
185 | """feature
186 | @foo(bar)
187 | Feature: Feature
188 | Scenario: One
189 | Then the multi option tag "foo" is set to ["bar"]
190 | @foo(baz)
191 | Scenario: Two
192 | Then the multi option tag "foo" is set to ["bar", "baz"]
193 | """
194 | When I run cucumber-js
195 | Then it passes
196 |
197 | Scenario: Checking for an attribute tag on the feature
198 | Given a file named "features/a.feature" with:
199 | """feature
200 | @foo({"bar":1})
201 | Feature: Feature
202 | Scenario: One
203 | Then the attribute tag "foo" is set to { "bar": 1 }
204 | Scenario: Two
205 | Then the attribute tag "foo" is set to { "bar": 1 }
206 | """
207 | When I run cucumber-js
208 | Then it passes
209 |
210 | Scenario: Checking for an attribute tag on the scenario
211 | Given a file named "features/a.feature" with:
212 | """feature
213 | Feature: Feature
214 | @foo({"bar":1})
215 | Scenario: One
216 | Then the attribute tag "foo" is set to { "bar": 1 }
217 | @foo({"bar":2})
218 | Scenario: Two
219 | Then the attribute tag "foo" is set to { "bar": 2 }
220 | """
221 | When I run cucumber-js
222 | Then it passes
223 |
224 | Scenario: Checking for an attribute tag on the scenario overriding one on the feature
225 | Given a file named "features/a.feature" with:
226 | """feature
227 | @foo({"bar":1})
228 | Feature: Feature
229 | Scenario: One
230 | Then the attribute tag "foo" is set to { "bar": 1 }
231 | @foo({"not-bar":2})
232 | Scenario: Two
233 | Then the attribute tag "foo" is set to { "not-bar": 2 }
234 | """
235 | When I run cucumber-js
236 | Then it passes
237 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cucumber-tsflow-specs",
3 | "version": "3.4.1",
4 | "private": true,
5 | "description": "Specification for 'cucumber-tsflow'.",
6 | "license": "MIT",
7 | "author": "Tim Roberts ",
8 | "main": "./index.js",
9 | "typings": "./dist/index.d.ts",
10 | "dependencies": {
11 | "@cucumber/query": "^13.2.0",
12 | "cucumber-tsflow": "^4",
13 | "expect": "^29.7.0",
14 | "fs-extra": "^11.1.0",
15 | "verror": "^1.10.1"
16 | },
17 | "devDependencies": {
18 | "@types/fs-extra": "^11.0.1",
19 | "@types/verror": "^1.10.6"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/step_definitions/cucumber_steps.ts:
--------------------------------------------------------------------------------
1 | import { binding, then, when } from "cucumber-tsflow";
2 | import expect from "expect";
3 | import { parseEnvString } from "../support/helpers";
4 | import { TestRunner } from "../support/runner";
5 |
6 | @binding([TestRunner])
7 | class CucumberSteps {
8 | public constructor(private readonly runner: TestRunner) {}
9 |
10 | @when("my env includes {string}")
11 | public setEnvironment(envString: string) {
12 | this.runner.sharedEnv = parseEnvString(envString);
13 | }
14 |
15 | @when("I run cucumber-js with env `{}`")
16 | public async runCucumberWithEnv(envString: string) {
17 | await this.runner.run(parseEnvString(envString));
18 | }
19 |
20 | @when("I run cucumber-js")
21 | public async runCucumber() {
22 | await this.runner.run();
23 | }
24 |
25 | @then("it passes")
26 | public checkPassed() {
27 | const { lastRun } = this.runner;
28 |
29 | if (lastRun?.error != null) {
30 | throw new Error(
31 | `Last run errored unexpectedly. Output:\n\n${lastRun.output}\n\n` +
32 | `Error Output:\n\n${lastRun.errorOutput}`,
33 | );
34 | }
35 | }
36 |
37 | @then("it fails")
38 | public ensureFailure() {
39 | const exitCode = this.runner.lastRun.error?.code ?? 0;
40 | expect(exitCode).not.toBe(0);
41 |
42 | this.runner.verifiedLastRunError = true;
43 | }
44 |
45 | @then("the output contains {string}")
46 | @then("the output contains text:")
47 | public checkStdoutContains(text: string) {
48 | expect(this.runner.lastRun.output).toContain(text);
49 | }
50 |
51 | @then("the output contains {string} once")
52 | @then("the output contains text once:")
53 | public checkStdoutContainsOnce(text: string) {
54 | const { output } = this.runner.lastRun;
55 |
56 | expect(output).toContain(text);
57 |
58 | const firstOccurrence = output.indexOf(text);
59 | const remaining = output.substring(firstOccurrence + 1);
60 |
61 | expect(remaining).not.toContain(text);
62 | }
63 |
64 | @then("the output does not contain {string}")
65 | @then("the output does not contain text:")
66 | public checkStdoutDoesNotContains(text: string) {
67 | expect(this.runner.lastRun.output).not.toContain(text);
68 | }
69 |
70 | @then("the error output contains {string}")
71 | @then("the error output contains text:")
72 | public checkStderrContains(text: string) {
73 | expect(this.runner.lastRun.errorOutput).toContain(text);
74 | }
75 |
76 | @then("the error output does not contain {string}")
77 | @then("the error output does not contain text:")
78 | public checkStderrDoesNotContains(text: string) {
79 | expect(this.runner.lastRun.errorOutput).not.toContain(text);
80 | }
81 | }
82 |
83 | export = CucumberSteps;
84 |
85 | // Then(
86 | // "the output contains these types and quantities of message:",
87 | // function(this: World, expectedMessages: DataTable) {
88 | // const envelopes = this.lastRun.output
89 | // .split("\n")
90 | // .filter((line) => !!line)
91 | // .map((line) => JSON.parse(line));
92 | // expectedMessages.rows().forEach(([type, count]) => {
93 | // expect(envelopes.filter((envelope) => !!envelope[type])).to.have.length(
94 | // Number(count),
95 | // `Didn't find expected number of ${type} messages`
96 | // );
97 | // });
98 | // }
99 | // );
100 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/step_definitions/file_steps.ts:
--------------------------------------------------------------------------------
1 | import { binding, given } from "cucumber-tsflow";
2 | import { TestRunner } from "../support/runner";
3 |
4 | @binding([TestRunner])
5 | class FileSteps {
6 | public constructor(private readonly runner: TestRunner) {}
7 |
8 | @given("a file named {string} with:")
9 | public newFile(filePath: string, fileContent: string) {
10 | this.runner.dir.writeFile(filePath, fileContent);
11 | }
12 |
13 | @given("an empty file named {string}")
14 | public newEmptyFile(filePath: string) {
15 | return this.newFile(filePath, "");
16 | }
17 |
18 | @given("a directory named {string}")
19 | public async newDir(filePath: string) {
20 | this.runner.dir.mkdir(filePath);
21 | }
22 | }
23 |
24 | export = FileSteps;
25 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/step_definitions/prepare.ts:
--------------------------------------------------------------------------------
1 | import { formatterHelpers, ITestCaseHookParameter } from "@cucumber/cucumber";
2 | import { after, before, binding } from "cucumber-tsflow";
3 | import fsExtra from "fs-extra";
4 | import path from "path";
5 | import { TestRunner } from "../support/runner";
6 |
7 | const projectPath = path.join(__dirname, "..", "..", "..");
8 | const projectNodeModulePath = path.join(projectPath, "node_modules");
9 | const cucumberPath = path.join(projectNodeModulePath, "@cucumber", "cucumber");
10 | const tsNodePath = path.join(projectNodeModulePath, "ts-node");
11 | const projectLibPath = path.join(projectPath, "cucumber-tsflow");
12 | const log4jsPath = path.join(projectLibPath, "node_modules", "log4js");
13 |
14 | @binding([TestRunner])
15 | class Prepare {
16 | public constructor(private readonly runner: TestRunner) {}
17 |
18 | @before()
19 | public setupTestDir({ gherkinDocument, pickle }: ITestCaseHookParameter) {
20 | const { line } = formatterHelpers.PickleParser.getPickleLocation({
21 | gherkinDocument,
22 | pickle,
23 | });
24 |
25 | const tmpDir = path.join(
26 | projectPath,
27 | "tmp",
28 | `${path.basename(pickle.uri)}_${line.toString()}`,
29 | );
30 |
31 | fsExtra.removeSync(tmpDir);
32 |
33 | this.runner.dir.path = tmpDir;
34 |
35 | this.setupNodeModules();
36 |
37 | const tags = [
38 | ...pickle.tags.map((tag) => tag.name),
39 | ...(gherkinDocument.feature?.tags.map((tag) => tag.name) ?? []),
40 | ];
41 |
42 | this.writeDefaultFiles(tags);
43 | }
44 |
45 | @after()
46 | public tearDownTestDir() {
47 | const { lastRun } = this.runner;
48 |
49 | if (lastRun?.error != null && !this.runner.verifiedLastRunError) {
50 | throw new Error(
51 | `Last run errored unexpectedly. Output:\n\n${lastRun.output}\n\n` +
52 | `Error Output:\n\n${lastRun.errorOutput}`,
53 | );
54 | }
55 | }
56 |
57 | private setupNodeModules() {
58 | const tmpDirNodeModulesPath = this.runner.dir.mkdir("node_modules");
59 |
60 | fsExtra.ensureSymlinkSync(
61 | cucumberPath,
62 | path.join(tmpDirNodeModulesPath, "@cucumber", "cucumber"),
63 | );
64 | fsExtra.ensureSymlinkSync(
65 | tsNodePath,
66 | path.join(tmpDirNodeModulesPath, "ts-node"),
67 | );
68 | fsExtra.ensureSymlinkSync(
69 | projectLibPath,
70 | path.join(tmpDirNodeModulesPath, "cucumber-tsflow"),
71 | );
72 | fsExtra.ensureSymlinkSync(
73 | log4jsPath,
74 | path.join(tmpDirNodeModulesPath, "log4js"),
75 | );
76 | }
77 |
78 | private writeDefaultFiles(tags: string[]) {
79 | if (!tags.includes("custom-tsconfig")) {
80 | this.runner.dir.writeFile(
81 | "tsconfig.json",
82 | JSON.stringify({
83 | compilerOptions: {
84 | experimentalDecorators: true,
85 | },
86 | }),
87 | );
88 | }
89 |
90 | if (!tags.includes("no-logging")) {
91 | this.runner.dir.writeFile(
92 | "a-logging.ts",
93 | `
94 | import * as log4js from 'log4js';
95 |
96 | log4js.configure({
97 | appenders: {
98 | logfile: {
99 | type: "file",
100 | filename: "output.log",
101 | }
102 | },
103 | categories: {
104 | default: { appenders: ["logfile"], level: "trace" },
105 | }
106 | });
107 | `,
108 | );
109 | }
110 | }
111 | }
112 |
113 | export = Prepare;
114 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/step_definitions/scenario_steps.ts:
--------------------------------------------------------------------------------
1 | import { DataTable } from "@cucumber/cucumber";
2 | import * as messages from "@cucumber/messages";
3 | import assert from "assert";
4 | import { binding, then } from "cucumber-tsflow";
5 | import expect from "expect";
6 | import { Extractor } from "../support/helpers";
7 | import { TestRunner } from "../support/runner";
8 |
9 | const ENCODING_MAP: { [key: string]: messages.AttachmentContentEncoding } = {
10 | IDENTITY: messages.AttachmentContentEncoding.IDENTITY,
11 | BASE64: messages.AttachmentContentEncoding.BASE64,
12 | };
13 |
14 | @binding([TestRunner])
15 | class ScenarioSteps {
16 | public constructor(private readonly runner: TestRunner) {}
17 |
18 | @then("it runs {int} scenarios")
19 | public checkScenarioCount(expectedCount: number) {
20 | const startedTestCases = this.runner.lastRun.envelopes.reduce(
21 | (acc, e) => (e.testCaseStarted == null ? acc : acc + 1),
22 | 0,
23 | );
24 |
25 | expect(startedTestCases).toBe(expectedCount);
26 | }
27 |
28 | @then("it runs the scenario {string}")
29 | public checkRunSingleScenario(scenario: string) {
30 | const actualNames =
31 | this.runner.extractor.getPickleNamesInOrderOfExecution();
32 | expect(actualNames).toEqual([scenario]);
33 | }
34 |
35 | @then("it runs the scenarios:")
36 | public checkScenarios(table: DataTable) {
37 | const expectedNames = table.rows().map((row) => row[0]);
38 | const actualNames =
39 | this.runner.extractor.getPickleNamesInOrderOfExecution();
40 | expect(actualNames).toEqual(expectedNames);
41 | }
42 |
43 | @then("scenario {string} has status {string}")
44 | public checkScenarioStatus(name: string, status: string) {
45 | const result = this.runner.extractor.getTestCaseResult(name);
46 |
47 | expect(result.status).toEqual(status.toUpperCase());
48 | }
49 |
50 | @then("scenario {string} step {string} has the attachments:")
51 | public checkStepAttachment(
52 | pickleName: string,
53 | stepText: string,
54 | table: DataTable,
55 | ) {
56 | const expectedAttachments = table.hashes().map((x) => {
57 | return {
58 | body: x.DATA,
59 | mediaType: x["MEDIA TYPE"],
60 | contentEncoding: ENCODING_MAP[x["MEDIA ENCODING"]],
61 | };
62 | });
63 |
64 | const actualAttachments = this.runner.extractor
65 | .getAttachmentsForStep(pickleName, stepText)
66 | .map(Extractor.simplifyAttachment);
67 |
68 | expect(actualAttachments).toEqual(expectedAttachments);
69 | }
70 |
71 | @then("scenario {string} {string} hook has the attachments:")
72 | public checkHookAttachment(
73 | pickleName: string,
74 | hookKeyword: string,
75 | table: DataTable,
76 | ) {
77 | const expectedAttachments: messages.Attachment[] = table
78 | .hashes()
79 | .map((x) => {
80 | return {
81 | body: x.DATA,
82 | mediaType: x["MEDIA TYPE"],
83 | contentEncoding: ENCODING_MAP[x["MEDIA ENCODING"]],
84 | };
85 | });
86 |
87 | const actualAttachments = this.runner.extractor
88 | .getAttachmentsForHook(pickleName, hookKeyword === "Before")
89 | .map(Extractor.simplifyAttachment);
90 |
91 | expect(actualAttachments).toEqual(expectedAttachments);
92 | }
93 |
94 | @then("scenario {string} step {string} has the logs:")
95 | public checkStepLogs(pickleName: string, stepName: string, logs: DataTable) {
96 | const expectedLogs = logs.raw().map((row) => row[0]);
97 | const actualLogs = Extractor.logsFromAttachments(
98 | this.runner.extractor.getAttachmentsForStep(pickleName, stepName),
99 | );
100 |
101 | expect(actualLogs).toStrictEqual(expectedLogs);
102 | }
103 |
104 | @then("scenario {string} step {string} has no attachments")
105 | public checkNoStepLogs(pickleName: string, stepName: string) {
106 | const attachments = this.runner.extractor.getAttachmentsForStep(
107 | pickleName,
108 | stepName,
109 | );
110 |
111 | expect(attachments).toStrictEqual([]);
112 | }
113 |
114 | @then("the hook {string} was executed on scenario {string}")
115 | public checkNamedHookExecution(hookName: string, scenarioName: string) {
116 | const hook = this.runner.extractor.getHookByName(hookName);
117 | const executions = this.runner.extractor.getHookExecutions(
118 | scenarioName,
119 | hook.id,
120 | );
121 |
122 | assert(
123 | executions.length === 1,
124 | `Hook ${hookName} executed ${executions.length} times on scenario "${scenarioName}"`,
125 | );
126 | }
127 | }
128 |
129 | export = ScenarioSteps;
130 |
131 | // Then(
132 | // "the scenario {string} has the steps:",
133 | // function(this: World, name: string, table: DataTable) {
134 | // const actualTexts = getTestStepResults(this.lastRun.envelopes, name).map(
135 | // (s) => s.text
136 | // );
137 | // const expectedTexts = table.rows().map((row) => row[0]);
138 | // expect(actualTexts).to.eql(expectedTexts);
139 | // }
140 | // );
141 | //
142 | // Then(
143 | // "scenario {string} step {string} has status {string}",
144 | // function(this: World, pickleName: string, stepText: string, status: string) {
145 | // const testStepResults = getTestStepResults(
146 | // this.lastRun.envelopes,
147 | // pickleName
148 | // );
149 | // const testStepResult = testStepResults.find((x) => x.text === stepText);
150 | // expect(testStepResult.result.status).to.eql(
151 | // status.toUpperCase() as messages.TestStepResultStatus
152 | // );
153 | // }
154 | // );
155 | //
156 | // Then(
157 | // "scenario {string} attempt {int} step {string} has status {string}",
158 | // function(
159 | // this: World,
160 | // pickleName: string,
161 | // attempt: number,
162 | // stepText: string,
163 | // status: string
164 | // ) {
165 | // const testStepResults = getTestStepResults(
166 | // this.lastRun.envelopes,
167 | // pickleName,
168 | // attempt
169 | // );
170 | // const testStepResult = testStepResults.find((x) => x.text === stepText);
171 | // expect(testStepResult.result.status).to.eql(
172 | // status.toUpperCase() as messages.TestStepResultStatus
173 | // );
174 | // }
175 | // );
176 | //
177 | // Then(
178 | // "scenario {string} {string} hook has status {string}",
179 | // function(
180 | // this: World,
181 | // pickleName: string,
182 | // hookKeyword: string,
183 | // status: string
184 | // ) {
185 | // const testStepResults = getTestStepResults(
186 | // this.lastRun.envelopes,
187 | // pickleName
188 | // );
189 | // const testStepResult = testStepResults.find((x) => x.text === hookKeyword);
190 | // expect(testStepResult.result.status).to.eql(
191 | // status.toUpperCase() as messages.TestStepResultStatus
192 | // );
193 | // }
194 | // );
195 | //
196 | // Then(
197 | // "scenario {string} step {string} failed with:",
198 | // function(
199 | // this: World,
200 | // pickleName: string,
201 | // stepText: string,
202 | // errorMessage: string
203 | // ) {
204 | // const testStepResults = getTestStepResults(
205 | // this.lastRun.envelopes,
206 | // pickleName
207 | // );
208 | // const testStepResult = testStepResults.find((x) => x.text === stepText);
209 | // if (semver.satisfies(process.version, ">=14.0.0")) {
210 | // errorMessage = errorMessage.replace(
211 | // "{ member: [Circular] }",
212 | // "[ { member: [Circular *1] }"
213 | // );
214 | // }
215 | // expect(testStepResult.result.status).to.eql(
216 | // messages.TestStepResultStatus.FAILED
217 | // );
218 | // expect(testStepResult.result.message).to.include(errorMessage);
219 | // }
220 | // );
221 | //
222 | // Then(
223 | // "scenario {string} attempt {int} step {string} failed with:",
224 | // function(
225 | // this: World,
226 | // pickleName: string,
227 | // attempt: number,
228 | // stepText: string,
229 | // errorMessage: string
230 | // ) {
231 | // const testStepResults = getTestStepResults(
232 | // this.lastRun.envelopes,
233 | // pickleName,
234 | // attempt
235 | // );
236 | // const testStepResult = testStepResults.find((x) => x.text === stepText);
237 | // expect(testStepResult.result.status).to.eql(
238 | // messages.TestStepResultStatus.FAILED
239 | // );
240 | // expect(testStepResult.result.message).to.include(errorMessage);
241 | // }
242 | // );
243 | //
244 | // Then(
245 | // "scenario {string} step {string} has the doc string:",
246 | // function(
247 | // this: World,
248 | // pickleName: string,
249 | // stepText: string,
250 | // docString: string
251 | // ) {
252 | // const pickleStep = getPickleStep(
253 | // this.lastRun.envelopes,
254 | // pickleName,
255 | // stepText
256 | // );
257 | // expect(pickleStep.argument.docString.content).to.eql(docString);
258 | // }
259 | // );
260 | //
261 | // Then(
262 | // "scenario {string} step {string} has the data table:",
263 | // function(
264 | // this: World,
265 | // pickleName: string,
266 | // stepText: string,
267 | // dataTable: DataTable
268 | // ) {
269 | // const pickleStep = getPickleStep(
270 | // this.lastRun.envelopes,
271 | // pickleName,
272 | // stepText
273 | // );
274 | // expect(new DataTable(pickleStep.argument.dataTable)).to.eql(dataTable);
275 | // }
276 | // );
277 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/support/formatter_output_helpers.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IJsonFeature,
3 | IJsonScenario,
4 | IJsonStep,
5 | } from "@cucumber/cucumber/lib/formatter/json_formatter";
6 | import { valueOrDefault } from "@cucumber/cucumber/lib/value_checker";
7 | import * as messages from "@cucumber/messages";
8 |
9 | function normalizeExceptionAndUri(exception: string, cwd: string): string {
10 | return exception
11 | .replace(cwd, "")
12 | .replace(/\\/g, "/")
13 | .replace("/features", "features")
14 | .split("\n")[0];
15 | }
16 |
17 | function normalizeMessage(obj: any, cwd: string): void {
18 | if (obj.uri != null) {
19 | obj.uri = normalizeExceptionAndUri(obj.uri, cwd);
20 | }
21 | if (obj.sourceReference?.uri != null) {
22 | obj.sourceReference.uri = normalizeExceptionAndUri(
23 | obj.sourceReference.uri,
24 | cwd,
25 | );
26 | }
27 | if (obj.testStepResult != null) {
28 | if (obj.testStepResult.duration != null) {
29 | obj.testStepResult.duration.nanos = 0;
30 | }
31 | if (obj.testStepResult.message != null) {
32 | obj.testStepResult.message = normalizeExceptionAndUri(
33 | obj.testStepResult.message,
34 | cwd,
35 | );
36 | }
37 | }
38 | }
39 |
40 | export function normalizeMessageOutput(
41 | envelopeObjects: messages.Envelope[],
42 | cwd: string,
43 | ): messages.Envelope[] {
44 | envelopeObjects.forEach((e: any) => {
45 | for (const key of Object.keys(e)) {
46 | normalizeMessage(e[key], cwd);
47 | }
48 | });
49 | return envelopeObjects;
50 | }
51 |
52 | export function stripMetaMessages(
53 | envelopeObjects: messages.Envelope[],
54 | ): messages.Envelope[] {
55 | return envelopeObjects.filter((e: any) => {
56 | // filter off meta objects, almost none of it predictable/useful for testing
57 | return e.meta == null;
58 | });
59 | }
60 |
61 | export function normalizeJsonOutput(str: string, cwd: string): IJsonFeature[] {
62 | const json: IJsonFeature[] = JSON.parse(valueOrDefault(str, "[]"));
63 | json.forEach((feature: IJsonFeature) => {
64 | if (feature.uri != null) {
65 | feature.uri = normalizeExceptionAndUri(feature.uri, cwd);
66 | }
67 | feature.elements.forEach((element: IJsonScenario) => {
68 | element.steps.forEach((step: IJsonStep) => {
69 | if (step.match != null && step.match.location != null) {
70 | step.match.location = normalizeExceptionAndUri(
71 | step.match.location,
72 | cwd,
73 | );
74 | }
75 | if (step.result != null) {
76 | if (step.result.duration != null) {
77 | step.result.duration = 0;
78 | }
79 | if (step.result.error_message != null) {
80 | step.result.error_message = normalizeExceptionAndUri(
81 | step.result.error_message,
82 | cwd,
83 | );
84 | }
85 | }
86 | });
87 | });
88 | });
89 | return json;
90 | }
91 |
92 | export const ignorableKeys = [
93 | "meta",
94 | // sources
95 | "uri",
96 | "line",
97 | // ids
98 | "astNodeId",
99 | "astNodeIds",
100 | "hookId",
101 | "id",
102 | "pickleId",
103 | "pickleStepId",
104 | "stepDefinitionIds",
105 | "testCaseId",
106 | "testCaseStartedId",
107 | "testStepId",
108 | // time
109 | "nanos",
110 | "seconds",
111 | // errors
112 | "message",
113 | ];
114 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/support/helpers.ts:
--------------------------------------------------------------------------------
1 | // Adapted from:
2 | // https://github.com/cucumber/cucumber-js/blob/6505e61abce385787767f270b6ce2077eb3d7c1c/features/support/message_helpers.ts
3 | import { getGherkinStepMap } from "@cucumber/cucumber/lib/formatter/helpers/gherkin_document_parser";
4 | import {
5 | getPickleStepMap,
6 | getStepKeyword,
7 | } from "@cucumber/cucumber/lib/formatter/helpers/pickle_parser";
8 | import { doesHaveValue } from "@cucumber/cucumber/lib/value_checker";
9 | import * as messages from "@cucumber/messages";
10 | import { Query } from "@cucumber/query";
11 | import * as assert from "node:assert";
12 | import util, { inspect } from "util";
13 |
14 | export function parseEnvString(str: string): NodeJS.ProcessEnv {
15 | const result: NodeJS.ProcessEnv = {};
16 | if (doesHaveValue(str)) {
17 | try {
18 | Object.assign(result, JSON.parse(str));
19 | } catch {
20 | str
21 | .split(/\s+/)
22 | .map((keyValue) => keyValue.split("="))
23 | .forEach((pair) => (result[pair[0]] = pair[1]));
24 | }
25 | }
26 | return result;
27 | }
28 |
29 | export function dump(val: unknown): string {
30 | return inspect(val, { depth: null });
31 | }
32 |
33 | export interface IStepTextAndResult {
34 | text: string;
35 |
36 | result: messages.TestStepResult;
37 | }
38 |
39 | export type SimpleAttachment = Pick<
40 | messages.Attachment,
41 | "body" | "mediaType" | "contentEncoding"
42 | >;
43 |
44 | export class Extractor {
45 | public constructor(private readonly envelopes: messages.Envelope[]) {}
46 |
47 | public static logsFromAttachments(
48 | attachments: messages.Attachment[],
49 | ): string[] {
50 | return attachments
51 | .filter(
52 | (att) =>
53 | att.contentEncoding === messages.AttachmentContentEncoding.IDENTITY &&
54 | att.mediaType === "text/x.cucumber.log+plain",
55 | )
56 | .map((att) => att.body);
57 | }
58 |
59 | public static simplifyAttachment(
60 | attachment: messages.Attachment,
61 | ): SimpleAttachment {
62 | return {
63 | body: attachment.body,
64 | mediaType: attachment.mediaType,
65 | contentEncoding: attachment.contentEncoding,
66 | };
67 | }
68 |
69 | public getPickleNamesInOrderOfExecution(): string[] {
70 | const pickleNameMap: Record = {};
71 | const testCaseToPickleNameMap: Record = {};
72 | const result: string[] = [];
73 | this.envelopes.forEach((e) => {
74 | if (e.pickle != null) {
75 | pickleNameMap[e.pickle.id] = e.pickle.name;
76 | } else if (e.testCase != null) {
77 | testCaseToPickleNameMap[e.testCase.id] =
78 | pickleNameMap[e.testCase.pickleId];
79 | } else if (e.testCaseStarted != null) {
80 | result.push(testCaseToPickleNameMap[e.testCaseStarted.testCaseId]);
81 | }
82 | });
83 | return result;
84 | }
85 |
86 | public getPickleStep(
87 | pickleName: string,
88 | stepText: string,
89 | ): messages.PickleStep {
90 | const pickle = this.getPickle(pickleName);
91 | const gherkinDocument = this.getGherkinDocument(pickle.uri);
92 | return this.getPickleStepByStepText(pickle, gherkinDocument, stepText);
93 | }
94 |
95 | public getHookByName(hookName: string): messages.Hook {
96 | const hookEnvelope = this.envelopes.find(
97 | ({ hook }) => hook?.name === hookName,
98 | );
99 |
100 | assert.ok(hookEnvelope, `Unknown hook ${hookName}`);
101 |
102 | return hookEnvelope.hook!;
103 | }
104 |
105 | public getHookExecutions(
106 | pickleName: string,
107 | hookId: string,
108 | ): messages.TestStep[] {
109 | const pickle = this.getPickle(pickleName);
110 | const testCase = this.getTestCase(pickle.id);
111 |
112 | return testCase.testSteps.filter((step) => step.hookId === hookId);
113 | }
114 |
115 | public getTestCaseResult(pickleName: string): messages.TestStepResult {
116 | const query = new Query();
117 | this.envelopes.forEach((envelope) => query.update(envelope));
118 | const pickle = this.getPickle(pickleName);
119 | return messages.getWorstTestStepResult(
120 | query.getPickleTestStepResults([pickle.id]),
121 | );
122 | }
123 |
124 | public getTestStepResults(
125 | pickleName: string,
126 | attempt = 0,
127 | ): IStepTextAndResult[] {
128 | const pickle = this.getPickle(pickleName);
129 | const gherkinDocument = this.getGherkinDocument(pickle.uri);
130 | const testCase = this.getTestCase(pickle.id);
131 | const testCaseStarted = this.getTestCaseStarted(testCase.id, attempt);
132 | const testStepIdToResultMap: Record = {};
133 | this.envelopes.forEach((e) => {
134 | if (
135 | e.testStepFinished != null &&
136 | e.testStepFinished.testCaseStartedId === testCaseStarted.id
137 | ) {
138 | testStepIdToResultMap[e.testStepFinished.testStepId] =
139 | e.testStepFinished.testStepResult;
140 | }
141 | });
142 | const gherkinStepMap = getGherkinStepMap(gherkinDocument);
143 | const pickleStepMap = getPickleStepMap(pickle);
144 | let isBeforeHook = true;
145 | return testCase.testSteps.map((testStep) => {
146 | let text;
147 | if (testStep.pickleStepId == null) {
148 | text = isBeforeHook ? "Before" : "After";
149 | } else {
150 | isBeforeHook = false;
151 | const pickleStep = pickleStepMap[testStep.pickleStepId];
152 | const keyword = getStepKeyword({ pickleStep, gherkinStepMap });
153 | text = `${keyword}${pickleStep.text}`;
154 | }
155 | return { text, result: testStepIdToResultMap[testStep.id] };
156 | });
157 | }
158 |
159 | public getAttachmentsForStep(
160 | pickleName: string,
161 | stepText: string,
162 | ): messages.Attachment[] {
163 | const pickle = this.getPickle(pickleName);
164 | const gherkinDocument = this.getGherkinDocument(pickle.uri);
165 | const testCase = this.getTestCase(pickle.id);
166 | const pickleStep = this.getPickleStepByStepText(
167 | pickle,
168 | gherkinDocument,
169 | stepText,
170 | );
171 | assert.ok(
172 | pickleStep,
173 | `Step "${stepText}" not found in pickle ${dump(pickle)}`,
174 | );
175 |
176 | const testStep = testCase.testSteps.find(
177 | (s) => s.pickleStepId === pickleStep.id,
178 | )!;
179 | const testCaseStarted = this.getTestCaseStarted(testCase.id);
180 | return this.getTestStepAttachments(testCaseStarted.id, testStep.id);
181 | }
182 |
183 | public getAttachmentsForHook(
184 | pickleName: string,
185 | isBeforeHook: boolean,
186 | ): messages.Attachment[] {
187 | const pickle = this.getPickle(pickleName);
188 | const testCase = this.getTestCase(pickle.id);
189 | // Ignore the first Before hook and the last After hook
190 | // Those are used to set up and tear down the tsflow harness
191 | const testStepIndex = isBeforeHook ? 1 : testCase.testSteps.length - 2;
192 | const testStep = testCase.testSteps[testStepIndex];
193 | const testCaseStarted = this.getTestCaseStarted(testCase.id);
194 | return this.getTestStepAttachments(testCaseStarted.id, testStep.id);
195 | }
196 |
197 | private getPickle(pickleName: string): messages.Pickle {
198 | const pickleEnvelope = this.envelopes.find(
199 | (e) => e.pickle != null && e.pickle.name === pickleName,
200 | );
201 | if (pickleEnvelope == null) {
202 | throw new Error(
203 | `No pickle with name "${pickleName}" in this.envelopes:\n ${util.inspect(
204 | this.envelopes,
205 | )}`,
206 | );
207 | }
208 | return pickleEnvelope.pickle!;
209 | }
210 |
211 | private getGherkinDocument(uri: string): messages.GherkinDocument {
212 | const gherkinDocumentEnvelope = this.envelopes.find(
213 | (e) => e.gherkinDocument != null && e.gherkinDocument.uri === uri,
214 | );
215 | if (gherkinDocumentEnvelope == null) {
216 | throw new Error(
217 | `No gherkinDocument with uri "${uri}" in this.envelopes:\n ${util.inspect(
218 | this.envelopes,
219 | )}`,
220 | );
221 | }
222 | return gherkinDocumentEnvelope.gherkinDocument!;
223 | }
224 |
225 | private getTestCase(pickleId: string): messages.TestCase {
226 | const testCaseEnvelope = this.envelopes.find(
227 | (e) => e.testCase != null && e.testCase.pickleId === pickleId,
228 | );
229 | if (testCaseEnvelope == null) {
230 | throw new Error(
231 | `No testCase with pickleId "${pickleId}" in this.envelopes:\n ${util.inspect(
232 | this.envelopes,
233 | )}`,
234 | );
235 | }
236 | return testCaseEnvelope.testCase!;
237 | }
238 |
239 | private getTestCaseStarted(
240 | testCaseId: string,
241 | attempt = 0,
242 | ): messages.TestCaseStarted {
243 | const testCaseStartedEnvelope = this.envelopes.find(
244 | (e) =>
245 | e.testCaseStarted != null &&
246 | e.testCaseStarted.testCaseId === testCaseId &&
247 | e.testCaseStarted.attempt === attempt,
248 | );
249 | if (testCaseStartedEnvelope == null) {
250 | throw new Error(
251 | `No testCaseStarted with testCaseId "${testCaseId}" in this.envelopes:\n ${util.inspect(
252 | this.envelopes,
253 | )}`,
254 | );
255 | }
256 | return testCaseStartedEnvelope.testCaseStarted!;
257 | }
258 |
259 | private getPickleStepByStepText(
260 | pickle: messages.Pickle,
261 | gherkinDocument: messages.GherkinDocument,
262 | stepText: string,
263 | ): messages.PickleStep {
264 | const gherkinStepMap = getGherkinStepMap(gherkinDocument);
265 | return pickle.steps.find((s) => {
266 | const keyword = getStepKeyword({ pickleStep: s, gherkinStepMap });
267 | return `${keyword}${s.text}` === stepText;
268 | })!;
269 | }
270 |
271 | private getTestStepAttachments(
272 | testCaseStartedId: string,
273 | testStepId: string,
274 | ): messages.Attachment[] {
275 | return this.envelopes
276 | .filter(
277 | (e) =>
278 | e.attachment != null &&
279 | e.attachment.testCaseStartedId === testCaseStartedId &&
280 | e.attachment.testStepId === testStepId,
281 | )
282 | .map((e) => e.attachment!);
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/support/runner.ts:
--------------------------------------------------------------------------------
1 | // Adapted from:
2 | // https://github.com/cucumber/cucumber-js/blob/6505e61abce385787767f270b6ce2077eb3d7c1c/features/support/world.ts
3 | import * as messageStreams from "@cucumber/message-streams";
4 | import * as messages from "@cucumber/messages";
5 | import assert from "assert";
6 | import { execFile } from "child_process";
7 | import expect from "expect";
8 | import path from "path";
9 | import { Writable } from "stream";
10 | import { finished } from "stream/promises";
11 | import stripAnsi from "strip-ansi";
12 | import VError from "verror";
13 | import { Extractor } from "./helpers";
14 | import { TestDir } from "./testDir";
15 |
16 | const projectPath = path.join(__dirname, "..", "..", "..");
17 | const cucumberBinPath = path.join(
18 | projectPath,
19 | "node_modules",
20 | ".bin",
21 | "cucumber-js",
22 | );
23 |
24 | interface IRunResult {
25 | error: any;
26 |
27 | stderr: string;
28 |
29 | stdout: string;
30 | }
31 |
32 | interface ILastRun {
33 | error: any;
34 |
35 | errorOutput: string;
36 |
37 | envelopes: messages.Envelope[];
38 |
39 | output: string;
40 | }
41 |
42 | export class TestRunner {
43 | public readonly dir = new TestDir();
44 |
45 | public sharedEnv?: NodeJS.ProcessEnv;
46 |
47 | public spawn: boolean = false;
48 |
49 | public debug: boolean = false;
50 |
51 | public worldParameters?: any;
52 |
53 | public verifiedLastRunError!: boolean;
54 |
55 | private _lastRun?: ILastRun;
56 |
57 | public get lastRun(): ILastRun {
58 | assert(this._lastRun, "Cucumber has not executed yet.");
59 | return this._lastRun;
60 | }
61 |
62 | public get extractor(): Extractor {
63 | return new Extractor(this.lastRun.envelopes);
64 | }
65 |
66 | public async run(
67 | envOverride: NodeJS.ProcessEnv | null = null,
68 | ): Promise {
69 | const messageFilename = "message.ndjson";
70 | const env = { ...process.env, ...this.sharedEnv, ...envOverride };
71 |
72 | const result = await new Promise((resolve) => {
73 | execFile(
74 | "node",
75 | [
76 | cucumberBinPath,
77 | "--format",
78 | `message:${messageFilename}`,
79 | "--require-module",
80 | "ts-node/register",
81 | "--require",
82 | "a-logging.ts",
83 | "--require",
84 | "step_definitions/**/*.ts",
85 | "--publish-quiet",
86 | ],
87 | { cwd: this.dir.path, env },
88 | (error, stdout, stderr) => {
89 | resolve({ error, stdout, stderr });
90 | },
91 | );
92 | });
93 |
94 | const stderrSuffix =
95 | result.error != null ? VError.fullStack(result.error) : "";
96 |
97 | const envelopes: messages.Envelope[] = [];
98 | const messageOutputStream = this.dir
99 | .readFileStream(messageFilename)
100 | ?.pipe(new messageStreams.NdjsonToMessageStream())
101 | .pipe(
102 | new Writable({
103 | objectMode: true,
104 | write(envelope: messages.Envelope, _: BufferEncoding, callback) {
105 | envelopes.push(envelope);
106 | callback();
107 | },
108 | }),
109 | );
110 |
111 | if (messageOutputStream !== undefined) {
112 | await finished(messageOutputStream);
113 | }
114 |
115 | this._lastRun = {
116 | error: result.error,
117 | errorOutput: result.stderr + stderrSuffix,
118 | envelopes,
119 | output: stripAnsi(result.stdout),
120 | };
121 | this.verifiedLastRunError = false;
122 |
123 | expect(this._lastRun.output).not.toContain("Unhandled rejection.");
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/src/support/testDir.ts:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 | import fs, { ReadStream, WriteStream } from "fs";
3 | import path from "path";
4 |
5 | export class TestDir {
6 | private _path?: string;
7 |
8 | public get path(): string {
9 | assert(this._path, "Test directory not configured");
10 | return this._path;
11 | }
12 |
13 | public set path(newPath: string) {
14 | this._path = newPath;
15 | }
16 |
17 | public readFileStream(
18 | ...pathParts: string[] | [string[]]
19 | ): ReadStream | null {
20 | const filePath = this.getPath(...pathParts);
21 |
22 | if (fs.existsSync(filePath)) {
23 | return fs.createReadStream(filePath, { encoding: "utf-8" });
24 | }
25 |
26 | return null;
27 | }
28 |
29 | public writeFileStream(...pathParts: string[] | [string[]]): WriteStream {
30 | const filePath = this.getPath(...pathParts);
31 |
32 | return fs.createWriteStream(filePath, { encoding: "utf-8" });
33 | }
34 |
35 | public readFile(...pathParts: string[] | [string[]]): string | null {
36 | const filePath = this.getPath(...pathParts);
37 |
38 | if (fs.existsSync(filePath)) {
39 | return fs.readFileSync(filePath, { encoding: "utf-8" });
40 | }
41 |
42 | return null;
43 | }
44 |
45 | public writeFile(pathParts: string | string[], data: string): void {
46 | const pathArgs = (
47 | typeof pathParts === "string" ? [pathParts] : pathParts
48 | ).flatMap((part) => part.split("/"));
49 |
50 | if (pathArgs.length > 1) {
51 | this.mkdir(pathArgs.slice(0, pathArgs.length - 1));
52 | }
53 |
54 | const filePath = this.getPath(pathArgs);
55 |
56 | fs.writeFileSync(filePath, data, { encoding: "utf-8" });
57 | }
58 |
59 | public mkdir(...pathParts: string[] | [string[]]): string {
60 | const dirPath = this.getPath(...pathParts);
61 | fs.mkdirSync(dirPath, { recursive: true });
62 | return dirPath;
63 | }
64 |
65 | private getPath(...pathParts: string[] | [string[]]): string {
66 | return path.join(
67 | this.path,
68 | ...(pathParts.length === 1 && Array.isArray(pathParts[0])
69 | ? pathParts[0]
70 | : (pathParts as string[])),
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/cucumber-tsflow-specs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "moduleResolution": "node",
5 | "module": "umd",
6 | "target": "es2018",
7 | "declaration": true,
8 | "declarationMap": true,
9 | "sourceMap": true,
10 | "strict": true,
11 | "skipLibCheck": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "removeComments": false,
15 | "esModuleInterop": true,
16 | "noEmit": true,
17 | "experimentalDecorators": true,
18 | "outDir": "./dist",
19 | "rootDir": "./src"
20 | },
21 | "include": ["./src/**/*.ts"],
22 | "references": [
23 | {
24 | "path": "../cucumber-tsflow"
25 | }
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/cucumber-tsflow/.npmignore:
--------------------------------------------------------------------------------
1 | *.ts
2 | tsconfig.json
3 | typings.json
4 | typings
5 | .npmignore
6 | *.tsbuildinfo
7 | !dist/**/*.d.ts
8 |
--------------------------------------------------------------------------------
/cucumber-tsflow/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/cucumber-tsflow/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cucumber-tsflow",
3 | "description": "Provides 'specflow' like bindings for CucumberJS 7.0.0+ in TypeScript 1.7+.",
4 | "version": "4.0.0",
5 | "author": "Tim Roberts ",
6 | "maintainers": [
7 | {
8 | "name": "Luiz Ferraz",
9 | "email": "luiz@lferraz.com",
10 | "url": "https://github.com/Fryuni"
11 | }
12 | ],
13 | "license": "MIT",
14 | "main": "./dist",
15 | "keywords": [
16 | "testing",
17 | "bdd",
18 | "cucumber",
19 | "gherkin",
20 | "tests",
21 | "typescript",
22 | "specflow"
23 | ],
24 | "repository": {
25 | "type": "git",
26 | "url": "https://github.com/timjroberts/cucumber-js-tsflow.git"
27 | },
28 | "dependencies": {
29 | "callsites": "^4.2.0",
30 | "log4js": "^6.3.0",
31 | "source-map-support": "^0.5.19",
32 | "underscore": "^1.8.3"
33 | },
34 | "peerDependencies": {
35 | "@cucumber/cucumber": "^7 || ^8 || ^9 || ^10 || ^11"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/binding-decorator.ts:
--------------------------------------------------------------------------------
1 | import {
2 | After,
3 | AfterAll,
4 | AfterStep,
5 | Before,
6 | BeforeAll,
7 | BeforeStep,
8 | Given,
9 | Then,
10 | When,
11 | World,
12 | } from "@cucumber/cucumber";
13 | import {
14 | IDefineStepOptions,
15 | IDefineTestStepHookOptions,
16 | } from "@cucumber/cucumber/lib/support_code_library_builder/types";
17 | import { PickleTag } from "@cucumber/messages";
18 | import * as _ from "underscore";
19 | import { BindingRegistry, DEFAULT_TAG } from "./binding-registry";
20 | import logger from "./logger";
21 | import {
22 | ManagedScenarioContext,
23 | ScenarioContext,
24 | ScenarioInfo,
25 | } from "./managed-scenario-context";
26 | import {
27 | CucumberAttachments,
28 | CucumberLog,
29 | WorldParameters,
30 | } from "./provided-context";
31 | import { StepBinding, StepBindingFlags } from "./step-binding";
32 | import { ContextType, StepPattern, TypeDecorator } from "./types";
33 |
34 | interface WritableWorld extends World {
35 | [key: string]: any;
36 | }
37 |
38 | /**
39 | * The property name of the current scenario context that will be attached to the Cucumber
40 | * world object.
41 | */
42 | const SCENARIO_CONTEXT_SLOTNAME: string = "__SCENARIO_CONTEXT";
43 |
44 | /**
45 | * A set of step patterns that have been registered with Cucumber.
46 | *
47 | * In order to support scoped (or tagged) step definitions, we must ensure that any step binding is
48 | * only registered with Cucumber once. The binding function for that step pattern then becomes
49 | * responsible for looking up and execuing the step binding based on the context that is in scope at
50 | * the point of invocation.
51 | */
52 | const stepPatternRegistrations = new Map();
53 |
54 | // tslint:disable:no-bitwise
55 |
56 | function ensureNoCyclicDependencies(target: any, currentPath: any[] = []) {
57 | const dependencies = BindingRegistry.instance.getContextTypesForTarget(
58 | target.prototype,
59 | );
60 |
61 | if (dependencies.length === 0) {
62 | return;
63 | }
64 |
65 | for (const dependency of dependencies) {
66 | if (dependency === undefined) {
67 | throw new Error(
68 | `Undefined dependency detected in ${target.name}. You possibly have an import cycle.\n` +
69 | "See https://nodejs.org/api/modules.html#modules_cycles",
70 | );
71 | }
72 |
73 | if (currentPath.includes(dependency)) {
74 | throw new Error(
75 | `Cyclic dependency detected: ${dependency.name} -> ${target.name} -> ${currentPath.map((t) => t.name).join(" -> ")}`,
76 | );
77 | }
78 |
79 | ensureNoCyclicDependencies(dependency, [...currentPath, target]);
80 | }
81 | }
82 |
83 | /**
84 | * A class decorator that marks the associated class as a CucumberJS binding.
85 | *
86 | * @param requiredContextTypes An optional array of Types that will be created and passed into the created
87 | * object for each scenario.
88 | *
89 | * An instance of the decorated class will be created for each scenario.
90 | */
91 | export function binding(requiredContextTypes?: ContextType[]): TypeDecorator {
92 | return (target: new (...args: any[]) => T) => {
93 | ensureSystemBindings();
94 | const bindingRegistry = BindingRegistry.instance;
95 | bindingRegistry.registerContextTypesForTarget(
96 | target.prototype,
97 | requiredContextTypes,
98 | );
99 |
100 | ensureNoCyclicDependencies(target);
101 |
102 | const allBindings: StepBinding[] = [
103 | ...bindingRegistry.getStepBindingsForTarget(target),
104 | ...bindingRegistry.getStepBindingsForTarget(target.prototype),
105 | ];
106 |
107 | for (const stepBinding of allBindings) {
108 | if (stepBinding.bindingType & StepBindingFlags.StepDefinitions) {
109 | let stepBindingFlags = stepPatternRegistrations.get(
110 | stepBinding.stepPattern.toString(),
111 | );
112 |
113 | if (stepBindingFlags === undefined) {
114 | stepBindingFlags = StepBindingFlags.none;
115 | }
116 |
117 | if (stepBindingFlags & stepBinding.bindingType) {
118 | return;
119 | }
120 |
121 | const bound = bindStepDefinition(stepBinding);
122 |
123 | if (bound) {
124 | stepPatternRegistrations.set(
125 | stepBinding.stepPattern.toString(),
126 | stepBindingFlags | stepBinding.bindingType,
127 | );
128 | }
129 | } else if (stepBinding.bindingType & StepBindingFlags.Hooks) {
130 | bindHook(stepBinding);
131 | } else {
132 | logger.trace("Ignored binding", stepBinding);
133 | }
134 | }
135 | };
136 | }
137 |
138 | function getContextFromWorld(world: World): ScenarioContext {
139 | const context: unknown = (world as Record)[
140 | SCENARIO_CONTEXT_SLOTNAME
141 | ];
142 |
143 | if (context instanceof ManagedScenarioContext) {
144 | return context;
145 | }
146 |
147 | throw new Error(
148 | "Scenario context have not been initialized in the provided World object.",
149 | );
150 | }
151 |
152 | export function getBindingFromWorld(
153 | world: World,
154 | contextType: T,
155 | ): InstanceType {
156 | const context = getContextFromWorld(world);
157 |
158 | return context.getContextInstance(contextType);
159 | }
160 |
161 | export function ensureWorldIsInitialized() {
162 | ensureSystemBindings();
163 | }
164 |
165 | /**
166 | * Ensures that the 'cucumber-tsflow' hooks are bound to Cucumber.
167 | *
168 | * @param cucumber The cucumber object.
169 | *
170 | * The hooks will only be registered with Cucumber once regardless of which binding invokes the
171 | * function.
172 | */
173 | const ensureSystemBindings = _.once(() => {
174 | Before(function (this: WritableWorld, scenario) {
175 | logger.trace(
176 | "Setting up scenario context for scenario:",
177 | JSON.stringify(scenario),
178 | );
179 |
180 | const scenarioInfo = new ScenarioInfo(
181 | scenario.pickle.name!,
182 | _.map(scenario.pickle.tags!, (tag: PickleTag) => tag.name!),
183 | );
184 |
185 | const scenarioContext = new ManagedScenarioContext(scenarioInfo);
186 |
187 | this[SCENARIO_CONTEXT_SLOTNAME] = scenarioContext;
188 |
189 | scenarioContext.addExternalObject(scenarioInfo);
190 | scenarioContext.addExternalObject(new WorldParameters(this.parameters));
191 | scenarioContext.addExternalObject(new CucumberLog(this.log.bind(this)));
192 | scenarioContext.addExternalObject(
193 | new CucumberAttachments(this.attach.bind(this)),
194 | );
195 | });
196 |
197 | After(function (this: WritableWorld) {
198 | const scenarioContext = this[
199 | SCENARIO_CONTEXT_SLOTNAME
200 | ] as ManagedScenarioContext;
201 |
202 | if (scenarioContext) {
203 | scenarioContext.dispose();
204 | }
205 | });
206 |
207 | try {
208 | const stackFilter = require("@cucumber/cucumber/lib/filter_stack_trace");
209 | const path = require("path");
210 |
211 | const originalFileNameFilter = stackFilter.isFileNameInCucumber;
212 |
213 | if (originalFileNameFilter !== undefined) {
214 | const projectRootPath = path.join(__dirname, "..") + "/";
215 |
216 | Object.defineProperty(stackFilter, "isFileNameInCucumber", {
217 | value: (fileName: string) =>
218 | originalFileNameFilter(fileName) ||
219 | fileName.startsWith(projectRootPath) ||
220 | fileName.includes("node_modules"),
221 | configurable: true,
222 | enumerable: true,
223 | });
224 | }
225 | } catch {
226 | // Ignore errors, proper stack filtering is not officially supported
227 | // so we override on a best effor basis only
228 | }
229 |
230 | // Decorate the Cucumber step definition snippet builder so that it uses our syntax
231 |
232 | // let currentSnippetBuilder = cucumberSys.SupportCode.StepDefinitionSnippetBuilder;
233 |
234 | // cucumberSys.SupportCode.StepDefinitionSnippetBuilder = function (step, syntax) {
235 | // return currentSnippetBuilder(step, {
236 | // build: function (functionName: string, pattern, parameters, comment) {
237 | // let callbackName = parameters[parameters.length - 1];
238 |
239 | // return `@${functionName.toLowerCase()}(${pattern})\n` +
240 | // `public ${functionName}XXX (${parameters.join(", ")}): void {\n` +
241 | // ` // ${comment}\n` +
242 | // ` ${callbackName}.pending();\n` +
243 | // `}\n`;
244 | // }
245 | // });
246 | // }
247 | });
248 |
249 | /**
250 | * Binds a step definition to Cucumber.
251 | *
252 | * @param stepBinding The [[StepBinding]] that represents a 'given', 'when', or 'then' step definition.
253 | */
254 | function bindStepDefinition(stepBinding: StepBinding): boolean {
255 | const bindingFunc = function (this: WritableWorld): any {
256 | const bindingRegistry = BindingRegistry.instance;
257 |
258 | const scenarioContext = this[
259 | SCENARIO_CONTEXT_SLOTNAME
260 | ] as ManagedScenarioContext;
261 |
262 | const matchingStepBindings = bindingRegistry.getStepBindings(
263 | stepBinding.stepPattern.toString(),
264 | );
265 |
266 | const contextTypes = bindingRegistry.getContextTypesForTarget(
267 | matchingStepBindings[0].targetPrototype,
268 | );
269 | const bindingObject = scenarioContext.getOrActivateBindingClass(
270 | matchingStepBindings[0].targetPrototype,
271 | contextTypes,
272 | );
273 |
274 | return (
275 | bindingObject[matchingStepBindings[0].targetPropertyKey] as () => void
276 | ).apply(bindingObject, arguments as any);
277 | };
278 |
279 | Object.defineProperty(bindingFunc, "length", {
280 | value: stepBinding.argsLength,
281 | });
282 |
283 | logger.trace("Binding step:", stepBinding);
284 |
285 | const bindingOptions: IDefineStepOptions & IDefineTestStepHookOptions = {
286 | timeout: stepBinding.timeout,
287 | wrapperOptions: stepBinding.wrapperOption,
288 | tags: stepBinding.tag === DEFAULT_TAG ? undefined : stepBinding.tag,
289 | };
290 |
291 | if (stepBinding.bindingType & StepBindingFlags.given) {
292 | Given(stepBinding.stepPattern, bindingOptions, bindingFunc);
293 | } else if (stepBinding.bindingType & StepBindingFlags.when) {
294 | When(stepBinding.stepPattern, bindingOptions, bindingFunc);
295 | } else if (stepBinding.bindingType & StepBindingFlags.then) {
296 | Then(stepBinding.stepPattern, bindingOptions, bindingFunc);
297 | } else {
298 | return false;
299 | }
300 |
301 | return true;
302 | }
303 |
304 | /**
305 | * Binds a hook to Cucumber.
306 | *
307 | * @param cucumber The cucumber object.
308 | * @param stepBinding The [[StepBinding]] that represents a 'before', or 'after', step definition.
309 | */
310 | function bindHook(stepBinding: StepBinding): void {
311 | const bindingFunc = function (this: any): any {
312 | const scenarioContext = this[
313 | SCENARIO_CONTEXT_SLOTNAME
314 | ] as ManagedScenarioContext;
315 | const contextTypes = BindingRegistry.instance.getContextTypesForTarget(
316 | stepBinding.targetPrototype,
317 | );
318 | const bindingObject = scenarioContext.getOrActivateBindingClass(
319 | stepBinding.targetPrototype,
320 | contextTypes,
321 | );
322 |
323 | return (bindingObject[stepBinding.targetPropertyKey] as () => void).apply(
324 | bindingObject,
325 | arguments as any,
326 | );
327 | };
328 |
329 | const globalBindFunc = () => {
330 | const targetPrototype = stepBinding.targetPrototype;
331 | const targetPrototypeKey = stepBinding.targetPropertyKey;
332 |
333 | return targetPrototype[targetPrototypeKey].apply(targetPrototype);
334 | };
335 |
336 | Object.defineProperty(bindingFunc, "length", {
337 | value: stepBinding.argsLength,
338 | });
339 |
340 | const bindingOptions: IDefineTestStepHookOptions = {
341 | timeout: stepBinding.timeout,
342 | tags: stepBinding.tag === DEFAULT_TAG ? undefined : stepBinding.tag,
343 | ...(stepBinding.hookOptions ?? {}),
344 | };
345 |
346 | logger.trace("Binding hook:", stepBinding);
347 |
348 | switch (stepBinding.bindingType) {
349 | case StepBindingFlags.before:
350 | Before(bindingOptions, bindingFunc);
351 | break;
352 | case StepBindingFlags.after:
353 | After(bindingOptions, bindingFunc);
354 | break;
355 | case StepBindingFlags.beforeAll:
356 | BeforeAll(globalBindFunc);
357 | break;
358 | case StepBindingFlags.beforeStep:
359 | BeforeStep(bindingFunc);
360 | break;
361 | case StepBindingFlags.afterStep:
362 | AfterStep(bindingFunc);
363 | break;
364 | case StepBindingFlags.afterAll:
365 | AfterAll(globalBindFunc);
366 | break;
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/binding-registry.ts:
--------------------------------------------------------------------------------
1 | import logger from "./logger";
2 |
3 | import { StepBinding } from "./step-binding";
4 | import { ContextType, StepPattern } from "./types";
5 |
6 | /**
7 | * Describes the binding metadata that is associated with a binding class.
8 | */
9 | interface TargetBinding {
10 | /**
11 | * A reference to the step bindings that are associated with the binding class.
12 | */
13 | stepBindings: StepBinding[];
14 |
15 | /**
16 | * The context types that are to be injected into the binding class during execution.
17 | */
18 | contextTypes: ContextType[];
19 | }
20 |
21 | /**
22 | * Represents the default step pattern.
23 | */
24 | export const DEFAULT_STEP_PATTERN: string = "/.*/";
25 |
26 | /**
27 | * Represents the default tag.
28 | */
29 | export const DEFAULT_TAG: string = "*";
30 |
31 | /**
32 | * A metadata registry that captures information about bindings and their bound step bindings.
33 | */
34 | export class BindingRegistry {
35 | private _bindings = new Map();
36 |
37 | private _targetBindings = new Map();
38 |
39 | /**
40 | * Gets the binding registry singleton.
41 | *
42 | * @returns A [[BindingRegistry]].
43 | */
44 | public static get instance(): BindingRegistry {
45 | const BINDING_REGISTRY_SLOTNAME: string =
46 | "__CUCUMBER_TSFLOW_BINDINGREGISTRY";
47 |
48 | const registry = (global as any)[BINDING_REGISTRY_SLOTNAME];
49 |
50 | if (!registry) {
51 | (global as any)[BINDING_REGISTRY_SLOTNAME] = new BindingRegistry();
52 | }
53 |
54 | return registry || (global as any)[BINDING_REGISTRY_SLOTNAME];
55 | }
56 |
57 | /**
58 | * Updates the binding registry with information about the context types required by a
59 | * binding class.
60 | *
61 | * @param targetPrototype The class representing the binding (constructor function).
62 | * @param contextTypes An array of [[ContextType]] that define the types of objects that
63 | * should be injected into the binding class during a scenario execution.
64 | */
65 | public registerContextTypesForTarget(
66 | targetPrototype: any,
67 | contextTypes?: ContextType[],
68 | ): void {
69 | if (!contextTypes) {
70 | return;
71 | }
72 |
73 | let targetDecorations = this._targetBindings.get(targetPrototype);
74 |
75 | if (!targetDecorations) {
76 | targetDecorations = {
77 | stepBindings: [],
78 | contextTypes: [],
79 | };
80 |
81 | this._targetBindings.set(targetPrototype, targetDecorations);
82 | }
83 |
84 | targetDecorations.contextTypes = contextTypes;
85 | }
86 |
87 | /**
88 | * Retrieves the context types that have been registered for a given binding class.
89 | *
90 | * @param targetPrototype The class representing the binding (constructor function).
91 | *
92 | * @returns An array of [[ContextType]] that have been registered for the specified
93 | * binding class.
94 | */
95 | public getContextTypesForTarget(targetPrototype: any): ContextType[] {
96 | const targetBinding = this._targetBindings.get(targetPrototype);
97 |
98 | if (!targetBinding) {
99 | return [];
100 | }
101 |
102 | return targetBinding.contextTypes;
103 | }
104 |
105 | /**
106 | * Updates the binding registry indexes with a step binding.
107 | *
108 | * @param stepBinding The step binding that is to be registered with the binding registry.
109 | */
110 | public registerStepBinding(stepBinding: StepBinding): void {
111 | if (!stepBinding.tag) {
112 | stepBinding.tag = DEFAULT_TAG;
113 | }
114 |
115 | const stepPattern: StepPattern = stepBinding.stepPattern
116 | ? stepBinding.stepPattern.toString()
117 | : DEFAULT_STEP_PATTERN;
118 |
119 | let stepBindings = this._bindings.get(stepPattern);
120 |
121 | if (!stepBindings) {
122 | stepBindings = [];
123 |
124 | this._bindings.set(stepPattern, stepBindings);
125 | }
126 |
127 | logger.trace("Attempting to register step binding", stepBinding);
128 |
129 | if (!stepBindings.some((b) => isSameStepBinding(stepBinding, b))) {
130 | logger.trace("Saving new step binding.");
131 | stepBindings.push(stepBinding);
132 | }
133 |
134 | // Index the step binding for the target
135 |
136 | let targetBinding = this._targetBindings.get(stepBinding.targetPrototype);
137 |
138 | if (!targetBinding) {
139 | targetBinding = {
140 | stepBindings: [],
141 | contextTypes: [],
142 | };
143 |
144 | this._targetBindings.set(stepBinding.targetPrototype, targetBinding);
145 | }
146 |
147 | if (
148 | !targetBinding.stepBindings.some((b) => isSameStepBinding(stepBinding, b))
149 | ) {
150 | logger.trace("Saving new step binding to target.");
151 | targetBinding.stepBindings.push(stepBinding);
152 | }
153 |
154 | logger.trace(
155 | "All target step bindings",
156 | targetBinding.stepBindings.map(
157 | (binding) => `${binding.stepPattern} ${binding.tag}`,
158 | ),
159 | );
160 |
161 | function isSameStepBinding(a: StepBinding, b: StepBinding) {
162 | return (
163 | a.callsite.filename === b.callsite.filename &&
164 | a.callsite.lineNumber === b.callsite.lineNumber &&
165 | String(a.stepPattern) === String(b.stepPattern) &&
166 | a.targetPropertyKey === b.targetPropertyKey
167 | );
168 | }
169 | }
170 |
171 | /**
172 | * Retrieves the step bindings that have been registered for a given binding class.
173 | *
174 | * @param targetPrototype The class representing the binding (constructor function).
175 | *
176 | * @returns An array of [[StepBinding]] objects that have been registered for the specified
177 | * binding class.
178 | */
179 | public getStepBindingsForTarget(targetPrototype: any): StepBinding[] {
180 | const targetBinding = this._targetBindings.get(targetPrototype);
181 |
182 | if (!targetBinding) {
183 | return [];
184 | }
185 |
186 | return targetBinding.stepBindings;
187 | }
188 |
189 | /**
190 | * Retrieves the step bindings for a given step pattern and collection of tag names.
191 | *
192 | * @param stepPattern The step pattern to search.
193 | *
194 | * @returns An array of [[StepBinding]] that map to the given step pattern and set of tag names.
195 | */
196 | public getStepBindings(stepPattern: StepPattern): StepBinding[] {
197 | return this._bindings.get(stepPattern) ?? [];
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/hook-decorators.ts:
--------------------------------------------------------------------------------
1 | import {
2 | IDefineTestCaseHookOptions,
3 | IDefineTestRunHookOptions,
4 | IDefineTestStepHookOptions,
5 | } from "@cucumber/cucumber/lib/support_code_library_builder/types";
6 | import { BindingRegistry } from "./binding-registry";
7 | import { Callsite } from "./our-callsite";
8 | import { StepBinding, StepBindingFlags } from "./step-binding";
9 | import { normalizeTag } from "./tag-normalization";
10 |
11 | // Replace `tags` with `tag` for backwards compatibility
12 | type HookOptions = Omit & {
13 | tag?: string;
14 | };
15 |
16 | function overloadedOption(tag?: string | HookOptions): HookOptions {
17 | if (tag === undefined || typeof tag === "string") {
18 | return { tag };
19 | }
20 |
21 | return tag;
22 | }
23 |
24 | function createHookDecorator(
25 | flag: StepBindingFlags,
26 | tagOrOption?: string | HookOptions,
27 | ): MethodDecorator {
28 | const callsite = Callsite.capture(2);
29 |
30 | const { tag, timeout, ...hookOptions } = overloadedOption(tagOrOption);
31 |
32 | return (
33 | target: any,
34 | propertyKey: string | symbol,
35 | descriptor: TypedPropertyDescriptor,
36 | ) => {
37 | const stepBinding: StepBinding = {
38 | stepPattern: "",
39 | bindingType: flag,
40 | targetPrototype: target,
41 | targetPropertyKey: propertyKey,
42 | argsLength: target[propertyKey].length,
43 | tag: normalizeTag(tag),
44 | callsite: callsite,
45 | timeout: timeout,
46 | hookOptions: hookOptions,
47 | };
48 |
49 | BindingRegistry.instance.registerStepBinding(stepBinding);
50 |
51 | return descriptor;
52 | };
53 | }
54 |
55 | /**
56 | * A method decorator that marks the associated function as a 'Before Scenario' step. The function is
57 | * executed before each scenario.
58 | *
59 | * @param tagOrOption An optional tag or hook options object.
60 | */
61 | export function before(tagOrOption?: string | HookOptions): MethodDecorator {
62 | return createHookDecorator(StepBindingFlags.before, tagOrOption);
63 | }
64 |
65 | /**
66 | * A method decorator that marks the associated function as an 'After Scenario' step. The function is
67 | * executed after each scenario.
68 | *
69 | * @param tagOrOption An optional tag or hook options object.
70 | */
71 | export function after(tagOrOption?: string | HookOptions): MethodDecorator {
72 | return createHookDecorator(StepBindingFlags.after, tagOrOption);
73 | }
74 |
75 | /**
76 | * A method decorator that marks the associated function as a 'Before Scenario' step. The function is
77 | * executed before each scenario.
78 | *
79 | * @param options Optional hook options object.
80 | */
81 | export function beforeAll(
82 | options?: IDefineTestRunHookOptions,
83 | ): MethodDecorator {
84 | return createHookDecorator(StepBindingFlags.beforeAll, options);
85 | }
86 |
87 | /**
88 | * A method decorator that marks the associated function as an 'After Scenario' step. The function is
89 | * executed after each scenario.
90 | *
91 | * @param options Optional hook options object.
92 | */
93 | export function afterAll(options?: IDefineTestRunHookOptions): MethodDecorator {
94 | return createHookDecorator(StepBindingFlags.afterAll, options);
95 | }
96 |
97 | /**
98 | * A method decorator that marks the associated function as a 'Before Step' step. The function is
99 | * executed before each step.
100 | *
101 | * @param options Optional hook options object.
102 | */
103 | export function beforeStep(
104 | options?: IDefineTestStepHookOptions,
105 | ): MethodDecorator {
106 | return createHookDecorator(StepBindingFlags.beforeStep, options);
107 | }
108 |
109 | /**
110 | * A method decorator that marks the associated function as an 'After Step' step. The function is
111 | * executed after each step.
112 | *
113 | * @param options Optional hook options object.
114 | */
115 | export function afterStep(
116 | options?: IDefineTestStepHookOptions,
117 | ): MethodDecorator {
118 | return createHookDecorator(StepBindingFlags.afterStep, options);
119 | }
120 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./binding-decorator";
2 | export * from "./hook-decorators";
3 | export * from "./step-definition-decorators";
4 | export { ScenarioContext, ScenarioInfo } from "./scenario-context";
5 | export * from "./provided-context";
6 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/logger.ts:
--------------------------------------------------------------------------------
1 | import * as log4js from "log4js";
2 |
3 | const logger = log4js.getLogger("cucumber-js.tsflow");
4 |
5 | export default logger;
6 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/managed-scenario-context.ts:
--------------------------------------------------------------------------------
1 | import * as _ from "underscore";
2 | import { BindingRegistry } from "./binding-registry";
3 | import { ScenarioContext } from "./scenario-context";
4 | import { ScenarioInfo } from "./scenario-info";
5 | import { ContextType, isProvidedContextType } from "./types";
6 |
7 | /**
8 | * Represents a [[ScenarioContext]] implementation that manages a collection of context objects that
9 | * are created and used by binding classes during a running Cucumber scenario.
10 | */
11 | export class ManagedScenarioContext implements ScenarioContext {
12 | private _activeObjects = new Map();
13 |
14 | constructor(private readonly _scenarioInfo: ScenarioInfo) {}
15 |
16 | /**
17 | * Gets information about the scenario.
18 | */
19 | public get scenarioInfo(): ScenarioInfo {
20 | return this._scenarioInfo;
21 | }
22 |
23 | public getOrActivateBindingClass(
24 | targetPrototype: any,
25 | contextTypes: ContextType[],
26 | ): any {
27 | return this.getOrActivateObject(targetPrototype, () => {
28 | return this.activateBindingClass(targetPrototype, contextTypes);
29 | });
30 | }
31 |
32 | public dispose(): void {
33 | this._activeObjects.forEach((value: any) => {
34 | if (typeof value.dispose === "function") {
35 | value.dispose();
36 | }
37 | });
38 | }
39 |
40 | /**
41 | * @internal
42 | */
43 | public getContextInstance(contextType: ContextType) {
44 | return this.getOrActivateObject(contextType.prototype, () => {
45 | if (isProvidedContextType(contextType)) {
46 | throw new Error(
47 | `The requested type "${contextType.name}" should be provided by cucumber-tsflow, but was not registered. Please report a bug.`,
48 | );
49 | }
50 |
51 | return new contextType();
52 | });
53 | }
54 |
55 | /**
56 | * @internal
57 | */
58 | public addExternalObject(value: unknown) {
59 | if (value == null) {
60 | return;
61 | }
62 |
63 | const proto = value.constructor.prototype;
64 |
65 | const existingObject = this._activeObjects.get(proto);
66 |
67 | if (existingObject !== undefined) {
68 | throw new Error(
69 | `Conflicting objects of type "${proto.name}" registered.`,
70 | );
71 | }
72 |
73 | this._activeObjects.set(proto, value);
74 | }
75 |
76 | private activateBindingClass(
77 | targetPrototype: any,
78 | contextTypes: ContextType[],
79 | ): any {
80 | const invokeBindingConstructor = (args: any[]): any => {
81 | return new (targetPrototype.constructor as any)(...args);
82 | };
83 |
84 | const contextObjects = _.map(contextTypes, (contextType) => {
85 | return this.getOrActivateBindingClass(
86 | contextType.prototype,
87 | BindingRegistry.instance.getContextTypesForTarget(
88 | contextType.prototype,
89 | ),
90 | );
91 | });
92 |
93 | return invokeBindingConstructor(contextObjects);
94 | }
95 |
96 | private getOrActivateObject(
97 | targetPrototype: any,
98 | activatorFunc: () => any,
99 | ): any {
100 | let activeObject = this._activeObjects.get(targetPrototype);
101 |
102 | if (activeObject) {
103 | return activeObject;
104 | }
105 |
106 | activeObject = activatorFunc();
107 |
108 | this._activeObjects.set(targetPrototype, activeObject);
109 |
110 | return activeObject;
111 | }
112 | }
113 |
114 | export * from "./scenario-context";
115 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/our-callsite.ts:
--------------------------------------------------------------------------------
1 | import callsites from "callsites";
2 | // @ts-ignore
3 | import * as sourceMapSupport from "source-map-support";
4 |
5 | /**
6 | * Represents a callsite of where a step binding is being applied.
7 | */
8 | export class Callsite {
9 | /**
10 | * Initializes a new [[Callsite]].
11 | *
12 | * @param filename The filename of the callsite.
13 | * @param lineNumber The line number of the callsite.
14 | */
15 | constructor(
16 | public filename: string,
17 | public lineNumber: number,
18 | ) {}
19 |
20 | /**
21 | * Captures the current [[Callsite]] object.
22 | */
23 | public static capture(up = 1): Callsite {
24 | const stack = callsites()[up + 1];
25 | const tsStack = sourceMapSupport.wrapCallSite(stack);
26 | return new Callsite(
27 | tsStack.getFileName() || "",
28 | tsStack.getLineNumber() || -1,
29 | );
30 | }
31 |
32 | /**
33 | * Returns a string representation of the callsite.
34 | *
35 | * @returns A string representing the callsite formatted with the filename and line
36 | * number.
37 | */
38 | public toString(): string {
39 | return `${this.filename}:${this.lineNumber}`;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/provided-context.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:max-classes-per-file */
2 | import {
3 | ICreateAttachment,
4 | ICreateLog,
5 | } from "@cucumber/cucumber/lib/runtime/attachment_manager";
6 | import { Readable } from "stream";
7 |
8 | export class WorldParameters {
9 | public constructor(public readonly value: T) {}
10 | }
11 |
12 | export class CucumberLog {
13 | public constructor(private readonly target: ICreateLog) {}
14 |
15 | public log(text: string): void | Promise {
16 | return this.target(text);
17 | }
18 | }
19 |
20 | export class CucumberAttachments {
21 | public constructor(private readonly target: ICreateAttachment) {}
22 |
23 | public attach(data: string, mediaType?: string): void;
24 | public attach(data: Buffer, mediaType: string): void;
25 | public attach(data: Readable, mediaType: string): Promise;
26 | public attach(data: Readable, mediaType: string, callback: () => void): void;
27 | public attach(...args: any): void | Promise {
28 | return this.target.apply(this, args);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/scenario-context.ts:
--------------------------------------------------------------------------------
1 | import { ScenarioInfo } from "./scenario-info";
2 |
3 | /**
4 | * Provides context for the currently running Cucumber scenario.
5 | */
6 | export interface ScenarioContext {
7 | /**
8 | * Gets information about the scenario.
9 | *
10 | */
11 | scenarioInfo: ScenarioInfo;
12 |
13 | /**
14 | * Gets or sets an arbitary object within the running scenario.
15 | */
16 | [key: string]: any;
17 | }
18 |
19 | export * from "./scenario-info";
20 |
--------------------------------------------------------------------------------
/cucumber-tsflow/src/scenario-info.ts:
--------------------------------------------------------------------------------
1 | import logger from "./logger";
2 | import { TagName } from "./types";
3 |
4 | /**
5 | * Provides information about a running Cucumber scenario.
6 | */
7 | export class ScenarioInfo {
8 | private _attributeTags?: Map;
9 |
10 | private _optionTags?: Map;
11 |
12 | private _flagTags?: Set;
13 |
14 | /**
15 | * Initializes the [[ScenarioInfo]] object.
16 | *
17 | * @param scenarioTitle The string title of the currently running Cucumber scenario.
18 | * @param tags An array of [[TagName]] representing the tags that are in scope for the currently
19 | * running Cucumber scenario.
20 | */
21 | constructor(
22 | public scenarioTitle: string,
23 | public tags: TagName[],
24 | ) {}
25 |
26 | private static parseAttributeTags(tags: TagName[]): Map {
27 | const RGX = /^@?(?[\w-]+)\((?.+?)\)$/s;
28 |
29 | const result = new Map();
30 |
31 | for (const tag of tags) {
32 | const match = tag.match(RGX)?.groups;
33 |
34 | if (match !== undefined) {
35 | const { attributeName, value } = match;
36 | result.set(attributeName, JSON.parse(value));
37 | }
38 | }
39 |
40 | logger.trace("Parsed attribute tags", { fromTags: tags, options: result });
41 |
42 | return result;
43 | }
44 |
45 | private static parseOptionTags(tags: TagName[]): Map {
46 | const RGX = /^@?(?]