cypress-io / cypress

Fast, easy and reliable testing for anything that runs in a browser.
https://cypress.io
MIT License
46.7k stars 3.16k forks source link

cy.type + enter doesn't trigger a `click` on a button or anchor element #8267

Open NicholasBoll opened 4 years ago

NicholasBoll commented 4 years ago

When a button or a element is focused, hitting the enter key will trigger a click event in all browsers, but cy.type('{enter}') will not trigger a click event while using Cypress.

I've created a plugin that gets around this: https://github.com/NicholasBoll/cypress-enter-plugin

Update: Upon further inspection, Cypress does trigger a click on any non-focused element when typing any characters. This is not realistic. I'm attempting another plugin that tries to be more accurate about keyboard. Another issue cy.type has is the detection of valid elements to send keys to. This detection cannot be disabled via force: true which is only disabling actionability checks. I've had to manually use trigger which is cumbersome to trigger all related events to be more accurate, but that's for another issue.

Current behavior:

The click is not fired

Desired behavior:

The click event should fire

Test code to reproduce

https://github.com/NicholasBoll/cypress-test-tiny

cypress/fixtures/index.html

<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />

    <title>Test</title>
    <meta name="description" content="Test" />
  </head>

  <body>
    <button id="button">My Button</button>
    <output id="output"></output>
    <script>
      document.getElementById("button").addEventListener("click", function () {
        document.getElementById("output").textContent = "Clicked!";
      });
    </script>
  </body>
</html>

cypress/integration/spec.js

describe("page", () => {
  beforeEach(() => {
    cy.visit("cypress/fixtures/index.html");
  });

  it("should trigger a click action when typing {enter}", () => {
    cy.get("button").focus().type("{enter}"); // this doesn't trigger a click
    cy.get("output").should("contain", "Clicked!");
  });

  it("should not trigger a click action when typing random characters", () => {
    cy.get("button").type("foobar"); // this does trigger a click
    cy.get("output").should("not.contain", "Clicked!");
  });
});

Versions

All

jennifer-shehane commented 4 years ago

This specfile does pass for me in both cypress open and cypress run in Chrome, Firefox and Electron. What am I missing? 🤔 Are you using the latest Cypress?

spec.js

it('should trigger the click action when typing {enter}', () => {
  cy.visit('index.html');
  cy.get('button').type('{enter}')
  cy.get('output').should('contain', 'Clicked!')
});

index.html

<html>
<body>
  <button id="button">My Button</button>
  <output id="output"></output>
  <script>
    document.getElementById("button").addEventListener("click", function () {
      document.getElementById("output").textContent = "Clicked!";
    });
  </script>
</body>
</html>
Screen Shot 2020-08-14 at 3 36 10 PM Screen Shot 2020-08-14 at 3 37 30 PM
pcvandamcb commented 4 years ago

I am actually running into the same issue at the moment trying to trigger the enter key on links, to test keyboard (accessibility) navigation. I found no way (with type nor trigger) how to get a link to be "entered".

What would be the correct way to do this? (I am using Chrome 84 and Cypress 4.10)

jennifer-shehane commented 4 years ago

@pcvandamcb @NicholasBoll Please provide a fully reproducible example of a situation where the enter typing is not triggering the click event. We'd be more than happy to look into it. I wasn't able to reproduce this behavior.

LisaManresa commented 3 years ago

I'm running into this issue as well on Cypress 5.1.0. Here's a repo that reproduces the issue: https://github.com/LisaManresa/cypress-test-button-enter-press

Edit: I just realized that my issue is most likely related to cypress-plugin-tab. Tabbing into an element and pressing {enter} does not work. But cy.get('.my-button').type('{enter}') does. So my issue might belong over in the cypress-plugin-tab repo.

Badlapje commented 3 years ago

For anyone stumbling on this issue: i had the same issue and thanks to this ticket was able to resolve it. I selected the button like so:

cy.get(selector).focus().type('{enter}');

But the .focus() somehow breaks the enter. What you need to do is simply: cy.get(selector).type('{enter}')

NicholasBoll commented 3 years ago

Interesting. It seems to be correct that if an element is first focused, then {enter} will not work:

cy.get(selector).type('{enter}') // works

cy.get(selector).focus().type('{enter}') // doesn't work

// doesn't work
cy.get(selector).focus()
cy.focused().type('{enter}')

// doesn't work
cy.get(selector).focus()
cy.get(selector).type('{enter}')

// works
cy.get(selector).focus()
cy.get(someOtherSelector).focus()
cy.get(selector).type('{enter}')

It seems the only difference is if the element has focus before .type is called, it will not work.

This didn't make sense to me because the reason keyboard commands don't work at all are due to Cypress firing synthetic events instead of native events. #311 is the issue for Cypress to support native events. The reason synthetic events don't fire default actions (a click on enter or space bar keys is a default action). Read more about trusted events here. Only a click event will trigger default actions (activating a button is the default action for a click event).

So why does Cypress seem to click when you do cy.get(selector).type('{enter}')? I did some digging and this is what I found: https://github.com/cypress-io/cypress/blob/49db3a685d77655dde17fa39e2211dcacd7d0323/packages/driver/src/cy/commands/actions/type.js#L396-L417

The difference is if the element does not have focus, cy.now('click', ... is performed. This means you could type literally anything to an unfocused element (a button, for instance) and it will click the button. This happens to do what we want in the case of cy.get('button').type('{enter}').

In reality, this is what happens:

You can also do the following to click a button: cy.get('button').type('foobar')

Whoops! This is certainly not what we want!

NicholasBoll commented 3 years ago

@jennifer-shehane I've updated my original test to reflect this new information. You were right that the original code did not trigger the error since I didn't .focus() first.

I've updated the original issue as well.

https://github.com/NicholasBoll/cypress-test-tiny

jennifer-shehane commented 3 years ago

Yeah, definitely an oversight on how the events are triggered with enter keys on special form elements.

@NicholasBoll The second example in your repo is debateable. If the button does not have focus, then there's no way to type into the button without it first being clicked or focused. If you do cy.get("button").focus().type("foobar"); then it correctly does not fire the click event. I guess you could argue that Cypress should just automatically focus when being told to type into a button, a, etc.

Reproducible example

spec.js

it("should trigger a click action when typing {enter}", () => {
  cy.visit("index.html");
  cy.get("button").focus().type("{enter}");
  cy.get("output").should("contain", "Clicked!");
});

index.html

<html>
<body>
  <button id="button">My Button</button>
  <output id="output"></output>
  <script>
    document.getElementById("button").addEventListener("click", function () {
      document.getElementById("output").textContent = "Clicked!";
    });
  </script>
</body>
</html>
Screen Shot 2020-12-14 at 5 57 54 PM
Badlapje commented 3 years ago

This should really be mentioned far more clearly in the docs of the cy.type page. This is a big gotcha for our accessibility tests emulating keyboard navigation!

NicholasBoll commented 3 years ago

@NicholasBoll The second example in your repo is debateable. If the button does not have focus, then there's no way to type into the button without it first being clicked or focused. If you do cy.get("button").focus().type("foobar"); then it correctly does not fire the click event. I guess you could argue that Cypress should just automatically focus when being told to type into a button, a, etc.

I agree that you cannot type without first giving focus. I might disagree that a click is the way to do it. I probably didn't even notice at first because I always call .focus() before .type(), but not everyone does this. Omitting the .focus() changes the behavior of .type()

I see mention of this here: https://docs.cypress.io/api/commands/type.html#When-element-is-not-in-focus

I think type is meant for form inputs and not really for other elements. This is true of valid element detection.

I created a new plugin to help make keyboard interaction more realistic: https://github.com/NicholasBoll/cypress-keyboard-plugin. It works by wrapping cy.type with extra detection and features. It handles this issue as well as spacebar on checkboxes and radio inputs.

Badlapje commented 3 years ago

I created a new plugin to help make keyboard interaction more realistic: https://github.com/NicholasBoll/cypress-keyboard-plugin. It works by wrapping cy.type with extra detection and features. It handles this issue as well as spacebar on checkboxes and radio inputs.

This doesn't resolve the accessibility testing scenario. It uses a click event to emulate an enter, which means i'm not testing entering a button. Instead i'm just writing a second test for the mousebeviour of clicking the button.

It's a good plugin to recommend until such time as native events are incorporated into cypress. Nice work!

NicholasBoll commented 3 years ago

This doesn't resolve the accessibility testing scenario. It uses a click event to emulate an enter, which means i'm not testing entering a button. Instead i'm just writing a second test for the mousebeviour of clicking the button.

Agreed. Anything other than native events is simulation.

I disagree that you're just writing a second test for the mouse behavior for a click.

Take this example:

<button id="working-button">My Button</button>
<script>
  document
    .getElementById("working-button")
    .addEventListener("click", function () {
      console.log('clicked!')
    });
</script>

If you focus on the button without clicking it and then hit either the space bar key or the enter key, "clicked!" will appear in your console. This is because a click event is also fired in scenario as an activation trigger. There is no such thing as a hidden keydown + event.key === 'Enter' special action. It quite literally fires click event.

The plugin does its best to simulate the normal behavior. In the button scenario, this is what happens.

cy.get('button').focus().type('{enter}')
  1. The plugin registers a click event listener
  2. The plugin registers a keydown event listener
  3. The plugin calls the native cy.type which goes through a series of simulations to simulate a user pressing a key (keydown, keyup, keypress) as well as actionability to ensure a real user could perform the action
  4. The plugin then runs a series of checks
    1. If the registered click event listener was called, the application must have called it manually - there is nothing to do. The default activation trigger was manually called - the user will not know the difference
    2. If the registered click event listener was not called, it checks the defaultPrevented boolean on the keydown event. If it gets here, that means the application did not simulate a click and prevented the activation trigger with event.preventDefault(). An accessibility error is thrown in this case warning the user to check out #311 for more info
  5. click and keydown event listeners are removed
alexmkio commented 3 years ago

I ran into this issue when trying to test keyboard accessibility. I wanted to get the body element, tab twice to reach a link, and press enter to test that the link took me to a specific url.

I was already using a 3rd party plugin for tab functionality since this is also not natively supported by Cypress.

cy.get('body').tab().tab().type('{enter}')
cy.url().should('include', '/good_weather') 

Currently on Cypress v 8.2.0

bahmutov commented 3 years ago

I suggest you use cypress-real-events plugin for now to achieve this, read https://glebbahmutov.com/blog/test-app-using-the-keyboard/

Sent from my iPhone

On Aug 8, 2021, at 20:09, Alex Kio @.***> wrote:

 I ran into this issue when trying to test keyboard accessibility. I wanted to get the body element, tab twice to reach a link, and press enter to test that the link took me to a specific url.

I was already using a 3rd party plugin for tab functionality since this is also not natively supported by Cypress.

cy.get('body').tab().tab().type('{enter}') cy.url().should('include', '/good_weather') Currently on Cypress v 8.2.0

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub, or unsubscribe.

agrawalaayushi commented 3 years ago

cy.get("button").type("{enter}") doesn't work in firefox. But works as intended in Chrome and Electron.

mikaelkarlsson-se commented 2 years ago

I stumbled upon this as well when using Chrome (94) or Electron (91). In Firefox (93) it works as expected. Using Cypress 8.3.1.

        cy.contains(accordionContent).should("not.be.visible");
        cy.get("@toggleButton").click();
        cy.contains(accordionContent).should("be.visible");
        cy.get("@toggleButton").focus().type("{enter}");
        cy.contains(accordionContent).should("not.be.visible");
TheDutchCoder commented 2 years ago

There's something really fundamentally broken with how events are simulated. I have an accessible dropdown, which also responds to keyboard inputs and I can't focus the button and hit enter.

If I do it without the focus, it "works", but fires twice.

If I type '{tab}' (which Cypress claims doesn't exist), it actually works: image

It would be great if this system could be prioritized, because it's leading to a lot of issues on our end testing accessibility...

In my case it seems as if the button, when receiving a keyboard command, bubbles the event up to the parent that then closes the dropdown again, instead of triggering a click event which is how a button handles spacebar in the browser.

Attaching an event handler with stop propagation on the button also doesn't work, so event bubbling is not simulated like how they work in the browser. But I think we already reached that conclusion :)

sussexrick commented 2 years ago

cy.get("button").type("{enter}") doesn't work in firefox. But works as intended in Chrome and Electron.

I've found the same when using .type("{enter}") on a text input. It should submit the form. It does in Edge, but not in Firefox.