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.