[TFUI] asserting HTML by example

Phlip <[email protected]> Wed, 11 Mar 2009 22:27:56 -0700
Newsgroups gmane.comp.programming.test-first-user-interfaces
Message-ID <[email protected]>
--VqpyiMwu6J0RTUkZ2WX-i5JLcfYg9sFU8r9hAlY
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 7bit

extremists:

HTML is easy to test and hard to test well. A unit test must intercept and parse 
it, hopefully _before_ it goes thru a web server.

I just wrote an assertion which turns that problem on its head. Instead of 
asserting how to parse that HTML, we will assert how to build an example of HTML 
that the production HTML must match.

This post is the high-level, language-agnostic, overview of these announcement 
posts:

   http://www.ruby-forum.com/topic/181145#792950

Here's the example:

      user = users(:Moses)
      get :edit_user, :id => user.id   #  fetches a web page

      assert_xhtml do

        form :action => '/users' do
          fieldset do
            legend 'Personal Information'
            label 'First name'
            input :type => 'text',
                  :name => 'user[first_name]'
                  :value => user.first_name
          end
        end

      end

That's all. Inside the assertion's block, you declare the HTML you want, using a 
Builder notation. (Nokogiri, in Ruby's case.)

The assertion skips over any intervening elements, such as <ul>, <table>, etc, 
that the block did not build. In the above example, <fieldset> only needs to 
appear somewhere inside the <form> - not necessarily its first element. But 
those <label> and <input> fields must appear somewhere inside that <fieldset>, 
and in the given order!

To create this assertion in your language, start with your nearest HTML builder. 
Allow the user-programmer to build an example, then traverse its DOM, and 
convert each item into an XPath seeking that item in the production HTML. 
Declare a match if all the specified attributes match, and if most of the text 
matches - disregarding blanks.

Then, for each match, recursively match each built child element, applying its 
XPath relative to the current node in the production HTML's DOM.

When you find a fault, build an error message by rendering the current nodes to 
HTML, and return this out of your loop statements. This Gist explicates the 
algorithm:

    http://gist.github.com/76136

The algorithm essentially matches two trees, depth-first and in-order. The 
example HTML must be a subset of the production HTML.

If you survive all that recursing and matching then your assertion passes. You 
verified a huge number of details, using extremely lean code, and you ignored 
many more details, allowing them to change freely as you upgrade your website.

-- 
   Phlip
   http://www.zeroplayer.com/

--VqpyiMwu6J0RTUkZ2WX-i5JLcfYg9sFU8r9hAlY
Content-Type: text/html; charset=ISO-8859-1
Content-Transfer-Encoding: 7bit



<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
</head>




<body style="background-color: #ffffff;">

<!--~-|**|PrettyHtmlStartT|**|-~-->
<div id="ygrp-mlmsg" style="width:655px; position:relative;">
<div id="ygrp-msg" style="width: 470px; margin:0; padding:0 25px 0 0; float:left; z-index:1;">
<!--~-|**|PrettyHtmlEndT|**|-~-->

    <div id="ygrp-text">
            <p>extremists:<br>
<br>
HTML is easy to test and hard to test well. A unit test must intercept and parse <br>
it, hopefully _before_ it goes thru a web server.<br>
<br>
I just wrote an assertion which turns that problem on its head. Instead of <br>
asserting how to parse that HTML, we will assert how to build an example of HTML <br>
that the production HTML must match.<br>
<br>
This post is the high-level, language-agnostic, overview of these announcement <br>
posts:<br>
<br>
<a href="http://www.ruby-forum.com/topic/181145#792950">http://www.ruby-<wbr>forum.com/<wbr>topic/181145#<wbr>792950</a><br>
<br>
Here's the example:<br>
<br>
user = users(:Moses)<br>
      get :edit_user, :id =&gt; user.id   #  fetches a web page<br>
<br>
assert_xhtml do<br>
<br>
form :action =&gt; '/users' do<br>
          fieldset do<br>
            legend 'Personal Information'<br>
            label 'First name'<br>
            input :type =&gt; 'text',<br>
                  :name =&gt; 'user[first_<wbr>name]'<br>
                  :value =&gt; user.first_name<br>
          end<br>
        end<br>
<br>
end<br>
<br>
That's all. Inside the assertion's block, you declare the HTML you want, using a <br>
Builder notation. (Nokogiri, in Ruby's case.)<br>
<br>
The assertion skips over any intervening elements, such as &lt;ul&gt;, &lt;table&gt;, etc, <br>
that the block did not build. In the above example, &lt;fieldset&gt; only needs to <br>
appear somewhere inside the &lt;form&gt; - not necessarily its first element. But <br>
those &lt;label&gt; and &lt;input&gt; fields must appear somewhere inside that &lt;fieldset&gt;, <br>
and in the given order!<br>
<br>
To create this assertion in your language, start with your nearest HTML builder. <br>
Allow the user-programmer to build an example, then traverse its DOM, and <br>
convert each item into an XPath seeking that item in the production HTML. <br>
Declare a match if all the specified attributes match, and if most of the text <br>
matches - disregarding blanks.<br>
<br>
Then, for each match, recursively match each built child element, applying its <br>
XPath relative to the current node in the production HTML's DOM.<br>
<br>
When you find a fault, build an error message by rendering the current nodes to <br>
HTML, and return this out of your loop statements. This Gist explicates the <br>
algorithm:<br>
<br>
<a href="http://gist.github.com/76136">http://gist.<wbr>github.com/<wbr>76136</a><br>
<br>
The algorithm essentially matches two trees, depth-first and in-order. The <br>
example HTML must be a subset of the production HTML.<br>
<br>
If you survive all that recursing and matching then your assertion passes. You <br>
verified a huge number of details, using extremely lean code, and you ignored <br>
many more details, allowing them to change freely as you upgrade your website.<br>
<br>
-- <br>
   Phlip<br>
   <a href="http://www.zeroplayer.com/">http://www.zeroplay<wbr>er.com/</a><br>
</p>
 

    </div>  

    <!--~-|**|PrettyHtmlStart|**|-~-->
    <div width="1" style="color: white; clear: both;">__._,_.___</div>

             
    <!-- Start the section with Message In topic -->
    <div id="ygrp-actbar" style="clear: both; margini-bottom: 25px; white-space:nowrap; color: #666; text-align: right;">
              <span class="left" style="float: left; white-space:nowrap;">
          <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/message/1037;_ylc=X3oDMTM0NGZxbmNjBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BG1zZ0lkAzEwMzcEc2VjA2Z0cgRzbGsDdnRwYwRzdGltZQMxMjM2ODM1NzAyBHRwY0lkAzEwMzc-">
            Messages in this topic          </a> (<span class="bld" style="font-weight:bold;">1</span>)
        </span>
        <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/post;_ylc=X3oDMTJwa2ZoMTVoBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BG1zZ0lkAzEwMzcEc2VjA2Z0cgRzbGsDcnBseQRzdGltZQMxMjM2ODM1NzAy?act=reply&messageNum=1037">
          <span class="bld" style="font-weight:bold;">
            Reply          </span> (via web post)
        </a>  | 
        <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/post;_ylc=X3oDMTJlbmptN2dvBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA250cGMEc3RpbWUDMTIzNjgzNTcwMg--" class="bld" style="font-weight:bold;">
          Start a new topic        </a>
          </div> 
    <!-------     Start Nav Bar  ------>
    <!-- |**|begin egp html banner|**| -->
    <div id="ygrp-vitnav" style="padding-top: 10px; font-family: Verdana; font-size: 77%; margin: 0;">
                <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/messages;_ylc=X3oDMTJlYzBlcjhqBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA21zZ3MEc3RpbWUDMTIzNjgzNTcwMg--">Messages</a>  
            |    <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/files;_ylc=X3oDMTJmbmxhMnA5BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2ZpbGVzBHN0aW1lAzEyMzY4MzU3MDI-">Files</a>  
            |    <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/photos;_ylc=X3oDMTJlZzFrODVzBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA3Bob3QEc3RpbWUDMTIzNjgzNTcwMg--">Photos</a>  
            |    <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/links;_ylc=X3oDMTJmMDFhNXNiBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2xpbmtzBHN0aW1lAzEyMzY4MzU3MDI-">Links</a>  
            |    <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/database;_ylc=X3oDMTJjMmgwMm1oBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2RiBHN0aW1lAzEyMzY4MzU3MDI-">Database</a>  
            |    <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/polls;_ylc=X3oDMTJmNG81cThiBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA3BvbGxzBHN0aW1lAzEyMzY4MzU3MDI-">Polls</a>  
            |    <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/members;_ylc=X3oDMTJlbWF1ZmN1BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA21icnMEc3RpbWUDMTIzNjgzNTcwMg--">Members</a>  
            |    <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/calendar;_ylc=X3oDMTJkajJyM2dqBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2NhbARzdGltZQMxMjM2ODM1NzAy">Calendar</a>  
    </div>  
    <!-- |**|end egp html banner|**| -->

                <div id="ygrp-grfd" style="font-family: Verdana; font-size: 77%; padding: 15px 0;">
                  

              </div>
    
    <!-- yahoo logo -->
    <!-- |**|begin egp html banner|**| -->
    <div id="ygrp-ft" style="font-family: verdana; font-size: 77%; border-top: 1px solid #666; padding: 5px 0;">
      <a href="http://groups.yahoo.com/;_ylc=X3oDMTJkMmpraGl1BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2dmcARzdGltZQMxMjM2ODM1NzAy">
      <img src="http://us.i1.yimg.com/us.yimg.com/i/yg/img/logo/ma_grp_160.gif" height="15" width="106" border="0" alt="Yahoo! Groups"></a> <br>
      <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces/join;_ylc=X3oDMTJmdDJpbW1sBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA3N0bmdzBHN0aW1lAzEyMzY4MzU3MDI-">Change settings via the Web</a> (Yahoo! ID required) <br>
      Change settings via email: <a href="mailto:TestFirstUserInterfaces-digest-hHKSG33TihhbjbujkaE4pw@public.gmane.org?subject=Email Delivery: Digest">Switch delivery to Daily Digest</a> | <a href = "mailto:TestFirstUserInterfaces-traditional-hHKSG33TihhbjbujkaE4pw@public.gmane.org?subject=Change Delivery Format: Traditional">Switch format to Traditional</a> <br>

      <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces;_ylc=X3oDMTJkN2NmNDk0BF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwNmdHIEc2xrA2hwZgRzdGltZQMxMjM2ODM1NzAy">
        Visit Your Group 
      </a> |
      <a href="http://docs.yahoo.com/info/terms/">
        Yahoo! Groups Terms of Use      </a> |
      <a href="mailto:TestFirstUserInterfaces-unsubscribe-hHKSG33TihhbjbujkaE4pw@public.gmane.org?subject=">
        Unsubscribe      </a> 
    </div>     <!-- |**|end egp html banner|**| -->
  </div> <!-- ygrp-msg -->

  
  <!-- Sponsor -->
  <!-- |**|begin egp html banner|**| -->
  <div id="ygrp-sponsor" style="width:160px; float:right; clear:none; margin:0 0 25px 0; background: white;">
    <!-- Network content -->
    
<!-- Start Recommendations -->
<div id="ygrp-reco">
     </div>
<!-- End Recommendations -->

	    <!-- Start vitality -->
	    <div id="ygrp-vital" style="background-color: #e0ecee; margin-bottom: 20px; padding: 2px 0 8px 8px;">
	      		<div id="vithd" style="font-size: 77%; font-family: Verdana; font-weight: bold; color: #333; text-transform: uppercase;">Recent Activity</div>
		<ul style="list-style-type:none; padding: 0; margin: 2px 0;">
		    
		    
		    
		    
		    
		    
		</ul>
	      	      <a href="http://groups.yahoo.com/group/TestFirstUserInterfaces;_ylc=X3oDMTJlNDI2YjJyBF9TAzk3MzU5NzE0BGdycElkAzg3NDQ3MDQEZ3Jwc3BJZAMxNzA1MDA3MjA3BHNlYwN2dGwEc2xrA3ZnaHAEc3RpbWUDMTIzNjgzNTcwMg--">
		Visit Your Group	      </a>
	    </div> 
	    	    	      
	    <!-- Network content -->
	    	      <div style="background-color:#EEEEEE; margin-bottom:20px; padding:10px 8px;">    <div id="nc">
              <div class="ad" style="color:#628C2A; font-family:Arial; font-weight:bold;">
                        <div id="hd1">Give Back</div>
  <p><a href="http://us.lrd.yahoo.com/_ylc=X3oDMTJtZnYyamhwBF9TAzk3MzU5NzE0BF9wAzEEZ3JwSWQDODc0NDcwNARncnBzcElkAzE3MDUwMDcyMDcEc2VjA25jbW9kBHNsawNicmFuZARzdGltZQMxMjM2ODM1NzAy;_ylg=1/SIG=11314uv3k/**http%3A//brand.yahoo.com/forgood" style="text-decoration:none; color:#1E66AE; font-weight: normal;">Yahoo! for Good</a></p>
  <p style="color: #000000; font-weight: normal;">Get inspired</p>
  <p style="color: #000000; font-weight: normal;">by a good cause.</p>
                  </div>
                    <div class="ad" style="color:#628C2A; font-family:Arial; font-weight:bold;">
                        <div id="hd1">Y! Toolbar</div>
  <p><a href="http://us.lrd.yahoo.com/_ylc=X3oDMTJvcWVubjRlBF9TAzk3MzU5NzE0BF9wAzIEZ3JwSWQDODc0NDcwNARncnBzcElkAzE3MDUwMDcyMDcEc2VjA25jbW9kBHNsawN0b29sYmFyBHN0aW1lAzEyMzY4MzU3MDI-;_ylg=1/SIG=11c6dvmk9/**http%3A//toolbar.yahoo.com/%3F.cpdl=ygrps" style="text-decoration:none; color:#1E66AE; font-weight: normal;">Get it Free!</a></p>
  <p style="color: #000000; font-weight: normal;">easy 1-click access</p>
  <p style="color: #000000; font-weight: normal;">to your groups.</p>
                  </div>
                    <div class="ad" style="color:#628C2A; font-family:Arial; font-weight:bold;">
                        <div id="hd1">Yahoo! Groups</div>
  <p><a href="http://groups.yahoo.com/start;_ylc=X3oDMTJvbzJhcXN0BF9TAzk3MzU5NzE0BF9wAzMEZ3JwSWQDODc0NDcwNARncnBzcElkAzE3MDUwMDcyMDcEc2VjA25jbW9kBHNsawNncm91cHMyBHN0aW1lAzEyMzY4MzU3MDI-" style="text-decoration:none; color:#1E66AE; font-weight: normal;">Start a group</a></p>
  <p style="color: #000000; font-weight: normal;">in 3 easy steps.</p>
  <p style="color: #000000; font-weight: normal;">Connect with others.</p>
                  </div>
          </div>
</div>
	    
	  </div> 	  <!-- |**|end egp html banner|**| -->
	  <div style="clear:both; color: #FFF; font-size:1px;">.</div>
	</div> 		  <img src="http://geo.yahoo.com/serv?s=97359714/grpId=8744704/grpspId=1705007207/msgId=1037/stime=1236835702/nc1=1/nc2=2/nc3=3" width="1" height="1"> <br>
	
	<div style="color: white; clear: both;">__,_._,___</div>
	<!--~-|**|PrettyHtmlEnd|**|-~-->
	</body>
	<!--~-|**|PrettyHtmlStart|**|-~-->
	<head>
<style type="text/css">
<!--
#ygrp-mkp{
  border: 1px solid #d8d8d8;
  font-family: Arial;
  margin: 14px 0px;
  padding: 0px 14px;
}
#ygrp-mkp hr{
  border: 1px solid #d8d8d8;
}
#ygrp-mkp #hd{
  color: #628c2a;
  font-size: 85%;
  font-weight: bold;
  line-height: 122%;
  margin: 10px 0px;
}
#ygrp-mkp #ads{
  margin-bottom: 10px;
}
#ygrp-mkp .ad{
  padding: 0 0;
}
#ygrp-mkp .ad a{
  color: #0000ff;
  text-decoration: none;
}
-->
</style>
</head>
	<head>
<style type="text/css">
<!--
#ygrp-sponsor #ygrp-lc{
  font-family: Arial;
}
#ygrp-sponsor #ygrp-lc #hd{
  margin: 10px 0px;
  font-weight: bold;
  font-size: 78%;
  line-height: 122%;
}
#ygrp-sponsor #ygrp-lc .ad{
  margin-bottom: 10px;
  padding: 0 0;
}
-->
</style>
</head>
	<head>
	<style type="text/css">
	<!--
	#ygrp-mlmsg {font-size:13px; font-family: arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}
	#ygrp-mlmsg table {font-size:inherit;font:100%;}
	#ygrp-mlmsg select, input, textarea {font:99% arial,helvetica,clean,sans-serif;}
	#ygrp-mlmsg pre, code {font:115% monospace;*font-size:100%;}
	#ygrp-mlmsg * {line-height:1.22em;}
	#ygrp-text{
	    font-family: Georgia;	
	}
	#ygrp-text p{
	    margin: 0 0 1em 0;
	}

	dd.last p a {
          font-family: Verdana;
          font-weight: bold;
	}

	#ygrp-vitnav{
		padding-top: 10px;
		font-family: Verdana;
		font-size: 77%;
		margin: 0;
	}
	#ygrp-vitnav a{
		padding: 0 1px;
	}
	#ygrp-mlmsg #logo{
	  padding-bottom: 10px;
	}

	#ygrp-reco {
	margin-bottom: 20px;
	padding: 0px;
	}
	#ygrp-reco #reco-head {
		font-weight: bold;
		color: #ff7900;
	}

	#reco-category{
        	font-size: 77%;
	}
	#reco-desc{
        	font-size: 77%;
	}

	#ygrp-vital a{
		text-decoration: none;
	}

	#ygrp-vital a:hover{
	  text-decoration: underline;
	}

	#ygrp-sponsor #ov ul{
		padding: 0 0 0 8px;
		margin: 0;
	}
	#ygrp-sponsor #ov li{
		list-style-type: square;
		padding: 6px 0;
		font-size: 77%;
	}
	#ygrp-sponsor #ov li a{
		text-decoration: none;
		font-size: 130%;
	}
	#ygrp-sponsor #nc{
	  background-color: #eee;
	  margin-bottom: 20px;
	  padding: 0 8px;
	}
	#ygrp-sponsor .ad{
		padding: 8px 0;
	}
	#ygrp-sponsor .ad #hd1{
		font-family: Arial;
		font-weight: bold;
		color: #628c2a;
		font-size: 100%;
		line-height: 122%;
	}
	#ygrp-sponsor .ad a{
		text-decoration: none;
	}
	#ygrp-sponsor .ad a:hover{
		text-decoration: underline;
	}
	#ygrp-sponsor .ad p{
		margin: 0;
	}
	o{font-size: 0; }
	.MsoNormal{
	   margin: 0 0 0 0;
	}
	#ygrp-text tt{
	  font-size: 120%;
	}
	blockquote{margin: 0 0 0 4px;}
	.replbq{margin:4}

dd.last p span {
  margin-right: 10px;
  font-family: Verdana;
  font-weight: bold;
}

dd.last p span.yshortcuts {
  margin-right: 0;
}

div.photo-title a,
div.photo-title a:active,
div.photo-title a:hover,
div.photo-title a:visited {
    text-decoration: none;
}

div.file-title a,
div.file-title a:active,
div.file-title a:hover,
div.file-title a:visited {
    text-decoration: none;
}

#ygrp-msg p {
    clear: both;
    padding: 15px 0 3px 0;
    overflow: hidden;
}

#ygrp-msg p span {
    color: #1E66AE;
    font-weight: bold;
}

div#ygrp-mlmsg #ygrp-msg p a span.yshortcuts {
    font-family: Verdana;
    font-size: 10px;
    font-weight: normal;
}

#ygrp-msg p a {
    font-family: Verdana;
    font-size: 10px;
}

#ygrp-mlmsg a {
    color: #1E66AE;
}

div.attach-table div div a {
    text-decoration: none;
}

div.attach-table {
    width: 400px;
}

	-->
	</style>
	</head>
	<!--~-|**|PrettyHtmlEnd|**|-~-->
	</html><!--End group email -->


--VqpyiMwu6J0RTUkZ2WX-i5JLcfYg9sFU8r9hAlY--