Testing User input: Zombie.js vs JSDOM?

Tell us what’s happening:
I am trying to work through testing my sudoku solver project. But, I am having trouble with the headless browser, JSDOM suggested in the boilerplate.

How do I simulate a user input with JSDOM, for the unit tests? I did the first two suites by adding check functions, but need to interact with the virtual DOM for the rest.

Should I use Zombie.js instead? Google search makes me think using its fill method might be better suited.
And also the challenges taught us a bit about Zombie, but nothing about JSDOM.

TL;DR - How can I simulate user input for testing? Zombie.js vs JSDOM, I know the prior is based on JSDOM but what is the practical difference related to testing?

Your code so far

sudoku-solver.js:

const textArea = document.getElementById('text-input');
const errorArea = document.getElementById('error-msg');
const solve = document.getElementById('solve-button');
const clear = document.getElementById('clear-button');
const cells = document.querySelectorAll('.sudoku-input');

import { puzzlesAndSolutions } from './puzzle-strings.js';

document.addEventListener('DOMContentLoaded', () => {
  // Load a simple puzzle into the text area
  textArea.value = '..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';

  textChange();
});

let checkGridInput = (input) => {
  var numberRE = /^[1-9]$|^[\0]$/;

  return numberRE.test(input.toString());
}

let checkTextInput = (input) => {
  var numberRE = /^[1-9.]*$/;

  return numberRE.test(input.toString());
}

let textChange = () => {
  errorArea.innerText = '';
  var numberRE = /^[1-9.]*$/;

  if(numberRE.test(textArea.value) === false) {
    errorArea.innerText = "Error: Invalid character.";
    return;
  }

  let text = textArea.value.split('');
  if(text.length !== 81) {
    errorArea.innerText = "Error: Expected puzzle to be 81 characters long.";
    return;
  }

  text.forEach((ele, index) => {
    if(ele === '.') {
      ele = '';
    }
    cells[index].value = ele;
  });
};

let gridChange = () => {
  errorArea.innerText = '';
  var numberRE = /^[1-9]$|^[\0]$/;
  
  let textString = '';
  cells.forEach((cell, index) => {
    let cellValue = cell.value ? cell.value.toString() : '\0';
    if(numberRE.test(cellValue)) {
      if(cellValue == '\0') {
        cellValue = '.';
      } 
      textString += cellValue;
    }

    else {
      errorArea.innerText = "Error: Invalid Grid character";
      return;
    }
  });
  //console.log(textString.length);

  textArea.value = textString;
}

let solveSudoku = () => {
  errorArea.innerText = '';

  puzzlesAndSolutions.forEach((element, index) => {
    if(textArea.value === element[0]) {
      console.log("We have a match at: ", index);
      textArea.value = element[1];
      textChange();
      return;
    }
  })
}

let clearAll = () => {
  textArea.value = '';
  cells.forEach((cell) => {
    cell.value = '';
  })
}

textArea.oninput = textChange;

cells.forEach((cell) => {
  cell.oninput = gridChange;
})

solve.onclick = solveSudoku;
clear.onclick = clearAll;

/* 
  Export your functions for testing in Node.
  Note: The `try` block is to prevent errors on
  the client side
*/
try {
  module.exports = {
    checkGridInput: checkGridInput,
    checkTextInput: checkTextInput,
    textChange: textChange,
    gridChange: gridChange,
    solveSudoku: solveSudoku,
    clear: clearAll
  }
} catch (e) {}

tests.js:

const chai = require('chai');
const chaiHttp = require('chai-http');
const assert = chai.assert;
const server = require('../server');

chai.use(chaiHttp)

const jsdom = require('jsdom');
const { JSDOM } = jsdom;
let Solver;

suite('UnitTests', () => {
  suiteSetup(() => {
    // Mock the DOM for testing and load Solver
    return JSDOM.fromFile('./views/index.html')
      .then((dom) => {
        global.window = dom.window;
        global.document = dom.window.document;

        Solver = require('../public/sudoku-solver.js');
      });
  });
  
  // Only the digits 1-9 are accepted
  // as valid input for the puzzle grid
  suite('Function checkGridInput()', () => {
    test('Valid "1-9" characters', (done) => {
      const input = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];

      input.forEach((ele) => {
        assert.equal(Solver.checkGridInput(ele), true);
      });
      
      done();
    });

    // Invalid characters or numbers are not accepted 
    // as valid input for the puzzle grid
    test('Invalid characters (anything other than "1-9") are not accepted', (done) => {
      const input = ['!', 'a', '/', '+', '-', '0', '10', 0, '.'];

      input.forEach((ele) => {
        if(ele === '10') {
          assert.equal(Solver.checkGridInput('1'), true);
        }
        else {
          assert.equal(Solver.checkGridInput(ele), false);
        }
      });

      done();
    });
  });
  
  suite('Function checkTextInput()', () => {
    test('Parses a valid puzzle string into an object', done => {
      const input = '..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
      
      assert.equal(Solver.checkTextInput(input), true);
      done();
    });
    
    // Puzzles that are not 81 numbers/periods long show the message 
    // "Error: Expected puzzle to be 81 characters long." in the
    // `div` with the id "error-msg"
    test('Shows an error for puzzles that are not 81 numbers long', done => {
      const shortStr = '83.9.....6.62.71...9......1945....4.37.4.3..6..';
      const longStr = '..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6...';
      const errorMsg = 'Error: Expected puzzle to be 81 characters long.';
      const errorDiv = document.getElementById('error-msg');
      
      done();
    });
  });

  suite('Function ____()', () => {
    // Valid complete puzzles pass
    test('Valid puzzles pass', done => {
      const input = '769235418851496372432178956174569283395842761628713549283657194516924837947381625';

      // done();
    });

    // Invalid complete puzzles fail
    test('Invalid puzzles fail', done => {
      const input = '779235418851496372432178956174569283395842761628713549283657194516924837947381625';

      // done();
    });
  });
  
  
  suite('Function ____()', () => {
    // Returns the expected solution for a valid, incomplete puzzle
    test('Returns the expected solution for an incomplete puzzle', done => {
      const input = '..9..5.1.85.4....2432......1...69.83.9.....6.62.71...9......1945....4.37.4.3..6..';
      
      // done();
    });
  });
});

Your browser information:

User Agent is: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36.

Challenge: Run Functional Tests using a Headless Browser

Link to the challenge:

Hello!

It may be a little long/verbose, but since JSDOM builds a window object, you can trigger the clicks/key[up|down]/etc. like this:

dom.document.querySelector('button').click(); // This triggers a click
dom.document.querySelector('input[type=text]').dispatchEvent(new KeyboardEvent('keyup', {
	key: 'a',
  keyCode: 65
}))

See/play here: https://jsfiddle.net/skaparate/swg9mtx8/

Looked further into dispatchEvent method and it solves the problem. Thank you!

For fellow wanderers,
Some of its settings seem arcane, like bubbles etc. But, found a way to adapt it by creating a new Event instance and passing ‘input’ as the type, gotta play around with its settings to fully understand them though. Does the trick anyhow.

1 Like

Awesome!

Thanks for the heads up :slight_smile:!