Add reveal()
to that test case,
run the test batch, wait for a web browser to pop up, and click on a test
button. The target IFRAME
should
display, with a familiar "missing web page" error. The IFRAME
can't navigate to a bogus src
URI.
Until now, our YAML has been fictitious. We need to create a new Wiki page, to test our own Wiki.
def set_wiki_test_up source = "--- !omap - setup: - !omap - script: "" - test_WikiTestPage: - !omap - color: green - page: /wiki/WikiTestPage - script: something tests our Wiki - teardown: - !omap - script: "" " @yar = YarWiki.new('TestWiki') @yar.save_page(source) end def self_test set_wiki_test_up get :index, :id => @yar.page_name assert_select 'td#wiki_panel' do |wiki_panel| return wiki_panel end end def test_surf_self wiki_panel = self_test reveal() end
The new page, TestWiki, will soon have commands that test our sample page, WikiTestPage. Then we will use those commands to test-first features into our Wiki.
But for now, all we can do is reveal()
this new page, click the test
button, and drop our jaws:
The inner Wiki is fully functional, so if it had any more features, we could click on them, and manually test them.
Now gaze at that script node. All it says is "something tests our Wiki". We need to do better than that if we actually want to test it!
Our test runner needs to develop these abilities:
Write Ruby source into that script field
Evaluate it at click time
Pass a JavaScriptGenerator
object into the evaluating Ruby source
Collect the JavaScript that this emits
Pass that JavaScript into the IFRAME
Wait until the IFRAME
's
page fully loads, then evaluate that JavaScript
Extend the RJS system to include assertions
Bubble errors up, so test failures are safe and valuable
Oh, and turning the test_WikiTestPage node green if we pass will be a nice final touch. If we can get everything else working, we can use our new test rig itself to create that feature.
To embed Ruby source into the script field, upgrade the test's sample data:
- test_WikiTestPage:
- !omap
- color: green
- page: /wiki/WikiTestPage
- script: "rjs.click 'link_node:test_uncle_wiggily'"
Now at click time we must evaluate it. The evaluating method is
generate_
-iframe
. We rename it test_iframe
, and write another test for
it.
This test forces us to populate the IFRAME
's onload
event handler:
def test_iframe_onload set_wiki_test_up assert_xml @controller.send(:test_iframe, @yar.page_name, '//test_WikiTestPage') assert_xpath '/iframe' do |iframe| assert_match /link.node.test_uncle_wiggily/, iframe.onload end end
We don't yet force the contents to be JavaScript, so the production code cheats:
def test_iframe(page_name, ypath) yar = YarWiki.new(page_name) y = YAML.parse_file(yar.yaml_path) page = y.select(ypath+'//page')[0].value script = y.select(ypath+'//script')[0].value x = Builder::XmlMarkup.new x.iframe :id => :test_frame, :src => page, :onload => script, :width => '100%', :height => '100%' return x.target! end
That function cheats by passing Ruby, not JavaScript, into the
IFRAME
's onload
event handler.
Now we refactor. Because refactoring is a Good Thing, we can refactor anything we want, so long as all tests pass, even if the production code is lying. The script variable still contains raw Ruby, not cooked JavaScript. That can wait, while we refactor whatever's most offensive.
Always find simple refactors first, and do them before complex ones. That helps simplify the complex refactors. (Exception: rename things last, after they settle down!)
We have seen the sequence with parse_file
, y.select
, and [0].value
before. And that statement would
crash if the YPath didn't find a valid node. So let's clean all that up,
and let YarWiki
take care of
everything. Add these methods to it:
def get_node_string(ypath) nodes = @y.select(ypath) return '' unless nodes and nodes.size > 0 return(nodes.first.value or ||) end def select(key_path, value_path) ypath = key_path + value_path @y ||= YAML.parse_file(yaml_path) return get_node_string(ypath) end def eval_rjs(ypath, script) return script end
Those methods tolerate more faults, and they make this look safer:
def test_iframe(page_name, ypath) yar = YarWiki.new(page_name) page = yar.select(ypath, '//page') script = yar.select(ypath, '//script') x = Builder::XmlMarkup.new x.iframe :id => :test_frame, :src => page, :onload => yar.eval_rjs(ypath, script), :width => '100%', :height => '100%' return x.target! end
(This project might have a few more stray .select
statements which could upgrade like
that!)
I added a new method, YarWiki#eval_rjs()
; it will soon convert Ruby
into JavaScript. Right now, it needs a new test, because our current
test cases are too far away from it.
In terms of the AAA Pattern, a test case should not Assemble too many things before testing just one of them. And your test case shouldn't call methods that call methods that indirectly call your tested method. When you find yourself about to write such a test case, refactor your code first, to make the actual target method easier to Assemble and Activate.
To test eval_rjs
directly—that
is, to get closer to the problem without thinking too much about the
extra stuff around it—write a little test like this:
def test_eval_rjs yar = YarWiki.new('NotUsed') assert_js yar.eval_rjs('//not//used//yet', 'rjs.show "Daisy_Mae"') end
The test fails because yar.eval_rjs
returns no JavaScript for
assert_js
to lex. To pass the test,
the production code shall concoct an RJS object, and evaluate the Ruby
code:
def get_rjs ActionView::Helpers::PrototypeHelper:: JavaScriptGenerator.new(self) do |rjs| return rjs end end def eval_rjs(ypath, script) rjs = get_rjs eval(script) return rjs.to_s end
If we trace the return value of eval(script)
, we find it now contains
"Element.show("Daisy_Mae");
".
We are done, right? When we click on a test button, the system will display our target page, evaluate whatever RJS we write, and use it to test our target page, right?
Nope, we are not done, because the onload
event evaluates using our outer page's
context. The JavaScript statements might not find the target page's
namespace of id
s.
The fix adds one more layer to the JavaScript. After generating
our outgoing JavaScript, we must convert it into an anonymous function,
stuff this into a javascript:void()
URI, and put this into the window's location address. That forces the
IFRAME
to evaluate the JavaScript
inside the hosted page's context. Otherwise YarWiki would test its own
page, and that would be very bad.
This all must happen after the onload
event, and after the page completely
renders.
Until now, YarWiki has worked with every web browser that
supports prototype.js
. When we
start using the javascript:void()
URI, however, we lose the mighty KDE Konqueror (version 3.4.2). Its
location system doesn't support that useful but non-standard protocol.
Test your browser's YarWiki compliance by entering javascript:void(alert ('compliant'))
into
its location field.
The glue is a JavaScript function, in the outer frame's context,
that delivers the testing JavaScript to the inner frame. Put this
monstrosity in application.js
:
function update_iframe(sauce) { var test_frame = $('test_frame'), test_frame.onload = function(){}; // only test once! var special_sauce = 'javascript:void(function(){'+sauce+'}())'; if (special_sauce == test_frame.src) return; test_frame.src = special_sauce; }
The JavaScript (which will test our page) enters that function as
a string, irreverently named "sauce
".
We merge it with javascript:
-void()
into a giant pseudo-URI, and assign this
to test_frame.src
. But void()
accepts only one expression. So, as
another wrapping layer, we put our sauce
inside an anonymous function. The extra
function(){
...}
allows our sauce
to contain more than one statement. And
we add a conditional return
statement, to appease the browsers that can't update onload
correctly!
(Don't worry if you have lost track by now; trust me, it all works, and we get to play with it very soon.)
Back inside the Ruby code, to build the sauce
string, we borrow escape_javascript
from ActionView, and put
quotes around its return value:
def escape_javascript(javascript) (javascript || '').gsub(/ | | /, "\n").gsub(/["']/) { |m| "\#{m}" } end def eval_rjs(ypath, script) rjs = get_rjs eval(script) js = rjs.to_s js = '"' + escape_javascript(js) + '";' return js end
Now slip update_iframe
around
that string:
js = 'update_iframe("' + escape_javascript(js) + '");'
And upgrade the test to pass if we successfully nested JavaScript inside JavaScript:
def test_eval_rjs yar = YarWiki.new('NotUsed') assert_js yar.eval_rjs('//not//used//yet', 'rjs.show "Daisy_Mae"') do assert_xpath 'Statement[1]//ArgumentList[1]' do sauce = assert_argument(1) sauce.gsub!('\"', '"') assert_js sauce end end end
(Imagine if you didn't read this Short Cut, and tried to understand that test case. It sure looks nothing like the Rails we started with!)
Despite how helpful our new assertions are, we don't really know if we are building a valid web page or not. We need to finish the TestWiki page, so it will use all these neat features we are adding.
Before writing this Short Cut, I researched all these features by writing a scratch project, and hammered out all the details. Then I started the project again to write this Short Cut. I know the prototype site works, but that doesn't mean this one does...
The TestWiki page has a script
that calls rjs.click
on a test node
in the WikiTestPage. But...
The JavaScriptGenerator
module
has no click
method. Ordinary Rails
projects correctly rely on users to click things, not Ajax. Our test
cases must simulate user interactions, so we Monkey Patch RJS:
class ActionView::Helpers::PrototypeHelper::JavaScriptGenerator def click(button) self << "document.getElementById('#{button}').click();" end end
Even though that works with some browsers, others provide A
nchor objects without click()
methods. Add this magic code to your
application.js
, to Monkey Patch a
JavaScript click()
event:
try { HTMLElement.prototype.click = function() { var evt = this.ownerDocument.createEvent('MouseEvents'), evt.initMouseEvent( 'click', true, true, this.ownerDocument.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null ); this.dispatchEvent(evt); } } catch(e) {}
Have you lost track yet of how many systems we have Monkey Patched today? We have a few more to go!
Our Wiki now has enough of the features needed to start testing web pages. We have a "go" for launch. Cross your breath, hold your fingers, hit http://localhost:3000/wiki/WikiTest, and click on the test icon.
Figure 7. You should see a Wiki containing an IFRAME containing a Wiki containing an IFRAME containing an error.
We expect that error, because the inner Wiki has no real web site to test. Test-Driven Development is all about correctly predicting the result of each test run, and we are successfully test-infecting a web browser.
3.142.235.144