End-to-end Testing
How-tos & Guides
8 min read

Simulating hovers in Cypress

Despite having no native support for hovers, there are several workarounds that allow you to trigger hover states within your Cypress tests.

Paul Akinyemi
Published September 8, 2022

Despite being among Cypress’s most requested features, native support for triggering hover actions remains unsupported in Cypress. In other words, there’s no cy.hover() command that you can call to get the element to act like you hovered over it. If you try to use cy.hover(), you’ll get a detailed error message that looks like the one below:

Not to fear: in this article we’ll cover several different workarounds that allow you to test end-to-end scenarios that require hover actions.

Why isn’t there a cy.hover()?

Since the Cypress test runner runs within the browser process, Cypress must simulate user actions and the corresponding events via JavaScript. This means that unlike other tools like Selenium and Playwright, all Cypress events are untrusted events, and lack their associated privileges.

But what exactly is the difference between a trusted and an untrusted event? This is what w3.org has to say:

Events that are generated by the user agent, either as a result of user interaction, or as a direct result of changes to the DOM, are trusted by the user agent with privileges that are not afforded to events generated by script through the createEvent() method, modified using the initEvent() method, or dispatched via the dispatchEvent() method. The isTrusted attribute of trusted events has a value of true, while untrusted events have an isTrusted attribute value of false.

Most untrusted events will not trigger default actions, with the exception of the click event. This event always triggers the default action, even if the isTrusted attribute is false (this behavior is retained for backward-compatibility). All other untrusted events behave as if the preventDefault() method had been called on that event.”

Source: w3.org’s DOM Level 3 Events documentation

In other words, any event not generated by a browser acts like preventDefault() was called on it, with the exception of click.

When you combine the fact that all native Cypress events are simulated with the fact that there’s no way to directly trigger CSS pseudo classes like :hover from JavaScript, it becomes clear why cy.hover() has been missing since 2015.

Workarounds

While Cypress doesn’t natively support simulating hover, you can still accomplish your testing goals, with some caveats. Generally, there are a couple of reasons why you might want to simulate hover in your tests:

We’ll cover how to implement each of these scenarios using the sample page below:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hover testing</title>
  </head>
  <body>
    <p data-cy="hover-p">I'm a pretty basic paragraph</p>

    <div>
      I'm a div with a surprise on hover!
      <button>Surprise!</button>
    </div>

    <p id="message"></p>
  </body>

  <style>
    p:hover {
      color: red;
    }

    button {
      display: none;
    }

    div:hover button {
      display: inline;
    }
  </style>

  <script>
    document.querySelector("button").addEventListener("click", () => {
      document.getElementById("message").textContent = "the button was clicked";
    });

    const events = ["mouseover", "mouseout", "mouseenter", "mouseleave"];

    events.forEach((event) => {
      document.getElementById("message").addEventListener(event, () => {
        document.getElementById("message").textContent = `the event ${event} was fired`;
      });
    });
  </script>
</html>

To follow along, simply set up a local dev server that contains this page and add Cypress to that project.

Option 1: The force attribute

If the reason you want cy.hover() is to interact with hidden elements, you have a couple of options. The first option is to force the event to occur even though the target element is not visible. Here’s an example of how to do that:

cy.get("button").click({ force: true });

On our web page, the <button> element is hidden and only shows up on the page when the user hovers over its parent element. Normally, this action would fail, because Cypress performs visibility checks on elements before performing any actions on them. If the element fails any of those checks, the action will fail and Cypress will display an error like this:

By passing {force: true} to click(), you tell Cypress it should ignore whether or not the element is visible and instead just click it anyway. This works, but it isn’t always a good idea - Cypress does those visibility checks for a good reason. For example, imagine if this button were hidden not because the user hasn’t hovered, but rather because the user has not completed some prior part of the workflow. By passing { force: true }, we run the risk of completing actions that a real user could never complete, and thus miss out on potential bugs lurking in our application.

Option 2: The invoke() command

An alternate workaround is to explicitly make the element visible and then perform your desired action on it. Here’s how you do that:

cy.get("button").invoke("show").click();

The Cypress invoke() command allows you to call a function. In this case, the function you’re calling is the jQuery show method. Calling this function forces the element to be displayed on the page by setting the display CSS property to true. It then returns the associated element, allowing you to invoke click() on it without first triggering a hover.

As a final improvement, we can make sure our target is invisible before we force it into visibility like this:

cy.get("button").should("be.hidden").invoke("show").click();

If the element is visible prior to calling invoke(), the should assertion will fail.

Option 3: Simulate the hover via custom JavaScript

Unfortunately, many hover scenarios are not as straightforward as marking an element as visible prior to executing the next action. As we’ve already discussed, Cypress can’t trigger the :hover pseudo class, but there are many other potential situations that wouldn’t be covered by the two previous workarounds.

A more robust approach to simulating hovers is to emit simulated mouse events via custom JavaScript. Consider the following JavaScript snippet from the test page above:

const events = ["mouseover", "mouseout", "mouseenter", "mouseleave"];

events.forEach((event) => {
  document.getElementById("message").addEventListener(event, () => {
    document.getElementById("message").textContent = `the event ${event} was fired`;
  });
});

On our example page, an element with an id of message is updated whenever the user hovers over it.

We’ll test this behavior by triggering events in our Cypress code using cy.trigger(). Here’s a code example that tests whether the message element has the correct content:

["mouseover", "mouseout", "mouseenter", "mouseleave"].forEach((event) => {
  it(`tests event: '${event}`, function () {
    cy.get("button").should("be.hidden").invoke("show").click();
    cy.get("#message").trigger(event);
    cy.get("#message").should("contain", `the event ${event} was fired`);
  });
});

In this example we first force the buttons to be visible, then click the element to ensure the element has some initial text, and finally trigger the associated mouse event before asserting the final text of the element.

This method works, but in the end it’s not a fool-proof solution, since these simulated events are untrusted. Thankfully, there’s one last workaround: using real events instead of simulated events in JavaScript.

How is this possible? Using the Chrome DevTools Protocol (CDP). The CDP is a set of APIs that allow you to control the various operations of the browser at a very low level. Essentially, it’s an interface that allows you to programmatically control a browser instance.

Cypress already uses the CDP for a variety of tasks under the hood, so you don’t need to install anything extra to use it. The only thing you need to add to your project is a plugin called cypress-real-events. Adding that provides a set of custom Cypress commands you can use to fire real browser events, hover being one of them.

It does come with a few caveats though:

That said, let’s see how to use it. Install the library by running the following in your terminal:

npm install cypress-real-events

And then add the following line in your cypress/support/index.{js,ts} or your cypress/support/e2e.js file:

import "cypress-real-events/support";

And you’re good to go! Let’s use this package to test for hover in our page:

it("tests real hovers", function () {
  cy.get("[data-cy=hover-p]").realHover().should("have.css", "color", "rgb(255, 0, 0)");

  cy.get("div")
    .realHover()
    .then(() => {
      cy.get("button").click();
      cy.get("#message").should("contain", "the button was clicked");
    });
});

It’s as simple as that! You get the element you want, you call .realHover() to simulate a hover event, and then you assert that the page is in the state we expect it to be. In the first assertion, we’re testing that the paragraph has the color red then hovered over, and in the second, we’re testing that the button is visible and can be clicked when the <div> is hovered over.

Reflect: A testing tool with support for generating real hover events

Reflect is a no-code testing tool that records your actions and turns them into a repeatable test. Virtually any action you can take on a browser is detected by Reflect, including hover actions. And since Reflect uses standards-based browser APIs to control the browser, every action appears as if it’s run from a real end user.

Get automated test coverage for your app today — try it for free.

Get started with Reflect today

Create your first test in 2 minutes, no installation or setup required. Accelerate your testing efforts with fast and maintainable test suites without writing a line of code.

Copyright © 2022 Reflect Software Inc. All Rights Reserved.