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 TD
s. 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.
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
.
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!(''', ''')
@xdoc = Document.new(contents)
end
Now the tests all pass, and nodes like test_uncle_wiggily render a remote link:
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.
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.
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
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.
3.147.83.8