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:
- Move Nitrolinks out into its own gem ✔
- Create tests within Nitrolinks that mirror tests in Pondo ✔
- Remove Nitrolinks-specific tests out of Pondo
- Rewrite Nitrolinks in ES6
We are going to do Step 3.
Nitrolinks Bump
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