5. BY MICHAEL
FEATHERS
WORKING EFFECTIVELY WITH LEGACY CODE
Similar situation
"Legacy code" == code w/o tests
How to put tests on something w/o tests
But also a little different
Want to convert languages
Subject to the predefined browser API and a particular way of
interacting with it
6. THE SUBMITTED CODE
Added a new if clause to OpalHotReloader#reload() to handle
CSS hot reloading
Implemented in Javascript via Opal x-string.
7. ORIGINAL RELOAD() METHOD
def reload(e)
# original code
reload_request = JSON.parse(`e.data`)
if reload_request[:type] == "ruby"
puts "Reloading ruby #{reload_request[:filename]}"
eval reload_request[:source_code]
if @reload_post_callback
@reload_post_callback.call
else
puts "not reloading code"
end
end
8. ADDED IF CODE TO RELOAD() METHOD
# the new css hot reloading code
if reload_request[:type] == "css"
url = reload_request[:url]
puts "Reloading CSS: #{url}"
# Work outsources Javascript via x-string
%x{
var toAppend = "t_hot_reload=" + (new Date()).getTime();
var links = document.getElementsByTagName("link");
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.rel === "stylesheet" && link.href.indexOf(#{url}) >= 0) {
if (link.href.indexOf("?") === -1) {
link.href += "?" + toAppend;
} else {
if (link.href.indexOf("t_hot_reload") === -1) {
link.href += "&" + toAppend;
} else {
link.href = link.href.replace(/t_hot_reload=d{13}/, toAppend)
}
}
}
}
}
end
end
10. REFACTORING
Extract Method to handle new responsibilty
Extract class to hold that responsibilty (SRP)
Delegate to instance new class
New instance @css_reloader to be created in initialize method
11. REFACTORED RELOAD()
def reload(e)
reload_request = JSON.parse(`e.data`)
if reload_request[:type] == "ruby"
puts "Reloading ruby #{reload_request[:filename]}"
eval reload_request[:source_code]
if @reload_post_callback
@reload_post_callback.call
else
puts "not reloading code"
end
end
if reload_request[:type] == "css"
@css_reloader.reload(reload_request) # extracted method called here
end
end
12. THE NEW CSSRELOADER CLASS
class OpalHotReloader
class CssReloader
def reload(reload_request)
url = reload_request[:url]
%x{
var toAppend = "t_hot_reload=" + (new Date()).getTime();
var links = document.getElementsByTagName("link");
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.rel === "stylesheet" && link.href.indexOf(#{url}) >= 0) {
if (link.href.indexOf("?") === -1) {
link.href += "?" + toAppend;
} else {
if (link.href.indexOf("t_hot_reload") === -1) {
link.href += "&" + toAppend;
} else {
link.href = link.href.replace(/t_hot_reload=d{13}/, toAppend)
}
}
}
}
}
end
end
end
14. WRITING AUTOMATED TESTS
Refactored works, we can now add automated tests
Using - minitest also available for those who don't like rspecopal-rspec
15. A PROBLEM
Technique to update css involves manipulating style links in the global
document object
Could
Create links in the actual DOM of the spec runner (not hard)
But don't like the non transparency of this
Don't like code that calls out directly to global document
Would be nice to be able to inject a test document
16. DEPENDENCY INJECTION
Desired outcome: Inject test doc for test, inject the real document for
application
Don't need standard dependency injection methods (constructor, setter,
interface)
Able to just pass document as a parameter
17. MORE ISSUES FOR DOCUMENT TEST DOUBLE
Limited options for hot reloading of CSS - have to do it a certain way
document interface not under my control - must match it
Stubbing Javascript objects in Opal
Opal/JS interface a problem here - Opal objects different
Opal-rspec has powerful mock/stub capability, but only Opal objects
Need to create own method
18. 2 CONVENIENCE METHODS
Creates javascript objects directly via x-strings
create_link() to create the link DOM object that will get altered to
facillitate the css hot reloading and
fake_links_document() a convenience method which returns both a
test double for global document object, which responds to the
getElementsByTagName('link') call and a test double for the link
itself, that I will inspect to see whether it has been correctly altered.
19. CODE
def create_link( href)
%x|
var ss = document.createElement("link");
ss.type = "text/css";
ss.rel = "stylesheet";
ss.href = #{href};
return ss;
|
end
def fake_links_document(href)
link = create_link(href)
doc = `{ getElementsByTagName: function(name) { links = [ #{link}]; return links;}}`
{ link: link, document: doc}
end
20. ADD DOCUMENT TO RELOAD() SIGNATURE
# change from
def reload(reload_request)
# to
def reload(reload_request, document)
21. CALL WITH THE NEW SIGNATURE
# in OpalHotReloader#reload()
# instead of calling it this way
@css_reloader.reload(reload_request)
# we pass in the real browser document
@css_reloader.reload(reload_request, `document`)
22. MODIFY CSSRELOADER TO TAKE
DOCUMENT
class OpalHotReloader
class CssReloader
def reload(reload_request, document) # pass in the "document"
url = reload_request[:url]
%x{
var toAppend = "t_hot_reload=" + (new Date()).getTime();
// invoke it here
var links = #{document}.getElementsByTagName("link");
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.rel === "stylesheet" && link.href.indexOf(#{url}) >= 0) {
if (link.href.indexOf("?") === -1) {
link.href += "?" + toAppend;
} else {
if (link.href.indexOf("t_hot_reload") === -1) {
link.href += "&" + toAppend;
} else {
link.href = link.href.replace(/t_hot_reload=d{13}/, toAppend)
}
}
}
}
}
end
end
end
24. REQUIRED TEST CASES
A plain stylesheet link where we add the hot reload argument for the first
time.
Updating a link that has already been updated with a hot reload argument.
Appending an additional hot reload argument parameter to a stylesheet
link that already has a parameter.
25. WRITING SPECS FOR THESE CASES
require 'native'
require 'opal_hot_reloader'
require 'opal_hot_reloader/css_reloader'
describe OpalHotReloader::CssReloader do
def create_link( href)
%x|
var ss = document.createElement("link");
ss.type = "text/css";
ss.rel = "stylesheet";
ss.href = #{href};
return ss;
|
end
def fake_links_document(href)
link = create_link(href)
doc = `{ getElementsByTagName: function(name) { links = [ #{link}]; return links;}}`
{ link: link, document: doc}
end
context 'Rack::Sass::Plugin' do
it 'should add t_hot_reload to a css path' do
css_path = 'stylesheets/base.css'
doubles = fake_links_document(css_path)
link = Native(doubles[:link])
expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
subject.reload({ url: css_path}, doubles[:document])
expect(link[:href]).to match /#{Regexp.escape(css_path)}?t_hot_reload=d+/
end
26. PAGE 2
it 'should update t_hot_reload argument if there is one already' do
css_path = 'stylesheets/base.css?t_hot_reload=1111111111111'
doubles = fake_links_document(css_path)
link = Native(doubles[:link])
expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
subject.reload({ url: css_path}, doubles[:document])
expect(link[:href]).to match /#{Regexp.escape('stylesheets/base.css?t_hot_reload=')
}(d)+/
expect($1).to_not eq '1111111111111'
end
it 'should append t_hot_reload if there are existing arguments' do
css_path = 'stylesheets/base.css?some-arg=1'
doubles = fake_links_document(css_path)
link = Native(doubles[:link])
expect(link[:href]).to match /#{Regexp.escape(css_path)}$/
subject.reload({ url: css_path}, doubles[:document])
expect(link[:href]).to match /#{Regexp.escape(css_path)}&t_hot_reload=(d)+/
end
end
end
27. SPECS PASS - SAFE TO REFACTOR
Added automated test coverage for the 3 cases
Now safe to rewrite the reload() method in Ruby/Opal
Spec provide safety net to prove we don't break the desired functionality
A trick - have reload() delegate to reload_ruby() reload_js() to
have both code side by side - handy for development and debugging
28. THE TRICK
require 'native'
class OpalHotReloader
class CssReloader
def reload(reload_request, document)
# currently using the Ruby version
reload_ruby(reload_request, document)
# reload_js(reload_request, document)
end
def reload_ruby(reload_request, document)
url = reload_request[:url]
puts "Reloading CSS: #{url}"
to_append = "t_hot_reload=#{Time.now.to_i}"
links = Native(`document.getElementsByTagName("link")`)
(0..links.length-1).each { |i|
link = links[i]
if link.rel == 'stylesheet' && link.href.index(url)
if link.href !~ /?/
link.href += "?#{to_append}"
else
if link.href !~ /t_hot_reload/
link.href += "&#{to_append}"
else
link.href = link.href.sub(/t_hot_reload=d{13}/, to_append)
end
end
end
}
end
29. PAGE 2
def reload_js(reload_request, document)
url = reload_request[:url]
%x{
var toAppend = "t_hot_reload=" + (new Date()).getTime();
var links = #{document}.getElementsByTagName("link");
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.rel === "stylesheet" && link.href.indexOf(#{url}) >= 0) {
if (link.href.indexOf("?") === -1) {
link.href += "?" + toAppend;
} else {
if (link.href.indexOf("t_hot_reload") === -1) {
link.href += "&" + toAppend;
} else {
link.href = link.href.replace(/t_hot_reload=d{13}/, toAppend)
}
}
}
}
}
end
end
end
31. OPAL ONLY CSSRELOADER
require 'native'
class OpalHotReloader
class CssReloader
def reload(reload_request, document)
url = reload_request[:url]
puts "Reloading CSS: #{url}"
to_append = "t_hot_reload=#{Time.now.to_i}"
links = Native(`document.getElementsByTagName("link")`)
(0..links.length-1).each { |i|
link = links[i]
if link.rel == 'stylesheet' && link.href.index(url)
if link.href !~ /?/
link.href += "?#{to_append}"
else
if link.href !~ /t_hot_reload/
link.href += "&#{to_append}"
else
link.href = link.href.sub(/t_hot_reload=d{13}/, to_append)
end
end
end
}
end
end
end
32. ONWARD
Now have specs
Converted code to Ruby
Much easier to implment hot reloading of Rails CSS/SASS
Already did it, it was easy, used TDD for that
33. LINKS TO BLOG POST
Blogger - code syntax highlighted
Medium - prettier but no syntax highlighting
http://funkworks.blogspot.com/2016/06/wewljcio-working-effectively-
with.html
https://medium.com/@fkchang2000/wewljcio-working-effectively-with-
legacy-javascript-code-in-opal-4fd624693de4