Rspec custom matchers and be_valid_xhtml

September 28th, 2007

Ok. C'mon. Who's as anal as me when it comes to validating their xhtml? It's such a beautiful crystaline structure. You don't want to debalance it by leaving out an angle bracket. The whole thing will fall over.

There's a nice plugin available at http://www.realityforge.org/svn/code/assert-valid-asset/trunk/ that provides the ability to hit the w3c validator up with your data, but I'm kicking ass and taking names with rspec these days, so I thought I'd wrap it up as a custom matcher for rspec (download here):

RUBY:
  1. class BeValidXhtml
  2.   # require 'action_controller/test_process'
  3.   # require 'test/unit'
  4.   require 'net/http'
  5.   require 'md5'
  6.   require 'ftools'
  7.  
  8.   def initialize
  9.   end
  10.  
  11.   # Assert that markup (html/xhtml) is valid according the W3C validator web service.
  12.   # By default, it validates the contents of @response.body, which is set after calling
  13.   # one of the get/post/etc helper methods. You can also pass it a string to be validated.
  14.   # Validation errors, if any, will be included in the output. The input fragment and
  15.   # response from the validator service will be cached in the $RAILS_ROOT/tmp directory to
  16.   # minimize network calls.
  17.   #
  18.   # For example, if you have a FooController with an action Bar, put this in foo_controller_test.rb:
  19.   #
  20.   #   def test_bar_valid_markup
  21.   #     get :bar
  22.   #     assert_valid_markup
  23.   #   end
  24.   #
  25.   MARKUP_VALIDATOR_HOST = ENV['MARKUP_VALIDATOR_HOST'] || 'validator.w3.org'
  26.   MARKUP_VALIDATOR_PATH = ENV['MARKUP_VALIDATOR_PATH'] || '/check'
  27.   CSS_VALIDATOR_HOST = ENV['CSS_VALIDATOR_HOST'] || 'jigsaw.w3.org'
  28.   CSS_VALIDATOR_PATH = ENV['CSS_VALIDATOR_PATH'] || '/css-validator/validator'
  29.  
  30.   @@display_invalid_content = false
  31.   cattr_accessor :display_invalid_content
  32.  
  33.   @@auto_validate = false
  34.   cattr_accessor :auto_validate
  35.  
  36.   class_inheritable_accessor :auto_validate_excludes
  37.   class_inheritable_accessor :auto_validate_includes
  38.  
  39.  
  40.   def matches?(response)
  41.     fn = response.rendered_file
  42.     fragment = response.body
  43.     return true if validity_checks_disabled?
  44.     base_filename = cache_resource('markup',fragment,'html',fn)
  45.    
  46.     return false unless base_filename
  47.     results_filename =  base_filename + '-results.yml'
  48.  
  49.     begin
  50.       response = File.open(results_filename) do |f| Marshal.load(f) end
  51.     rescue
  52.       response = http.start(MARKUP_VALIDATOR_HOST).post2(MARKUP_VALIDATOR_PATH, "fragment=#{CGI.escape(fragment)}&output=xml")
  53.       File.open(results_filename, 'w+') do |f| Marshal.dump(response, f) end
  54.     end
  55.     markup_is_valid = response['x-w3c-validator-status'] == 'Valid'
  56.     @message = ''
  57.     unless markup_is_valid
  58.       fragment.split($/).each_with_index{|line, index| message <<"#{'%04i' % (index+1)} : #{line}#{$/}"} if @@display_invalid_content
  59.       @message <<XmlSimple.xml_in(response.body)['messages'][0]['msg'].collect{ |m| "Invalid markup: line #{m['line']}: #{CGI.unescapeHTML(m['content'])}" }.join("\n")
  60.     end
  61.     if markup_is_valid
  62.       return true
  63.     else
  64.       return false
  65.     end
  66.   end
  67.  
  68.   def description
  69.     "be valid xhtml"
  70.   end
  71.  
  72.   def failure_message
  73.    " expected xhtml to be valid, but validation produced these errors:\n #{@message}"
  74.   end
  75.  
  76.   def negative_failure_message
  77.     " expected to not be valid, but was (missing validation?)"
  78.   end
  79.  
  80.   private
  81.     def validity_checks_disabled?
  82.       ENV["NONET"] == 'true'
  83.     end
  84.  
  85.     def text_to_multipart(key,value)
  86.       return "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"\r\n\r\n#{value}\r\n"
  87.     end
  88.  
  89.     def file_to_multipart(key,filename,mime_type,content)
  90.       return "Content-Disposition: form-data; name=\"#{CGI::escape(key)}\"; filename=\"#{filename}\"\r\n" +
  91.                 "Content-Transfer-Encoding: binary\r\nContent-Type: #{mime_type}\r\n\r\n#{content}\r\n"
  92.     end
  93.  
  94.     def cache_resource(base,resource,extension,fn)
  95.       resource_md5 = MD5.md5(resource).to_s
  96.       file_md5 = nil
  97.  
  98.       output_dir = "#{RAILS_ROOT}/tmp/#{base}"
  99.       base_filename = File.join(output_dir, fn)
  100.       filename = base_filename + extension
  101.  
  102.       parent_dir = File.dirname(filename)
  103.       File.makedirs(parent_dir) unless File.exists?(parent_dir)
  104.  
  105.       File.open(filename, 'r') do |f|
  106.         file_md5 = MD5.md5(f.read(f.stat.size)).to_s
  107.       end if File.exists?(filename)
  108.  
  109.       if file_md5 != resource_md5
  110.         Dir["#{base_filename}[^.]*"] .each {|f| File.delete(f)}
  111.         File.open(filename, 'w+') do |f| f.write(resource); end
  112.       end 
  113.       base_filename
  114.     end
  115.  
  116.     def http
  117.       if Module.constants.include?("ApplicationConfig") && ApplicationConfig.respond_to?(:proxy_config)
  118.         Net::HTTP::Proxy(ApplicationConfig.proxy_config['host'], ApplicationConfig.proxy_config['port'])
  119.       else
  120.         Net::HTTP
  121.       end
  122.     end
  123.  
  124. end
  125.  
  126. def be_valid_xhtml
  127.   BeValidXhtml.new
  128. end

ruby, snippits, programming, ruby on rails | Comments | Trackback Jump to the top of this page

4 comments on “Rspec custom matchers and be_valid_xhtml”

  1. 01

    I added be_valid_xhtml_fragment which auto wraps the response in an XHTML header. Code doesn’t go happily into this blog, so here’s a pastie:
    http://pastie.caboo.se/137110

    Now you can validate partial responses, or isolated view tests.

    Xavier Shay at January 9th, 2008 around 5:13 am
    Jump to the top of this page
  2. 02

    I’ve extended this (with limited testing) to allow for validation of a string. If you have a model that is generation templates (as I am) then getting a AC::Response is not really desirable.

    http://gist.github.com/9300

    Please feel free to improve. This doesn’t include Xavier’s additions.

    Ryan Garver at September 7th, 2008 around 3:33 pm
    Jump to the top of this page
  3. 03

    I’ve written this up into a Rails plugin. Available at http://github.com/unboxed/be_valid_asset/

    I’ve included CSS validation, and switched to using the validator soap12 result format as opposed to the (deprecated) xml output.

    Alex Tomlins at February 9th, 2009 around 5:54 am
    Jump to the top of this page
  4. 04

    Awesome work everyone. No-one should use this blog post anymore. Go and use the plugin from Alex.

    grant.mcinnes at February 10th, 2009 around 11:51 am
    Jump to the top of this page

Leave a Reply

  •  
  •  
  •  

You can keep track of new comments to this post with the comments feed.

Recently on Flickr

    mull - 21.jpgmull - 20.jpgmull - 19.jpgmull - 18.jpg

Recently Listened

Meta

The Carousell