Automating barcode scanner tests with Capybara

Nick Charlton and Silumesii Maboshe

A lot of barcode scanners are basic human interface devices — essentially behaving like a keyboard and not requiring any special drivers. Essentially, they just type on a keyboard very quickly. They’ll support lots of different styles of codes — from the 1D barcodes you see on a lot of products in shops to 2D barcodes (more commonly known as QR codes) which are able to hold much more data and that you might have found yourself scanning a lot more of in the past few years.

We were building a system that creates receipts with QR codes on them. The QR code contains a URL. We want to scan those receipts as part of our application. Thus the need for the test.

When we’re writing system tests with Capybara, we’re trying our best to replicate the real-world as much as we can. This is a big problem when it comes to hardware; we can’t ship a person and a barcode scanner to every container running our tests!

So what can we do?

We approached it by making use of [send_keys] in Capybara and Stimulus.

We defined a helper method scan_qr_code:

def scan_qr_code(url)
    page.send_keys(url, :enter)
end

Most barcode scanners will require the ENTER key to be sent manually. Some may need to be configured to send an ENTER as the end of input. We learned this the hard way. The barcode scanner’s manual will guide you or you can try it and see if fails.

We have a Rails form which includes hidden elements. We also have a manual backup for when a scan fails (a PIN is used).

<div class="settings-form" data-controller="scanner">
  <%= simple_form_for(
        [scan],
        url: scans_path
      ) do |form| %>
    <%= form.error :base, class: "form-error" %>
    <%= form.input :url, label: false, wrapper_html: {class: "form-hidden"} %>
    <%= form.button :submit, class: "form-hidden" %>
  <% end %>
</div>

We have a Stimulus Controller called scanner_controller.js defined as follows:

import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
  connect() {
    let buffer = [];

    document.addEventListener('keydown', function (event) {
      const charList =
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLPMNOQRSTUVWXYZ0123456789:/?=-_.~';
      const key = event.key;

      if (key === 'Enter') {
        let form = document.getElementById(
          'scan'
        );
        let urlInput = document.getElementById(
          'scan_url'
        );

        if (form && urlInput) {
          urlInput.value = buffer.join('');
          form.requestSubmit();
        }

        buffer = [];
      }

      if (charList.indexOf(key) === -1) {
        return;
      }

      buffer.push(key);
    });
  }
}

Finally, our actual QR code scanner test looked something like:

require "rails_helper"

RSpec.describe "Creating a scan" do
  scenario "from the scan page", js: true do
    url = "http://example.org/qr-code"

    visit new_scan_path

    page.send_keys(url, :enter)

    expect(page).to have_content("something")
  end
end

We are listening for the keydown event and filter out characters that are base64 URL safe values. The ENTER key triggers the end of our “barcode scan”. We replicate the barcode scanning by using send_keys.

For different keyboard layouts there might be an issue. Currently we are debugging an issue with Colemak and Dvorak not working as expected. We had a fun set of test failures that were difficult to debug. It turns out that send_keys might behave different depending on your keyboard layout. If you happen to have a solution for this, please reach out.