Using an IFRAME to Test Web Sites

An IFRAME is like a miniature web browser inside a web page. If we can reliably transmit extra JavaScript commands into this browser, they can test its web page. To achieve that effect is why we built all this Wiki stuff; to put both the test cases and the tested web browser into the same user interface. Now that we have just enough Wiki features, we use them to add the IFRAME.

First, we find a home in our Wiki for a new IFRAME. Our Wiki runs inside a TABLE with three TDs. The first TD contains the page name, and the second contains our Wiki's contents.

The third TD is hiding, waiting for us to do this to it:

  • Make the test_ prefix on each YAML node clickable

  • Clicking it will:

    • open the right TD panel

    • push an IFRAME into it

    • navigate the IFRAME to the value of our test's page node

    • evaluate the script node as Ruby with a JavaScriptGenerator

    • route the output JavaScript into that IFRAME's page

    • test that target web page

  • Then extend RJS to provide assertions

  • And bubble all errors up

That will be enough to start this project, and finish this Short Cut. The remaining features, such as adding a normal Wiki interface to our Wiki, can wait for all those hard parts. And very soon we will have an Ajax Wiki that can test Ajax web sites, including ones written with Rails and Brand X.

Oh, and if we teach the Wiki to interpret more YAML markup, and accept different test runner engines, we will grow an extremely light and flexible acceptance test runner, useful far beyond mere Ajax!

That's why I put the Wiki files into a top-level Rails folder, not a database model. The test cases belong to the development team and to the Rails source, not to end-users. The same version controller that versions your Rails will version your Wiki files, including the files that test your Rails project. So, without any further features, you automatically get synchronized collaboration and version control.

link_to_remote

Our Wiki needs to test its nodes, so we need a button to launch that behavior. We will put the button directly into the Wiki's current outline of YAML nodes. Here's the line that formats lines like test_uncle_wiggily:

       @x.strong( key )

Upgrade it to detect the test prefix, and decorate that with a remote link:

    @x.strong do
      if key =~ /^(test)(.*)/
        @x << @page.link_to_remote($1)
        @x.text! $2
      else
        @x.text! key
      end
    end

The method link_to_remote requires the current ActionView context, so it can find all its support things. We must pass that context through the YarWiki object so these statements can access it.

page is a new argument to format_yaml:

  def format_yaml(page, x)
    return unless File.exist?(yaml_path)
    @y = YAML::parse_file(yaml_path)
    @x = x
    @page = page
    format_seq(@y, 'node')
  end

Upgrade the calling line, yar.format_-yaml(x), to yar.format_yaml(self, x). Also upgrade all the tests that call format_yaml, and pass self into them, too.

Pass the tests by giving the test suite a stub implementation of link_to_-remote:

  def link_to_remote(*x)
    return x.first.to_s
  end

Tests that don't use the ActionView rendering system need that mock function. Tests that use get or xhr will reach the real link_to_remote.

Escaping Too Little or Too Much!

And when the tests reach get or xhr, and that calls link_to_remote, all heck breaks loose:

malformed XML: missing tag start
Line:
Position:
Last 80 unconsumed characters:
<a href='#' onclick='new Ajax.Request('', {asynchronous:true, evalScripts:true}

That happened because link_to_remote escapes ' characters with '. Everyone likes those except REXML, which chokes on them.

While we wait for someone to read the standards, and upgrade either REXML or Rails, we can fix the tests directly. The error came from this method, which invokes the REXML parser:

  def assert_xml(contents)
    @xdoc = Document.new(contents.to_s)
  end

We can upgrade it to whack in a fix. Fortunately, we don't care if the fix works for web browsers, because they will never see this output.

  def assert_xml(contents)
    contents.gsub!(''', '&apos;')
    @xdoc = Document.new(contents)
  end

Now the tests all pass, and nodes like test_uncle_wiggily render a remote link:

Clicking on a test link will soon open the IFRAME into the right panel.

Figure 5. Clicking on a test link will soon open the IFRAME into the right panel.

This new test forces link_to_remote to call a new action, display_iframe:

  def test_test_nodes_link_to_display_iframe
    render_wiki(get_omap)

    assert_xpath '//strong/a[ . = "test" ]' do |a|
      assert_equal '#', a.href
      assert_js 'ajax = ' + a.onclick do
        assert_xpath 'Statement[1]//ArgumentList[1]' do |node|
          assert_equal '/wiki/display_iframe', assert_argument(1)
        end
      end
    end
  end

Our new link_to_remote line upgrades like this:

        href = { :url => { :action => 'display_iframe' } }
        @x << @page.link_to_remote($1, href)
        @x.text! $2

Now we write a test that requires the display_iframe method to change our TABLE's layout. The Wiki's left TD gets smaller to make room for the IFRAME. Our new action will soon push one in:

  def test_display_iframe
    xhr :get, :display_iframe
    assert_rjs :page, 'wiki_panel', :width=, '50%'
    assert_rjs :page, 'test_panel', :width=, '50%'
    assert_rjs :replace_html, 'test_panel', /iframe.*character.hammy/
  end

That test requires this matching syntax in the WikiController:

  def display_iframe
    iframe = generate_iframe()

    render :update do |rjs|
      rjs['wiki_panel'].width = '50%'
      rjs['test_panel'].width = '50%'
      rjs.replace_html 'test_panel', iframe
    end
  end

private
  def generate_iframe()
    x = Builder::XmlMarkup.new
    x.iframe
    return x.target!
  end

Note that I call my RJS object rjs, not page. That is simply because the World Wide Web has far too many different variables in it called "page".

Because we guess that building our new IFRAME object will be hard, we create a new method for it, and create it with a Builder::XmlMarkup. We plan to build a new IFRAME each time a test case runs.

Test Isolation

Each time we click the test button, our browser will throw away any existing IFRAME and its contents, and will push in a new one. That might be slow, but it isolates each test case from the others. Hopefully, our browsers rebuild each IFRAME's internal data from scratch, and side effects from each test case won't spill into the next one.

To switch the IFRAME to our target page, the page node has a URI that must travel into that IFRAME's src attribute. So that variable must route through these objects:

  • The Wiki's YAML file, which passes that page node's YPath into...

  • The link_to_remote, which passes this YPath into...

  • display_iframe, which passes the page_name and YPath into...

  • generate_iframe, which:

    • keys the YAML with the YPath

    • reads the URI from the page node

    • navigates its IFRAME to that URI

Let's do all that work in reverse order, each time under test. This new test case calls generate_iframe directly:

  def test_generate_iframe
    render_wiki(get_omap)
    assert_xml @controller.send(:generate_iframe, 'WikiTestPage',
'//test_uncle_wiggily')
    assert_xpath '/iframe[ @src = "/character/uncle_wiggily" ]'
  end

(The .send method sneaks past the private keyword. Ruby should have named that keyword slightly_less_convenient_to_call.)

These new lines pass that test:

private
  def generate_iframe(page_name = nil, ypath = nil)
                      #  TODO  take out those = nil
      page = nil

      if page_name
      yar  = YarWiki.new(page_name)
      y    = YAML.parse_file(yar.yaml_path)
      page = y.select(ypath+'//page')[0].value
    end

    x = Builder::XmlMarkup.new
    x.iframe :src => page
    return x.target!
  end

That method now has two callers, and one passes more arguments. Before we upgrade the other, the method needs excessive conditional statements to work without those variables. We shouldn't upgrade the old caller until the new test passes.

Change Isolation

Test-Driven Development works by isolating and insulating two different kinds of changes from each other. When you add behavior, you should not change structure. That's why, while a test is failing, you should only perform simple edits to pass the test. Don't change too much. Only after the test passes do you refactor, to merge the new code with the existing code. That discipline improves the odds that you merge the correct behavior into the existing code.

Now add the page_name and YPath to display_iframe:

  def test_display_iframe
    xhr :get,
        :display_iframe,
        :page_name => 'WikiTestPage',
        :ypath => '//test_hammy_squirrel'

    assert_rjs :page, 'wiki_panel', :width=, '50%'
    assert_rjs :page, 'test_panel', :width=, '50%'
    assert_rjs :replace_html, 'test_panel', /iframe.*character.hammy/
  end

Then fix the other call to generate_iframe:

  def display_iframe
    iframe = generate_iframe(params[:page_name], params[:ypath])

    render :update do |rjs|
      rjs['wiki_panel'].width = '50%'
      rjs['test_panel'].width = '50%'
      rjs.replace_html 'test_panel', iframe
      rjs.show 'test_panel'
    end
  end

Take the scaffolding out of generate_iframe, and slip in some enhancements:

  def generate_iframe(page_name, ypath)
    yar  = YarWiki.new(page_name)
    y    = YAML.parse_file(yar.yaml_path)
    page = y.select(ypath+'//page')[0].value
    x    = Builder::XmlMarkup.new

    x.iframe :src    =>  page,
             :id     => :test_frame,
             :width  => '100%',
             :height => '100%'

    return x.target!
  end

Errata

That src => page line is one of several in this Wiki that need to prepend request.relative_url_root. Similarly, our images need image_path. These would help our Wiki operate correctly under web servers that don't place our web site at their root / path.

Next, the link_to_remote call must pass its page_name and YPath to display_iframe. The link should pass more information on its URI. Here's a first shot at test-firsting that from test_test_nodes_link_to_display_-iframe:

        assert_xpath 'Statement[1]//ArgumentList[1]' do |node|
          assert_equal '/wiki/display_iframe?page_name=
WikiTestPage&ypath=//test_uncle_wiggily',
                                   assert_argument(1)
        end

That test looks suspicious. As a general guideline, never call assert_equal on such long strings. As usual, our first attempt to write a test will target syntax, instead of semantics, and we won't even get this syntax right! The test will fail for two reasons: because the code isn't there yet, and because the code will return a different string.

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

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