Eric Radman : a Journal

Web UI Validation

For many years myself and others have thought about the tools and techniques used to test server-side code. I consider this a solved problem, as long as tests are run using the real database a test harness can provide a responsive feedback loop that provides a high level of confidence in each change.

Validating the HTML, CSS and JavaScript delivered over HTTP an altogether different challenge because the runtime that interprets these resources is a web browser. Fragments of the client response can be tested in isolation using a mock DOM, but realistically automating the web browser is the only meaningful runtime to evaluate the results.

WebDriver

Selenium is well established, and has the feature of cooperating with multiple web browsers. This exemples requires the geckodriver package

require 'selenium-webdriver'

Selenium::WebDriver.logger.level = :debug
Selenium::WebDriver.logger.output = '/tmp/selenium.log'

# listens on port 444
opts = Selenium::WebDriver::Firefox::Options.new(log_level: :trace)
driver = Selenium::WebDriver.for :firefox, options: opts

url = "http://eradman.com"

driver.navigate.to url
element = driver.find_element(tag_name: 'h1')

There is a specification relaying errors to the web driver, but in my testing JavaScript exceptions never make it.

CDP

Chrome DevTools Protocol is a more versitile concept because it's not merely an automation toolkit, it an event bus that provides access to very detailed event logs.

Here we will set up a browser instance that captures any JavaScript exceptions

require 'ferrum'

class FerrumLogger
  attr_reader :exceptions

  def truncate
    @exceptions = []
  end

  def puts(log_line)
    _log_symbol, _log_time, log_body = log_line.strip.split(' ', 3)
    log_detail = JSON.parse(log_body)
    return unless log_detail['method'] == 'Runtime.exceptionThrown'

    params = log_detail['params']
    @exceptions << params['exceptionDetails']['exception']['description']
  end
end

options = {
  headless: true,
  logger: FerrumLogger.new
}
browser = Ferrum::Browser.new options
browser.playback_rate = 10
browser.options.logger.truncate

Unit Tests

Targeted UI tests can now be composed of a set of actions and finally an assertion that no exception occurred.

require 'minitest/autorun'

class ClientTest < Minitest::Test
  def test_start_comment
    url = "file://./page1.html"
    @tab.go_to(url)

    heading = @tab.css('h2')[0]
    x, y = heading.find_position
    @tab.mouse.click x: x, y: y, count: 3

    # assertions
    refute @tab.options.logger.exceptions.pop
  end
end

For the purpose of this sort of test I highly recommend using static files are helpful so that we don't try to turn the crank on the entire stack.