Wiki Tests Wiki

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:

Our Wiki opens a panel now, revealing our Wiki.

Figure 6. Our Wiki opens a panel now, revealing our Wiki.

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!

Drive our Test Page with JavaScript

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.

Refactor the Low-Hanging Fruit

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.

Lean Test Cases

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 ids.

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.

Aloha Konqueror

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.

JavaScript Glue

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...

JavaScriptGenerator#click

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 Anchor 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.

You should see a Wiki containing an IFRAME containing a Wiki containing an IFRAME containing an error.

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.235.144