├── .npmrc
├── examples
├── webpack-ts
│ ├── tsconfig.json
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.ts
│ ├── package.json
│ └── cypress.config.ts
├── browserify-cjs
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.js
│ ├── package.json
│ └── cypress.config.js
├── browserify-esm
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.mjs
│ ├── package.json
│ └── cypress.config.mjs
├── browserify-ts
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.ts
│ ├── package.json
│ └── cypress.config.ts
├── esbuild-cjs
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.js
│ ├── package.json
│ └── cypress.config.js
├── esbuild-esm
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.mjs
│ ├── package.json
│ └── cypress.config.mjs
├── esbuild-ts
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.ts
│ ├── package.json
│ └── cypress.config.ts
├── webpack-cjs
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.js
│ ├── package.json
│ └── cypress.config.js
├── webpack-esm
│ ├── .npmrc
│ ├── cypress
│ │ └── e2e
│ │ │ ├── duckduckgo.feature
│ │ │ └── duckduckgo.mjs
│ ├── package.json
│ └── cypress.config.mjs
└── readme.md
├── .mocharc.json
├── docs
├── vscode-integration.md
├── localisation.md
├── readme.md
├── faq.md
├── test-configuration.md
├── step-definitions.md
├── quick-start.md
├── state-management.md
├── configuration.md
├── tags.md
├── json-report.md
└── cucumber-basics.md
├── lib
├── loader.ts
├── debug.ts
├── tag-parser
│ ├── errors.ts
│ ├── index.ts
│ ├── tokenizer.ts
│ └── parser.ts
├── environment-helpers.ts
├── types.ts
├── helpers.ts
├── constants.ts
├── assertions.ts
├── type-guards.ts
├── data_table.ts
├── preprocessor-configuration.test.ts
├── template.ts
├── tag-parser.test.ts
├── index.ts
├── data_table.test.ts
├── methods.ts
├── ast-helpers.ts
├── registry.ts
├── step-definitions.ts
├── step-definitions.test.ts
└── add-cucumber-preprocessor-plugin.ts
├── declarations.d.ts
├── methods.ts
├── types
├── index.d.ts
├── testState.ts
├── tsconfig.json
└── methods.ts
├── cucumber.json
├── .gitignore
├── .editorconfig
├── cucumber.d.ts
├── features
├── undefined_step.feature
├── parse_error.feature
├── support
│ ├── helpers.ts
│ ├── world.ts
│ └── hooks.ts
├── localisation.feature
├── ambiguous_keywords.feature
├── issues
│ ├── 713.feature
│ ├── 731.feature
│ ├── 724.feature
│ ├── 736.feature
│ └── 705.feature
├── skip.feature
├── nested_steps.feature
├── suite_options.feature
├── custom_parameter_type.feature
├── unambiguous_step_definitions.feature
├── fixtures
│ ├── passed-example.json
│ ├── attachments
│ │ ├── string.json
│ │ └── screenshot.json
│ ├── failing-before.json
│ ├── failing-after.json
│ ├── undefined-steps.json
│ ├── failing-step.json
│ ├── retried.json
│ ├── passed-outline.json
│ ├── pending-steps.json
│ └── multiple-features.json
├── asynchronous_world.feature
├── step_definitions
│ ├── file_steps.ts
│ ├── config_steps.ts
│ ├── cli_steps.ts
│ └── json_steps.ts
├── tags
│ ├── spec_filter.feature
│ ├── tagged_hooks.feature
│ ├── test_filter.feature
│ ├── target_specific_scenarios_by_tag.feature
│ ├── skip_tag.feature
│ └── only_tag.feature
├── configuration_overrides.feature
├── loaders
│ ├── esbuild.feature
│ ├── browserify.feature
│ └── webpack.feature
├── mixing_types.feature
├── step_definitions.feature
├── doc_string.feature
├── hooks_ordering.feature
├── world_example.feature
├── scenario_outlines.feature
├── attachments.feature
└── data_table.feature
├── tsconfig.json
├── webpack.ts
├── esbuild.ts
├── LICENSE
├── README.md
├── browserify.ts
├── test
└── run-all-specs.ts
└── package.json
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/webpack-ts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/examples/browserify-cjs/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/browserify-esm/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/browserify-ts/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/esbuild-cjs/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/esbuild-esm/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/esbuild-ts/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/webpack-cjs/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/webpack-esm/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/examples/webpack-ts/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 |
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "require": "ts-node/register"
3 | }
4 |
--------------------------------------------------------------------------------
/docs/vscode-integration.md:
--------------------------------------------------------------------------------
1 | # Integration with VSCode
2 |
3 | Coming soon.
4 |
--------------------------------------------------------------------------------
/lib/loader.ts:
--------------------------------------------------------------------------------
1 | import { compile } from "./template";
2 |
3 | export default compile;
4 |
--------------------------------------------------------------------------------
/declarations.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@cypress/browserify-preprocessor";
2 |
3 | declare module "pngjs";
4 |
--------------------------------------------------------------------------------
/lib/debug.ts:
--------------------------------------------------------------------------------
1 | import debug from "debug";
2 |
3 | export default debug("cypress-cucumber-preprocessor");
4 |
--------------------------------------------------------------------------------
/methods.ts:
--------------------------------------------------------------------------------
1 | export * from "./lib/methods";
2 |
3 | export { default as DataTable } from "./lib/data_table";
4 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | // Minimum TypeScript Version: 3.9
2 |
3 | ///
4 | ///
5 |
--------------------------------------------------------------------------------
/lib/tag-parser/errors.ts:
--------------------------------------------------------------------------------
1 | export class TagError extends Error {}
2 |
3 | export class TagTokenizerError extends TagError {}
4 |
5 | export class TagParserError extends TagError {}
6 |
--------------------------------------------------------------------------------
/examples/esbuild-cjs/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/esbuild-esm/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/esbuild-ts/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/webpack-cjs/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/webpack-esm/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/webpack-ts/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/browserify-cjs/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/browserify-esm/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/examples/browserify-ts/cypress/e2e/duckduckgo.feature:
--------------------------------------------------------------------------------
1 | Feature: duckduckgo.com
2 | Scenario: visting the frontpage
3 | When I visit duckduckgo.com
4 | Then I should see a search bar
5 |
--------------------------------------------------------------------------------
/lib/environment-helpers.ts:
--------------------------------------------------------------------------------
1 | export function getTags(env: Record): string | null {
2 | const tags = env.TAGS ?? env.tags;
3 |
4 | return tags == null ? null : String(tags);
5 | }
6 |
--------------------------------------------------------------------------------
/types/testState.ts:
--------------------------------------------------------------------------------
1 | window.testState.gherkinDocument; // $ExpectType IGherkinDocument
2 | window.testState.pickles; // $ExpectType IPickle[]
3 | window.testState.pickle; // $ExpectType IPickle
4 |
--------------------------------------------------------------------------------
/examples/webpack-cjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@cypress/webpack-preprocessor": "latest",
5 | "cypress": "latest"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/webpack-esm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@cypress/webpack-preprocessor": "latest",
5 | "cypress": "latest"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/cucumber.json:
--------------------------------------------------------------------------------
1 | {
2 | "default": {
3 | "publishQuiet": true,
4 | "format": ["@cucumber/pretty-formatter"],
5 | "requireModule": ["ts-node/register"],
6 | "require": ["features/**/*.ts"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | **/*.js
2 | **/*.d.ts
3 | !examples/**/*
4 | !features/**/*
5 | !cucumber.d.ts
6 | !cucumber.js
7 | !declarations.d.ts
8 | !types/index.d.ts
9 |
10 | # Temporary directory for test execution
11 | tmp/
12 |
--------------------------------------------------------------------------------
/examples/browserify-cjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@cypress/browserify-preprocessor": "latest",
5 | "cypress": "latest"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/browserify-esm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@cypress/browserify-preprocessor": "latest",
5 | "cypress": "latest"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/esbuild-cjs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@bahmutov/cypress-esbuild-preprocessor": "latest",
5 | "cypress": "latest"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/esbuild-esm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@bahmutov/cypress-esbuild-preprocessor": "latest",
5 | "cypress": "latest"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/examples/browserify-ts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@cypress/browserify-preprocessor": "latest",
5 | "cypress": "latest",
6 | "typescript": "latest"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/esbuild-ts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@bahmutov/cypress-esbuild-preprocessor": "latest",
5 | "cypress": "latest",
6 | "typescript": "latest"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/examples/webpack-ts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "@klaveness/cypress-cucumber-preprocessor": "latest",
4 | "@cypress/webpack-preprocessor": "latest",
5 | "cypress": "latest",
6 | "ts-loader": "latest",
7 | "typescript": "latest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | charset = utf-8
3 | end_of_line = lf
4 | indent_style = space
5 | indent_size = 2
6 | insert_final_newline = true
7 |
8 | [*.{js, ts}]
9 | max_line_length = 80
10 | bracket_spacing = true
11 | jsx_bracket_same_line = false
12 | trim_trailing_whitespace = true
13 |
--------------------------------------------------------------------------------
/lib/tag-parser/index.ts:
--------------------------------------------------------------------------------
1 | import Parser from "./parser";
2 |
3 | export function tagToCypressOptions(tag: string): Cypress.TestConfigOverrides {
4 | return new Parser(tag).parse();
5 | }
6 |
7 | export function looksLikeOptions(tag: string) {
8 | return tag.includes("(");
9 | }
10 |
--------------------------------------------------------------------------------
/cucumber.d.ts:
--------------------------------------------------------------------------------
1 | declare module "@cucumber/cucumber" {
2 | interface IWorld {
3 | tmpDir: string;
4 | verifiedLastRunError: boolean;
5 | lastRun: {
6 | stdout: string;
7 | stderr: string;
8 | output: string;
9 | exitCode: number;
10 | };
11 | }
12 | }
13 |
14 | export {};
15 |
--------------------------------------------------------------------------------
/types/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "skipLibCheck": true,
4 | "strict": true,
5 | "esModuleInterop": true,
6 | "moduleResolution": "Node",
7 | "resolveJsonModule": true,
8 | "target": "ES2017",
9 | "module": "CommonJS",
10 | "lib": ["dom", "ES2019"]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/docs/localisation.md:
--------------------------------------------------------------------------------
1 | # Localisation
2 |
3 | Cucumber keywords have been translated to multiple languages and can be used like shown below. See Cucumber's own [documentation](https://cucumber.io/docs/gherkin/languages/) for supported languages.
4 |
5 | ```gherkin
6 | # language: no
7 | Egenskap: en funksjonalitet
8 | Scenario: et scenario
9 | Gitt et steg
10 | ```
11 |
--------------------------------------------------------------------------------
/docs/readme.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | * [Quick start](quick-start.md)
4 | * [Cucumber basics](cucumber-basics.md)
5 | * [State management](state-management.md)
6 | * [Step definitions](step-definitions.md)
7 | * [Tags](tags.md)
8 | * [JSON report](json-report.md)
9 | * [Localisation](localisation.md)
10 | * [Configuration](configuration.md)
11 | * [Test configuration](test-configuration.md)
12 | * [Frequently asked questions](faq.md)
13 |
--------------------------------------------------------------------------------
/examples/esbuild-esm/cypress/e2e/duckduckgo.mjs:
--------------------------------------------------------------------------------
1 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/examples/esbuild-ts/cypress/e2e/duckduckgo.ts:
--------------------------------------------------------------------------------
1 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/examples/webpack-esm/cypress/e2e/duckduckgo.mjs:
--------------------------------------------------------------------------------
1 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/examples/webpack-ts/cypress/e2e/duckduckgo.ts:
--------------------------------------------------------------------------------
1 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/examples/browserify-esm/cypress/e2e/duckduckgo.mjs:
--------------------------------------------------------------------------------
1 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/examples/browserify-ts/cypress/e2e/duckduckgo.ts:
--------------------------------------------------------------------------------
1 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/examples/esbuild-cjs/cypress/e2e/duckduckgo.js:
--------------------------------------------------------------------------------
1 | const { When, Then } = require("@klaveness/cypress-cucumber-preprocessor");
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/examples/webpack-cjs/cypress/e2e/duckduckgo.js:
--------------------------------------------------------------------------------
1 | const { When, Then } = require("@klaveness/cypress-cucumber-preprocessor");
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/features/undefined_step.feature:
--------------------------------------------------------------------------------
1 | Feature: undefined Steps
2 |
3 | Scenario:
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given an undefined step
9 | """
10 | When I run cypress
11 | Then it fails
12 | And the output should contain
13 | """
14 | Step implementation missing for: an undefined step
15 | """
16 |
--------------------------------------------------------------------------------
/examples/browserify-cjs/cypress/e2e/duckduckgo.js:
--------------------------------------------------------------------------------
1 | const { When, Then } = require("@klaveness/cypress-cucumber-preprocessor");
2 |
3 | When("I visit duckduckgo.com", () => {
4 | cy.visit("https://duckduckgo.com/");
5 | });
6 |
7 | Then("I should see a search bar", () => {
8 | cy.get("input").should(
9 | "have.attr",
10 | "placeholder",
11 | "Search the web without being tracked"
12 | );
13 |
14 | assert.deepEqual({}, {});
15 | });
16 |
--------------------------------------------------------------------------------
/features/parse_error.feature:
--------------------------------------------------------------------------------
1 | Feature: parse errors
2 |
3 | Scenario: tagged rules
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | @tag
8 | Rule: a rule name
9 | """
10 | When I run cypress
11 | Then it fails
12 | And the output should contain
13 | """
14 | (3:3): expected: #TagLine, #ScenarioLine, #Comment, #Empty, got 'Rule: a rule name'
15 | """
16 |
17 |
--------------------------------------------------------------------------------
/lib/types.ts:
--------------------------------------------------------------------------------
1 | export interface IParameterTypeDefinition {
2 | name: string;
3 | regexp: RegExp;
4 | transformer: (this: Mocha.Context, ...match: string[]) => T;
5 | }
6 |
7 | export interface IHookBody {
8 | (this: Mocha.Context): void;
9 | }
10 |
11 | export interface IStepDefinitionBody {
12 | (this: Mocha.Context, ...args: T): void;
13 | }
14 |
15 | export type YieldType = T extends Generator
16 | ? R
17 | : never;
18 |
--------------------------------------------------------------------------------
/features/support/helpers.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { promises as fs } from "fs";
3 | import { version as cypressVersion } from "cypress/package.json";
4 |
5 | export async function writeFile(filePath: string, fileContent: string) {
6 | await fs.mkdir(path.dirname(filePath), { recursive: true });
7 | await fs.writeFile(filePath, fileContent);
8 | }
9 |
10 | export function isPost10() {
11 | return cypressVersion.startsWith("10.");
12 | }
13 |
14 | export function isPre10() {
15 | return !isPost10();
16 | }
17 |
--------------------------------------------------------------------------------
/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | export function ensureIsAbsolute(root: string, maybeRelativePath: string) {
4 | if (path.isAbsolute(maybeRelativePath)) {
5 | return maybeRelativePath;
6 | } else {
7 | return path.join(root, maybeRelativePath);
8 | }
9 | }
10 |
11 | export function ensureIsRelative(root: string, maybeRelativePath: string) {
12 | if (path.isAbsolute(maybeRelativePath)) {
13 | return path.relative(root, maybeRelativePath);
14 | } else {
15 | return maybeRelativePath;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/features/localisation.feature:
--------------------------------------------------------------------------------
1 | Feature: localisation
2 | Scenario: norwegian
3 | Given a file named "cypress/e2e/a.feature" with:
4 | """
5 | # language: no
6 | Egenskap: en funksjonalitet
7 | Scenario: et scenario
8 | Gitt et steg
9 | """
10 | And a file named "cypress/support/step_definitions/steps.js" with:
11 | """
12 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
13 | Given("et steg", function() {});
14 | """
15 | When I run cypress
16 | Then it passes
17 |
--------------------------------------------------------------------------------
/examples/readme.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | The examples illustrates using each bundler in each language flavor.
4 |
5 | | | CJS | ESM | TS |
6 | |------------|------------------------|------------------------|-----------------------|
7 | | Browserify | [Link](browserify-cjs) | [Link](browserify-esm) | [Link](browserify-ts) |
8 | | Esbuild | [Link](esbuild-cjs) | [Link](esbuild-esm) | [Link](esbuild-ts) |
9 | | Webpack | [Link](webpack-cjs) | [Link](webpack-esm) | [Link](webpack-ts) |
10 |
--------------------------------------------------------------------------------
/features/ambiguous_keywords.feature:
--------------------------------------------------------------------------------
1 | Feature: ambiguous keyword
2 |
3 | Scenario: wrongly keyworded step matching
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a step
9 | """
10 | And a file named "cypress/support/step_definitions/steps.js" with:
11 | """
12 | const { When } = require("@klaveness/cypress-cucumber-preprocessor");
13 | When("a step", function() {});
14 | """
15 | When I run cypress
16 | Then it passes
17 |
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const TASK_APPEND_MESSAGES =
2 | "cypress-cucumber-preprocessor:append-messages";
3 |
4 | export const TASK_TEST_STEP_STARTED =
5 | "cypress-cucumber-preprocessor:test-step-started";
6 |
7 | export const TASK_CREATE_STRING_ATTACHMENT =
8 | "cypress-cucumber-preprocessor:create-string-attachment";
9 |
10 | export const INTERNAL_PROPERTY_NAME =
11 | "__cypress_cucumber_preprocessor_dont_use_this";
12 |
13 | export const HOOK_FAILURE_EXPR =
14 | /Because this error occurred during a `[^`]+` hook we are skipping all of the remaining tests\./;
15 |
--------------------------------------------------------------------------------
/features/issues/713.feature:
--------------------------------------------------------------------------------
1 | # https://github.com/badeball/cypress-cucumber-preprocessor/issues/713
2 |
3 | Feature: returning chains
4 | Scenario: returning a chain
5 | Given a file named "cypress/e2e/a.feature" with:
6 | """
7 | Feature: a feature
8 | Scenario: a scenario
9 | Given a step
10 | """
11 | And a file named "cypress/support/step_definitions/steps.js" with:
12 | """
13 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
14 | Given("a step", () => cy.log("foo"))
15 | """
16 | When I run cypress
17 | Then it passes
18 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "ts-node": {
3 | "files": true
4 | },
5 | "compilerOptions": {
6 | "declaration": true,
7 | "skipLibCheck": true,
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "moduleResolution": "Node",
11 | "resolveJsonModule": true,
12 | "target": "ES2017",
13 | "module": "CommonJS",
14 | "types": ["node", "cypress"],
15 | "lib": ["dom", "ES2019"]
16 | },
17 | "include": [
18 | "lib/**/*.ts",
19 | "test/**/*.ts",
20 | "methods.ts",
21 | "declarations.d.ts",
22 | "browserify.ts",
23 | "esbuild.ts",
24 | "webpack.ts"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/features/skip.feature:
--------------------------------------------------------------------------------
1 | Feature: skip
2 |
3 | Scenario: calling skip()
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a step
9 | """
10 | And a file named "cypress/support/step_definitions/steps.js" with:
11 | """
12 | const { When } = require("@klaveness/cypress-cucumber-preprocessor");
13 | When("a step", function() {
14 | this.skip();
15 | });
16 | """
17 | When I run cypress
18 | Then it passes
19 | And it should appear to have skipped the scenario "a scenario name"
20 |
--------------------------------------------------------------------------------
/features/nested_steps.feature:
--------------------------------------------------------------------------------
1 | Feature: nesten steps
2 |
3 | Scenario: invoking step from another step
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a nested step
9 | """
10 | And a file named "cypress/e2e/a.js" with:
11 | """
12 | const { Given, Step } = require("@klaveness/cypress-cucumber-preprocessor");
13 | Given("a nested step", function() {
14 | Step(this, "another step");
15 | });
16 | Given("another step", function() {});
17 | """
18 | When I run cypress
19 | Then it passes
20 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | # Frequently asked questions
2 |
3 | ### `--env` / `tags` isn't picked up
4 |
5 | This might be because you're trying to specify `-e / --env` multiple times, but [multiple values should be comma-separated](https://docs.cypress.io/guides/guides/command-line#cypress-run-env-lt-env-gt).
6 |
7 | ---
8 |
9 | ### I get `fs_1.promises.rm is not a function`
10 |
11 | Upgrade your node version to at least [v14.14.0](https://nodejs.org/api/fs.html#fspromisesrmpath-options).
12 |
13 | ---
14 |
15 | ### I get `spawn cucumber-json-formatter ENOENT`
16 |
17 | You need to install `cucumber-json-formatter` **yourself**, as per [documentation](json-report.md).
18 |
--------------------------------------------------------------------------------
/features/suite_options.feature:
--------------------------------------------------------------------------------
1 | Feature: suite options
2 | Scenario: suite specific retry
3 | Given a file named "cypress/e2e/a.feature" with:
4 | """
5 | @retries(2)
6 | Feature: a feature
7 | Scenario: a scenario
8 | Given a step
9 | """
10 | And a file named "cypress/support/step_definitions/steps.js" with:
11 | """
12 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
13 | let attempt = 0;
14 | Given("a step", () => {
15 | if (attempt++ === 0) {
16 | throw "some error";
17 | }
18 | });
19 | """
20 | When I run cypress
21 | Then it passes
22 |
--------------------------------------------------------------------------------
/features/issues/731.feature:
--------------------------------------------------------------------------------
1 | # https://github.com/badeball/cypress-cucumber-preprocessor/issues/731
2 |
3 | Feature: blank titles
4 | Scenario: blank scenario outline title
5 | Given a file named "cypress/e2e/a.feature" with:
6 | """
7 | Feature: a feature
8 | Scenario Outline:
9 | Given a step
10 |
11 | Examples:
12 | | value |
13 | | foo |
14 | """
15 | And a file named "cypress/support/step_definitions/steps.js" with:
16 | """
17 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
18 | Given("a step", () => {})
19 | """
20 | When I run cypress
21 | Then it passes
22 |
--------------------------------------------------------------------------------
/examples/browserify-esm/cypress.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 | import preprocessor from "@klaveness/cypress-cucumber-preprocessor";
3 | import browserify from "@klaveness/cypress-cucumber-preprocessor/browserify.js";
4 |
5 | export async function setupNodeEvents(on, config) {
6 | await preprocessor.addCucumberPreprocessorPlugin(on, config);
7 |
8 | on("file:preprocessor", browserify.default(config));
9 |
10 | // Make sure to return the config object as it might have been modified by the plugin.
11 | return config;
12 | }
13 |
14 | export default defineConfig({
15 | e2e: {
16 | specPattern: "**/*.feature",
17 | supportFile: false,
18 | setupNodeEvents,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/examples/browserify-cjs/cypress.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require("cypress");
2 | const preprocessor = require("@klaveness/cypress-cucumber-preprocessor");
3 | const browserify = require("@klaveness/cypress-cucumber-preprocessor/browserify");
4 |
5 | async function setupNodeEvents(on, config) {
6 | await preprocessor.addCucumberPreprocessorPlugin(on, config);
7 |
8 | on("file:preprocessor", browserify.default(config));
9 |
10 | // Make sure to return the config object as it might have been modified by the plugin.
11 | return config;
12 | }
13 |
14 | module.exports = defineConfig({
15 | e2e: {
16 | specPattern: "**/*.feature",
17 | supportFile: false,
18 | setupNodeEvents,
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/webpack.ts:
--------------------------------------------------------------------------------
1 | import { compile } from "./lib/template";
2 |
3 | /**
4 | * Can be removed once Webpack v5 is supported by Cypress' preprocessor, because Webpack v5 ships
5 | * with these types, ref. https://github.com/webpack/webpack/pull/13164.
6 | */
7 | interface ILoaderContext {
8 | async: () => (err: Error | null, result?: string) => void;
9 | resourcePath: string;
10 | query: any;
11 | }
12 |
13 | interface ILoaderDefinition {
14 | (this: ILoaderContext, data: string): void;
15 | }
16 |
17 | const loader: ILoaderDefinition = function (data) {
18 | const callback = this.async();
19 |
20 | compile(this.query, data, this.resourcePath).then(
21 | (result) => callback(null, result),
22 | (error) => callback(error)
23 | );
24 | };
25 |
26 | export default loader;
27 |
--------------------------------------------------------------------------------
/features/custom_parameter_type.feature:
--------------------------------------------------------------------------------
1 | Feature: custom parameter type
2 | Scenario: definition after usage
3 | Given a file named "cypress/e2e/a.feature" with:
4 | """
5 | Feature: a feature
6 | Scenario: a scenario
7 | Given a step in blue
8 | """
9 | And a file named "cypress/support/step_definitions/steps.js" with:
10 | """
11 | const { Given, defineParameterType } = require("@klaveness/cypress-cucumber-preprocessor");
12 | Given("a step in {color}", function(color) {});
13 | defineParameterType({
14 | name: "color",
15 | regexp: /red|yellow|blue/,
16 | transformer(color) {
17 | return color;
18 | }
19 | });
20 | """
21 | When I run cypress
22 | Then it passes
23 |
--------------------------------------------------------------------------------
/features/unambiguous_step_definitions.feature:
--------------------------------------------------------------------------------
1 | Feature: unambiguous step definitions
2 |
3 | Scenario: step matching two definitions
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a step
9 | """
10 | And a file named "cypress/support/step_definitions/steps.js" with:
11 | """
12 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
13 | Given("a step", function() {});
14 | Given(/a step/, function() {});
15 | """
16 | When I run cypress
17 | Then it fails
18 | And the output should contain
19 | """
20 | Error: Multiple matching step definitions for: a step
21 | a step
22 | /a step/
23 | """
24 |
--------------------------------------------------------------------------------
/lib/assertions.ts:
--------------------------------------------------------------------------------
1 | import { isString } from "./type-guards";
2 |
3 | import { homepage } from "../package.json";
4 |
5 | export function fail(message: string) {
6 | throw new Error(
7 | `${message} (this might be a bug, please report at ${homepage})`
8 | );
9 | }
10 |
11 | export function assert(value: any, message: string): asserts value {
12 | if (value) {
13 | return;
14 | }
15 |
16 | fail(message);
17 | }
18 |
19 | export function assertAndReturn(
20 | value: T,
21 | message: string
22 | ): Exclude {
23 | assert(value, message);
24 | return value as Exclude;
25 | }
26 |
27 | export function assertIsString(
28 | value: any,
29 | message: string
30 | ): asserts value is string {
31 | assert(isString(value), message);
32 | }
33 |
--------------------------------------------------------------------------------
/lib/type-guards.ts:
--------------------------------------------------------------------------------
1 | export function isString(value: unknown): value is string {
2 | return typeof value === "string";
3 | }
4 |
5 | export function isBoolean(value: unknown): value is boolean {
6 | return typeof value === "boolean";
7 | }
8 |
9 | export function isFalse(value: unknown): value is false {
10 | return value === false;
11 | }
12 |
13 | export function isStringOrFalse(value: unknown): value is string | false {
14 | return isString(value) || isFalse(value);
15 | }
16 |
17 | export function isStringOrStringArray(
18 | value: unknown
19 | ): value is string | string[] {
20 | return (
21 | typeof value === "string" || (Array.isArray(value) && value.every(isString))
22 | );
23 | }
24 |
25 | export function notNull(value: T | null | undefined): value is T {
26 | return value != null;
27 | }
28 |
--------------------------------------------------------------------------------
/esbuild.ts:
--------------------------------------------------------------------------------
1 | import esbuild from "esbuild";
2 |
3 | import { ICypressConfiguration } from "@klaveness/cypress-configuration";
4 |
5 | import { compile } from "./lib/template";
6 |
7 | export { ICypressConfiguration };
8 |
9 | export function createEsbuildPlugin(
10 | configuration: ICypressConfiguration
11 | ): esbuild.Plugin {
12 | return {
13 | name: "feature",
14 | setup(build) {
15 | const fs = require("fs") as typeof import("fs");
16 |
17 | build.onLoad({ filter: /\.feature$/ }, async (args) => {
18 | const content = await fs.promises.readFile(args.path, "utf8");
19 |
20 | return {
21 | contents: await compile(configuration, content, args.path),
22 | loader: "js",
23 | };
24 | });
25 | },
26 | };
27 | }
28 |
29 | export default createEsbuildPlugin;
30 |
--------------------------------------------------------------------------------
/features/fixtures/passed-example.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a step",
16 | "result": {
17 | "duration": 0,
18 | "status": "passed"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | }
23 | }
24 | ],
25 | "type": "scenario"
26 | }
27 | ],
28 | "id": "a-feature",
29 | "keyword": "Feature",
30 | "line": 1,
31 | "name": "a feature",
32 | "uri": "cypress/e2e/a.feature"
33 | }
34 | ]
35 |
--------------------------------------------------------------------------------
/examples/esbuild-esm/cypress.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 | import createBundler from "@bahmutov/cypress-esbuild-preprocessor";
3 | import preprocessor from "@klaveness/cypress-cucumber-preprocessor";
4 | import createEsbuildPlugin from "@klaveness/cypress-cucumber-preprocessor/esbuild.js";
5 |
6 | export async function setupNodeEvents(on, config) {
7 | await preprocessor.addCucumberPreprocessorPlugin(on, config);
8 |
9 | on(
10 | "file:preprocessor",
11 | createBundler({
12 | plugins: [createEsbuildPlugin.default(config)],
13 | })
14 | );
15 |
16 | // Make sure to return the config object as it might have been modified by the plugin.
17 | return config;
18 | }
19 |
20 | export default defineConfig({
21 | e2e: {
22 | specPattern: "**/*.feature",
23 | supportFile: false,
24 | setupNodeEvents,
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/examples/esbuild-cjs/cypress.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require("cypress");
2 | const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
3 | const preprocessor = require("@klaveness/cypress-cucumber-preprocessor");
4 | const createEsbuildPlugin = require("@klaveness/cypress-cucumber-preprocessor/esbuild");
5 |
6 | async function setupNodeEvents(on, config) {
7 | await preprocessor.addCucumberPreprocessorPlugin(on, config);
8 |
9 | on(
10 | "file:preprocessor",
11 | createBundler({
12 | plugins: [createEsbuildPlugin.default(config)],
13 | })
14 | );
15 |
16 | // Make sure to return the config object as it might have been modified by the plugin.
17 | return config;
18 | }
19 |
20 | module.exports = defineConfig({
21 | e2e: {
22 | specPattern: "**/*.feature",
23 | supportFile: false,
24 | setupNodeEvents,
25 | },
26 | });
27 |
--------------------------------------------------------------------------------
/features/issues/724.feature:
--------------------------------------------------------------------------------
1 | # https://github.com/badeball/cypress-cucumber-preprocessor/issues/724
2 |
3 | Feature: outputting merely messages
4 | Scenario: enabling *only* messages
5 | Given additional preprocessor configuration
6 | """
7 | {
8 | "messages": {
9 | "enabled": true
10 | }
11 | }
12 | """
13 | And a file named "cypress/e2e/a.feature" with:
14 | """
15 | Feature: a feature
16 | Scenario: a scenario
17 | Given a step
18 | """
19 | And a file named "cypress/support/step_definitions/steps.js" with:
20 | """
21 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
22 | Given("a step", function() {})
23 | """
24 | When I run cypress
25 | Then it passes
26 | And there should be no JSON output
27 | But there should be a messages report
28 |
--------------------------------------------------------------------------------
/examples/browserify-ts/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 | import { addCucumberPreprocessorPlugin } from "@klaveness/cypress-cucumber-preprocessor";
3 | import browserify from "@klaveness/cypress-cucumber-preprocessor/browserify";
4 |
5 | async function setupNodeEvents(
6 | on: Cypress.PluginEvents,
7 | config: Cypress.PluginConfigOptions
8 | ): Promise {
9 | await addCucumberPreprocessorPlugin(on, config);
10 |
11 | on(
12 | "file:preprocessor",
13 | browserify(config, {
14 | typescript: require.resolve("typescript"),
15 | })
16 | );
17 |
18 | // Make sure to return the config object as it might have been modified by the plugin.
19 | return config;
20 | }
21 |
22 | export default defineConfig({
23 | e2e: {
24 | specPattern: "**/*.feature",
25 | supportFile: false,
26 | setupNodeEvents,
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/features/asynchronous_world.feature:
--------------------------------------------------------------------------------
1 | Feature: asynchronous world
2 |
3 | Scenario: Assigning to world asynchronously
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a step asynchronously assigning to World
9 | And a step accessing said assignment synchronously
10 | """
11 | And a file named "cypress/support/step_definitions/steps.js" with:
12 | """
13 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
14 | Given("a step asynchronously assigning to World", function() {
15 | cy.then(() => {
16 | this.foo = "bar";
17 | });
18 | });
19 | Given("a step accessing said assignment synchronously", function() {
20 | expect(this.foo).to.equal("bar");
21 | });
22 | """
23 | When I run cypress
24 | Then it passes
25 |
--------------------------------------------------------------------------------
/examples/esbuild-ts/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 | import createBundler from "@bahmutov/cypress-esbuild-preprocessor";
3 | import { addCucumberPreprocessorPlugin } from "@klaveness/cypress-cucumber-preprocessor";
4 | import createEsbuildPlugin from "@klaveness/cypress-cucumber-preprocessor/esbuild";
5 |
6 | async function setupNodeEvents(
7 | on: Cypress.PluginEvents,
8 | config: Cypress.PluginConfigOptions
9 | ): Promise {
10 | await addCucumberPreprocessorPlugin(on, config);
11 |
12 | on(
13 | "file:preprocessor",
14 | createBundler({
15 | plugins: [createEsbuildPlugin(config)],
16 | })
17 | );
18 |
19 | // Make sure to return the config object as it might have been modified by the plugin.
20 | return config;
21 | }
22 |
23 | export default defineConfig({
24 | e2e: {
25 | specPattern: "**/*.feature",
26 | supportFile: false,
27 | setupNodeEvents,
28 | },
29 | });
30 |
--------------------------------------------------------------------------------
/features/step_definitions/file_steps.ts:
--------------------------------------------------------------------------------
1 | import { Given } from "@cucumber/cucumber";
2 | import stripIndent from "strip-indent";
3 | import path from "path";
4 | import { isPost10, isPre10, writeFile } from "../support/helpers";
5 |
6 | Given("a file named {string} with:", async function (filePath, fileContent) {
7 | const absoluteFilePath = path.join(this.tmpDir, filePath);
8 |
9 | await writeFile(absoluteFilePath, stripIndent(fileContent));
10 | });
11 |
12 | Given(
13 | "a file named {string} or {string} \\(depending on Cypress era) with:",
14 | async function (filePathPre10, filePathPost10, fileContent) {
15 | await writeFile(
16 | path.join(this.tmpDir, isPre10() ? filePathPre10 : filePathPost10),
17 | stripIndent(fileContent)
18 | );
19 | }
20 | );
21 |
22 | Given("an empty file named {string}", async function (filePath) {
23 | const absoluteFilePath = path.join(this.tmpDir, filePath);
24 |
25 | await writeFile(absoluteFilePath, "");
26 | });
27 |
--------------------------------------------------------------------------------
/features/tags/spec_filter.feature:
--------------------------------------------------------------------------------
1 | Feature: filter spec
2 |
3 | Scenario: 1 / 2 specs matching
4 | Given additional preprocessor configuration
5 | """
6 | {
7 | "filterSpecs": true
8 | }
9 | """
10 | And a file named "cypress/e2e/a.feature" with:
11 | """
12 | @foo
13 | Feature: some feature
14 | Scenario: first scenario
15 | Given a step
16 | """
17 | And a file named "cypress/e2e/b.feature" with:
18 | """
19 | @bar
20 | Feature: some other feature
21 | Scenario: second scenario
22 | Given a step
23 | """
24 | And a file named "cypress/support/step_definitions/steps.js" with:
25 | """
26 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
27 | Given("a step", function() {})
28 | """
29 | When I run cypress with "--env tags=@foo"
30 | Then it passes
31 | And it should appear to not have ran spec "b.feature"
32 |
--------------------------------------------------------------------------------
/features/issues/736.feature:
--------------------------------------------------------------------------------
1 | # https://github.com/badeball/cypress-cucumber-preprocessor/issues/736
2 |
3 | Feature: create output directories
4 | Background:
5 | Given additional preprocessor configuration
6 | """
7 | {
8 | "messages": {
9 | "enabled": true,
10 | "output": "foo/cucumber-messages.ndjson"
11 | },
12 | "json": {
13 | "enabled": true,
14 | "output": "bar/cucumber-report.json"
15 | }
16 | }
17 | """
18 | And I've ensured cucumber-json-formatter is installed
19 |
20 | Scenario:
21 | Given a file named "cypress/e2e/a.feature" with:
22 | """
23 | Feature: a feature
24 | Scenario: a scenario
25 | Given a step
26 | """
27 | And a file named "cypress/support/step_definitions/steps.js" with:
28 | """
29 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
30 | Given("a step", function() {})
31 | """
32 | When I run cypress
33 | Then it passes
34 |
--------------------------------------------------------------------------------
/features/fixtures/attachments/string.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a step",
16 | "result": {
17 | "duration": 0,
18 | "status": "passed"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | },
23 | "embeddings": [
24 | {
25 | "data": "Zm9vYmFy",
26 | "mime_type": "text/plain"
27 | }
28 | ]
29 | }
30 | ],
31 | "type": "scenario"
32 | }
33 | ],
34 | "id": "a-feature",
35 | "keyword": "Feature",
36 | "line": 1,
37 | "name": "a feature",
38 | "uri": "cypress/e2e/a.feature"
39 | }
40 | ]
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2020 Jonas Amundsen
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/features/fixtures/failing-before.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "before": [
12 | {
13 | "result": {
14 | "status": "failed",
15 | "error_message": "some error"
16 | },
17 | "match": {
18 | "location": "not available:0"
19 | }
20 | }
21 | ],
22 | "steps": [
23 | {
24 | "keyword": "Given ",
25 | "line": 3,
26 | "name": "a step",
27 | "result": {
28 | "status": "skipped"
29 | },
30 | "match": {
31 | "location": "not available:0"
32 | }
33 | }
34 | ],
35 | "type": "scenario"
36 | }
37 | ],
38 | "id": "a-feature",
39 | "keyword": "Feature",
40 | "line": 1,
41 | "name": "a feature",
42 | "uri": "cypress/e2e/a.feature"
43 | }
44 | ]
45 |
--------------------------------------------------------------------------------
/features/fixtures/failing-after.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a step",
16 | "result": {
17 | "duration": 0,
18 | "status": "passed"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | }
23 | }
24 | ],
25 | "after": [
26 | {
27 | "result": {
28 | "status": "failed",
29 | "error_message": "some error"
30 | },
31 | "match": {
32 | "location": "not available:0"
33 | }
34 | }
35 | ],
36 | "type": "scenario"
37 | }
38 | ],
39 | "id": "a-feature",
40 | "keyword": "Feature",
41 | "line": 1,
42 | "name": "a feature",
43 | "uri": "cypress/e2e/a.feature"
44 | }
45 | ]
46 |
--------------------------------------------------------------------------------
/features/fixtures/undefined-steps.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "an undefined step",
16 | "result": {
17 | "status": "undefined"
18 | },
19 | "match": {
20 | "location": "not available:0"
21 | }
22 | },
23 | {
24 | "keyword": "And ",
25 | "line": 4,
26 | "name": "another step",
27 | "result": {
28 | "status": "skipped"
29 | },
30 | "match": {
31 | "location": "not available:0"
32 | }
33 | }
34 | ],
35 | "type": "scenario"
36 | }
37 | ],
38 | "id": "a-feature",
39 | "keyword": "Feature",
40 | "line": 1,
41 | "name": "a feature",
42 | "uri": "cypress/e2e/a.feature"
43 | }
44 | ]
45 |
--------------------------------------------------------------------------------
/docs/test-configuration.md:
--------------------------------------------------------------------------------
1 | # Test configuration
2 |
3 | Some of Cypress' [configuration options](https://docs.cypress.io/guides/references/configuration) can be overridden per-test by leveraging tags. Below are all supported configuration options shown.
4 |
5 | ```gherkin
6 | @animationDistanceThreshold(5)
7 | @blockHosts('http://www.foo.com','http://www.bar.com')
8 | @defaultCommandTimeout(5)
9 | @execTimeout(5)
10 | @includeShadowDom(true)
11 | @includeShadowDom(false)
12 | @keystrokeDelay(5)
13 | @numTestsKeptInMemory(5)
14 | @pageLoadTimeout(5)
15 | @redirectionLimit(5)
16 | @requestTimeout(5)
17 | @responseTimeout(5)
18 | @retries(5)
19 | @retries(runMode=5)
20 | @retries(openMode=5)
21 | @retries(runMode=5,openMode=10)
22 | @retries(openMode=10,runMode=5)
23 | @screenshotOnRunFailure(true)
24 | @screenshotOnRunFailure(false)
25 | @scrollBehavior('center')
26 | @scrollBehavior('top')
27 | @scrollBehavior('bottom')
28 | @scrollBehavior('nearest')
29 | @slowTestThreshold(5)
30 | @viewportHeight(720)
31 | @viewportWidth(1280)
32 | @waitForAnimations(true)
33 | @waitForAnimations(false)
34 | Feature: a feature
35 | Scenario: a scenario
36 | Given a table step
37 | ```
38 |
--------------------------------------------------------------------------------
/features/fixtures/attachments/screenshot.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a step",
16 | "result": {
17 | "duration": 0,
18 | "status": "passed"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | },
23 | "embeddings": [
24 | {
25 | "data": "iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAAM0lEQVR4Aa3BAQEAAAiDMKR/51uC7QYjJDGJSUxiEpOYxCQmMYlJTGISk5jEJCYxiUnsARwEAibDACoRAAAAAElFTkSuQmCC",
26 | "mime_type": "image/png"
27 | }
28 | ]
29 | }
30 | ],
31 | "type": "scenario"
32 | }
33 | ],
34 | "id": "a-feature",
35 | "keyword": "Feature",
36 | "line": 1,
37 | "name": "a feature",
38 | "uri": "cypress/e2e/a.feature"
39 | }
40 | ]
41 |
--------------------------------------------------------------------------------
/features/fixtures/failing-step.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a failing step",
16 | "result": {
17 | "status": "failed",
18 | "error_message": "some error"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | }
23 | },
24 | {
25 | "keyword": "And ",
26 | "line": 4,
27 | "name": "another step",
28 | "result": {
29 | "status": "skipped"
30 | },
31 | "match": {
32 | "location": "not available:0"
33 | }
34 | }
35 | ],
36 | "type": "scenario"
37 | }
38 | ],
39 | "id": "a-feature",
40 | "keyword": "Feature",
41 | "line": 1,
42 | "name": "a feature",
43 | "uri": "cypress/e2e/a.feature"
44 | }
45 | ]
46 |
--------------------------------------------------------------------------------
/features/configuration_overrides.feature:
--------------------------------------------------------------------------------
1 | Feature: configuration overrides
2 | Scenario: overriding stepDefinitions through -e
3 | Given a file named "cypress/e2e/a.feature" with:
4 | """
5 | Feature: a feature name
6 | Scenario: a scenario name
7 | Given a step
8 | """
9 | And a file named "foobar.js" with:
10 | """
11 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
12 | Given("a step", function() {});
13 | """
14 | When I run cypress with "-e stepDefinitions=foobar.js"
15 | Then it passes
16 |
17 | Scenario: overriding stepDefinitions through environment variables
18 | Given a file named "cypress/e2e/a.feature" with:
19 | """
20 | Feature: a feature name
21 | Scenario: a scenario name
22 | Given a step
23 | """
24 | And a file named "foobar.js" with:
25 | """
26 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
27 | Given("a step", function() {});
28 | """
29 | When I run cypress with environment variables
30 | | Key | Value |
31 | | CYPRESS_stepDefinitions | foobar.js |
32 | Then it passes
33 |
--------------------------------------------------------------------------------
/features/loaders/esbuild.feature:
--------------------------------------------------------------------------------
1 | @no-default-plugin
2 | Feature: esbuild + typescript
3 | Scenario:
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a step
9 | """
10 | And a file named "cypress/plugins/index.js" or "setupNodeEvents.js" (depending on Cypress era) with:
11 | """
12 | <<<<<<< HEAD
13 | import createBundler from "@bahmutov/cypress-esbuild-preprocessor";
14 | import { createEsbuildPlugin } from "@klaveness/cypress-cucumber-preprocessor/esbuild";
15 | =======
16 | const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
17 | const { createEsbuildPlugin } = require("@klaveness/cypress-cucumber-preprocessor/esbuild");
18 | >>>>>>> master
19 |
20 | module.exports = async (on, config) => {
21 | on(
22 | "file:preprocessor",
23 | createBundler({
24 | plugins: [createEsbuildPlugin(config)]
25 | })
26 | );
27 | };
28 | """
29 | And a file named "cypress/support/step_definitions/steps.ts" with:
30 | """
31 | import { Given } from "@klaveness/cypress-cucumber-preprocessor";
32 | Given("a step", function(this: Mocha.Context) {});
33 | """
34 | When I run cypress
35 | Then it passes
36 |
--------------------------------------------------------------------------------
/features/loaders/browserify.feature:
--------------------------------------------------------------------------------
1 | @no-default-plugin
2 | Feature: browserify + typescript
3 | Scenario:
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a step
9 | """
10 | And a file named "cypress/plugins/index.js" or "setupNodeEvents.js" (depending on Cypress era) with:
11 | """
12 | <<<<<<< HEAD
13 | import browserify from "@cypress/browserify-preprocessor";
14 | import { preprocessor } from "@klaveness/cypress-cucumber-preprocessor/browserify";
15 | =======
16 | const browserify = require("@cypress/browserify-preprocessor");
17 | const { preprocessor } = require("@klaveness/cypress-cucumber-preprocessor/browserify");
18 | >>>>>>> master
19 |
20 | module.exports = async (on, config) => {
21 | on(
22 | "file:preprocessor",
23 | preprocessor(config, {
24 | ...browserify.defaultOptions,
25 | typescript: require.resolve("typescript")
26 | })
27 | );
28 | };
29 | """
30 | And a file named "cypress/support/step_definitions/steps.ts" with:
31 | """
32 | import { Given } from "@klaveness/cypress-cucumber-preprocessor";
33 | Given("a step", function(this: Mocha.Context) {});
34 | """
35 | When I run cypress
36 | Then it passes
37 |
--------------------------------------------------------------------------------
/docs/step-definitions.md:
--------------------------------------------------------------------------------
1 | # Step definitions
2 |
3 | Step definitions are resolved using search paths that are configurable through the `stepDefinitions` property. The preprocessor uses [cosmiconfig](https://github.com/davidtheclark/cosmiconfig), which means you can place configuration options in EG. `.cypress-cucumber-preprocessorrc.json` or `package.json`. The default search paths are shown below.
4 |
5 | ```json
6 | {
7 | "stepDefinitions": [
8 | "[filepath]/**/*.{js,ts}",
9 | "[filepath].{js,ts}",
10 | "cypress/support/step_definitions/**/*.{js,ts}",
11 | ]
12 | }
13 | ```
14 |
15 | This means that if you have a file `cypress/e2e/duckduckgo.feature`, it will match step definitions found in
16 |
17 | * `cypress/e2e/duckduckgo/steps.ts`
18 | * `cypress/e2e/duckduckgo.ts`
19 | * `cypress/support/step_definitions/duckduckgo.ts`
20 |
21 | ## Hierarchy
22 |
23 | There's also a `[filepart]` option available. Given a configuration shown below
24 |
25 | ```json
26 | {
27 | "stepDefinitions": [
28 | "[filepart]/step_definitions/**/*.{js,ts}"
29 | ]
30 | }
31 | ```
32 |
33 | ... and a feature file `cypress/e2e/foo/bar/baz.feature`, the preprocessor would look for step definitions in
34 |
35 | * `cypress/e2e/foo/bar/baz/step_definitions/**/*.{js,ts}`
36 | * `cypress/e2e/foo/bar/step_definitions/**/*.{js,ts}`
37 | * `cypress/e2e/foo/step_definitions/**/*.{js,ts}`
38 | * `cypress/e2e/step_definitions/**/*.{js,ts}`
39 |
--------------------------------------------------------------------------------
/examples/webpack-esm/cypress.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 | import webpack from "@cypress/webpack-preprocessor";
3 | import preprocessor from "@klaveness/cypress-cucumber-preprocessor";
4 |
5 | export async function setupNodeEvents(on, config) {
6 | await preprocessor.addCucumberPreprocessorPlugin(on, config);
7 |
8 | on(
9 | "file:preprocessor",
10 | webpack({
11 | webpackOptions: {
12 | resolve: {
13 | extensions: [".ts", ".js"],
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.ts$/,
19 | exclude: [/node_modules/],
20 | use: [
21 | {
22 | loader: "ts-loader",
23 | },
24 | ],
25 | },
26 | {
27 | test: /\.feature$/,
28 | use: [
29 | {
30 | loader: "@klaveness/cypress-cucumber-preprocessor/webpack",
31 | options: config,
32 | },
33 | ],
34 | },
35 | ],
36 | },
37 | },
38 | })
39 | );
40 |
41 | // Make sure to return the config object as it might have been modified by the plugin.
42 | return config;
43 | }
44 |
45 | export default defineConfig({
46 | e2e: {
47 | specPattern: "**/*.feature",
48 | supportFile: false,
49 | setupNodeEvents,
50 | },
51 | });
52 |
--------------------------------------------------------------------------------
/examples/webpack-cjs/cypress.config.js:
--------------------------------------------------------------------------------
1 | const { defineConfig } = require("cypress");
2 | const webpack = require("@cypress/webpack-preprocessor");
3 | const preprocessor = require("@klaveness/cypress-cucumber-preprocessor");
4 |
5 | async function setupNodeEvents(on, config) {
6 | await preprocessor.addCucumberPreprocessorPlugin(on, config);
7 |
8 | on(
9 | "file:preprocessor",
10 | webpack({
11 | webpackOptions: {
12 | resolve: {
13 | extensions: [".ts", ".js"],
14 | },
15 | module: {
16 | rules: [
17 | {
18 | test: /\.ts$/,
19 | exclude: [/node_modules/],
20 | use: [
21 | {
22 | loader: "ts-loader",
23 | },
24 | ],
25 | },
26 | {
27 | test: /\.feature$/,
28 | use: [
29 | {
30 | loader: "@klaveness/cypress-cucumber-preprocessor/webpack",
31 | options: config,
32 | },
33 | ],
34 | },
35 | ],
36 | },
37 | },
38 | })
39 | );
40 |
41 | // Make sure to return the config object as it might have been modified by the plugin.
42 | return config;
43 | }
44 |
45 | module.exports = defineConfig({
46 | e2e: {
47 | specPattern: "**/*.feature",
48 | supportFile: false,
49 | setupNodeEvents,
50 | },
51 | });
52 |
--------------------------------------------------------------------------------
/features/mixing_types.feature:
--------------------------------------------------------------------------------
1 | Feature: mixing feature and non-feature specs
2 | Background:
3 | Given if pre-v10, additional Cypress configuration
4 | """
5 | {
6 | "testFiles": "**/*.{spec.js,feature}"
7 | }
8 | """
9 | And if post-v10, additional Cypress configuration
10 | """
11 | {
12 | "e2e": {
13 | "specPattern": "**/*.{spec.js,feature}"
14 | }
15 | }
16 | """
17 |
18 | Scenario: one feature and non-feature specs
19 | Given a file named "cypress/e2e/a.feature" with:
20 | """
21 | @foo
22 | Feature: a feature name
23 | Scenario: a scenario name
24 | Given a step
25 | """
26 | And a file named "cypress/support/step_definitions/steps.js" with:
27 | """
28 | const { When, isFeature, doesFeatureMatch } = require("@klaveness/cypress-cucumber-preprocessor");
29 | When("a step", function() {
30 | expect(isFeature()).to.be.true;
31 | expect(doesFeatureMatch("@foo")).to.be.true;
32 | });
33 | """
34 | And a file named "cypress/e2e/b.spec.js" with:
35 | """
36 | const { isFeature } = require("@klaveness/cypress-cucumber-preprocessor");
37 | it("should work", () => {
38 | expect(isFeature()).to.be.false;
39 | });
40 | """
41 | When I run cypress
42 | Then it passes
43 | And it should appear to have ran spec "a.feature" and "b.spec.js"
44 |
--------------------------------------------------------------------------------
/features/fixtures/retried.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a step",
16 | "result": {
17 | "status": "failed",
18 | "error_message": "some error"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | }
23 | }
24 | ],
25 | "type": "scenario"
26 | },
27 | {
28 | "description": "",
29 | "id": "a-feature;a-scenario",
30 | "keyword": "Scenario",
31 | "line": 2,
32 | "name": "a scenario",
33 | "steps": [
34 | {
35 | "keyword": "Given ",
36 | "line": 3,
37 | "name": "a step",
38 | "result": {
39 | "duration": 0,
40 | "status": "passed"
41 | },
42 | "match": {
43 | "location": "not available:0"
44 | }
45 | }
46 | ],
47 | "type": "scenario"
48 | }
49 | ],
50 | "id": "a-feature",
51 | "keyword": "Feature",
52 | "line": 1,
53 | "name": "a feature",
54 | "uri": "cypress/e2e/a.feature"
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/features/fixtures/passed-outline.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario;;2",
8 | "keyword": "Scenario Outline",
9 | "line": 6,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a step",
16 | "result": {
17 | "duration": 0,
18 | "status": "passed"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | }
23 | }
24 | ],
25 | "type": "scenario"
26 | },
27 | {
28 | "description": "",
29 | "id": "a-feature;a-scenario;;3",
30 | "keyword": "Scenario Outline",
31 | "line": 7,
32 | "name": "a scenario",
33 | "steps": [
34 | {
35 | "keyword": "Given ",
36 | "line": 3,
37 | "name": "a step",
38 | "result": {
39 | "duration": 0,
40 | "status": "passed"
41 | },
42 | "match": {
43 | "location": "not available:0"
44 | }
45 | }
46 | ],
47 | "type": "scenario"
48 | }
49 | ],
50 | "id": "a-feature",
51 | "keyword": "Feature",
52 | "line": 1,
53 | "name": "a feature",
54 | "uri": "cypress/e2e/a.feature"
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/features/fixtures/pending-steps.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a pending step",
16 | "result": {
17 | "duration": 0,
18 | "status": "pending"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | }
23 | },
24 | {
25 | "keyword": "And ",
26 | "line": 4,
27 | "name": "another pending step",
28 | "result": {
29 | "status": "skipped"
30 | },
31 | "match": {
32 | "location": "not available:0"
33 | }
34 | },
35 | {
36 | "keyword": "And ",
37 | "line": 5,
38 | "name": "an implemented step",
39 | "result": {
40 | "status": "skipped"
41 | },
42 | "match": {
43 | "location": "not available:0"
44 | }
45 | }
46 | ],
47 | "type": "scenario"
48 | }
49 | ],
50 | "id": "a-feature",
51 | "keyword": "Feature",
52 | "line": 1,
53 | "name": "a feature",
54 | "uri": "cypress/e2e/a.feature"
55 | }
56 | ]
57 |
--------------------------------------------------------------------------------
/examples/webpack-ts/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 | import * as webpack from "@cypress/webpack-preprocessor";
3 | import { addCucumberPreprocessorPlugin } from "@klaveness/cypress-cucumber-preprocessor";
4 |
5 | async function setupNodeEvents(
6 | on: Cypress.PluginEvents,
7 | config: Cypress.PluginConfigOptions
8 | ): Promise {
9 | await addCucumberPreprocessorPlugin(on, config);
10 |
11 | on(
12 | "file:preprocessor",
13 | webpack({
14 | webpackOptions: {
15 | resolve: {
16 | extensions: [".ts", ".js"],
17 | },
18 | module: {
19 | rules: [
20 | {
21 | test: /\.ts$/,
22 | exclude: [/node_modules/],
23 | use: [
24 | {
25 | loader: "ts-loader",
26 | },
27 | ],
28 | },
29 | {
30 | test: /\.feature$/,
31 | use: [
32 | {
33 | loader: "@klaveness/cypress-cucumber-preprocessor/webpack",
34 | options: config,
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | },
41 | })
42 | );
43 |
44 | // Make sure to return the config object as it might have been modified by the plugin.
45 | return config;
46 | }
47 |
48 | export default defineConfig({
49 | e2e: {
50 | specPattern: "**/*.feature",
51 | supportFile: false,
52 | setupNodeEvents,
53 | },
54 | });
55 |
--------------------------------------------------------------------------------
/features/tags/tagged_hooks.feature:
--------------------------------------------------------------------------------
1 | Feature: tagged Hooks
2 |
3 | Background:
4 | Given a file named "cypress/support/step_definitions/steps.js" with:
5 | """
6 | const { Then } = require("@klaveness/cypress-cucumber-preprocessor");
7 | Then("{word} is true", function(prop) {
8 | expect(true).to.equal(this[prop])
9 | })
10 | Then("{word} is false", function(prop) {
11 | expect(false).to.equal(this[prop])
12 | })
13 | """
14 | And a file named "cypress/support/step_definitions/hooks.js" with:
15 | """
16 | const {
17 | Before
18 | } = require("@klaveness/cypress-cucumber-preprocessor");
19 | Before(function() {
20 | this.foo = false
21 | this.bar = false
22 | })
23 | Before({ tags: "@foo" }, function() {
24 | this.foo = true
25 | })
26 | Before({ tags: "@bar" }, function() {
27 | this.bar = true
28 | })
29 | """
30 |
31 | Scenario: hooks filtered by tags on scenario
32 | Given a file named "cypress/e2e/a.feature" with:
33 | """
34 | Feature:
35 | @foo
36 | Scenario:
37 | Then foo is true
38 | And bar is false
39 | """
40 | When I run cypress
41 | Then it passes
42 |
43 | Scenario: tags cascade from feature to scenario
44 | Given a file named "cypress/e2e/a.feature" with:
45 | """
46 | @foo
47 | Feature:
48 | Scenario:
49 | Then foo is true
50 | And bar is false
51 | """
52 | When I run cypress
53 | Then it passes
54 |
--------------------------------------------------------------------------------
/features/fixtures/multiple-features.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "",
4 | "elements": [
5 | {
6 | "description": "",
7 | "id": "a-feature;a-scenario",
8 | "keyword": "Scenario",
9 | "line": 2,
10 | "name": "a scenario",
11 | "steps": [
12 | {
13 | "keyword": "Given ",
14 | "line": 3,
15 | "name": "a step",
16 | "result": {
17 | "duration": 0,
18 | "status": "passed"
19 | },
20 | "match": {
21 | "location": "not available:0"
22 | }
23 | }
24 | ],
25 | "type": "scenario"
26 | }
27 | ],
28 | "id": "a-feature",
29 | "keyword": "Feature",
30 | "line": 1,
31 | "name": "a feature",
32 | "uri": "cypress/e2e/a.feature"
33 | },
34 | {
35 | "description": "",
36 | "elements": [
37 | {
38 | "description": "",
39 | "id": "another-feature;another-scenario",
40 | "keyword": "Scenario",
41 | "line": 2,
42 | "name": "another scenario",
43 | "steps": [
44 | {
45 | "keyword": "Given ",
46 | "line": 3,
47 | "name": "a step",
48 | "result": {
49 | "duration": 0,
50 | "status": "passed"
51 | },
52 | "match": {
53 | "location": "not available:0"
54 | }
55 | }
56 | ],
57 | "type": "scenario"
58 | }
59 | ],
60 | "id": "another-feature",
61 | "keyword": "Feature",
62 | "line": 1,
63 | "name": "another feature",
64 | "uri": "cypress/e2e/b.feature"
65 | }
66 | ]
67 |
--------------------------------------------------------------------------------
/features/step_definitions.feature:
--------------------------------------------------------------------------------
1 | Feature: step definitions
2 |
3 | Rule: it should by default look for step definitions in a couple of locations
4 |
5 | Example: step definitions with same filename
6 | Given a file named "cypress/e2e/a.feature" with:
7 | """
8 | Feature: a feature name
9 | Scenario: a scenario name
10 | Given a step
11 | """
12 | And a file named "cypress/e2e/a.js" with:
13 | """
14 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
15 | Given("a step", function() {});
16 | """
17 | When I run cypress
18 | Then it passes
19 |
20 | Example: step definitions in a directory with same name
21 | Given a file named "cypress/e2e/a.feature" with:
22 | """
23 | Feature: a feature name
24 | Scenario: a scenario name
25 | Given a step
26 | """
27 | And a file named "cypress/e2e/a/steps.js" with:
28 | """
29 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
30 | Given("a step", function() {});
31 | """
32 | When I run cypress
33 | Then it passes
34 |
35 | Example: step definitions in a common directory
36 | Given a file named "cypress/e2e/a.feature" with:
37 | """
38 | Feature: a feature name
39 | Scenario: a scenario name
40 | Given a step
41 | """
42 | And a file named "cypress/support/step_definitions/steps.js" with:
43 | """
44 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
45 | Given("a step", function() {});
46 | """
47 | When I run cypress
48 | Then it passes
49 |
--------------------------------------------------------------------------------
/features/tags/test_filter.feature:
--------------------------------------------------------------------------------
1 | Feature: test filter
2 |
3 | Scenario: with omitFiltered = false (default)
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: some feature
7 | Rule: first rule
8 | @foo
9 | Scenario: first scenario
10 | Given a step
11 |
12 | Rule: second rule
13 | Scenario: second scenario
14 | Given a step
15 | """
16 | And a file named "cypress/support/step_definitions/steps.js" with:
17 | """
18 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
19 | Given("a step", function() {})
20 | """
21 | When I run cypress with "--env tags=@foo"
22 | Then it passes
23 | And it should appear to have skipped the scenario "second scenario"
24 |
25 | Scenario: with omitFiltered = true
26 | Given additional preprocessor configuration
27 | """
28 | {
29 | "omitFiltered": true
30 | }
31 | """
32 | And a file named "cypress/e2e/a.feature" with:
33 | """
34 | Feature: some feature
35 | Rule: first rule
36 | @foo
37 | Scenario: first scenario
38 | Given a step
39 |
40 | Rule: second rule
41 | Scenario: second scenario
42 | Given a step
43 | """
44 | And a file named "cypress/support/step_definitions/steps.js" with:
45 | """
46 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
47 | Given("a step", function() {})
48 | """
49 | When I run cypress with "--env tags=@foo"
50 | Then it passes
51 | And it should appear as if only a single test ran
52 | And I should not see "second rule" in the output
53 |
--------------------------------------------------------------------------------
/docs/quick-start.md:
--------------------------------------------------------------------------------
1 | # Installation
2 |
3 | See [badeball/cypress-cucumber-preprocessor](https://github.com/badeball/cypress-cucumber-preprocessor) for a public, community-maintained edition and installation instructions.
4 |
5 | # Configuration
6 |
7 | [Configure](https://docs.cypress.io/guides/references/configuration) `specPattern` with `"**/*.feature"`, using EG. `cypress.config.ts`.
8 |
9 | ```js
10 | import { defineConfig } from "cypress";
11 |
12 | export default defineConfig({
13 | e2e: {
14 | specPattern: "**/*.feature"
15 | }
16 | });
17 | ```
18 |
19 | Configure your preferred bundler to process features files, with examples for
20 |
21 | * [Browserify](../examples/browserify)
22 | * [Webpack](../examples/webpack)
23 | * [Esbuild](../examples/esbuild)
24 |
25 | Read more about configuration options at [docs/configuration.md](configuration.md).
26 |
27 | # Write a test
28 |
29 | Write Gherkin documents and add a file for type definitions with a corresponding name (read more about how step definitions are resolved in [docs/step-definitions.md](step-definitions.md)). Reading [docs/cucumber-basics.md](cucumber-basics.md) is highly recommended.
30 |
31 | ```cucumber
32 | # cypress/e2e/duckduckgo.feature
33 | Feature: duckduckgo.com
34 | Scenario: visting the frontpage
35 | When I visit duckduckgo.com
36 | Then I should see a search bar
37 | ```
38 |
39 | ```ts
40 | // cypress/e2e/duckduckgo.ts
41 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
42 |
43 | When("I visit duckduckgo.com", () => {
44 | cy.visit("https://www.duckduckgo.com");
45 | });
46 |
47 | Then("I should see a search bar", () => {
48 | cy.get("input").should(
49 | "have.attr",
50 | "placeholder",
51 | "Search the web without being tracked"
52 | );
53 | });
54 | ```
55 |
--------------------------------------------------------------------------------
/features/loaders/webpack.feature:
--------------------------------------------------------------------------------
1 | @no-default-plugin
2 | Feature: webpack + typescript
3 | Scenario:
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature name
7 | Scenario: a scenario name
8 | Given a step
9 | """
10 | And a file named "cypress/plugins/index.js" or "setupNodeEvents.js" (depending on Cypress era) with:
11 | """
12 | const webpack = require("@cypress/webpack-preprocessor");
13 |
14 | module.exports = async (on, config) => {
15 | on(
16 | "file:preprocessor",
17 | webpack({
18 | webpackOptions: {
19 | resolve: {
20 | extensions: [".ts", ".js"]
21 | },
22 | module: {
23 | rules: [
24 | {
25 | test: /\.ts$/,
26 | exclude: [/node_modules/],
27 | use: [
28 | {
29 | loader: "ts-loader"
30 | }
31 | ]
32 | },
33 | {
34 | test: /\.feature$/,
35 | use: [
36 | {
37 | loader: "@klaveness/cypress-cucumber-preprocessor/webpack",
38 | options: config
39 | }
40 | ]
41 | }
42 | ]
43 | }
44 | }
45 | })
46 | );
47 | };
48 | """
49 | And a file named "cypress/support/step_definitions/steps.ts" with:
50 | """
51 | import { Given } from "@klaveness/cypress-cucumber-preprocessor";
52 | Given("a step", function(this: Mocha.Context) {});
53 | """
54 | When I run cypress
55 | Then it passes
56 |
--------------------------------------------------------------------------------
/features/doc_string.feature:
--------------------------------------------------------------------------------
1 | Feature: doc string
2 |
3 | Scenario: as only step definition argument
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature
7 | Scenario: a scenario
8 | Given a doc string step
9 | \"\"\"
10 | The cucumber (Cucumis sativus) is a widely cultivated plant in the gourd family Cucurbitaceae.
11 | \"\"\"
12 | """
13 | And a file named "cypress/support/step_definitions/steps.js" with:
14 | """
15 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
16 | Given("a doc string step", function(docString) {
17 | expect(docString).to.equal("The cucumber (Cucumis sativus) is a widely " +
18 | "cultivated plant in the gourd family Cucurbitaceae.")
19 | })
20 | """
21 | When I run cypress
22 | Then it passes
23 |
24 | Scenario: with other step definition arguments
25 | Given a file named "cypress/e2e/a.feature" with:
26 | """
27 | Feature: a feature
28 | Scenario: a scenario
29 | Given a "doc string" step
30 | \"\"\"
31 | The cucumber (Cucumis sativus) is a widely cultivated plant in the gourd family Cucurbitaceae.
32 | \"\"\"
33 | """
34 | And a file named "cypress/support/step_definitions/steps.js" with:
35 | """
36 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
37 | Given("a {string} step", function(type, docString) {
38 | expect(type).to.equal("doc string")
39 | expect(docString).to.equal("The cucumber (Cucumis sativus) is a widely " +
40 | "cultivated plant in the gourd family Cucurbitaceae.")
41 | })
42 | """
43 | When I run cypress
44 | Then it passes
45 |
--------------------------------------------------------------------------------
/docs/state-management.md:
--------------------------------------------------------------------------------
1 | # State management
2 |
3 | A step definition can transfer state to a subsequent step definition by storing state in instance variables, as shown below.
4 |
5 | ```ts
6 | import { Given } from "@klaveness/cypress-cucumber-preprocessor";
7 |
8 | Given("a step asynchronously assigning to World", function() {
9 | cy.then(() => {
10 | this.foo = "bar";
11 | });
12 | });
13 |
14 | Given("a step accessing said assignment synchronously", function() {
15 | expect(this.foo).to.equal("bar");
16 | });
17 | ```
18 |
19 | Please note that if you use arrow functions, you won’t be able to share state between steps!
20 |
21 | ## Replicating `setWorldConstructor`
22 |
23 | Even though `setWorldConstructor` isn't implemented, it's behavior can be closely replicated like shown below.
24 |
25 | ```gherkin
26 | # cypress/e2e/math.feature
27 | Feature: Replicating setWorldConstructor()
28 | Scenario: easy maths
29 | Given a variable set to 1
30 | When I increment the variable by 1
31 | Then the variable should contain 2
32 | ```
33 |
34 | ```ts
35 | // cypress/support/e2e.ts
36 | beforeEach(function () {
37 | const world = {
38 | variable: 0,
39 |
40 | setTo(number) {
41 | this.variable = number;
42 | },
43 |
44 | incrementBy(number) {
45 | this.variable += number;
46 | }
47 | };
48 |
49 | Object.assign(this, world);
50 | });
51 | ```
52 |
53 | ```ts
54 | // cypress/support/step_definitions/steps.js
55 | import { Given, When, Then } from "@klaveness/cypress-cucumber-preprocessor";
56 |
57 | Given("a variable set to {int}", function(number) {
58 | this.setTo(number);
59 | });
60 |
61 | When("I increment the variable by {int}", function(number) {
62 | this.incrementBy(number);
63 | });
64 |
65 | Then("the variable should contain {int}", function(number) {
66 | expect(this.variable).to.equal(number);
67 | });
68 | ````
69 |
--------------------------------------------------------------------------------
/features/issues/705.feature:
--------------------------------------------------------------------------------
1 | # https://github.com/badeball/cypress-cucumber-preprocessor/issues/705
2 |
3 | @no-default-plugin
4 | Feature: overriding event handlers
5 | Background:
6 | Given additional preprocessor configuration
7 | """
8 | {
9 | "json": {
10 | "enabled": true
11 | }
12 | }
13 | """
14 |
15 | Scenario: overriding after:screenshot
16 | Given a file named "cypress/e2e/a.feature" with:
17 | """
18 | Feature: a feature
19 | Scenario: a scenario
20 | Given a failing step
21 | """
22 | And a file named "cypress/support/step_definitions/steps.js" with:
23 | """
24 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
25 | Given("a failing step", function() {
26 | throw "some error"
27 | })
28 | """
29 | And a file named "cypress/plugins/index.js" or "setupNodeEvents.js" (depending on Cypress era) with:
30 | """
31 | const { addCucumberPreprocessorPlugin, afterScreenshotHandler } = require("@klaveness/cypress-cucumber-preprocessor");
32 | const { createEsbuildPlugin } = require("@klaveness/cypress-cucumber-preprocessor/esbuild");
33 | const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
34 |
35 | module.exports = async (on, config) => {
36 | await addCucumberPreprocessorPlugin(on, config, { omitAfterScreenshotHandler: true });
37 |
38 | on("after:screenshot", (details) => afterScreenshotHandler(config, details))
39 |
40 | on(
41 | "file:preprocessor",
42 | createBundler({
43 | plugins: [createEsbuildPlugin(config)]
44 | })
45 | );
46 |
47 | return config;
48 | }
49 | """
50 |
51 | When I run cypress
52 | Then it fails
53 | And the JSON report should contain an image attachment for what appears to be a screenshot
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cypress-cucumber-preprocessor
2 |
3 | This preprocessor aims to provide a developer experience and behavior similar to that of [Cucumber](https://cucumber.io/), to Cypress.
4 |
5 | ## Installation
6 |
7 | See [badeball/cypress-cucumber-preprocessor](https://github.com/badeball/cypress-cucumber-preprocessor) for a public, community-maintained edition and installation instructions.
8 |
9 | ## Introduction
10 |
11 | The preprocessor (with its dependencies) parses Gherkin documents and allows you to write tests as shown below.
12 |
13 | ```cucumber
14 | # cypress/e2e/duckduckgo.feature
15 | Feature: duckduckgo.com
16 | Scenario: visting the frontpage
17 | When I visit duckduckgo.com
18 | Then I should see a search bar
19 | ```
20 |
21 | ```ts
22 | // cypress/e2e/duckduckgo.ts
23 | import { When, Then } from "@klaveness/cypress-cucumber-preprocessor";
24 |
25 | When("I visit duckduckgo.com", () => {
26 | cy.visit("https://www.duckduckgo.com");
27 | });
28 |
29 | Then("I should see a search bar", () => {
30 | cy.get("input").should(
31 | "have.attr",
32 | "placeholder",
33 | "Search the web without being tracked"
34 | );
35 | });
36 | ```
37 |
38 | ## User guide
39 |
40 | For further documentation see [docs](docs/readme.md) and [docs/quick-start.md](docs/quick-start.md).
41 |
42 | ## Contributing
43 |
44 | See [badeball/cypress-cucumber-preprocessor](https://github.com/badeball/cypress-cucumber-preprocessor) for a public, community-maintained edition and contribution instructions.
45 |
46 | ## Building
47 |
48 | Building can be done once using:
49 |
50 | ```
51 | $ npm run build
52 | ```
53 |
54 | Or upon file changes with:
55 |
56 | ```
57 | $ npm run watch
58 | ```
59 |
60 | There are multiple types of tests, all ran using npm scripts:
61 |
62 | ```
63 | $ npm run test:fmt
64 | $ npm run test:types
65 | $ npm run test:unit
66 | $ npm run test:integration # make sure to build first
67 | $ npm run test # runs all of the above
68 | ```
69 |
--------------------------------------------------------------------------------
/features/hooks_ordering.feature:
--------------------------------------------------------------------------------
1 | Feature: hooks ordering
2 |
3 | Hooks should be executed in the following order:
4 | - before
5 | - beforeEach
6 | - Before
7 | - Background steps
8 | - Ordinary steps
9 | - After
10 | - afterEach
11 | - after
12 |
13 | Scenario: with all hooks incrementing a counter
14 | Given a file named "cypress/e2e/a.feature" with:
15 | """
16 | Feature: a feature
17 | Background:
18 | Given a background step
19 | Scenario: a scenario
20 | Given an ordinary step
21 | """
22 | And a file named "cypress/support/step_definitions/steps.js" with:
23 | """
24 | const {
25 | Given,
26 | Before,
27 | After
28 | } = require("@klaveness/cypress-cucumber-preprocessor")
29 | let counter;
30 | before(function() {
31 | counter = 0;
32 | })
33 | beforeEach(function() {
34 | expect(counter++, "Expected beforeEach() to be called after before()").to.equal(0)
35 | })
36 | Before(function() {
37 | expect(counter++, "Expected Before() to be called after beforeEach()").to.equal(1)
38 | })
39 | Given("a background step", function() {
40 | expect(counter++, "Expected a background step to be called after Before()").to.equal(2)
41 | })
42 | Given("an ordinary step", function() {
43 | expect(counter++, "Expected an ordinary step to be called after a background step").to.equal(3)
44 | })
45 | After(function() {
46 | expect(counter++, "Expected After() to be called after ordinary steps").to.equal(4)
47 | })
48 | afterEach(function() {
49 | expect(counter++, "Expected afterEach() to be called after After()").to.equal(5)
50 | })
51 | after(function() {
52 | expect(counter++, "Expected after() to be called after afterEach()").to.equal(6)
53 | })
54 | """
55 | When I run cypress
56 | Then it passes
57 |
--------------------------------------------------------------------------------
/features/tags/target_specific_scenarios_by_tag.feature:
--------------------------------------------------------------------------------
1 | Feature: target specific scenario
2 |
3 | Background:
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: some feature
7 | @a
8 | Scenario: first scenario
9 | Given a step
10 | @b
11 | Scenario Outline: second scenario -
12 | Given a step
13 | @c
14 | Examples:
15 | | ID |
16 | | X |
17 | | Y |
18 | @d
19 | Examples:
20 | | ID |
21 | | Z |
22 | """
23 | And a file named "cypress/support/step_definitions/steps.js" with:
24 | """
25 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
26 | Given("a step", function() {})
27 | """
28 |
29 | Scenario: run a single scenario
30 | When I run cypress with "--env tags=@a"
31 | Then it passes
32 | And it should appear to have run the scenario "first scenario"
33 |
34 | Scenario: filter out scenarios with ~
35 | When I run cypress with "--env 'tags=not @b'"
36 | Then it passes
37 | And it should appear to have run the scenario "first scenario"
38 |
39 | Scenario: run a single scenario outline
40 | When I run cypress with "--env tags=@b"
41 | Then it passes
42 | And it should appear to have run the scenarios
43 | | Name |
44 | | second scenario - X (example #1) |
45 | | second scenario - Y (example #2) |
46 | | second scenario - Z (example #3) |
47 |
48 | Scenario: run a single scenario outline examples
49 | When I run cypress with "--env tags=@d"
50 | Then it passes
51 | And it should appear to have run the scenario "second scenario - Z (example #3)"
52 | But it should appear to not have run the scenarios
53 | | Name |
54 | | second scenario - X (example #1) |
55 | | second scenario - Y (example #2) |
56 |
--------------------------------------------------------------------------------
/features/world_example.feature:
--------------------------------------------------------------------------------
1 | Feature: world
2 |
3 | Scenario: example of an World
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: Simple maths
7 | In order to do maths
8 | As a developer
9 | I want to increment variables
10 |
11 | Scenario: easy maths
12 | Given a variable set to 1
13 | When I increment the variable by 1
14 | Then the variable should contain 2
15 |
16 | Scenario Outline: much more complex stuff
17 | Given a variable set to
18 | When I increment the variable by
19 | Then the variable should contain
20 |
21 | Examples:
22 | | var | increment | result |
23 | | 100 | 5 | 105 |
24 | | 99 | 1234 | 1333 |
25 | | 12 | 5 | 17 |
26 | """
27 | And a file named "cypress/support/e2e.js" with:
28 | """
29 | beforeEach(function () {
30 | // This mimics setWorldConstructor of cucumber-js.
31 | const world = {
32 | variable: 0,
33 |
34 | setTo(number) {
35 | this.variable = number;
36 | },
37 |
38 | incrementBy(number) {
39 | this.variable += number;
40 | }
41 | };
42 |
43 | Object.assign(this, world);
44 | });
45 | """
46 | And a file named "cypress/support/step_definitions/steps.js" with:
47 | """
48 | const { Given, When, Then } = require("@klaveness/cypress-cucumber-preprocessor");
49 |
50 | Given("a variable set to {int}", function(number) {
51 | this.setTo(number);
52 | });
53 |
54 | When("I increment the variable by {int}", function(number) {
55 | this.incrementBy(number);
56 | });
57 |
58 | Then("the variable should contain {int}", function(number) {
59 | expect(this.variable).to.equal(number);
60 | });
61 | """
62 | When I run cypress
63 | Then it passes
64 |
--------------------------------------------------------------------------------
/features/step_definitions/config_steps.ts:
--------------------------------------------------------------------------------
1 | import { Given } from "@cucumber/cucumber";
2 | import path from "path";
3 | import { promises as fs } from "fs";
4 | import { isPost10, isPre10 } from "../support/helpers";
5 | import { insertValuesInConfigFile } from "../support/configFileUpdater";
6 |
7 | async function updateJsonConfiguration(
8 | absoluteConfigPath: string,
9 | additionalJsonContent: any
10 | ) {
11 | const existingConfig = JSON.parse(
12 | (await fs.readFile(absoluteConfigPath)).toString()
13 | );
14 |
15 | await fs.writeFile(
16 | absoluteConfigPath,
17 | JSON.stringify(
18 | {
19 | ...existingConfig,
20 | ...additionalJsonContent,
21 | },
22 | null,
23 | 2
24 | )
25 | );
26 | }
27 |
28 | Given("additional preprocessor configuration", async function (jsonContent) {
29 | const absoluteConfigPath = path.join(
30 | this.tmpDir,
31 | ".cypress-cucumber-preprocessorrc"
32 | );
33 |
34 | await updateJsonConfiguration(absoluteConfigPath, JSON.parse(jsonContent));
35 | });
36 |
37 | Given("additional Cypress configuration", async function (jsonContent) {
38 | if (isPost10()) {
39 | await insertValuesInConfigFile(
40 | path.join(this.tmpDir, "cypress.config.js"),
41 | JSON.parse(jsonContent)
42 | );
43 | } else {
44 | await updateJsonConfiguration(
45 | path.join(this.tmpDir, "cypress.json"),
46 | JSON.parse(jsonContent)
47 | );
48 | }
49 | });
50 |
51 | Given(
52 | "if post-v10, additional Cypress configuration",
53 | async function (jsonContent) {
54 | if (isPost10()) {
55 | await insertValuesInConfigFile(
56 | path.join(this.tmpDir, "cypress.config.js"),
57 | JSON.parse(jsonContent)
58 | );
59 | }
60 | }
61 | );
62 |
63 | Given(
64 | "if pre-v10, additional Cypress configuration",
65 | async function (jsonContent) {
66 | if (isPre10()) {
67 | await updateJsonConfiguration(
68 | path.join(this.tmpDir, "cypress.json"),
69 | JSON.parse(jsonContent)
70 | );
71 | }
72 | }
73 | );
74 |
--------------------------------------------------------------------------------
/features/scenario_outlines.feature:
--------------------------------------------------------------------------------
1 | Feature: scenario outlines and examples
2 |
3 | Scenario: placeholder in step
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature
7 | Scenario Outline: a scenario
8 | Given a step
9 | Examples:
10 | | value |
11 | | foo |
12 | """
13 | And a file named "cypress/support/step_definitions/steps.js" with:
14 | """
15 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
16 | Given("a foo step", function() {})
17 | """
18 | When I run cypress
19 | Then it passes
20 |
21 | Scenario: placeholder in docstring
22 | Given a file named "cypress/e2e/a.feature" with:
23 | """
24 | Feature: a feature
25 | Scenario Outline: a scenario
26 | Given a doc string step
27 | \"\"\"
28 | a doc string
29 | \"\"\"
30 | Examples:
31 | | value |
32 | | foo |
33 | """
34 | And a file named "cypress/support/step_definitions/steps.js" with:
35 | """
36 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
37 | Given("a doc string step", function(docString) {
38 | expect(docString).to.equal("a foo doc string")
39 | })
40 | """
41 | When I run cypress
42 | Then it passes
43 |
44 | Scenario: placeholder in table
45 | Given a file named "cypress/e2e/a.feature" with:
46 | """
47 | Feature: a feature
48 | Scenario Outline: a scenario
49 | Given a table step
50 | | |
51 | Examples:
52 | | value |
53 | | foo |
54 | """
55 | And a file named "cypress/support/step_definitions/steps.js" with:
56 | """
57 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
58 | Given("a table step", function(tableData) {
59 | expect(tableData.raw()[0][0]).to.equal("foo")
60 | })
61 | """
62 | When I run cypress
63 | Then it passes
64 |
--------------------------------------------------------------------------------
/features/support/world.ts:
--------------------------------------------------------------------------------
1 | import { setWorldConstructor, IWorld } from "@cucumber/cucumber";
2 | import path from "path";
3 | import childProcess from "child_process";
4 | import { PassThrough, Readable } from "stream";
5 | import { WritableStreamBuffer } from "stream-buffers";
6 |
7 | const projectPath = path.join(__dirname, "..", "..");
8 |
9 | const isWin = process.platform === "win32";
10 |
11 | function combine(...streams: Readable[]) {
12 | return streams.reduce((combined, stream) => {
13 | stream.pipe(combined, { end: false });
14 | stream.once(
15 | "end",
16 | () => streams.every((s) => s.readableEnded) && combined.emit("end")
17 | );
18 | return combined;
19 | }, new PassThrough());
20 | }
21 |
22 | class World {
23 | async run(this: IWorld, extraArgs = [], extraEnv = {}) {
24 | const child = childProcess.spawn(
25 | path.join(
26 | projectPath,
27 | "node_modules",
28 | ".bin",
29 | isWin ? "cypress.cmd" : "cypress"
30 | ),
31 | ["run", ...extraArgs],
32 | {
33 | stdio: ["ignore", "pipe", "pipe"],
34 | cwd: this.tmpDir,
35 | env: {
36 | ...process.env,
37 | NO_COLOR: "1",
38 | ...extraEnv,
39 | },
40 | }
41 | );
42 |
43 | const combined = combine(child.stdout, child.stderr);
44 |
45 | if (process.env.DEBUG) {
46 | child.stdout.pipe(process.stdout);
47 | child.stderr.pipe(process.stderr);
48 | }
49 |
50 | const stdoutBuffer = child.stdout.pipe(new WritableStreamBuffer());
51 | const stderrBuffer = child.stderr.pipe(new WritableStreamBuffer());
52 | const outputBuffer = combined.pipe(new WritableStreamBuffer());
53 |
54 | const exitCode = await new Promise((resolve) => {
55 | child.on("close", resolve);
56 | });
57 |
58 | const stdout = stdoutBuffer.getContentsAsString() || "";
59 | const stderr = stderrBuffer.getContentsAsString() || "";
60 | const output = outputBuffer.getContentsAsString() || "";
61 |
62 | this.verifiedLastRunError = false;
63 |
64 | this.lastRun = {
65 | stdout,
66 | stderr,
67 | output,
68 | exitCode,
69 | };
70 | }
71 | }
72 |
73 | setWorldConstructor(World);
74 |
--------------------------------------------------------------------------------
/lib/data_table.ts:
--------------------------------------------------------------------------------
1 | import { messages } from "@cucumber/messages";
2 |
3 | import { assert, assertAndReturn } from "./assertions";
4 |
5 | function zip(collectionA: A[], collectionB: B[]) {
6 | return collectionA.map<[A, B]>((element, index) => [
7 | element,
8 | collectionB[index],
9 | ]);
10 | }
11 |
12 | export default class DataTable {
13 | private readonly rawTable: string[][];
14 |
15 | constructor(
16 | sourceTable: messages.PickleStepArgument.IPickleTable | string[][]
17 | ) {
18 | if (sourceTable instanceof Array) {
19 | this.rawTable = sourceTable;
20 | } else {
21 | this.rawTable = assertAndReturn(
22 | sourceTable.rows,
23 | "Expected a PicleTable to have rows"
24 | ).map((row) =>
25 | assertAndReturn(
26 | row.cells,
27 | "Expected a PicleTableRow to have cells"
28 | ).map((cell) => {
29 | const { value } = cell;
30 | assert(value != null, "Expected a PicleTableCell to have a value");
31 | return value;
32 | })
33 | );
34 | }
35 | }
36 |
37 | hashes(): any[] {
38 | const copy = this.raw();
39 | const keys = copy[0];
40 | const valuesArray = copy.slice(1);
41 | return valuesArray.map((values) => Object.fromEntries(zip(keys, values)));
42 | }
43 |
44 | raw(): string[][] {
45 | return this.rawTable.slice(0);
46 | }
47 |
48 | rows(): string[][] {
49 | const copy = this.raw();
50 | copy.shift();
51 | return copy;
52 | }
53 |
54 | rowsHash() {
55 | return Object.fromEntries(
56 | this.raw().map<[string, string]>((values) => {
57 | const [first, second, ...rest] = values;
58 |
59 | if (first == null || second == null || rest.length !== 0) {
60 | throw new Error(
61 | "rowsHash can only be called on a data table where all rows have exactly two columns"
62 | );
63 | }
64 |
65 | return [first, second];
66 | })
67 | );
68 | }
69 |
70 | transpose() {
71 | const transposed = this.rawTable[0].map((x, i) =>
72 | this.rawTable.map((y) => y[i])
73 | );
74 |
75 | return new DataTable(transposed);
76 | }
77 |
78 | toString() {
79 | return "[object DataTable]";
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/features/attachments.feature:
--------------------------------------------------------------------------------
1 | Feature: attachments
2 |
3 | Background:
4 | Given additional preprocessor configuration
5 | """
6 | {
7 | "json": {
8 | "enabled": true
9 | }
10 | }
11 | """
12 | And I've ensured cucumber-json-formatter is installed
13 |
14 | Scenario: string identity
15 | Given a file named "cypress/e2e/a.feature" with:
16 | """
17 | Feature: a feature
18 | Scenario: a scenario
19 | Given a step
20 | """
21 | And a file named "cypress/support/step_definitions/steps.js" with:
22 | """
23 | const { Given, attach } = require("@klaveness/cypress-cucumber-preprocessor");
24 | Given("a step", function() {
25 | attach("foobar")
26 | })
27 | """
28 | When I run cypress
29 | Then it passes
30 | And there should be a JSON output similar to "fixtures/attachments/string.json"
31 |
32 | Scenario: array buffer
33 | Given a file named "cypress/e2e/a.feature" with:
34 | """
35 | Feature: a feature
36 | Scenario: a scenario
37 | Given a step
38 | """
39 | And a file named "cypress/support/step_definitions/steps.js" with:
40 | """
41 | const { Given, attach } = require("@klaveness/cypress-cucumber-preprocessor");
42 | Given("a step", function() {
43 | attach(new TextEncoder().encode("foobar").buffer, "text/plain")
44 | })
45 | """
46 | When I run cypress
47 | Then it passes
48 | And there should be a JSON output similar to "fixtures/attachments/string.json"
49 |
50 | Scenario: string encoded
51 | Given a file named "cypress/e2e/a.feature" with:
52 | """
53 | Feature: a feature
54 | Scenario: a scenario
55 | Given a step
56 | """
57 | And a file named "cypress/support/step_definitions/steps.js" with:
58 | """
59 | const { fromByteArray } = require("base64-js");
60 | const { Given, attach } = require("@klaveness/cypress-cucumber-preprocessor");
61 | Given("a step", function() {
62 | attach(fromByteArray(new TextEncoder().encode("foobar")), "base64:text/plain")
63 | })
64 | """
65 | When I run cypress
66 | Then it passes
67 | And there should be a JSON output similar to "fixtures/attachments/string.json"
68 |
--------------------------------------------------------------------------------
/docs/configuration.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | The preprocessor uses [cosmiconfig](https://github.com/davidtheclark/cosmiconfig), which means you can place configuration options in EG. `.cypress-cucumber-preprocessorrc.json` or `package.json`, with corresponding examples shown below.
4 |
5 | ```
6 | // .cypress-cucumber-preprocessorrc.json
7 | {
8 | "json": {
9 | "enabled": true
10 | }
11 | }
12 | ```
13 |
14 | ```
15 | // package.json
16 | {
17 | "dependencies": {
18 | "@klaveness/cypress-cucumber-preprocessor": "latest"
19 | },
20 | "cypress-cucumber-preprocessor": {
21 | "json": {
22 | "enabled": true
23 | }
24 | }
25 | }
26 | ```
27 |
28 | ## Configuration overrides
29 |
30 | Configuration options can be overriden using (Cypress-) [environment variable](https://docs.cypress.io/guides/guides/environment-variables). The `filterSpecs` options (described in [docs/tags.md](tags.md)) can for instance be overriden by running Cypress like shown below.
31 |
32 | ```
33 | $ cypress run -e filterSpecs=true
34 | ```
35 |
36 | Cypress environment variables can also be configured through ordinary environment variables, like shown below.
37 |
38 | ```
39 | $ CYPRESS_filterSpecs=true cypress run
40 | ```
41 |
42 | Every configuration option has a similar key which can be use to override it, shown in the table below.
43 |
44 | | JSON path | Environment key | Example(s) |
45 | |--------------------|-------------------|------------------------------------------|
46 | | `stepDefinitions` | `stepDefinitions` | `[filepath].{js,ts}` |
47 | | `messages.enabled` | `messagesEnabled` | `true`, `false` |
48 | | `messages.output` | `messagesOutput` | `cucumber-messages.ndjson` |
49 | | `json.enabled` | `jsonEnabled` | `true`, `false` |
50 | | `json.formatter` | `jsonFormatter` | `/usr/bin/cucumber-json-formatter` |
51 | | `json.output` | `jsonOutput` | `cucumber-report.json` |
52 | | `filterSpecs` | `filterSpecs` | `true`, `false` |
53 | | `omitFiltered` | `omitFiltered` | `true`, `false` |
54 |
55 | ## Test configuration
56 |
57 | Some of Cypress' [configuration options](https://docs.cypress.io/guides/references/configuration) can be overridden per-test, [Test configuration](test-configuration.md).
58 |
--------------------------------------------------------------------------------
/lib/preprocessor-configuration.test.ts:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 |
3 | import { resolve } from "./preprocessor-configuration";
4 |
5 | describe("resolve()", () => {
6 | it("overriding stepDefinitions", async () => {
7 | const { stepDefinitions } = await resolve(
8 | "/foo/bar",
9 | { stepDefinitions: "foo/bar/**" },
10 | () => null
11 | );
12 |
13 | assert.strictEqual(stepDefinitions, "foo/bar/**");
14 | });
15 |
16 | it("overriding messages.enabled (1)", async () => {
17 | const {
18 | messages: { enabled },
19 | } = await resolve("/foo/bar", { messagesEnabled: "" }, () => ({
20 | messages: { enabled: true },
21 | }));
22 |
23 | assert.strictEqual(enabled, true);
24 | });
25 |
26 | it("overriding messages.enabled (2)", async () => {
27 | const {
28 | messages: { enabled },
29 | } = await resolve("/foo/bar", { messagesEnabled: "true" }, () => ({
30 | messages: { enabled: false },
31 | }));
32 |
33 | assert.strictEqual(enabled, true);
34 | });
35 |
36 | it("overriding messages.enabled (3)", async () => {
37 | const {
38 | messages: { enabled },
39 | } = await resolve("/foo/bar", { messagesEnabled: "foobar" }, () => ({
40 | messages: { enabled: false },
41 | }));
42 |
43 | assert.strictEqual(enabled, true);
44 | });
45 |
46 | it("overriding messages.enabled (4)", async () => {
47 | const {
48 | messages: { enabled },
49 | } = await resolve("/foo/bar", { messagesEnabled: true }, () => ({
50 | messages: { enabled: false },
51 | }));
52 |
53 | assert.strictEqual(enabled, true);
54 | });
55 |
56 | it("overriding messages.enabled (5)", async () => {
57 | const {
58 | messages: { enabled },
59 | } = await resolve("/foo/bar", { messagesEnabled: "false" }, () => ({
60 | messages: { enabled: true },
61 | }));
62 |
63 | assert.strictEqual(enabled, false);
64 | });
65 |
66 | it("overriding messages.enabled (6)", async () => {
67 | const {
68 | messages: { enabled },
69 | } = await resolve("/foo/bar", { messagesEnabled: "false" }, () => ({
70 | messages: { enabled: true },
71 | }));
72 |
73 | assert.strictEqual(enabled, false);
74 | });
75 |
76 | it("overriding messages.enabled (7)", async () => {
77 | const {
78 | messages: { enabled },
79 | } = await resolve("/foo/bar", { messagesEnabled: false }, () => ({
80 | messages: { enabled: true },
81 | }));
82 |
83 | assert.strictEqual(enabled, false);
84 | });
85 | });
86 |
--------------------------------------------------------------------------------
/browserify.ts:
--------------------------------------------------------------------------------
1 | import { PassThrough, Transform, TransformCallback } from "stream";
2 |
3 | import { EventEmitter } from "events";
4 |
5 | import browserify from "@cypress/browserify-preprocessor";
6 |
7 | import { ICypressConfiguration } from "@klaveness/cypress-configuration";
8 |
9 | import debug from "./lib/debug";
10 |
11 | import { compile } from "./lib/template";
12 |
13 | export function transform(
14 | configuration: ICypressConfiguration,
15 | filepath: string
16 | ) {
17 | if (!filepath.match(".feature$")) {
18 | return new PassThrough();
19 | }
20 |
21 | debug(`compiling ${filepath}`);
22 |
23 | let buffer = Buffer.alloc(0);
24 |
25 | return new Transform({
26 | transform(chunk: any, encoding: string, done: TransformCallback) {
27 | buffer = Buffer.concat([buffer, chunk]);
28 | done();
29 | },
30 | async flush(done: TransformCallback) {
31 | try {
32 | done(
33 | null,
34 | await compile(configuration, buffer.toString("utf8"), filepath)
35 | );
36 |
37 | debug(`compiled ${filepath}`);
38 | } catch (e: any) {
39 | done(e);
40 | }
41 | },
42 | });
43 | }
44 |
45 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#File-object
46 | type ICypressPreprocessorFile = EventEmitter & {
47 | filePath: string;
48 | outputPath: string;
49 | shouldWatch: boolean;
50 | };
51 |
52 | function preprendTransformerToOptions(
53 | configuration: ICypressConfiguration,
54 | options: any
55 | ) {
56 | let wrappedTransform;
57 |
58 | if (
59 | !options.browserifyOptions ||
60 | !Array.isArray(options.browserifyOptions.transform)
61 | ) {
62 | wrappedTransform = [transform.bind(null, configuration)];
63 | } else {
64 | wrappedTransform = [
65 | transform.bind(null, configuration),
66 | ...options.browserifyOptions.transform,
67 | ];
68 | }
69 |
70 | return {
71 | ...options,
72 | browserifyOptions: {
73 | ...(options.browserifyOptions || {}),
74 | transform: wrappedTransform,
75 | },
76 | };
77 | }
78 |
79 | export function preprocessor(
80 | configuration: ICypressConfiguration,
81 | options = browserify.defaultOptions,
82 | { prependTransform = true }: { prependTransform?: boolean } = {}
83 | ) {
84 | if (prependTransform) {
85 | options = preprendTransformerToOptions(configuration, options);
86 | }
87 |
88 | return function (file: ICypressPreprocessorFile) {
89 | return browserify(options)(file);
90 | };
91 | }
92 |
93 | export { ICypressConfiguration };
94 |
95 | export { compile };
96 |
97 | export default preprocessor;
98 |
--------------------------------------------------------------------------------
/lib/template.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | import { generateMessages } from "@cucumber/gherkin";
4 |
5 | import { IdGenerator } from "@cucumber/messages";
6 |
7 | import { ICypressConfiguration } from "@klaveness/cypress-configuration";
8 |
9 | import { assertAndReturn } from "./assertions";
10 |
11 | import { resolve } from "./preprocessor-configuration";
12 |
13 | import { getStepDefinitionPaths } from "./step-definitions";
14 |
15 | import { notNull } from "./type-guards";
16 |
17 | const { stringify } = JSON;
18 |
19 | export async function compile(
20 | this: any,
21 | configuration: ICypressConfiguration,
22 | data: string,
23 | uri: string = this.resourcePath
24 | ) {
25 | const options = {
26 | includeSource: false,
27 | includeGherkinDocument: true,
28 | includePickles: true,
29 | newId: IdGenerator.uuid(),
30 | };
31 |
32 | const relativeUri = path.relative(configuration.projectRoot, uri);
33 |
34 | const envelopes = generateMessages(data, relativeUri, options);
35 |
36 | if (envelopes[0].parseError) {
37 | throw new Error(
38 | assertAndReturn(
39 | envelopes[0].parseError.message,
40 | "Expected parse error to have a description"
41 | )
42 | );
43 | }
44 |
45 | const gherkinDocument = assertAndReturn(
46 | envelopes.map((envelope) => envelope.gherkinDocument).find(notNull),
47 | "Expected to find a gherkin document amongst the envelopes."
48 | );
49 |
50 | const pickles = envelopes.map((envelope) => envelope.pickle).filter(notNull);
51 |
52 | const preprocessor = await resolve(
53 | configuration.projectRoot,
54 | configuration.env
55 | );
56 |
57 | const stepDefinitions = await getStepDefinitionPaths(
58 | {
59 | cypress: configuration,
60 | preprocessor,
61 | },
62 | uri
63 | );
64 |
65 | const prepareLibPath = (...parts: string[]) =>
66 | stringify(path.join(__dirname, ...parts));
67 |
68 | const createTestsPath = prepareLibPath("create-tests");
69 |
70 | const registryPath = prepareLibPath("registry");
71 |
72 | return `
73 | const { default: createTests } = require(${createTestsPath});
74 | const { withRegistry } = require(${registryPath});
75 |
76 | const registry = withRegistry(() => {
77 | ${stepDefinitions
78 | .map((stepDefintion) => `require(${stringify(stepDefintion)});`)
79 | .join("\n ")}
80 | });
81 |
82 | registry.finalize();
83 |
84 | createTests(
85 | registry,
86 | ${stringify(data)},
87 | ${stringify(gherkinDocument)},
88 | ${stringify(pickles)},
89 | ${preprocessor.messages.enabled},
90 | ${preprocessor.omitFiltered}
91 | );
92 | `;
93 | }
94 |
--------------------------------------------------------------------------------
/types/methods.ts:
--------------------------------------------------------------------------------
1 | import DataTable from "../lib/data_table";
2 |
3 | import {
4 | Given,
5 | When,
6 | Then,
7 | Step,
8 | defineParameterType,
9 | Before,
10 | After,
11 | } from "../methods";
12 |
13 | Given("foo", function (foo, bar: number, baz: string) {
14 | this; // $ExpectType Context
15 | foo; // $ExpectType unknown
16 | bar; // $ExpectType number
17 | baz; // $ExpectType string
18 | });
19 |
20 | Given(/foo/, function (foo, bar: number, baz: string) {
21 | this; // $ExpectType Context
22 | foo; // $ExpectType unknown
23 | bar; // $ExpectType number
24 | baz; // $ExpectType string
25 | });
26 |
27 | When("foo", function (foo, bar: number, baz: string) {
28 | this; // $ExpectType Context
29 | foo; // $ExpectType unknown
30 | bar; // $ExpectType number
31 | baz; // $ExpectType string
32 | });
33 |
34 | When(/foo/, function (foo, bar: number, baz: string) {
35 | this; // $ExpectType Context
36 | foo; // $ExpectType unknown
37 | bar; // $ExpectType number
38 | baz; // $ExpectType string
39 | });
40 |
41 | Then("foo", function (foo, bar: number, baz: string) {
42 | this; // $ExpectType Context
43 | foo; // $ExpectType unknown
44 | bar; // $ExpectType number
45 | baz; // $ExpectType string
46 | });
47 |
48 | Then(/foo/, function (foo, bar: number, baz: string) {
49 | this; // $ExpectType Context
50 | foo; // $ExpectType unknown
51 | bar; // $ExpectType number
52 | baz; // $ExpectType string
53 | });
54 |
55 | declare const table: DataTable;
56 |
57 | Then("foo", function () {
58 | // Step should consume Mocha.Context.
59 | Step(this, "foo");
60 | });
61 |
62 | Then("foo", function () {
63 | // Step should consume DataTable's.
64 | Step(this, "foo", table);
65 | });
66 |
67 | Then("foo", function () {
68 | // Step should consume doc strings.
69 | Step(this, "foo", "bar");
70 | });
71 |
72 | defineParameterType({
73 | name: "foo",
74 | regexp: /foo/,
75 | transformer(foo, bar, baz) {
76 | this; // $ExpectType Context
77 | foo; // $ExpectType string
78 | bar; // $ExpectType string
79 | baz; // $ExpectType string
80 | },
81 | });
82 |
83 | Before(function () {
84 | this; // $ExpectType Context
85 | });
86 |
87 | Before({}, function () {
88 | this; // $ExpectType Context
89 | });
90 |
91 | Before({ tags: "foo" }, function () {
92 | this; // $ExpectType Context
93 | });
94 |
95 | After(function () {
96 | this; // $ExpectType Context
97 | });
98 |
99 | After({}, function () {
100 | this; // $ExpectType Context
101 | });
102 |
103 | After({ tags: "foo" }, function () {
104 | this; // $ExpectType Context
105 | });
106 |
--------------------------------------------------------------------------------
/docs/tags.md:
--------------------------------------------------------------------------------
1 | # Tags
2 |
3 | Tests can be filtered using the (Cypress-) [environment variable](https://docs.cypress.io/guides/guides/environment-variables) `tags` or `TAGS`. Note that the term "environment variable" here does **not** refer to OS-level environment variables.
4 |
5 | A feature or scenario can have as many tags as you like, separated by spaces. Tags can be placed above the following Gherkin elements.
6 |
7 | * `Feature`
8 | * `scenario`
9 | * `Scenario Outline`
10 | * `Examples`
11 |
12 | In `Scenario Outline`, you can use tags on different example like below.
13 |
14 | ```cucumber
15 | Scenario Outline: Steps will run conditionally if tagged
16 | Given user is logged in
17 | When user clicks
18 | Then user will be logged out
19 |
20 | @mobile
21 | Examples:
22 | | link |
23 | | logout link on mobile |
24 |
25 | @desktop
26 | Examples:
27 | | link |
28 | | logout link on desktop |
29 | ```
30 |
31 | It is not possible to place tags above `Background` or steps (`Given`, `When`, `Then`, `And` and `But`).
32 |
33 | ## Tag inheritance
34 |
35 | Tags are inherited by child elements. Tags that are placed above a `Feature` will be inherited by `Scenario`, `Scenario Outline`, or `Examples`. Tags that are placed above a `Scenario Outline` will be inherited by `Examples`.
36 |
37 | ## Running a subset of scenarios
38 |
39 | Normally when running a subset of scenarios using `cypress run --env tags=@foo`, you could potentially encounter files containing no matching scenarios. These can be pre-filtered away by setting `filterSpecs` to `true`, thus saving you execution time. This **requires** you to have registered this module in your [plugin file](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Plugins-file), as shown below.
40 |
41 | ```ts
42 | import { addCucumberPreprocessorPlugin } from "@klaveness/cypress-cucumber-preprocessor";
43 |
44 | export default async (
45 | on: Cypress.PluginEvents,
46 | config: Cypress.PluginConfigOptions
47 | ): Promise => {
48 | await addCucumberPreprocessorPlugin(on, config);
49 |
50 | // Make sure to return the config object as it might have been modified by the plugin.
51 | return config;
52 | }
53 | ```
54 |
55 | ## Omit filtered tests
56 |
57 | By default, all filtered tests are made *pending* using `it.skip` method. If you want to completely omit them, set `omitFiltered` to `true`.
58 |
59 | ## Smart tagging
60 |
61 | In the absence of a `tags` value and presence of a scenario with `@only`, only said scenario will run. You can in other words use this tag as you would use `.only()` in Mocha.
62 |
63 | Similarly, scenarios tagged with `@skip` will always be skipped, despite being tagged with something else matching a tag filter.
64 |
--------------------------------------------------------------------------------
/lib/tag-parser/tokenizer.ts:
--------------------------------------------------------------------------------
1 | import { TagTokenizerError } from "./errors";
2 |
3 | export const isAt = (char: string): boolean => char === "@";
4 | export const isOpeningParanthesis = (char: string): boolean => char === "(";
5 | export const isClosingParanthesis = (char: string): boolean => char === ")";
6 | export const isWordChar = (char: string): boolean => /[a-zA-Z]/.test(char);
7 | export const isQuote = (char: string): boolean => char === '"' || char === "'";
8 | export const isDigit = (char: string): boolean => /[0-9]/.test(char);
9 | export const isComma = (char: string): boolean => char === ",";
10 | export const isEqual = (char: string): boolean => char === "=";
11 |
12 | export class Tokenizer {
13 | public constructor(private content: string) {}
14 |
15 | public *tokens(): Generator<
16 | {
17 | value: string;
18 | position: number;
19 | },
20 | void,
21 | unknown
22 | > {
23 | let position = 0;
24 |
25 | while (position < this.content.length) {
26 | const curchar = this.content[position];
27 |
28 | if (
29 | isAt(curchar) ||
30 | isOpeningParanthesis(curchar) ||
31 | isClosingParanthesis(curchar) ||
32 | isComma(curchar) ||
33 | isEqual(curchar)
34 | ) {
35 | yield {
36 | value: curchar,
37 | position,
38 | };
39 |
40 | position++;
41 | } else if (isDigit(curchar)) {
42 | const start = position;
43 |
44 | while (
45 | isDigit(this.content[position]) &&
46 | position < this.content.length
47 | ) {
48 | position++;
49 | }
50 |
51 | yield {
52 | value: this.content.slice(start, position),
53 | position: start,
54 | };
55 | } else if (isWordChar(curchar)) {
56 | const start = position;
57 |
58 | while (
59 | isWordChar(this.content[position]) &&
60 | position < this.content.length
61 | ) {
62 | position++;
63 | }
64 |
65 | yield {
66 | value: this.content.slice(start, position),
67 | position: start,
68 | };
69 | } else if (isQuote(curchar)) {
70 | const start = position++;
71 |
72 | while (
73 | !isQuote(this.content[position]) &&
74 | position < this.content.length
75 | ) {
76 | position++;
77 | }
78 |
79 | if (position === this.content.length) {
80 | throw new TagTokenizerError("Unexpected end-of-string");
81 | } else {
82 | position++;
83 | }
84 |
85 | yield {
86 | value: this.content.slice(start, position),
87 | position: start,
88 | };
89 | } else {
90 | throw new TagTokenizerError(`Unknown token at ${position}: ${curchar}`);
91 | }
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/tag-parser.test.ts:
--------------------------------------------------------------------------------
1 | import util from "util";
2 |
3 | import assert from "assert";
4 |
5 | import { tagToCypressOptions } from "./tag-parser";
6 |
7 | function example(tag: string, expectedOptions: any) {
8 | it(`should return ${util.inspect(expectedOptions)} for ${tag}`, () => {
9 | const actualOptions = tagToCypressOptions(tag);
10 |
11 | assert.deepStrictEqual(actualOptions, expectedOptions);
12 | });
13 | }
14 |
15 | describe("tagToCypressOptions", () => {
16 | example("@animationDistanceThreshold(5)", { animationDistanceThreshold: 5 });
17 | // example("@baseUrl('http://www.foo.com')'", { baseUrl: "http://www.foo.com" });
18 | example("@blockHosts('http://www.foo.com')", {
19 | blockHosts: "http://www.foo.com",
20 | });
21 | example("@blockHosts('http://www.foo.com','http://www.bar.com')", {
22 | blockHosts: ["http://www.foo.com", "http://www.bar.com"],
23 | });
24 | example("@defaultCommandTimeout(5)", { defaultCommandTimeout: 5 });
25 | example("@execTimeout(5)", { execTimeout: 5 });
26 | // example("@experimentalSessionAndOrigin(true)", {
27 | // experimentalSessionAndOrigin: 5,
28 | // });
29 | // example("@experimentalSessionAndOrigin(false)", {
30 | // experimentalSessionAndOrigin: 5,
31 | // });
32 | example("@includeShadowDom(true)", { includeShadowDom: true });
33 | example("@includeShadowDom(false)", { includeShadowDom: false });
34 | example("@keystrokeDelay(5)", { keystrokeDelay: 5 });
35 | example("@numTestsKeptInMemory(5)", { numTestsKeptInMemory: 5 });
36 | example("@pageLoadTimeout(5)", { pageLoadTimeout: 5 });
37 | example("@redirectionLimit(5)", { redirectionLimit: 5 });
38 | example("@requestTimeout(5)", { requestTimeout: 5 });
39 | example("@responseTimeout(5)", { responseTimeout: 5 });
40 | example("@retries(5)", { retries: 5 });
41 | example("@retries(runMode=5)", { retries: { runMode: 5 } });
42 | example("@retries(openMode=5)", { retries: { openMode: 5 } });
43 | example("@retries(runMode=5,openMode=10)", {
44 | retries: { runMode: 5, openMode: 10 },
45 | });
46 | example("@retries(openMode=10,runMode=5)", {
47 | retries: { runMode: 5, openMode: 10 },
48 | });
49 | example("@screenshotOnRunFailure(true)", { screenshotOnRunFailure: true });
50 | example("@screenshotOnRunFailure(false)", { screenshotOnRunFailure: false });
51 | example("@scrollBehavior('center')", { scrollBehavior: "center" });
52 | example("@scrollBehavior('top')", { scrollBehavior: "top" });
53 | example("@scrollBehavior('bottom')", { scrollBehavior: "bottom" });
54 | example("@scrollBehavior('nearest')", { scrollBehavior: "nearest" });
55 | example("@slowTestThreshold(5)", { slowTestThreshold: 5 });
56 | example("@viewportHeight(720)", { viewportHeight: 720 });
57 | example("@viewportWidth(1280)", { viewportWidth: 1280 });
58 | example("@waitForAnimations(true)", { waitForAnimations: true });
59 | example("@waitForAnimations(false)", { waitForAnimations: false });
60 | });
61 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | import { messages } from "@cucumber/messages";
2 |
3 | import DataTable from "./data_table";
4 |
5 | import {
6 | IHookBody,
7 | IParameterTypeDefinition,
8 | IStepDefinitionBody,
9 | } from "./types";
10 |
11 | import * as Methods from "./methods";
12 |
13 | declare global {
14 | interface Window {
15 | testState: {
16 | gherkinDocument: messages.IGherkinDocument;
17 | pickles: messages.IPickle[];
18 | pickle: messages.IPickle;
19 | };
20 | }
21 | }
22 |
23 | export { resolve as resolvePreprocessorConfiguration } from "./preprocessor-configuration";
24 |
25 | export { getStepDefinitionPaths } from "./step-definitions";
26 |
27 | export {
28 | default as addCucumberPreprocessorPlugin,
29 | beforeRunHandler,
30 | afterRunHandler,
31 | beforeSpecHandler,
32 | afterSpecHandler,
33 | afterScreenshotHandler,
34 | } from "./add-cucumber-preprocessor-plugin";
35 |
36 | /**
37 | * Everything below exist merely for the purpose of being nice with TypeScript. All of these methods
38 | * are exclusively used in the browser and the browser field in package.json points to ./methods.ts.
39 | */
40 | function createUnimplemented() {
41 | return new Error("Cucumber methods aren't available in a node environment");
42 | }
43 |
44 | export { NOT_FEATURE_ERROR } from "./methods";
45 |
46 | export function isFeature(): boolean {
47 | throw createUnimplemented();
48 | }
49 |
50 | export function doesFeatureMatch(expression: string): boolean {
51 | throw createUnimplemented();
52 | }
53 |
54 | export function defineStep(
55 | description: string | RegExp,
56 | implementation: IStepDefinitionBody
57 | ) {
58 | throw createUnimplemented();
59 | }
60 |
61 | export {
62 | defineStep as Given,
63 | defineStep as When,
64 | defineStep as Then,
65 | defineStep as And,
66 | defineStep as But,
67 | };
68 |
69 | export function Step(
70 | world: Mocha.Context,
71 | text: string,
72 | argument?: DataTable | string
73 | ) {
74 | throw createUnimplemented();
75 | }
76 |
77 | export function defineParameterType(options: IParameterTypeDefinition) {
78 | throw createUnimplemented();
79 | }
80 |
81 | export function attach(data: string | ArrayBuffer, mediaType?: string) {
82 | throw createUnimplemented();
83 | }
84 |
85 | export function Before(options: { tags?: string }, fn: IHookBody): void;
86 | export function Before(fn: IHookBody): void;
87 | export function Before(
88 | optionsOrFn: IHookBody | { tags?: string },
89 | maybeFn?: IHookBody
90 | ) {
91 | throw createUnimplemented();
92 | }
93 |
94 | export function After(options: { tags?: string }, fn: IHookBody): void;
95 | export function After(fn: IHookBody): void;
96 | export function After(
97 | optionsOrFn: IHookBody | { tags?: string },
98 | maybeFn?: IHookBody
99 | ) {
100 | throw createUnimplemented();
101 | }
102 |
103 | export { default as DataTable } from "./data_table";
104 |
--------------------------------------------------------------------------------
/docs/json-report.md:
--------------------------------------------------------------------------------
1 | # JSON reports
2 |
3 | JSON reports can be enabled using the `json.enabled` property. The preprocessor uses [cosmiconfig](https://github.com/davidtheclark/cosmiconfig), which means you can place configuration options in EG. `.cypress-cucumber-preprocessorrc.json` or `package.json`. An example configuration is shown below.
4 |
5 | ```json
6 | {
7 | "json": {
8 | "enabled": true
9 | }
10 | }
11 | ```
12 |
13 | This **requires** you to have registered this module in your [plugin file](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests#Plugins-file), as shown below.
14 |
15 | ```ts
16 | import { addCucumberPreprocessorPlugin } from "@klaveness/cypress-cucumber-preprocessor";
17 |
18 | export default async (
19 | on: Cypress.PluginEvents,
20 | config: Cypress.PluginConfigOptions
21 | ): Promise => {
22 | await addCucumberPreprocessorPlugin(on, config);
23 |
24 | // Make sure to return the config object as it might have been modified by the plugin.
25 | return config;
26 | }
27 | ```
28 |
29 | This also **requires** you to have downloaded and installed the [cucumber-json-formatter](https://github.com/cucumber/json-formatter) **yourself**. Arch Linux users can install it from [AUR](https://aur.archlinux.org/packages/cucumber-json-formatter).
30 |
31 | The location of the executable is configurable through the `json.formatter` property, but it will by default search for `cucumber-json-formatter` in your `PATH`.
32 |
33 | The report is outputted to `cucumber-report.json` in the project directory, but can be configured through the `json.output` property.
34 |
35 | ## Screenshots
36 |
37 | Screenshots are automatically added to JSON reports, including that of failed tests (unless you have disabled `screenshotOnRunFailure`).
38 |
39 | ## Attachments
40 |
41 | Text, images and other data can be added to the output of the messages and JSON reports with attachments.
42 |
43 | ```ts
44 | import { Given, attach } from "@klaveness/cypress-cucumber-preprocessor";
45 |
46 | Given("a step", function() {
47 | attach("foobar");
48 | });
49 | ```
50 |
51 | By default, text is saved with a MIME type of text/plain. You can also specify a different MIME type.
52 |
53 | ```ts
54 | import { Given, attach } from "@klaveness/cypress-cucumber-preprocessor";
55 |
56 | Given("a step", function() {
57 | attach('{ "name": "foobar" }', "application/json");
58 | });
59 | ```
60 |
61 | Images and other binary data can be attached using a ArrayBuffer. The data will be base64 encoded in the output.
62 |
63 | ```ts
64 | import { Given, attach } from "@klaveness/cypress-cucumber-preprocessor";
65 |
66 | Given("a step", function() {
67 | attach(new TextEncoder().encode("foobar").buffer, "text/plain");
68 | });
69 | ```
70 |
71 | If you've already got a base64-encoded string, you can prefix your mime type with `base64:` to indicate this.
72 |
73 | ```ts
74 | import { Given, attach } from "@klaveness/cypress-cucumber-preprocessor";
75 |
76 | Given("a step", function() {
77 | attach("Zm9vYmFy", "base64:text/plain");
78 | });
79 | ```
80 |
--------------------------------------------------------------------------------
/test/run-all-specs.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { promises as fs } from "fs";
3 | import child_process from "child_process";
4 | import { assertAndReturn } from "../lib/assertions";
5 |
6 | function aggregatedTitle(test: Mocha.Suite | Mocha.Test): string {
7 | if (test.parent?.title) {
8 | return `${aggregatedTitle(test.parent)} - ${test.title}`;
9 | } else {
10 | return test.title;
11 | }
12 | }
13 |
14 | async function writeFile(filePath: string, fileContent: string) {
15 | await fs.mkdir(path.dirname(filePath), { recursive: true });
16 | await fs.writeFile(filePath, fileContent);
17 | }
18 |
19 | const projectPath = path.join(__dirname, "..");
20 |
21 | describe("Run all specs", () => {
22 | beforeEach(async function () {
23 | const title = aggregatedTitle(
24 | assertAndReturn(
25 | this.test?.ctx?.currentTest,
26 | "Expected hook to have a context and a test"
27 | )
28 | );
29 |
30 | this.tmpDir = path.join(projectPath, "tmp", title.replace(/[\(\)\?]/g, ""));
31 |
32 | await fs.rm(this.tmpDir, { recursive: true, force: true });
33 |
34 | await writeFile(
35 | path.join(this.tmpDir, "cypress.json"),
36 | JSON.stringify({
37 | testFiles: "**/*.feature",
38 | video: false,
39 | })
40 | );
41 |
42 | await writeFile(
43 | path.join(this.tmpDir, "cypress", "plugins", "index.js"),
44 | `
45 | const { createEsbuildPlugin } = require("@klaveness/cypress-cucumber-preprocessor/esbuild");
46 | const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
47 |
48 | module.exports = (on, config) => {
49 | on(
50 | "file:preprocessor",
51 | createBundler({
52 | plugins: [createEsbuildPlugin(config)]
53 | })
54 | );
55 | }
56 | `
57 | );
58 |
59 | await fs.mkdir(path.join(this.tmpDir, "node_modules", "@klaveness"), {
60 | recursive: true,
61 | });
62 |
63 | await fs.symlink(
64 | projectPath,
65 | path.join(
66 | this.tmpDir,
67 | "node_modules",
68 | "@klaveness",
69 | "cypress-cucumber-preprocessor"
70 | )
71 | );
72 | });
73 |
74 | it("should work fine with seemingly (?) ambiguous step definitions", async function () {
75 | const feature = `
76 | Feature:
77 | Scenario:
78 | Given a step
79 | `;
80 |
81 | const steps = `
82 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
83 | Given("a step", function() {});
84 | `;
85 |
86 | await writeFile(
87 | path.join(this.tmpDir, "cypress", "integration", "a.feature"),
88 | feature
89 | );
90 |
91 | await writeFile(
92 | path.join(this.tmpDir, "cypress", "integration", "a.ts"),
93 | steps
94 | );
95 |
96 | await writeFile(
97 | path.join(this.tmpDir, "cypress", "integration", "b.feature"),
98 | feature
99 | );
100 |
101 | await writeFile(
102 | path.join(this.tmpDir, "cypress", "integration", "b.ts"),
103 | steps
104 | );
105 |
106 | child_process.spawnSync(
107 | path.join(projectPath, "node_modules", ".bin", "cypress"),
108 | ["open"],
109 | { cwd: this.tmpDir, stdio: "inherit" }
110 | );
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@klaveness/cypress-cucumber-preprocessor",
3 | "version": "11.2.0",
4 | "author": "Jonas Amundsen",
5 | "license": "MIT",
6 | "homepage": "https://github.com/Klaveness-Digital/cypress-cucumber-preprocessor",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/Klaveness-Digital/cypress-cucumber-preprocessor.git"
10 | },
11 | "keywords": [
12 | "cypress",
13 | "cypress-plugin",
14 | "cypress-preprocessor"
15 | ],
16 | "main": "lib/index.js",
17 | "types": "lib/index.d.ts",
18 | "browser": "methods.js",
19 | "files": [
20 | "lib/**/*.js",
21 | "lib/**/*.d.ts",
22 | "browserify.js",
23 | "browserify.d.ts",
24 | "esbuild.js",
25 | "esbuild.d.ts",
26 | "methods.js",
27 | "methods.d.ts",
28 | "webpack.js",
29 | "webpack.d.ts"
30 | ],
31 | "scripts": {
32 | "clean": "rm {browserify,esbuild,methods,webpack}.{js,d.ts} && bash -O globstar -c 'rm lib/**/*.{js,d.ts} test/**/*.{js,d.ts}'",
33 | "build": "tsc",
34 | "watch": "tsc --watch",
35 | "fmt": "prettier --ignore-path .gitignore --write '**/*.ts'",
36 | "test": "npm run test:fmt && npm run test:types && npm run test:unit && npm run test:integration",
37 | "test:fmt": "prettier --ignore-path .gitignore --check '**/*.ts'",
38 | "test:types": "dtslint --expectOnly types",
39 | "test:unit": "mocha lib/**/*.test.ts",
40 | "test:run-all-specs": "mocha --timeout 0 test/run-all-specs.ts",
41 | "test:integration": "cucumber-js",
42 | "prepublishOnly": "npm run clean && npm run build && npm run test"
43 | },
44 | "dependencies": {
45 | "@klaveness/cypress-configuration": "^3.0.0",
46 | "@cucumber/cucumber-expressions": "^15.0.1",
47 | "@cucumber/gherkin": "^15.0.2",
48 | "@cucumber/messages": "^13.2.1",
49 | "@cucumber/tag-expressions": "^4.1.0",
50 | "base64-js": "^1.5.1",
51 | "chalk": "^4.1.2",
52 | "cosmiconfig": "^7.0.1",
53 | "debug": "^4.2.0",
54 | "glob": "^7.2.0",
55 | "is-path-inside": "^3.0.3",
56 | "uuid": "^8.3.2"
57 | },
58 | "devDependencies": {
59 | "@bahmutov/cypress-esbuild-preprocessor": "^2.1.2",
60 | "@cucumber/cucumber": "^8.0.0",
61 | "@cucumber/pretty-formatter": "^1.0.0-alpha.0",
62 | "@cypress/browserify-preprocessor": "^3.0.2",
63 | "@cypress/webpack-preprocessor": "^5.11.1",
64 | "@types/debug": "^4.1.7",
65 | "@types/fs-extra": "^9.0.13",
66 | "@types/glob": "^7.2.0",
67 | "@types/prettier": "^2.6.3",
68 | "@types/stream-buffers": "^3.0.4",
69 | "ast-types": "^0.15.2",
70 | "cypress": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
71 | "dtslint": "^4.2.1",
72 | "esbuild": "^0.14.23",
73 | "fs-extra": "^10.1.0",
74 | "mocha": "^9.2.1",
75 | "pngjs": "^6.0.0",
76 | "prettier": "^2.5.1",
77 | "recast": "^0.21.1",
78 | "stream-buffers": "^3.0.2",
79 | "strip-indent": "^3.0.0",
80 | "ts-loader": "^9.2.6",
81 | "ts-node": "^10.5.0",
82 | "typescript": "^4.5.5",
83 | "webpack": "^5.69.1"
84 | },
85 | "peerDependencies": {
86 | "@cypress/browserify-preprocessor": "^3.0.1",
87 | "cypress": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
88 | "esbuild": "^0.14.23"
89 | },
90 | "peerDependenciesMeta": {
91 | "@cypress/browserify-preprocessor": {
92 | "optional": true
93 | },
94 | "esbuild": {
95 | "optional": true
96 | }
97 | },
98 | "engines": {
99 | "node": ">=14.14.0"
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/lib/data_table.test.ts:
--------------------------------------------------------------------------------
1 | import assert from "assert";
2 |
3 | import { messages } from "@cucumber/messages";
4 |
5 | import DataTable from "./data_table";
6 |
7 | describe("DataTable", () => {
8 | describe("table with headers", () => {
9 | const dataTable =
10 | messages.GherkinDocument.Feature.Step.DataTable.fromObject({
11 | rows: [
12 | {
13 | cells: [{ value: "header 1" }, { value: "header 2" }],
14 | },
15 | {
16 | cells: [{ value: "row 1 col 1" }, { value: "row 1 col 2" }],
17 | },
18 | {
19 | cells: [{ value: "row 2 col 1" }, { value: "row 2 col 2" }],
20 | },
21 | ],
22 | });
23 |
24 | describe("rows", () => {
25 | it("returns a 2-D array without the header", () => {
26 | assert.deepStrictEqual(new DataTable(dataTable).rows(), [
27 | ["row 1 col 1", "row 1 col 2"],
28 | ["row 2 col 1", "row 2 col 2"],
29 | ]);
30 | });
31 | });
32 |
33 | describe("hashes", () => {
34 | it("returns an array of object where the keys are the headers", () => {
35 | assert.deepStrictEqual(new DataTable(dataTable).hashes(), [
36 | { "header 1": "row 1 col 1", "header 2": "row 1 col 2" },
37 | { "header 1": "row 2 col 1", "header 2": "row 2 col 2" },
38 | ]);
39 | });
40 | });
41 |
42 | describe("transpose", () => {
43 | it("returns a new DataTable, with the data transposed", () => {
44 | assert.deepStrictEqual(new DataTable(dataTable).transpose().raw(), [
45 | ["header 1", "row 1 col 1", "row 2 col 1"],
46 | ["header 2", "row 1 col 2", "row 2 col 2"],
47 | ]);
48 | });
49 | });
50 | });
51 |
52 | describe("table without headers", () => {
53 | const dataTable =
54 | messages.GherkinDocument.Feature.Step.DataTable.fromObject({
55 | rows: [
56 | {
57 | cells: [{ value: "row 1 col 1" }, { value: "row 1 col 2" }],
58 | },
59 | {
60 | cells: [{ value: "row 2 col 1" }, { value: "row 2 col 2" }],
61 | },
62 | ],
63 | });
64 |
65 | describe("raw", () => {
66 | it("returns a 2-D array", () => {
67 | assert.deepStrictEqual(new DataTable(dataTable).raw(), [
68 | ["row 1 col 1", "row 1 col 2"],
69 | ["row 2 col 1", "row 2 col 2"],
70 | ]);
71 | });
72 | });
73 |
74 | describe("rowsHash", () => {
75 | it("returns an object where the keys are the first column", () => {
76 | assert.deepStrictEqual(new DataTable(dataTable).rowsHash(), {
77 | "row 1 col 1": "row 1 col 2",
78 | "row 2 col 1": "row 2 col 2",
79 | });
80 | });
81 | });
82 | });
83 |
84 | describe("table with something other than 2 columns", () => {
85 | const dataTable =
86 | messages.GherkinDocument.Feature.Step.DataTable.fromObject({
87 | rows: [
88 | {
89 | cells: [{ value: "row 1 col 1" }],
90 | },
91 | {
92 | cells: [{ value: "row 2 col 1" }],
93 | },
94 | ],
95 | });
96 |
97 | describe("rowsHash", () => {
98 | it("throws an error if not all rows have two columns", function () {
99 | assert.throws(() => {
100 | new DataTable(dataTable).rowsHash();
101 | }, new Error("rowsHash can only be called on a data table where all rows have exactly two columns"));
102 | });
103 | });
104 | });
105 | });
106 |
--------------------------------------------------------------------------------
/features/tags/skip_tag.feature:
--------------------------------------------------------------------------------
1 | Feature: @skip tag
2 |
3 | Rules:
4 | - Tests tagged with @skip are skipped
5 | - Presence of this tag override any other tag filter
6 |
7 | Scenario: 1 / 2 scenarios tagged with @skip
8 | Given a file named "cypress/e2e/a.feature" with:
9 | """
10 | Feature: a feature
11 | Scenario: a scenario
12 | Given a step
13 |
14 | @skip
15 | Scenario: another scenario
16 | """
17 | And a file named "cypress/support/step_definitions/steps.js" with:
18 | """
19 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
20 | Given("a step", function(table) {});
21 | """
22 | When I run cypress
23 | Then it should appear to have run the scenario "a scenario"
24 | And it should appear to have skipped the scenario "another scenario"
25 |
26 | Scenario: 2 / 2 scenarios tagged with @skip
27 | Given a file named "cypress/e2e/a.feature" with:
28 | """
29 | Feature: a feature
30 | @skip
31 | Scenario: a scenario
32 | Given a step
33 |
34 | @skip
35 | Scenario: another scenario
36 | """
37 | And a file named "cypress/support/step_definitions/steps.js" with:
38 | """
39 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
40 | Given("a step", function(table) {});
41 | """
42 | When I run cypress
43 | Then it should appear as if both tests were skipped
44 |
45 | Scenario: 1 / 2 example table tagged with @skip
46 | Given a file named "cypress/e2e/a.feature" with:
47 | """
48 | Feature: a feature
49 | Scenario Outline: a scenario
50 | Given a step
51 |
52 | Examples:
53 | | value |
54 | | foo |
55 |
56 | @skip
57 | Examples:
58 | | value |
59 | | bar |
60 |
61 | Scenario: another scenario
62 | """
63 | And a file named "cypress/support/step_definitions/steps.js" with:
64 | """
65 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
66 | Given("a step", function(table) {});
67 | """
68 | When I run cypress
69 | Then it should appear to have run the scenario "a scenario (example #1)"
70 | And it should appear to have skipped the scenario "a scenario (example #2)"
71 |
72 | Scenario: 2 / 2 example table tagged with @skip
73 | Given a file named "cypress/e2e/a.feature" with:
74 | """
75 | Feature: a feature
76 | Scenario Outline: a scenario
77 | Given a step
78 |
79 | @skip
80 | Examples:
81 | | value |
82 | | foo |
83 |
84 | @skip
85 | Examples:
86 | | value |
87 | | bar |
88 |
89 | Scenario: another scenario
90 | """
91 | And a file named "cypress/support/step_definitions/steps.js" with:
92 | """
93 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
94 | Given("a step", function(table) {});
95 | """
96 | When I run cypress
97 | Then it should appear to have skipped the scenario "a scenario (example #1)"
98 | And it should appear to have skipped the scenario "a scenario (example #2)"
99 |
100 | Scenario: specifying tags
101 | Given a file named "cypress/e2e/a.feature" with:
102 | """
103 | Feature: a feature
104 | @foo
105 | @skip
106 | Scenario: a scenario
107 | Given a step
108 | """
109 | And a file named "cypress/support/step_definitions/steps.js" with:
110 | """
111 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
112 | Given("a step", function(table) {});
113 | """
114 | When I run cypress with "-e tags=@foo"
115 | Then it should appear to have skipped the scenario "a scenario"
116 |
--------------------------------------------------------------------------------
/features/data_table.feature:
--------------------------------------------------------------------------------
1 | Feature: data tables
2 |
3 | Scenario: raw
4 | Given a file named "cypress/e2e/a.feature" with:
5 | """
6 | Feature: a feature
7 | Scenario: a scenario
8 | Given a table step
9 | | Cucumber | Cucumis sativus |
10 | | Burr Gherkin | Cucumis anguria |
11 | """
12 | And a file named "cypress/support/step_definitions/steps.js" with:
13 | """
14 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
15 | Given("a table step", function(table) {
16 | const expected = [
17 | ["Cucumber", "Cucumis sativus"],
18 | ["Burr Gherkin", "Cucumis anguria"]
19 | ];
20 | expect(table.raw()).to.deep.equal(expected);
21 | });
22 | """
23 | When I run cypress
24 | Then it passes
25 |
26 | Scenario: rows
27 | Given a file named "cypress/e2e/a.feature" with:
28 | """
29 | Feature: a feature
30 | Scenario: a scenario
31 | Given a table step
32 | | Vegetable | Rating |
33 | | Apricot | 5 |
34 | | Brocolli | 2 |
35 | | Cucumber | 10 |
36 | """
37 | And a file named "cypress/support/step_definitions/steps.js" with:
38 | """
39 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
40 | Given("a table step", function(table) {
41 | const expected = [
42 | ["Apricot", "5"],
43 | ["Brocolli", "2"],
44 | ["Cucumber", "10"]
45 | ];
46 | expect(table.rows()).to.deep.equal(expected);
47 | });
48 | """
49 | When I run cypress
50 | Then it passes
51 |
52 | Scenario: rowsHash
53 | Given a file named "cypress/e2e/a.feature" with:
54 | """
55 | Feature: a feature
56 | Scenario: a scenario
57 | Given a table step
58 | | Cucumber | Cucumis sativus |
59 | | Burr Gherkin | Cucumis anguria |
60 | """
61 | And a file named "cypress/support/step_definitions/steps.js" with:
62 | """
63 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
64 | Given("a table step", function(table) {
65 | const expected = {
66 | "Cucumber": "Cucumis sativus",
67 | "Burr Gherkin": "Cucumis anguria"
68 | };
69 | expect(table.rowsHash()).to.deep.equal(expected);
70 | });
71 | """
72 | When I run cypress
73 | Then it passes
74 |
75 | Scenario: hashes
76 | Given a file named "cypress/e2e/a.feature" with:
77 | """
78 | Feature: a feature
79 | Scenario: a scenario
80 | Given a table step
81 | | Vegetable | Rating |
82 | | Apricot | 5 |
83 | | Brocolli | 2 |
84 | | Cucumber | 10 |
85 | """
86 | And a file named "cypress/support/step_definitions/steps.js" with:
87 | """
88 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
89 | Given("a table step", function(table) {
90 | const expected = [
91 | {"Vegetable": "Apricot", "Rating": "5"},
92 | {"Vegetable": "Brocolli", "Rating": "2"},
93 | {"Vegetable": "Cucumber", "Rating": "10"}
94 | ];
95 | expect(table.hashes()).to.deep.equal(expected);
96 | });
97 | """
98 | When I run cypress
99 | Then it passes
100 |
101 | Scenario: empty cells
102 | Given a file named "cypress/e2e/a.feature" with:
103 | """
104 | Feature: a feature
105 | Scenario: a scenario
106 | Given a table step
107 | | Vegetable | Rating |
108 | | Apricot | |
109 | | Brocolli | |
110 | | Cucumber | |
111 | """
112 | And a file named "cypress/support/step_definitions/steps.js" with:
113 | """
114 | const { Given } = require("@klaveness/cypress-cucumber-preprocessor");
115 | Given("a table step", function(table) {
116 | const expected = [
117 | ["Apricot", ""],
118 | ["Brocolli", ""],
119 | ["Cucumber", ""]
120 | ];
121 | expect(table.rows()).to.deep.equal(expected);
122 | });
123 | """
124 | When I run cypress
125 | Then it passes
126 |
--------------------------------------------------------------------------------
/lib/methods.ts:
--------------------------------------------------------------------------------
1 | import parse from "@cucumber/tag-expressions";
2 | import { fromByteArray } from "base64-js";
3 | import { assertAndReturn } from "./assertions";
4 | import { collectTagNames } from "./ast-helpers";
5 |
6 | import {
7 | INTERNAL_PROPERTY_NAME,
8 | TASK_CREATE_STRING_ATTACHMENT,
9 | } from "./constants";
10 | import { InternalProperties } from "./create-tests";
11 |
12 | import DataTable from "./data_table";
13 |
14 | import { getRegistry } from "./registry";
15 |
16 | import {
17 | IHookBody,
18 | IParameterTypeDefinition,
19 | IStepDefinitionBody,
20 | } from "./types";
21 |
22 | function defineStep(
23 | description: string | RegExp,
24 | implementation: IStepDefinitionBody
25 | ) {
26 | getRegistry().defineStep(description, implementation);
27 | }
28 |
29 | function runStepDefininition(
30 | world: Mocha.Context,
31 | text: string,
32 | argument?: DataTable | string
33 | ) {
34 | getRegistry().runStepDefininition(world, text, argument);
35 | }
36 |
37 | function defineParameterType(options: IParameterTypeDefinition) {
38 | getRegistry().defineParameterType(options);
39 | }
40 |
41 | function defineBefore(options: { tags?: string }, fn: IHookBody): void;
42 | function defineBefore(fn: IHookBody): void;
43 | function defineBefore(
44 | optionsOrFn: IHookBody | { tags?: string },
45 | maybeFn?: IHookBody
46 | ) {
47 | if (typeof optionsOrFn === "function") {
48 | getRegistry().defineBefore({}, optionsOrFn);
49 | } else if (typeof optionsOrFn === "object" && typeof maybeFn === "function") {
50 | getRegistry().defineBefore(optionsOrFn, maybeFn);
51 | } else {
52 | throw new Error("Unexpected argument for Before hook");
53 | }
54 | }
55 |
56 | function defineAfter(options: { tags?: string }, fn: IHookBody): void;
57 | function defineAfter(fn: IHookBody): void;
58 | function defineAfter(
59 | optionsOrFn: IHookBody | { tags?: string },
60 | maybeFn?: IHookBody
61 | ) {
62 | if (typeof optionsOrFn === "function") {
63 | getRegistry().defineAfter({}, optionsOrFn);
64 | } else if (typeof optionsOrFn === "object" && typeof maybeFn === "function") {
65 | getRegistry().defineAfter(optionsOrFn, maybeFn);
66 | } else {
67 | throw new Error("Unexpected argument for After hook");
68 | }
69 | }
70 |
71 | function createStringAttachment(
72 | data: string,
73 | mediaType: string,
74 | encoding: "BASE64" | "IDENTITY"
75 | ) {
76 | cy.task(TASK_CREATE_STRING_ATTACHMENT, {
77 | data,
78 | mediaType,
79 | encoding,
80 | });
81 | }
82 |
83 | export function attach(data: string | ArrayBuffer, mediaType?: string) {
84 | if (typeof data === "string") {
85 | mediaType = mediaType ?? "text/plain";
86 |
87 | if (mediaType.startsWith("base64:")) {
88 | createStringAttachment(data, mediaType.replace("base64:", ""), "BASE64");
89 | } else {
90 | createStringAttachment(data, mediaType ?? "text/plain", "IDENTITY");
91 | }
92 | } else if (data instanceof ArrayBuffer) {
93 | if (typeof mediaType !== "string") {
94 | throw Error("ArrayBuffer attachments must specify a media type");
95 | }
96 |
97 | createStringAttachment(
98 | fromByteArray(new Uint8Array(data)),
99 | mediaType,
100 | "BASE64"
101 | );
102 | } else {
103 | throw Error("Invalid attachment data: must be a ArrayBuffer or string");
104 | }
105 | }
106 |
107 | function isFeature() {
108 | return Cypress.env(INTERNAL_PROPERTY_NAME) != null;
109 | }
110 |
111 | export const NOT_FEATURE_ERROR =
112 | "Expected to find internal properties, but didn't. This is likely because you're calling doesFeatureMatch() in a non-feature spec. Use doesFeatureMatch() in combination with isFeature() if you have both feature and non-feature specs";
113 |
114 | function doesFeatureMatch(expression: string) {
115 | const { pickle } = assertAndReturn(
116 | Cypress.env(INTERNAL_PROPERTY_NAME),
117 | NOT_FEATURE_ERROR
118 | ) as InternalProperties;
119 |
120 | return parse(expression).evaluate(collectTagNames(pickle.tags));
121 | }
122 |
123 | export {
124 | isFeature,
125 | doesFeatureMatch,
126 | defineStep as Given,
127 | defineStep as When,
128 | defineStep as Then,
129 | defineStep as And,
130 | defineStep as But,
131 | defineStep,
132 | runStepDefininition as Step,
133 | defineParameterType,
134 | defineBefore as Before,
135 | defineAfter as After,
136 | };
137 |
--------------------------------------------------------------------------------
/docs/cucumber-basics.md:
--------------------------------------------------------------------------------
1 | # Expressions
2 |
3 | A step definition’s *expression* can either be a regular expression or a [cucumber expression](https://github.com/cucumber/cucumber-expressions#readme). The examples in this section use cucumber expressions. If you prefer to use regular expressions, each *capture group* from the match will be passed as arguments to the step definition’s function.
4 |
5 | ```ts
6 | Given(/I have {int} cukes in my belly/, (cukes: number) => {});
7 | ```
8 |
9 | # Arguments
10 |
11 | Steps can be accompanied by [doc strings](https://cucumber.io/docs/gherkin/reference/#doc-strings) or [data tables](https://cucumber.io/docs/gherkin/reference/#data-tables), both which will be passed to the step definition as the last argument, as shown below.
12 |
13 | ```gherkin
14 | Feature: a feature
15 | Scenario: a scenario
16 | Given a table step
17 | | Cucumber | Cucumis sativus |
18 | | Burr Gherkin | Cucumis anguria |
19 | ```
20 |
21 | ```ts
22 | import { Given, DataTable } from "@klaveness/cypress-cucumber-preprocessor";
23 |
24 | Given(/^a table step$/, (table: DataTable) => {
25 | const expected = [
26 | ["Cucumber", "Cucumis sativus"],
27 | ["Burr Gherkin", "Cucumis anguria"]
28 | ];
29 | assert.deepEqual(table.raw(), expected);
30 | });
31 | ```
32 |
33 | See [here](https://github.com/cucumber/cucumber-js/blob/main/docs/support_files/data_table_interface.md) for `DataTable`'s interface.
34 |
35 | # Custom parameter types
36 |
37 | Custom parameter types can be registered using `defineParameterType()`. They share the same scope as tests and you can invoke `defineParameterType()` anywhere you define steps, though the order of definition is unimportant. The table below explains the various arguments you can pass when defining a parameter type.
38 |
39 | | Argument | Description |
40 | | ------------- | ----------- |
41 | | `name` | The name the parameter type will be recognised by in output parameters.
42 | | `regexp` | A regexp that will match the parameter. May include capture groups.
43 | | `transformer` | A function or method that transforms the match from the regexp. Must have arity 1 if the regexp doesn't have any capture groups. Otherwise the arity must match the number of capture groups in `regexp`. |
44 |
45 | # Pending steps
46 |
47 | You can return `"pending"` from a step defintion or a chain to mark a step as pending. This will halt the execution and Cypress will report the test as "skipped".
48 |
49 | ```ts
50 | import { When } from "@klaveness/cypress-cucumber-preprocessor";
51 |
52 | When("a step", () => {
53 | return "pending";
54 | });
55 | ```
56 |
57 | ```ts
58 | import { When } from "@klaveness/cypress-cucumber-preprocessor";
59 |
60 | When("a step", () => {
61 | cy.then(() => {
62 | return "pending";
63 | });
64 | });
65 | ```
66 |
67 | # Nested steps
68 |
69 | You can invoke other steps from a step using `Step()`, as shown below.
70 |
71 | ```ts
72 | import { When, Step } from "@klaveness/cypress-cucumber-preprocessor";
73 |
74 | When("I fill in the entire form", function () {
75 | Step(this, 'I fill in "john.doe" for "Username"');
76 | Step(this, 'I fill in "password" for "Password"');
77 | });
78 | ```
79 |
80 | `Step()` optionally accepts a `DataTable` or `string` argument.
81 |
82 | ```ts
83 | import {
84 | When,
85 | Step,
86 | DataTable
87 | } from "@klaveness/cypress-cucumber-preprocessor";
88 |
89 | When("I fill in the entire form", function () {
90 | Step(
91 | this,
92 | "I fill in the value",
93 | new DataTable([
94 | ["Field", "Value"],
95 | ["Username", "john.doe"],
96 | ["Password", "password"]
97 | ])
98 | );
99 | });
100 | ```
101 |
102 | # Hooks
103 |
104 | `Before()` and `After()` is similar to Cypress' `beforeEach()` and `afterEach()`, but they can be selected to conditionally run based on the tags of each scenario, as shown below. Furthermore, failure in these hooks does **not** result in remaining tests being skipped. This is contrary to Cypress' `beforeEach` and `afterEach`.
105 |
106 | ```ts
107 | import { Before } from "@klaveness/cypress-cucumber-preprocessor";
108 |
109 | Before(function () {
110 | // This hook will be executed before all scenarios.
111 | });
112 |
113 | Before({ tags: "@foo" }, function () {
114 | // This hook will be executed before scenarios tagged with @foo.
115 | });
116 |
117 | Before({ tags: "@foo and @bar" }, function () {
118 | // This hook will be executed before scenarios tagged with @foo and @bar.
119 | });
120 |
121 | Before({ tags: "@foo or @bar" }, function () {
122 | // This hook will be executed before scenarios tagged with @foo or @bar.
123 | });
124 | ```
125 |
--------------------------------------------------------------------------------
/features/step_definitions/cli_steps.ts:
--------------------------------------------------------------------------------
1 | import util from "util";
2 | import { Given, When, Then } from "@cucumber/cucumber";
3 | import assert from "assert";
4 | import childProcess from "child_process";
5 |
6 | function execAsync(
7 | command: string
8 | ): Promise<{ stdout: string; stderr: string }> {
9 | return new Promise((resolve, reject) => {
10 | childProcess.exec(command, (error, stdout, stderr) => {
11 | if (error) {
12 | reject(error);
13 | } else {
14 | resolve({ stdout, stderr });
15 | }
16 | });
17 | });
18 | }
19 |
20 | When("I run cypress", { timeout: 60 * 1000 }, async function () {
21 | await this.run();
22 | });
23 |
24 | When(
25 | "I run cypress with {string}",
26 | { timeout: 60 * 1000 },
27 | async function (unparsedArgs) {
28 | // Use user's preferred shell to split args.
29 | const { stdout } = await execAsync(
30 | `node -p "JSON.stringify(process.argv)" -- ${unparsedArgs}`
31 | );
32 |
33 | // Drop 1st arg, which is the path of node.
34 | const [, ...args] = JSON.parse(stdout);
35 |
36 | await this.run(args);
37 | }
38 | );
39 |
40 | When(
41 | "I run cypress with environment variables",
42 | { timeout: 60 * 1000 },
43 | async function (table) {
44 | await this.run([], Object.fromEntries(table.rows()));
45 | }
46 | );
47 |
48 | Then("it passes", function () {
49 | assert.equal(this.lastRun.exitCode, 0, "Expected a zero exit code");
50 | });
51 |
52 | Then("it fails", function () {
53 | assert.notEqual(this.lastRun.exitCode, 0, "Expected a non-zero exit code");
54 | this.verifiedLastRunError = true;
55 | });
56 |
57 | Then("it should appear as if only a single test ran", function () {
58 | assert.match(this.lastRun.stdout, /All specs passed!\s+\d+ms\s+1\s+1\D/);
59 | });
60 |
61 | Then("it should appear as if both tests ran", function () {
62 | assert.match(this.lastRun.stdout, /All specs passed!\s+\d+ms\s+2\s+2\D/);
63 | });
64 |
65 | Then("it should appear as if both tests were skipped", function () {
66 | assert.match(
67 | this.lastRun.stdout,
68 | /All specs passed!\s+\d+ms\s+2\s+-\s+-\s+2\D/
69 | );
70 | });
71 |
72 | Then("it should appear to not have ran spec {string}", function (spec) {
73 | assert.doesNotMatch(
74 | this.lastRun.stdout,
75 | new RegExp("Running:\\s+" + rescape(spec))
76 | );
77 | });
78 |
79 | Then(
80 | "it should appear to have ran spec {string} and {string}",
81 | function (a, b) {
82 | for (const spec of [a, b]) {
83 | assert.match(
84 | this.lastRun.stdout,
85 | new RegExp("Running:\\s+" + rescape(spec))
86 | );
87 | }
88 | }
89 | );
90 |
91 | Then("I should not see {string} in the output", function (string) {
92 | if (this.lastRun.stdout.includes(string)) {
93 | assert.fail(`Expected to not find ${util.inspect(string)}, but did`);
94 | }
95 | });
96 |
97 | /**
98 | * Shamelessly copied from the RegExp.escape proposal.
99 | */
100 | const rescape = (s: string) => String(s).replace(/[\\^$*+?.()|[\]{}]/g, "\\$&");
101 |
102 | const runScenarioExpr = (scenarioName: string) =>
103 | new RegExp(`(?:✓|√) ${rescape(scenarioName)}( \\(\\d+ms\\))?\\n`);
104 |
105 | const pendingScenarioExpr = (scenarioName: string) =>
106 | new RegExp(`- ${rescape(scenarioName)}\n`);
107 |
108 | Then(
109 | "it should appear to have run the scenario {string}",
110 | function (scenarioName) {
111 | assert.match(this.lastRun.stdout, runScenarioExpr(scenarioName));
112 | }
113 | );
114 |
115 | Then(
116 | "it should appear to not have run the scenario {string}",
117 | function (scenarioName) {
118 | assert.doesNotMatch(this.lastRun.stdout, runScenarioExpr(scenarioName));
119 | }
120 | );
121 |
122 | Then("it should appear to have run the scenarios", function (scenarioTable) {
123 | for (const { Name: scenarioName } of scenarioTable.hashes()) {
124 | assert.match(this.lastRun.stdout, runScenarioExpr(scenarioName));
125 | }
126 | });
127 |
128 | Then(
129 | "it should appear to not have run the scenarios",
130 | function (scenarioTable) {
131 | for (const { Name: scenarioName } of scenarioTable.hashes()) {
132 | assert.doesNotMatch(this.lastRun.stdout, runScenarioExpr(scenarioName));
133 | }
134 | }
135 | );
136 |
137 | Then("the output should contain", function (content) {
138 | assert.match(this.lastRun.stdout, new RegExp(rescape(content)));
139 | });
140 |
141 | Then("the output should match", function (content) {
142 | assert.match(this.lastRun.stdout, new RegExp(content));
143 | });
144 |
145 | Then(
146 | "it should appear to have skipped the scenario {string}",
147 | function (scenarioName) {
148 | assert.match(this.lastRun.stdout, pendingScenarioExpr(scenarioName));
149 | }
150 | );
151 |
--------------------------------------------------------------------------------
/features/support/hooks.ts:
--------------------------------------------------------------------------------
1 | import { version as cypressVersion } from "cypress/package.json";
2 | import { After, Before, formatterHelpers } from "@cucumber/cucumber";
3 | import path from "path";
4 | import assert from "assert";
5 | import { promises as fs } from "fs";
6 | import { isPost10, writeFile } from "./helpers";
7 |
8 | const projectPath = path.join(__dirname, "..", "..");
9 |
10 | Before(async function ({ gherkinDocument, pickle }) {
11 | assert(gherkinDocument.uri, "Expected gherkinDocument.uri to be present");
12 |
13 | const relativeUri = path.relative(process.cwd(), gherkinDocument.uri);
14 |
15 | const { line } = formatterHelpers.PickleParser.getPickleLocation({
16 | gherkinDocument,
17 | pickle,
18 | });
19 |
20 | this.tmpDir = path.join(projectPath, "tmp", `${relativeUri}_${line}`);
21 |
22 | await fs.rm(this.tmpDir, { recursive: true, force: true });
23 |
24 | await writeFile(
25 | path.join(this.tmpDir, ".cypress-cucumber-preprocessorrc"),
26 | "{}"
27 | );
28 |
29 | await writeFile(path.join(this.tmpDir, "cypress", "support", "e2e.js"), "");
30 |
31 | if (isPost10()) {
32 | await writeFile(
33 | path.join(this.tmpDir, "cypress.config.js"),
34 | `
35 | const { defineConfig } = require("cypress");
36 | const setupNodeEvents = require("./setupNodeEvents.js");
37 |
38 | module.exports = defineConfig({
39 | e2e: {
40 | specPattern: "cypress/e2e/**/*.feature",
41 | video: false,
42 | setupNodeEvents
43 | }
44 | })
45 | `
46 | );
47 | } else {
48 | await writeFile(
49 | path.join(this.tmpDir, "cypress.json"),
50 | JSON.stringify(
51 | {
52 | testFiles: "**/*.feature",
53 | integrationFolder: "cypress/e2e",
54 | supportFile: "cypress/support/e2e.js",
55 | video: false,
56 | nodeVersion: "system",
57 | },
58 | null,
59 | 2
60 | )
61 | );
62 | }
63 |
64 | await fs.mkdir(path.join(this.tmpDir, "node_modules", "@klaveness"), {
65 | recursive: true,
66 | });
67 |
68 | await fs.symlink(
69 | projectPath,
70 | path.join(
71 | this.tmpDir,
72 | "node_modules",
73 | "@klaveness",
74 | "cypress-cucumber-preprocessor"
75 | ),
76 | "dir"
77 | );
78 | });
79 |
80 | Before({ tags: "not @no-default-plugin" }, async function () {
81 | if (isPost10()) {
82 | await writeFile(
83 | path.join(this.tmpDir, "setupNodeEvents.js"),
84 | `
85 | const { addCucumberPreprocessorPlugin } = require("@klaveness/cypress-cucumber-preprocessor");
86 | const { createEsbuildPlugin } = require("@klaveness/cypress-cucumber-preprocessor/esbuild");
87 | const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
88 |
89 | module.exports = async function setupNodeEvents(on, config) {
90 | await addCucumberPreprocessorPlugin(on, config);
91 |
92 | on(
93 | "file:preprocessor",
94 | createBundler({
95 | plugins: [createEsbuildPlugin(config)],
96 | })
97 | );
98 |
99 | return config;
100 | };
101 | `
102 | );
103 | } else {
104 | await writeFile(
105 | path.join(this.tmpDir, "cypress", "plugins", "index.js"),
106 | `
107 | const { addCucumberPreprocessorPlugin } = require("@klaveness/cypress-cucumber-preprocessor");
108 | const { createEsbuildPlugin } = require("@klaveness/cypress-cucumber-preprocessor/esbuild");
109 | const createBundler = require("@bahmutov/cypress-esbuild-preprocessor");
110 |
111 | module.exports = async (on, config) => {
112 | await addCucumberPreprocessorPlugin(on, config);
113 |
114 | on(
115 | "file:preprocessor",
116 | createBundler({
117 | plugins: [createEsbuildPlugin(config)]
118 | })
119 | );
120 |
121 | return config;
122 | }
123 | `
124 | );
125 | }
126 | });
127 |
128 | After(function () {
129 | if (
130 | this.lastRun != null &&
131 | this.lastRun.exitCode !== 0 &&
132 | !this.verifiedLastRunError
133 | ) {
134 | throw new Error(
135 | `Last run errored unexpectedly. Output:\n\n${this.lastRun.output}`
136 | );
137 | }
138 | });
139 |
140 | function addCucumberPreprocessorPlugin(on: any, config: any) {
141 | throw new Error("Function not implemented.");
142 | }
143 |
144 | function createBundler(arg0: { plugins: any[] }): any {
145 | throw new Error("Function not implemented.");
146 | }
147 |
148 | function createEsbuildPlugin(config: any) {
149 | throw new Error("Function not implemented.");
150 | }
151 |
--------------------------------------------------------------------------------
/lib/ast-helpers.ts:
--------------------------------------------------------------------------------
1 | import { messages } from "@cucumber/messages";
2 |
3 | import { assertAndReturn } from "./assertions";
4 |
5 | export function* traverseGherkinDocument(
6 | gherkinDocument: messages.IGherkinDocument
7 | ) {
8 | yield gherkinDocument;
9 |
10 | if (gherkinDocument.feature) {
11 | yield* traverseFeature(gherkinDocument.feature);
12 | }
13 | }
14 |
15 | function* traverseFeature(feature: messages.GherkinDocument.IFeature) {
16 | yield feature;
17 |
18 | if (feature.location) {
19 | yield feature.location;
20 | }
21 |
22 | if (feature.tags) {
23 | for (const tag of feature.tags) {
24 | yield tag;
25 | }
26 | }
27 |
28 | if (feature.children) {
29 | for (const child of feature.children) {
30 | yield* traverseFeatureChild(child);
31 | }
32 | }
33 | }
34 |
35 | function* traverseFeatureChild(
36 | featureChild: messages.GherkinDocument.Feature.IFeatureChild
37 | ) {
38 | yield featureChild;
39 |
40 | if (featureChild.rule) {
41 | yield* traverseFeatureRule(featureChild.rule);
42 | }
43 |
44 | if (featureChild.background) {
45 | yield* traverseBackground(featureChild.background);
46 | }
47 |
48 | if (featureChild.scenario) {
49 | yield* traverseScenario(featureChild.scenario);
50 | }
51 | }
52 |
53 | function* traverseFeatureRule(
54 | rule: messages.GherkinDocument.Feature.FeatureChild.IRule
55 | ) {
56 | yield rule;
57 |
58 | if (rule.location) {
59 | yield rule.location;
60 | }
61 |
62 | if (rule.children) {
63 | for (const child of rule.children) {
64 | yield* traverseRuleChild(child);
65 | }
66 | }
67 | }
68 |
69 | function* traverseRuleChild(
70 | ruleChild: messages.GherkinDocument.Feature.FeatureChild.IRuleChild
71 | ) {
72 | yield ruleChild;
73 |
74 | if (ruleChild.background) {
75 | yield* traverseBackground(ruleChild.background);
76 | }
77 |
78 | if (ruleChild.scenario) {
79 | yield* traverseScenario(ruleChild.scenario);
80 | }
81 | }
82 |
83 | function* traverseBackground(
84 | backgorund: messages.GherkinDocument.Feature.IBackground
85 | ) {
86 | yield backgorund;
87 |
88 | if (backgorund.location) {
89 | yield backgorund.location;
90 | }
91 |
92 | if (backgorund.steps) {
93 | for (const step of backgorund.steps) {
94 | yield* traverseStep(step);
95 | }
96 | }
97 | }
98 |
99 | function* traverseScenario(
100 | scenario: messages.GherkinDocument.Feature.IScenario
101 | ) {
102 | yield scenario;
103 |
104 | if (scenario.location) {
105 | yield scenario.location;
106 | }
107 |
108 | if (scenario.steps) {
109 | for (const step of scenario.steps) {
110 | yield* traverseStep(step);
111 | }
112 | }
113 |
114 | if (scenario.examples) {
115 | for (const example of scenario.examples) {
116 | yield* traverseExample(example);
117 | }
118 | }
119 | }
120 |
121 | function* traverseStep(step: messages.GherkinDocument.Feature.IStep) {
122 | yield step;
123 |
124 | if (step.location) {
125 | yield step.location;
126 | }
127 |
128 | if (step.docString) {
129 | yield* traverseDocString(step.docString);
130 | }
131 |
132 | if (step.dataTable) {
133 | yield* traverseDataTable(step.dataTable);
134 | }
135 | }
136 |
137 | function* traverseDocString(
138 | docString: messages.GherkinDocument.Feature.Step.IDocString
139 | ) {
140 | yield docString;
141 |
142 | if (docString.location) {
143 | yield docString.location;
144 | }
145 | }
146 |
147 | function* traverseDataTable(
148 | dataTable: messages.GherkinDocument.Feature.Step.IDataTable
149 | ) {
150 | yield dataTable;
151 |
152 | if (dataTable.location) {
153 | yield dataTable.location;
154 | }
155 |
156 | if (dataTable.rows) {
157 | for (const row of dataTable.rows) {
158 | yield* traverseRow(row);
159 | }
160 | }
161 | }
162 |
163 | function* traverseRow(row: messages.GherkinDocument.Feature.ITableRow) {
164 | yield row;
165 |
166 | if (row.location) {
167 | yield row.location;
168 | }
169 |
170 | if (row.cells) {
171 | for (const cell of row.cells) {
172 | yield* traverseCell(cell);
173 | }
174 | }
175 | }
176 |
177 | function* traverseCell(
178 | cell: messages.GherkinDocument.Feature.TableRow.ITableCell
179 | ) {
180 | yield cell;
181 |
182 | if (cell.location) {
183 | yield cell.location;
184 | }
185 | }
186 |
187 | function* traverseExample(
188 | example: messages.GherkinDocument.Feature.Scenario.IExamples
189 | ) {
190 | yield example;
191 |
192 | if (example.location) {
193 | yield example.location;
194 | }
195 |
196 | if (example.tableHeader) {
197 | yield* traverseRow(example.tableHeader);
198 | }
199 |
200 | if (example.tableBody) {
201 | for (const row of example.tableBody) {
202 | yield* traverseRow(row);
203 | }
204 | }
205 | }
206 |
207 | export function collectTagNames(
208 | tags: messages.GherkinDocument.Feature.ITag[] | null | undefined
209 | ) {
210 | return (
211 | tags?.map((tag) =>
212 | assertAndReturn(tag.name, "Expected tag to have a name")
213 | ) ?? []
214 | );
215 | }
216 |
--------------------------------------------------------------------------------
/lib/tag-parser/parser.ts:
--------------------------------------------------------------------------------
1 | import { TagParserError } from "./errors";
2 |
3 | import {
4 | isAt,
5 | isClosingParanthesis,
6 | isComma,
7 | isDigit,
8 | isEqual,
9 | isOpeningParanthesis,
10 | isQuote,
11 | isWordChar,
12 | Tokenizer,
13 | } from "./tokenizer";
14 |
15 | function createUnexpectedEndOfString() {
16 | return new TagParserError("Unexpected end-of-string");
17 | }
18 |
19 | function createUnexpectedToken(
20 | token: TYield,
21 | expectation: string
22 | ) {
23 | return new Error(
24 | `Unexpected token at ${token.position}: ${token.value} (${expectation})`
25 | );
26 | }
27 |
28 | function expectToken(token: Token) {
29 | if (token.done) {
30 | throw createUnexpectedEndOfString();
31 | }
32 |
33 | return token;
34 | }
35 |
36 | function parsePrimitiveToken(token: Token) {
37 | if (token.done) {
38 | throw createUnexpectedEndOfString();
39 | }
40 |
41 | const value = token.value.value;
42 |
43 | const char = value[0];
44 |
45 | if (value === "false") {
46 | return false;
47 | } else if (value === "true") {
48 | return true;
49 | }
50 | if (isDigit(char)) {
51 | return parseInt(value);
52 | } else if (isQuote(char)) {
53 | return value.slice(1, -1);
54 | } else {
55 | throw createUnexpectedToken(
56 | token.value,
57 | "expected a string, a boolean or a number"
58 | );
59 | }
60 | }
61 |
62 | type TYield = T extends Generator ? R : never;
63 |
64 | type TReturn = T extends Generator ? R : never;
65 |
66 | interface IteratorYieldResult {
67 | done?: false;
68 | value: TYield;
69 | }
70 |
71 | interface IteratorReturnResult {
72 | done: true;
73 | value: TReturn;
74 | }
75 |
76 | type IteratorResult =
77 | | IteratorYieldResult>
78 | | IteratorReturnResult>;
79 |
80 | type TokenGenerator = ReturnType;
81 |
82 | type Token = IteratorResult;
83 |
84 | class BufferedGenerator {
85 | private tokens: (IteratorYieldResult | IteratorReturnResult)[] =
86 | [];
87 |
88 | private position = -1;
89 |
90 | constructor(generator: Generator) {
91 | do {
92 | this.tokens.push(generator.next());
93 | } while (!this.tokens[this.tokens.length - 1].done);
94 | }
95 |
96 | next() {
97 | if (this.position < this.tokens.length - 1) {
98 | this.position++;
99 | }
100 |
101 | return this.tokens[this.position];
102 | }
103 |
104 | peak(n: number = 1) {
105 | return this.tokens[this.position + n];
106 | }
107 | }
108 |
109 | type Primitive = string | boolean | number;
110 |
111 | export default class Parser {
112 | public constructor(private content: string) {}
113 |
114 | parse(): Record> {
115 | const tokens = new BufferedGenerator(new Tokenizer(this.content).tokens());
116 |
117 | let next: Token = expectToken(tokens.next());
118 |
119 | if (!isAt(next.value.value)) {
120 | throw createUnexpectedToken(next.value, "expected tag to begin with '@'");
121 | }
122 |
123 | next = expectToken(tokens.next());
124 |
125 | if (!isWordChar(next.value.value[0])) {
126 | throw createUnexpectedToken(
127 | next.value,
128 | "expected tag to start with a property name"
129 | );
130 | }
131 |
132 | const propertyName = next.value.value;
133 |
134 | next = expectToken(tokens.next());
135 |
136 | if (!isOpeningParanthesis(next.value.value)) {
137 | throw createUnexpectedToken(next.value, "expected opening paranthesis");
138 | }
139 |
140 | const isObjectMode = isEqual(expectToken(tokens.peak(2)).value.value);
141 | const entries: [string, Primitive][] = [];
142 | const values: Primitive[] = [];
143 |
144 | if (isObjectMode) {
145 | while (true) {
146 | const key = expectToken(tokens.next()).value.value;
147 |
148 | next = expectToken(tokens.next());
149 |
150 | if (!isEqual(next.value.value)) {
151 | throw createUnexpectedToken(next.value, "expected equal sign");
152 | }
153 |
154 | const value = parsePrimitiveToken(tokens.next());
155 |
156 | entries.push([key, value]);
157 |
158 | if (!isComma(expectToken(tokens.peak()).value.value)) {
159 | break;
160 | } else {
161 | tokens.next();
162 | }
163 | }
164 | } else {
165 | while (true) {
166 | const value = parsePrimitiveToken(tokens.next());
167 |
168 | values.push(value);
169 |
170 | if (!isComma(expectToken(tokens.peak()).value.value)) {
171 | break;
172 | } else {
173 | tokens.next();
174 | }
175 | }
176 | }
177 |
178 | next = expectToken(tokens.next());
179 |
180 | if (next.done) {
181 | throw createUnexpectedEndOfString();
182 | } else if (!isClosingParanthesis(next.value.value)) {
183 | throw createUnexpectedToken(next.value, "expected closing paranthesis");
184 | }
185 |
186 | if (isObjectMode) {
187 | return {
188 | [propertyName]: Object.fromEntries(entries),
189 | };
190 | } else {
191 | return {
192 | [propertyName]: values.length === 1 ? values[0] : values,
193 | };
194 | }
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/features/step_definitions/json_steps.ts:
--------------------------------------------------------------------------------
1 | import { Given, Then } from "@cucumber/cucumber";
2 | import path from "path";
3 | import { promises as fs } from "fs";
4 | import assert from "assert";
5 | import child_process from "child_process";
6 | import { toByteArray } from "base64-js";
7 | import { PNG } from "pngjs";
8 | import { version as cypressVersion } from "cypress/package.json";
9 |
10 | function isObject(object: any): object is object {
11 | return typeof object === "object" && object != null;
12 | }
13 |
14 | function hasOwnProperty(
15 | obj: X,
16 | prop: Y
17 | ): obj is X & Record {
18 | return obj.hasOwnProperty(prop);
19 | }
20 |
21 | function* traverseTree(object: any): Generator