przemuh.dev

Stop the time with cy.clock

4/22/2020

Today Iโ€™m going to show you how to stop the time with one command. Unfortunately only in cypress.io tests. If you know how to do it in real life please DM me. It would be a very helpful tip ๐Ÿ™‚. Ok, letโ€™s stop joking and get our hands dirty!

App description

First, we need to have something to test. Our app will be deadly-simple. We want to display the enter time and a counter shows how many seconds we spent in the app.

Enter time:

Time on page: 0

import React from "react"

export default () => {
  const [enterDate, setEnterDate] = React.useState()
  const [counter, setCounter] = React.useState(0)

  React.useEffect(() => {
    setEnterDate(Date.now())

    const intervalId = setInterval(() => {
      setCounter(prev => prev + 1)
    }, 1000)

    return () => clearInterval(intervalId)
  }, [])

  return (
    <div>
      <p>
        Enter time: <span data-testid="enter-time">{enterDate}</span>
      </p>
      <p>
        Time on page: <span data-testid="counter">{counter}</span>
      </p>
    </div>
  )
}

Ok, we have our app. Now it is time to write some cypress tests.

We are testing!

In our test scenario we would like to check:

  • if the enter time is displayed properly,
  • if the counter increases its value after a one-second tick.

Let's try this way:

cy.visit("/")
  .get("[data-testid=enter-time]")
  .should("have.text", Date.now().toString())

The test looks decent but it doesnโ€™t pass ๐Ÿ˜ข

Assertion error
Assertion error

If we would like to show some formatted date (eg. 22-04-2020) instead of a number of milliseconds, then it would not be a problem. But our client wants to display milliseconds and we need to live with this requirement ๐Ÿ˜‰

The cy.clock command comes with a rescue. It overrides native global functions related to time allowing them to be controlled synchronously via cy.tick() or the yielded clock object. This includes controlling:

  • setTimeout
  • clearTimeout
  • setInterval
  • clearInterval
  • Date

You can find more info about cy.clock in the cypress.io official documentation.

Now, letโ€™s try to add cy.clock to our test:

cy.clock()
  .visit("/")
  .get("[data-testid=enter-time]")
  .should("have.text", Date.now().toString())

We still get an error. But this time the error message is different.

expected <span> to have text '1587547901669', but the text was '0'

What is going on with that 0? Because the time represented in timestamp value is a number of seconds passed from the start of Unix epoch (1st January 1970). We could ask what will happen after the 19th of January 2038 but this is a topic for another blog post ๐Ÿ™‚.

Calling cy.clock without any arguments sets the date in our app to 1st January 1970. We could change it by passing an argument to the cy.clock:

const now = Date.now()
cy.clock(now)

Right now, with a little luck, our test will pass. It depends on how fast our computer is ๐Ÿ˜„. To fix this issue we need to remember that cy.clock overrides the time in our app, not in our tests (command chain). Thatโ€™s why we need to change Date.now() in our assertion to now value that weโ€™ve created at the beginning of the test.

const now = Date.now()
cy.clock(now)
  .visit("/")
  .get("[data-testid=enter-time]")
  .should("have.text", now.toString())

The test is green - always! - success! But there is one little difference in how our app works now. Before using cy.clock our timer has been running. Right now it stops on 0. Fortunately, it is expected behavior in our test-case scenario. We've set and stopped the time.

In order to move the time with some value, we need to call cy.tick command:

const now = Date.now()
cy.clock(now)
  .visit("/")
  .get("[data-testid=enter-time]")
  .should("have.text", now.toString())
  .get("[data-testid=counter]")
  .should("have.text", "0")
  .tick(1000)
  .get("[data-testid=counter]")
  .should("have.text", "1")

Tada ๐ŸŽ‰! We've just wrote the test checking the enter date and the value of the counter.

What if we would like to set the date only - without stopping the time? ๐Ÿค”

Thatโ€™s a great question. Sometimes we would like to override the Date object only, leaving the rest untouched (setTimeout, etc.). In this case, we need to pass a second argument to the cy.clock - an array of timing functions that we want to override.

cy.clock(Date.UTC(2020, 3, 22), ["Date"])

In this example we set the date/time to 22th April 2020 00:00 UTC (yeap - months in Date starts from 0 that's why April = 3 ๐Ÿ™‚). In the same time we don't override the setTimeout and the rest time functions.


That's all for today. I hope that with this knowledge you can go now and stop the time in your tests ๐Ÿ˜‰

Good luck!