Skip to main content

Jun 1, 2020 - 6 minute read - JavaScript Test Automation Evil Tester

Automate Game Play with JavaScript

TLDR; Study examples of automating a game in browser using JavaScript from the dev tools console, and an example of automating outside the browser using a standalone execution tool.

Tic Tac Toe

I noticed that Angie Jones had performed a live coding session against an online Tic Tac Toe game.

Initially I skipped through the video to see:

  • What app Angie had chosen to automate PlayTicTacToe.org
  • and To What Level - automate to randomly select cells to play

I didn’t watch the video first, because I wanted to create my own solution, and then watch the video to compare.

Comparative Solutions

Comparative solutions are very useful.

  • see different languages in use
  • see different coding styles
  • see different thought processes

This is something that can be hard to find online. But is one of the major benefits of pairing in teams, because we learn from the different approaches around us.

I created a solution, in JavaScript, in the browser.

In this post I will explain what I did, rather than compare my approach to Angie’s.

I’ll try and do a comparison in a different post. But I think it is worth pointing out that both solutions are “Automating” rather than “Test Automation”. But both, in their current form, can be used to support testing in different ways. And both offer a basis for refactoring into something which would be counted as Test Automation.

The main difference between “Automating” and “Test Automation” is that “Test Automation” mostly requires assertions, and has a focus on robust asserting on conditions during execution. Whereas “Automating” has a broader remit.

Video

The video below shows the development of a small bot to play the game from within the console.

Watch on YouTube

Steps

The steps to creating the bot were:

  • recon, learn the app
  • de-risk, snippets to implement main functional interaction
  • mvp bot, create a functional, minimum value bot (restart game automatically)
  • finish bot, add the snippets to finish the bot

Code

If you want to jump ahead and try the bot then the code is below, and published as a gist:

Recon - Implementation

The basic recon is to use the dev tools to investigate the game.

And by using the DOM view I can see that all the information I need to play the game is in the DOM itself.

  • all cells have a class ‘square’
  • finished game has an overlay with class ‘restart’
  • cells with an X in them contain a div with class ‘X’
  • cells with an O in them contain a div with class ‘O’

I can create CSS selectors to identify all of these

  • a Cell div.square
  • an empty Cell div.square > div:not(.x):not(.o)
  • Restart overlay div.restart

De-risk

I need to identify quickly if I can actually automate the game.

So the main things are:

  • can I click to restart the game?
  • can I click to insert an X in a cell?

Restart the Game

Restarting the game is pretty simple:

document.querySelector("div.restart").click();

I can issue a click event to the restart div.

But I also need to check if it is displayed. And I can use the display style value to do that:

document.querySelector("div.restart").style.display

Which is:

  • block when displayed and game is finished
  • none when the game is being played

Identifying Empty Cells

I want to be able to click on empty cells.

I can find the empty cells using a bit of CSS locator magic involving the ‘not’ pseudo-class.

div.square > div:not(.x):not(.o)

This returns all the child divs of square divs which do not have a class X and do not have a class O.

Click in a Cell

I tried to .click on a cell but that did not add a value.

Investigating the events on the DOM showed that I needed to trigger a mousedown event.

I can trigger a mouse down event on a cell:

var cell = document.querySelectorAll("div.square")[0];
cell.dispatchEvent(new Event('mousedown'));

But I really need to do this from the child cell which has the X, or O, because I only want to dlick on empty cells.

I could either do this from the ‘child div’ by sending the event to the parent.

var cell = document.querySelectorAll("div.square")[0];
var child = cell.getElementsByTagName("div")[0];
child.parentElement.dispatchEvent(new Event('mousedown'));

Or I could click on the child div, and have the events ‘bubble up’ to the parent.

var cell = document.querySelectorAll("div.square")[0];
var child = cell.getElementsByTagName("div")[0];
child.dispatchEvent(new Event('mousedown', { 'bubbles': true }));

MVP

To build the bot, I like to start simple and expand.

I can create a simple bot that adds value by having it reset the game for me automatically so I don’t have to click to start a new game.

function resetGame(){

    var restart = document.querySelector("div.restart");

    if(restart.style.display=="block"){
        restart.click();
    }
}

var playbot = window.setInterval(resetGame,500);

// window.clearInterval(playbot)

setInterval creates a ‘thread’ which executes the resetGame function every 500 milliseconds.

And this resetGame checks if the restart div is shown, and if it is, then clicks on it to restart the game.

I capture the ‘thread’ in a variable so that I can stop it with a clearInterval command if I want to. Otherwise it will keep running until the page is refreshed.

Bot

Next I write the bot.

The bot is a simple state machine that says:

  • if we are in ‘finished’ state then click restart
  • if we are in ‘playing’ state then find all empty squares, randomly click on one

The ‘find all empty squares, randomly click on one’ code is:

var squares = document.querySelectorAll(
                "div.square > div:not(.x):not(.o)");
var clickme = Math.floor(
                Math.random() * squares.length);
squares[clickme].parentElement.
          dispatchEvent(new Event('mousedown'));
  • Math.random() returns a number from 0 to 1
  • Math.random() * squares.length makes it a floating point number somewhere between 0 and the number of empty squares
  • Math.floor turns it into an integer
  • so we are generating an integer between 0 and the number of squares

then we use that random number as an index into the array to choose one of the empty squares and issue a mousedown event

Then I add that as the second state in the bot:

function playGame(){
    var restart = document.querySelector("div.restart");
    if(restart.style.display=="block"){
        restart.click()
    }else{
        var squares = document.querySelectorAll(
               "div.square > div:not(.x):not(.o)");
        var item=Math.floor(Math.random()*squares.length);
        squares[item].parentElement.
            dispatchEvent(new Event('mousedown'));
    }
}

var playBot = window.setInterval(playGame,500);

//window.clearInterval(playBot);

Effectiveness

This automates the game well.

This plays the game poorly since there is no strategy behind selecting cells.

It would take a little work to add a strategy in here to select the ‘best’ next square, rather than a ‘random’ square.

But this might help flush out any memory errors, or if there are any changes in rules for later levels.

Because it is ‘automating’ rather than ‘playing’ we would need a person to observe and interrogate it. So this would be an augmentation to a test approach, rather than implementing the assertion of specific conditions.

Compare

Now that you’ve seen my implementation, compare it with Angie’s so that you can understand the different design decisions we each made and see the implementation in different languages and tooling.

If you don’t know programming well then:

  • see how similar the languages are
  • see how readable the languages are
  • see how little code is required to make something happen
  • copy the code for each and try it out

You will need a Github account to comment. Or you can contact me with your comment.

I reserve the right to delete spam comments e.g. if your comment adds no value and its purpose is simply to create a backlink to another site offering training, or courses, etc.