Today a game has appeared that shows how tricky JavaScript ==
operator can be. You can check out the game yourself at https://slikts.github.io/js-equality-game/. You have to fill the field by clicking every cell where the row and column labels are equal when using ==
operator. For example true == 1
in JavaScript, so you should click that cell. Whenever you click on a cell, the symmetric cell is also checked, because true == 1
implies 1 == true
(at least JavaScript got this part right).
I got to admit, JavaScript equality as Minesweeper has been my jok for a long time, for example in this "Functional JavaScript" workshop from 2016 I start with these two slides
I don't even think that equality is that bad - the inequality <
operator table looks scarier!
Anyway, how do we solve the js-equality
game? We have to click a lot of cells, set the flags, then click on "Show Results" and hope we got all the true statements selected. Doing this by hand does seem tiresome. If only we could automate this ...
Enter Cypress - the end-to-end testing tool I am working on with bunch of awesome people. Let's use Cypress to drive the game. I will use Cypress because it is super powerful whenever something needs to be tested in a browser, and also because we can see it in action. Just npm i -D cypress
and we are ready to rumble.
You can find the complete project in https://github.com/bahmutov/js-equality-game repo.
1 | git clone [email protected]:bahmutov/js-equality-game.git |
Once we open Cypress for the first time using npx cypress open
, it scaffolds the folder cypress
. We can start a test file cypress/integration/spec.js
and the first thing we should do - is just visit the game's page.
1 | /// <reference types="cypress" /> |
Open Cypress with npx cypress open
... and see a cross-origin request error.
No biggie, this is just a font request to fonts.gstatic.com
, we can solve this error by disabling chromeWebSecurity
setting in cypress.json
1 | { |
Great, no more errors. Let us start coding. First we need to grab values to compare. We can grab these values from the header cells of the table. Just use cy.get('thead th')
and iterate over the elements, pushing values into a list.
1 | /// <reference types="cypress" /> |
Here is where this solution falls short: one of the labels has an empty object {}
which evaluates to undefined
in the expression eval('{}')
, hmm. You can see the evaluated values in the DevTools console; all values are correct except for this one.
We could "fix" this using an if / else
condition, but I left this shortcoming in. This will actually test how the game handles missed values, we expect NOT to get 100% right.
Whenever Cypress grabs DOM elements, it saves DOM snapshots with real elements. Thus we can click on any command in the Cypress command log and see real elements printed in the DevTools console. Hover or click on them and the browser highlights the actual element in the application.
Great, we got (almost) all values to compare, now let's actually play. We need to iterate over all cells in the game grid, and for each compute row label value == column label value
. If the expression is true, we should click that cell. Here is how I wrote it:
1 | // get values |
The command cy.get('tbody td')
returns all cells in the table, and we can iterate over them using .each
command. From the index k
that goes from 0 to 440 we can get the row and column coordinates of the cell, and grab the values to compare
1 | const row = Math.floor(k / values.length) |
If value x
is equal to value y
after casting we should click on that cell. Since we get a real element, we can just wrap it with Cypress and call click
.
1 | console.log(x, '==', y, '?', x == y) |
Great, but here is where we should be cautious and remember that our test runner has NO idea what the web application is doing under the hood. If we click cells really really quickly without checking that the app has registered clicks, we can get a flaky test, where some clicks are not registered. So every time we click, we should make sure the app has processed our action. The simplest observable UI change on each click is the "flag" counter.
1 | let clicks = 0 |
Cypress clicks on every cell with x == y
passing, and then waits long enough for the flag counter to change. All Cypress actions are enqueued automatically, thus cy.contains(...)
waits until cy.wrap(...).click()
finishes successfully.
Now it is time to finish and reveal our score.
1 | .then(() => { |
We get the right answer! When we click on cy.contains('Show Results').click()
command in the reporter on the left, it shows that there are "before" and "after" DOM snapshots. Cypress notices when a command modifies the DOM and stores two snapshots in these cases, allowing the user to inspect how the command changes the user interface.
It is a good time to note that Cypress includes full video recording by default, so I can show the script in action using npx cypress run
.
1 | $ npx cypress run |
Here is the video file - it was a quick game 😄