Search code examples
nuxt.jscypress

Why does a button not fire when clicked through cypress in a nuxt web app?


I have a button component in a Nuxt.js web app. The button component emits a click event when it is clicked. I use that button component on a Nuxt page and provide a page method as an event handler. When I click that button manually, the event is triggered and the application flow continues as expected. This is my code:

<MyButton id="button" @click="openModal()">
  {{ $t('open') }}
</MyButton>

However, when I click the same button using Cypress, nothing happens at all and the event handler is not called:

cy.get('#button').click()

After the test fails, I can press the button (in Cypress) manually and it will work. This led my to this blog post suggesting a race condition: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/


Solution

  • The blog post is indeed correct, this is a race condition. The button gets rendered server-side and sent to the browser. Cypress waits for the page to load but it doesn't know to also wait for the hydration. So the event handler is attached by some client-side code. Cypress is too fast here and clicks the button before the event handler is attached. That's also why it always works manually. Additionally, the click() method of cypress will not be retried automatically so the test will just fail.

    So basically, we'd want to wait on some signal from Nuxt that it is done hydrating. Unfortunately, I didn't find an API to do that. The best solution I came up with is adding a class to your layout to signal readiness and wait for that in Cypress:

    <template>
      <div :class="['default-layout', hydrated ? 'hydrated' : 'hydrating']">
        <nuxt />
      </div>
    </template>
    
    <script>
    export default {
      name: 'DefaultLayout',
      data () {
        return {
          hydrated: false,
        }
      },
      mounted () {
        this.$nextTick(() => {
          this.hydrated = true
        })
      }
    }
    </script>
    

    Then in cypress/support/commands.ts you can add a custom command to wait for the hydrated class:

    
    Cypress.Commands.add('waitForHydration', (timeout: number = 20_000) => {
      cy.log('Waiting for hydration ...')
      cy.get('.default-layout.hydrated', { log: false, timeout })
    })
    

    If you're using typescript, add the typings in cypress/support/index.d.ts:

    /// <reference types="cypress" />
    
    declare namespace Cypress {
      interface Chainable {
        waitForHydration(timeout?: number): Chainable<void>
      }
    }
    

    And finally, in your test suite, use it like this:

    it('can click the button', () => {
      cy.visit('/')
      cy.waitForHydration()
      cy.get('#button').click()
    })