Managing random dynamic elements in Cypress

Sometimes we may encounter elements that may (or may not) appear in the flow of our application, but are not the object of our test. An example of this may be the popup that prompts us to accept or deny cookies.

A potential solution to handle this problem could be to forge a cookie in the beforeEach() hook that makes our server detect that the cookies have already been accepted, but this could lead to other problems (such as Cloudfare, if it is the case, identifying the access as a potential forgery attack and returning a 403 error).

A more universal solution is to use a JavaScript snippet that generates an interval that checks for the presence of an element every “X” time and, if it appears, executes an action and discards the interval.

Since we are not “demanding” the presence of an element through a cy.get(), the interaction will only occur in parallel to the test in case the element appears.

It is important to be clear that this technique is invasive: we are altering the original behavior of the page, so it is necessary to study the use case in which to use it and make sure that it does not affect the scenario. Use with caution.

Pooling in the background

The trick is simple: we declare an interval that, in each execution, checks if the button exists and, if it does, presses it and eliminates the interval so that it is not executed again. In case the element does not appear, it will be executed again after poolingTimeMs milliseconds.

Clicking the button will cause the cookies to be accepted, causing the popup to disappear and not be displayed again until the cookies are deleted:

function tryToClickButtonUntilFound(
  buttonLocator: string,
  poolingIntervalMs: number
) {
  return new Cypress.Promise((resolve) => {
    const interval = setInterval(() => {
      const button = Cypress.$(buttonLocator);
      if (button.length) {
        button.trigger("click");
        clearInterval(interval);
        resolve("Button clicked");
      }
    }, poolingIntervalMs);
  });
}

Hook configuration

For our snippet to be executed in each test, it is enough to invoke it from the hook before(), inside our file e2e.js / e2e.ts:

before(): {
  tryToClickButtonUntilFound("accept-cookies-button", 3000);
}

Example

Let’s create a real example to illustrate this technique. We will create a scenario in which the user navigates to a page that may (or may not) display a popup to accept cookies.

We are going to use https://the-internet.herokuapp.com again, since it has an example of a popup managed by cookies. We will make our test randomly access this page or, failing that, another page that does not show the popup, closing the popup in case it appears but not failing in case the popup never appears.

Feature: Element that can be present or not is dismissed
    Scenario: Popup button is clicked if it appears, but it's not clicked otherwise
        Given the user navigates to a page that may show a popup or not
        When the user waits for 3 seconds
        Then the modal popup is not present in the page

We implement the step that navigates randomly between two pages:

import { Given, When, Then } from "@badeball/cypress-cucumber-preprocessor";

const ENTRY_AD_URL = "entry_ad";
const FLOATING_MENU_URL = "floating_menu";

const locators = {
  modal: "#modal:visible",
};

Given("the user navigates to a page that may show a popup or not", () => {
  cy.visit(Math.random() > 0.5 ? ENTRY_AD_URL : FLOATING_MENU_URL);
});

We add a step that forces an explicit wait so that, in case there is a popup, it can be displayed on the screen. This explicit wait is added in the example to simulate the time the test would take to continue running. Remember that in a real environment, explicit waits should be avoided at all costs:

When("the user waits for {int} seconds", (seconds: number) => {
  cy.log(`Waiting ${seconds} seconds...`);
  cy.wait(Math.ceil(seconds * 1000));
  cy.log(`Finished waiting`);
});

Finally, we check that the popup is not visible:

Then("the modal popup is not present in the page", () => {
  cy.get(locators.modal).should("not.exist");
});

For pooling to be executed before each test, we add the function to the hook before() in the e2e.ts file:

const COOKIE_POPUP_BUTTON_LOCATOR = "#modal:visible .modal-footer p";

function tryToClickButtonUntilFound(
  buttonLocator: string,
  poolingIntervalMs: number
) {
  return new Cypress.Promise((resolve) => {
    const intervalId = setInterval(() => {
      const button = Cypress.$(buttonLocator);
      if (button.length) {
        button.trigger("click");
        clearInterval(intervalId);
        resolve("Button clicked");
      }
    }, poolingIntervalMs);
  });
}

before(() => {
  tryToClickButtonUntilFound(COOKIE_POPUP_BUTTON_LOCATOR, 2000);
});

If the popup is not displayed, the test just goes on:

Otherwise, it will close the popup and will continue the test:

You can find the full example of the use of this technique here.

Advanced CSS locators for Cypress (or other automation tools)

Ideally, each element to be located in our tests should contain a specific attribute to uniquely identify it, such as data-testid or data-cy. However, sometimes we may not be able to add these attributes, such as when using third-party components.

It is, therefore, a good idea to become familiar with locating elements in other ways. The most common in Cypress is through CSS locators.

In this article I won’t go into detail on how to locate elements through their tagname or class, as there are hundreds of articles on how to do that, like this one or this one. Instead, I’ll discuss a couple of tricks for selecting elements in less typical situations.

Partial attribute values.

Let’s say we have the following HTML code:

<header>
  <nav>
    <ul>
      <li data-testid="header-menu-home-option">
          <span>Home</span>
      </li>
      <li data-testid="header-menu-cart-option">
          <span>Cart</span>
      </li>
      <li data-testid="header-menu-aboutus-option">
          <div>
              <span>About Us</span>
          </div>
      </li>
    </ul>
  </nav>
<header>
<main>
  <h1>This is my page</h1>
  <div>
    <span>Hello!</span>
  </div>
</main>

Each item contains a data-testid, so it is possible to select each of them uniquely. However, we may need to select all list items using a single selector to perform some kind of check (for example, to check that there are three items in the menu). To do this, we can use the values of the data-testid attributes using partial approximations. That is:

Items whose value contains a particular text:

cy.get('[data-testid*="-menu-"]');

Elements whose value begins with a particular text:

cy.get('[data-testid^="header-menu-"]');

Elements whose value ends with a given text:

cy.get('[data-testid$="-option"]');

And the most restrictive option: elements whose value begins with a given text and ends with another given text:

cy.get('[data-testid^="header-menu-"][data-testid$="-option"]');

This last selector will return all those items starting with “header-menu-” and ending with “-option”.

const expectedItems = 3;
cy.get('[data-testid^="header-menu-"][data-testid$="-option"]').should('have.length', expectedItems);

Direct descendants (children)

CSS offers a series of selectors that allow us to reference elements that are children of other elements or that share the same parent.

Thus, to obtain all the <span> elements that are direct children of <li>, we would use the following selector:

cy.get('li > span');

This selector will return only the span nodes that are direct children of a li node, that is to say:

<span>Home</span>
<span>Cart</span>

The <span> containing the text “About Us” would not be returned by this locator, since its parent is a <div>, not a <li>. Note the difference with the following selector:

 cy.get('li span');

In this case we would get all the spans that have an ancestor (parent, grandparent, great-grandparent, …) that is a li. Therefore, we would get the following:

<span>Home</span>
<span>Cart</span>
<span>About Us</span>

 We can also select elements that are grandchildren of other elements by concatenating the ‘>‘ operator with the wildcard ‘*‘. For example, the following selector will return the <ul> tags that are grandchildren of a <header> tag:

 cy.get('header >*> ul');

Consecutive elements (siblings).

We can also select elements that share the same parent node, that is, that are siblings. To do this we will use the operators ‘+‘ and ‘~‘.

 The ‘+‘ operator will return the next sibling of the previous element (and only the next sibling). Thus, the following selector:

 cy.get('[data-testid="header-menu-home-option"] + li');

will return the first sibling of the li whose data-testid is “header-menu-home-option”, that is: 

<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>

If we want to get all the siblings after a given element, we should use the selector ‘~‘:

 cy.get('[data-testid="header-menu-home-option"] + li');

The selector will return us all the siblings of the first <li>, ie:

<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>
<li data-testid="header-menu-aboutus-option">
  <div>
    <span>About Us</span>
  </div>
</li>

It is important to keep in mind that we will only get the siblings found after the element before ‘~‘, so if there were any other <li> element before it, it would not be retrieved using this selector.

Elements containing other elements. :has()

One of the big problems we face when locating elements with CSS is the impossibility of navigating upwards. That is: to obtain the parent of a given element. Although XPath offers us the possibility of doing this directly, CSS also allows us to do it by means of a little trick: indicating in the selector that we want to select an element that contains another element. For example, if we want to get those <li> that contain a <span>, we will use the :has() operator to indicate it:

 cy.get('li:has(span)');

This will return the following:

<li data-testid="header-menu-home-option">
  <span>Home</span>
</li>
<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>
<li data-testid="header-menu-aboutus-option">
  <div>
    <span>About Us</span>
  </div>
</li>

Note: We can take advantage of child and sibling filtering and perform more complex queries, for example, those <li> that have a <span> as a direct descendant (child):

 cy.get('li:has(> span)');

In this case, the result will omit the last <li>, which has a <div> as a direct child instead of a <span>

<li data-testid="header-menu-home-option">
  <span>Home</span>
</li>
<li data-testid="header-menu-cart-option">
  <span>Cart</span>
</li>

Elements that do not fulfill a condition. :not()

The :not() operator is also a good ally when selecting elements by discarding. Thus, the following selector will allow us to locate all the <li> elements but “About Us”:

 cy.get('[data-testid^="header-menu-"][data-testid$="-option"]:not([data-testid*="-aboutus-"])');

Elements containing a text. :contains()

Through the pseudo-class :contains() it is possible to locate elements containing a text, either directly or in a descendant. For example, the following selector:

cy.get('*:contains("Ho")');

will return the elements header, nav, ul, li and span corresponding to the following subtree:

<header>
  <nav>
    <ul>
      <li data-testid="header-menu-home-option">
        <span>Home</span>

We could also further refine the search by specifying other options:

cy.get('span:contains("Ho")');

Which would return us only the span element.

Note: :contains() is case-sensitive!!

Elements that fulfill one condition among several

As a last hint, we can create a selector in CSS that acts just like a logical OR by concatenating other selectors with commas (,). For example:

 cy.get('span:contains("Home"),span:contains("Cart")');

This selector will return all those span elements that contain either the substring “Homeor the substring “Cart“. This technique is very useful for scenarios in which two different elements can be shown before the same action.

More information:

Configuration of a new Cypress project using Typescript and Cucumber

I admit it: since I discovered Cypress, test automation has never been the same. Although my origins in this field go back to the combination of Java, Gradle, TestNG, Selenium and Jenkins, I have long since migrated to greener fields with Typescript, GitHub Actions, Cypress and Cucumber. Cypress’s element location management won me over from the start, so it didn’t take me long to start moving towards this technology.

While Cypress does not provide direct support for working with BDD, there are a number of packages that provide this versatility, making it possible to write test scenarios using natural language and run them directly in Cypress. This synergy facilitates communication between the different roles and improves test visibility across the team, which in turn promotes transparency and a common understanding of the requirements and functionality of the application. In short: it allows the Product Owner and other team members (technical and non-technical) to understand what functionality the tests are covering.

In this article we are going to learn how to set up the skeleton of a small project that will make use of the following technologies to automate the testing of a website:

  • Typescript
  • Cypress
  • Cucumber

For this purpose, we will use https://the-internet.herokuapp.com as the target of our tests, as it offers a number of typical functionalities in the world of automation.

Prerequisites

To carry out this project, we will need the following:

Creating the project on GitHub

Our first step is to log into our GitHub account and create a new repository. We’ll give it a representative name, a short description, add a README and specify “Node” as the .gitignore template.

With our project created, it’s time to clone it in our IDE. To do this, we will deploy the “<> Code” menu of the repository and copy the repository address.

After doing this, we open a console and go to the directory where we want to locally host the repository and run the following (changing the repository address to the one we just created):

git clone git@github.com:GarciaDan/cypress-ts-bdd.git

If the repository visibility is public, this will work fine. However, if we want to communicate with GitHub effectively and upload our local changes to the remote repository, we will need to use an ssh key.

To create a private key, run the following in a terminal:

ssh-keygen -t ed25519 -C "your_email@example.com"

This will generate a private key and a public key:

Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/daniel/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/daniel/.ssh/id_ed25519
Your public key has been saved in /home/daniel/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:+Fss5a3UMmvRY+t6EjbDJC9kVCnWtP0NxsToFsZAZFY your_email@example.com
The key's randomart image is:
+--[ED25519 256]--+
|         *O=Eo.  |
|        +ooo=o.  |
|       o ..o..+  |
|       .+ . oo o |
|      .oS=.o  . .|
|       ..+Oo+    |
|        ooB*oo   |
|         =o=o    |
|        ..+=.    |
+----[SHA256]-----+

Now, copy the public key content:

cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMkFnvZBzPiSscOcew1THpzm0URhwmNvP1Tfhlld/KWQ your_email@example.com

In GitHub, click on our Avatar, select “Settings” and go to the “SSH and GPG keys” section. Once there, click on “New SSH Key”.

Paste the content of the public key into the text box and save the changes.

With this we will now be able to use our private key to connect to our GitHub account. To do this, we’ll configure our git client. We open a terminal and type the following (if we haven’t already done so):

git config --global user.name "FIRST_NAME LAST_NAME"
git config --global user.email "your_email@example.com"

Finally, we run the SSH Agent and add the private key by passing the path to the file where we have generated it:

eval "$(ssh-agent -s)"
> Agent pid 59566
ssh-add ~/.ssh/id_ed25519

With this, we have communication between our remote repository and our local repository.

Creating the node project

In our local repository, we initialize a new NPM project.

npm init

Now install the necessary packages for our project:

  • Cypress
  • Cucumber Preprocessor
  • EsBuild bundler Preprocessor
  • Typescript
npm i -D cypress
npm i -D @badeball/cypress-cucumber-preprocessor
npm i -D @bahmutov/cypress-esbuild-preprocessor
npm i -D typescript
npm i -D @types/node @types/cypress-cucumber-preprocessor

Our package.json file will now look like this:

{
  "name": "cypress-ts-bdd",
  "version": "1.0.0",
  "description": "A simple Cypress+Typescript+BDD scaffolding project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/GarciaDan/cypress-ts-bdd.git"
  },
  "author": "Daniel Garcia",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/GarciaDan/cypress-ts-bdd/issues"
  },
  "homepage": "https://github.com/GarciaDan/cypress-ts-bdd#readme",
  "devDependencies": {
    "@badeball/cypress-cucumber-preprocessor": "^17.1.1",
    "@bahmutov/cypress-esbuild-preprocessor": "^2.2.0",
    "@types/cypress-cucumber-preprocessor": "^4.0.1",
    "@types/node": "^20.2.3",
    "cypress": "^12.12.0",
    "typescript": "^5.0.4"
  }
}

Cypress default configuration generation

Cypress generates a default configuration during the first run. Therefore, let’s open the Cypress interface:

npx cypress open

Click on “Continue >” and select the “E2E Testing” option, which is set to “Not Configured”.

This should generate the default configuration files:

Now, or project files should look like this:

Done this, we define the place where we will store the step definition (Typescript code). We add the following to our package.json:

  "cypress-cucumber-preprocessor": {
    "stepDefinitions": [
      "cypress/e2e/**/*.ts"
    ]
  }

So the file should now look like this:

{
  "name": "cypress-ts-bdd",
  "version": "1.0.0",
  "description": "A simple Cypress+Typescript+BDD scaffolding project",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/GarciaDan/cypress-ts-bdd.git"
  },
  "author": "Daniel Garcia",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/GarciaDan/cypress-ts-bdd/issues"
  },
  "homepage": "https://github.com/GarciaDan/cypress-ts-bdd#readme",
  "devDependencies": {
    "@badeball/cypress-cucumber-preprocessor": "^17.1.1",
    "@bahmutov/cypress-esbuild-preprocessor": "^2.2.0",
    "@types/cypress-cucumber-preprocessor": "^4.0.1",
    "@types/node": "^20.2.3",
    "cypress": "^12.12.0",
    "typescript": "^5.0.4"
  },
  "cypress-cucumber-preprocessor": {
    "stepDefinitions": [
      "cypress/e2e/**/*.ts"
    ]
  }
}

Typescript configuration

We will now configure Typescript by creating the tsconfig.json file in the root directory of the project. In it we will include the following:

{
    "compilerOptions": {
        "target": "ES2021",
        "lib": ["ES2021", "DOM"],
        "types": ["cypress", "node"],
        "moduleResolution": "node16",
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true
    },
    "include": ["**/*.ts"]
}

With these options we will tell typescript to transpile the Javascript code in the ES2021 version of ECMAScript, allowing us to use APIs such as array.include(), Object.entries(), string.trimEnd(), promise.any(), ….

  • By including the “DOM” library we will also be able to access DOM objects such as window or document.
  • We also include Cypress and NodeJS types, making the resolution of “node16” modules to support the latest versions of Typescript (from version 4.7 onwards).
  • Finally, we included the options esModuleInterop and allowSyntheticDefaultImports to avoid errors when importing certain modules (such as those that do not have a default export).

Cypress Configuration

It’s time to configure Cypress. Although it is not mandatory, we will start by creating a file to define the Cypress tasks. This way we will avoid declaring them directly in the configuration file and their modification will be much easier. For example, we will create a “dummy” task which we will call “stdout” and which will display a message on the console:

const tasks = {
    stdout: (...data: Array<any>) => {
        console.log(data)
    }
};

export default tasks;

We now edit the configuration file cypress.config.ts, which looks like this:

import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
});

We separately define the setupNodeEvents function and add the Cucumber preprocessor to it:

import { defineConfig } from "cypress";
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";

async function setupNodeEvents(
  on: Cypress.PluginEvents,
  config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
  await addCucumberPreprocessorPlugin(on, config);
  return config;
}

export default defineConfig({
  e2e: {
    setupNodeEvents,
  },
});

Now add, the esbuild bundler preprocessor:

import { defineConfig } from "cypress";
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
import createEsbuildPlugin from "@badeball/cypress-cucumber-preprocessor/esbuild";
import createBundler from "@bahmutov/cypress-esbuild-preprocessor/src";

async function setupNodeEvents(
  on: Cypress.PluginEvents,
  config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
  await addCucumberPreprocessorPlugin(on, config);
  on(
    "file:preprocessor",
    createBundler({ plugins: [createEsbuildPlugin(config)] })
  );
  return config;
}

export default defineConfig({
  e2e: {
    setupNodeEvents,
  },
});

We define the tasks. As we have created them in an external file, we import them and add them to the setupNodeEvents() function:

import { defineConfig } from "cypress";
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
import createEsbuildPlugin from "@badeball/cypress-cucumber-preprocessor/esbuild";
import createBundler from "@bahmutov/cypress-esbuild-preprocessor/src";
import tasks from "./cypress/support/tasks";

async function setupNodeEvents(
  on: Cypress.PluginEvents,
  config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
  await addCucumberPreprocessorPlugin(on, config);
  on(
    "file:preprocessor",
    createBundler({ plugins: [createEsbuildPlugin(config)] })
  );
  on("task", tasks);
  return config;
}

export default defineConfig({
  e2e: {
    setupNodeEvents,
  },
});

Finally, we add the following configuration to defineConfig.e2e in cypress.config.ts:

  • specPattern: tells Cypress where to look for (and with which pattern) the Gherkin language features.
  • supportFile: tells Cypress where to find the support file (e2e.ts).
  • baseUrl: sets the base URL of the project.
import { defineConfig } from "cypress";
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
import createEsbuildPlugin from "@badeball/cypress-cucumber-preprocessor/esbuild";
import createBundler from "@bahmutov/cypress-esbuild-preprocessor/src";
import tasks from "./cypress/support/tasks";

async function setupNodeEvents(
  on: Cypress.PluginEvents,
  config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
  await addCucumberPreprocessorPlugin(on, config);
  on(
    "file:preprocessor",
    createBundler({ plugins: [createEsbuildPlugin(config)] })
  );
  on("task", tasks);
  return config;
}

export default defineConfig({
  e2e: {
    setupNodeEvents,
    specPattern: "./cypress/e2e/**/*.{feature,features}",
    supportFile: "./cypress/support/e2e.ts",
	baseUrl: "https://the-internet.herokuapp.com"
  },
});

Environment variables

Instead of defining the environment variables in the configuration file, it is advisable to generate a cypress.env.json file and define them inside it. For example, we will define the username and password:

{
    "USERNAME": "tomsmith",
    "PASSWORD": "SuperSecretPassword!"
}

Creating a feature

Now it’s time to create our first test. Let’s start by creating the following folder structure:

  • cypress/e2e/login/features
  • cypress/e2e/login/steps

Inside cypress/e2e/login/features, we create the login-form.feature file and add the following test:

Feature: User is able to sign in the web application
    Scenario: User logs in successfully
        Given the user navigates to login page
        When the user provides valid credentials
        Then the main page is displayed to the authenticated user

The next step is, as we can imagine, to implement the steps.

We create the login-form.cy.ts file inside the cypress/e2e/login/steps/ folder and implement the three steps we just used in the feature.

The first step makes us visit the login URL:

import { Given, When, Then } from "@badeball/cypress-cucumber-preprocessor";

const LOGIN_PAGE_URL = "login";

Given("the user navigates to login page", () => {
  cy.visit(LOGIN_PAGE_URL);
});

The second step retrieves the environment variables USERNAME and PASSWORD from cypress.env.json and uses them to fill in the login form, to press the login button afterwards:

When("the user provides valid credentials", () => {
  const userName = Cypress.env("USERNAME");
  const password = Cypress.env("PASSWORD");

  cy.get("#username").clear().type(userName);
  cy.get("#password").clear().type(password);

  cy.get("button").click();
});

The last step is to perform a couple of checks to make sure that the page is displayed correctly.

Then("the main page is displayed to the authenticated user", () => {
  cy.get('[class="flash success"]').should("exist").and("be.visible");
  cy.get('a[href="/logout"]').should("exist").and("be.visible");
});

The complete file would be as follows:

import { Given, When, Then } from "@badeball/cypress-cucumber-preprocessor";

const LOGIN_PAGE_URL = "login";

Given("the user navigates to login page", () => {
  cy.visit(LOGIN_PAGE_URL);
});

When("the user provides valid credentials", () => {
  const userName = Cypress.env("USERNAME");
  const password = Cypress.env("PASSWORD");

  cy.get("#username").clear().type(userName);
  cy.get("#password").clear().type(password);

  cy.get("button").click();
});

Then("the main page is displayed to the authenticated user", () => {
  cy.get('[class="flash success"]').should("exist").and("be.visible");
  cy.get('a[href="/logout"]').should("exist").and("be.visible");
});

VSCode configuration with Cucumber support

If you use Visual Studio Code as an editor, it is highly recommended to support Cucumber with the CucumberAutoComplete extension. This extension allows, among other things, to navigate from step definition to implementation, so it can save a significant amount of time during development. You can get the extension here.

Once installed, edit the .vscode/settings.json file and add the following:

{
    "cucumberautocomplete.steps": [
        "cypress/e2e/*/**/steps/*.ts"
    ],
    "cucumberautocomplete.strictGherkinCompletion": false,
    "cucumberautocomplete.smartSnippets": true
}

Test Execution

Everything should be ready to run our first test using Cypress, Typescript and Cucumber. Let’s see the result. Open a terminal and run the following:

npx cypress open

Select the option “E2E Testing”.

Choose a browser (e.g. Chrome)

Click on login-form.feature to run the test and wait for the result.

Done! With this, our skeleton is complete and we can start adding more tests.

You can get the code related to this post in this repo.