This is Part 3 of a series of posts about refactoring Nitrolinks out of Pondo. Last time we’ve successfully copied the Nitrolinks tests from Pondo but left the original tests on Pondo. This time we’ll move them out of Pondo for good.

The Plan

Just to recap, here is the plan:

  1. Move Nitrolinks out into its own gem ✔
  2. Create tests within Nitrolinks that mirror tests in Pondo ✔
  3. Remove Nitrolinks-specific tests out of Pondo
  4. Rewrite Nitrolinks in ES6

We are going to do Step 3.

Since last time we bumped nitrolinks-rails gem’s version number. Let’s use that in pondo.

# Gemfile
gem "nitrolinks-rails", ">= 0.2.0"

Let’s do a test run to make sure everything is green.

$ cucumber

# Bunch of spec output

29 scenarios (29 passed)
148 steps (148 passed)
0m27.379s

Great! We’re in a good place now.

Cleanup

Next we’ll remove nitrolinks feature specs.

$ tree -l features
features
├── adding_income.feature
├── deducting_expense.feature
├── first_time.feature
├── inviting_subscribers.feature
├── login.feature
├── nitrolinks # DELETED
│   ├── errors.feature # DELETED
│   ├── get.feature # DELETED
│   ├── hash.feature # DELETED
│   └── loading.feature # DELETED
├── step_definitions
│   ├── authentication_steps.rb
│   ├── common_steps.rb
│   ├── first_time_steps.rb
│   ├── money_steps.rb
│   ├── nitrolinks_steps.rb
│   ├── pondo_specs
│   │   └── pages.rb
│   └── subscription_steps.rb
├── subscribing.feature
└── support
    ├── env.rb
    ├── pondo_testing_utils.rb
    └── wait_for_things.rb

How are we on the specs?

$ cucumber

# Bunch of spec output

15 scenarios (15 passed)
93 steps (93 passed)
0m21.142s

That reduced our scenario count from 29 to 15 and our steps from 148 to 93. Next let’s remove the nitrolinks step definitions and check our specs again.

tree -l features
features
├── adding_income.feature
├── deducting_expense.feature
├── first_time.feature
├── inviting_subscribers.feature
├── login.feature
├── step_definitions
│   ├── authentication_steps.rb
│   ├── common_steps.rb
│   ├── first_time_steps.rb
│   ├── money_steps.rb
│   ├── nitrolinks_steps.rb # DELETED
│   ├── pondo_specs
│   │   └── pages.rb
│   └── subscription_steps.rb
├── subscribing.feature
└── support
    ├── env.rb
    ├── pondo_testing_utils.rb
    └── wait_for_things.rb
$ cucumber

# Bunch of spec output

15 scenarios (15 passed)
93 steps (93 passed)
0m26.099s

Good we’re still green. Next we’ll edit our pages helper and step definitions to remove any trace of nitrolinks.

# features/step_definitions/pondo_specs/pages.rb
module PondoSpecs
  module Pages
    module_function

    {
      home: {
        path: '/'
      },

      welcome: {
        path: '/welcome'
      },

      # NITROLINKS WAS HERE

    }.each do |page_name, data|
      define_method :"#{page_name}_page" do
        data[:path]
      end

      define_method :"#{page_name}_page_content" do
        data[:content]
      end
    end

  end
end

# features/support/pondo_testing_utils.rb
module PondoTestingUtils
  def jscript(code)
    page.evaluate_script(code)
  end

  # NITROLINKS WAS HERE

  def pondo_page(name)
    PondoSpecs::Pages.send("#{ name }_page")
  end

  def pondo_page_content(name)
    PondoSpecs::Pages.send("#{ name }_page_content")
  end

  # ...AND HERE

  def safe_date_fill_in(finder, date)
    field = find_field(finder)
    date_formatted = date.strftime("%Y-%m-%d")
    jscript("document.evaluate('#{field.path}', document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.value='#{date_formatted}'")
  end

  # ...AND HERE
end

World(PondoTestingUtils)
World(ActiveJob::TestHelper)
$ cucumber

# Bunch of spec output

15 scenarios (15 passed)
93 steps (93 passed)
0m21.421s

We’re still green! This is getting a little repetetive so we’ll finish up with some more code cleanups by removing any unnecessary references to nitrolinks. That includes routes, controllers, and views related to just nitrolinks testing.

After all that, you know what’s coming…

$ cucumber

# Bunch of spec output

15 scenarios (15 passed)
93 steps (93 passed)
0m26.659s

We’re in a good place right now so let’s commit our code first. Here’s everything we’ve done so far.

Fearless Refactoring

You’ll notice that for every significant change we do, we always run the specs to ensure we haven’t introduced regressions. This is what great test coverage enables. We can remove code and refactor without fear. If we make a mistake, we get feedback on where it happened and what went wrong.

Introducing tests adds a lot of upfront cost to a project but it is all worth it. That upfront cost will payoff sooner than you think.

It’s important to note that great test coverage alone is not enough. We also need to make sure that we are testing correctly and that our tests are fast enough to encourage us to run them frequently.

Constraints in Testing Nitro Apps

There is still some nitro-specific thing we’re doing in Pondo and it starts here:

# app/assets/javascripts/testing.coffee
class PondoTesting

  # Bunch of code...

  markAsLoading: (from)->
    @active = true
    @body.addClass('testing-visiting')

  markAsDoneLoading: ->
    @active = false
    @body.removeClass('testing-visiting')

  listen: ->
    @document.on 'nitrolinks:visit', =>
      @markAsLoading('nitrolinks:visit')

    @document.on 'nitrolinks:load nitrolinks:load-blank', (e) =>
      @markAsDoneLoading()

  # etc....

…and used here:

# features/support/wait_for_things.rb
module WaitForThings
  def wait_for_page_load
    Timeout.timeout(Capybara.default_max_wait_time) do
      loop until finished_loading?
    end
  end

  def finished_loading?
    jscript('document.getElementsByClassName("testing-visiting").length').zero?
  end

  def wait_for_remote_request
    wait_for_page_load
  end

  # ...
end

Because nitrolinks overrides the navigation behavior by fetching the page through xhr, there’s no way for Capybara to know to wait for a new page load. This code allows us to tell Capybara to wait until the page has been loaded. It’s a clunky hack but it serves us for now.

The problem is that this really ought to be in Nitrolinks. We have to let nitrolinks handle this and provide a way for clients of the gem to use this feature in their tests.

We’ve duplicated this code on Nitrolinks. Let’s move it to the lib so we can requrie it from Pondo.

# lib/nitrolinks/capybara/wait_for_things.rb
module Nitrolinks
  module Capybara
    module WaitForThings
      def wait_for_page_load
        Timeout.timeout(::Capybara.default_max_wait_time) do
          loop until finished_loading?
        end
      end

      def finished_loading?
        jscript('document.getElementsByClassName("testing-visiting").length').zero?
      end

      def finished_all_ajax_requests?
        page.evaluate_script('jQuery.active').zero?
      end

      def pause_pls
        $stderr.write 'Press enter to continue'
        $stdin.gets
      end
    end
  end
end

Let’s also move that jscript method into another module:

# lib/nitrolinks/capybara/jscript.rb
module Nitrolinks
  module Capybara
    module Jscript
      def jscript(code)
        page.evaluate_script(code)
      end

      def expect_script(code, filter = nil)
        result = jscript(code)
        if filter
          result = result.send(filter)
        end
        expect(result)
      end
    end
  end
end

To use, we’ll only need to add this on pondo:

# features/support/wait_for_things.rb
require 'nitrolinks/capybara/jscript'
require 'nitrolinks/capybara/wait_for_things'

World(Nitrolinks::Capybara::Jscript)
World(Nitrolinks::Capybara::WaitForThings)

We’ll also refactor out the loading-related code into a separate script:

# app/assets/javascripts/nitrolinks/load-helper.coffee
whenReady = (fn) ->
  if (if document.attachEvent then document.readyState == 'complete' else document.readyState != 'loading')
    fn()
  else
    document.addEventListener "DOMContentLoaded", ->
      fn()

eventListen = (event, handler) ->
  document.addEventListener event, (e) ->
    handler.call document, e

class NitrolinksLoadHelper
  constructor: (@window, @document) ->
    @active = false

  body: ->
    document.querySelector('body')

  markAsLoading: (from)->
    @active = true
    @body().classList.add('testing-visiting')

  markAsDoneLoading: ->
    @active = false
    @body().classList.remove('testing-visiting')

  listen: ->
    eventListen 'nitrolinks:visit', =>
      @markAsLoading('nitrolinks:visit')

    loads = (e) =>
      @markAsDoneLoading()

    eventListen 'nitrolinks:load', loads
    eventListen 'nitrolinks:load-blank', loads

whenReady =>
  @nitroLoadHelper = new NitrolinksLoadHelper(window, document)
  @nitroLoadHelper.listen()
  @nitroLoadHelper.markAsDoneLoading()

On the pondo side, we’ll just require it on our testing javascript:

# app/assets/javascripts/nitrolinks/load-helper.coffee
#= require nitrolinks/load-helper

class PondoTesting
  constructor: (@window) ->
    @errors = []

  listen: ->
    @window.addEventListener 'error', (e) =>
      @addToErrors(e)

  addToErrors: (e) ->
    if e.error
      @errors.push e.error.message
    else
      @errors.push e
    console.log e

  hasJavascriptErrors: ->
    @error.length > 0

$ =>
  @pondoTesting = new PondoTesting(window)
  @pondoTesting.listen()

Bumping our nitrolinks-rails gem version and including it in Pondo, everything works as expected. Here are links to commits in Nitrolinks and Pondo 2.

$ cucumber

# Bunch of spec output

15 scenarios (15 passed)
93 steps (93 passed)
0m20.662s