Custom Search

Sunday, February 7, 2010

The bitchin web framework - template testing

I've been building web sites for nearly twenty years now. In that time, I've learned some lessons that apply here:

1) I shouldn't do visual or interface design. My eye for graphics sucks, and my mind doesn't work the way most users do when it comes to UI issues.

2) CSS is sufficient for visual design. The existence of "div soup" web pages demonstrates that. Since those can go in a stylesheet file, we get automatic separation of design and content. Given #1, this is good for me.

3) If you're doing HTML, the reader always gets the final say about presentation! There are browser with options - or extensions that add options - to turn off CSS completely, or use the users own CSS. Worst comes to worst, they can use View Source on your page. Attempting to tell them how to set their browser will, if you're lucky, get you laughed at before they read your text. If you're not lucky, they'll take their business elsewhere.

With those in mind, bitchin is a programmers framework. Templates are meant to be embedded in code, with design decisions going into a style sheet.

A template is a data structure that can be "evaluated" in a context to fill in values from various data sources to provide content, producing HTML text - or a fragment thereof. In most programming languages, we have a tool for that already - it's called a function! Further, modern HTML is an XML application, and the parallels between S-expressions and XML have been pointed out numerous times. Given that Clojure functions are S-expressions - well, mostly - I'm going to use them as bitchin templates.

So a tag is a callable object. If the first argument is a Clojure map - or any associative object - the key/value pairs will turn into attributes for the tag. Any remaining arguments will be evaluated to generate the contents of the tag. So a typical simple web page template might look like:
(html (head (title "A short test"))
(body (h1 "A short test"))
(p "And some text")))
And then render as:
<title>A short test</title>
<h1>A short test</h1>
<p>And some text</p>
So the first step is test code. While I am a fan of test suites, I'm not a fan of test driven development. The problem is - the tests are code, and hence likely to have bugs. The only proper test for them is the code they're supposed to test. So to do TDD on the tests, you need to write the code to be tested first. The reality is that it doesn't matter which you write first - you're going to debug them in parallel. So I normally write them in parallel as well.

In this case, the tests are providing examples for how the code should behave. As such, that's worth writing out explicitly. Given the lose nature of XML, I'll probably still wind up debugging them in parallel.

The test code uses the clojure.test unit testing framework, dividing the tests up into groups, doing a simple equality comparison between the string we want out and the code that should generate it:
;; Unit tests

(ns org.mired.bitchin.test
[:use [clojure.test] :only (is)]
[:use org.mired.bitchin.html])

(deftest inline-elements
(is (= "<p />" (p)) "empty contents")
(is (= "<map />" (map_)) "keyword tag")
(is (= "<p>now</p>" (p "now")) "string contents")
(is (= "<p>now</p>" (p 'now)) "symbol contents")
(is (= "<p>:now</p>" (p :now)) "keyword contents")
(is (= "<p><br /></p>" (p (br))) "element content")
(is (= "<p>now &amp;</p>" (p "now &")) "ampersand encoding")
(is (= "<p>now &lt;</p>" (p "now <")) "less than encoding")
(is (= "<p>now is the</p>" (p "now " 'is " the")) "multipart contents"))

(deftest block-elements
(is (= "<div />" (div)) "empty contents")
(is (= "<meta />" (meta_)) "keyword tag")
(is (= "<div>\nthen\n</div>" (div "then")) "string contents")
(is (= "<div>\nthen\n</div>" (div 'then)) "symbol contents")
(is (= "<div>\n:then\n</div>" (div :then)) "keyword contents")
(is (= "<div>\n<br />\n</div>" (div (br))) "element contents")
(is (= "<p>now &amp;</p>" (p "now &")) "ampersand encoding")
(is (= "<p>now &lt;</p>" (p "now <")) "less than encoding")
(is (= "<div>\nthen\nwas\nthe\n</div>" (div "then" 'was "the"))
"multipart contents"))

(deftest mixed-content
(is (= "<div>\n<span>now is the</span>\n<b>time</b>\n</div>"
(div (span "now is the") (b 'time)))))

(deftest attributes-block
(is (= "<div border=\"0\" />" (div {:border 0})) "keyword name")
(is (= "<div border=\"0\" />" (div {'border 0})) "symbol name")
(is (= "<div border=\"0\" />" (div {"border" 0})) "string name")
(is (= "<div width=\"100%\" />" (div {:width "100%"})) "string value")
(is (= "<div border=\"0\" width=\"100%\" />" (div {:border 0, 'width "100%"}))
"multiple attributes")
(is (= "<div border=\"0\" width=\"100%\">\n<span width=\"90%\">Text</span>\n</div>"
(div {"border" 0, :width "100%"} (span {'width "90%"} "Text")))
"multiple attributes with content"))

(deftest attributes-inline
(is (= "<span border=\"0\" />" (span {:border 0})) "keyword name")
(is (= "<span border=\"0\" />" (span {'border 0})) "symbol name")
(is (= "<span border=\"0\" />" (span {"border" 0})) "string name")
(is (= "<span width=\"100%\" />" (span {:width "100%"})) "string value")
(is (= "<span border=\"0\" width=\"100%\" />" (span {:border 0, 'width "100%"}))
"multiple attributes")
(is (= "<span class=\"now&amp;then\" />" (span {:class "now&then"}))
"ampersand encoding")
(is (= "<span class=\"&quot;FOO&quot;\" />" (span {:class "\"FOO\""}))
"quote encoding")
(is (= "<span class=\"x&gt;3\" />" (span {:class "x>3"}))
"greater than encoding")
(is (= "<span border=\"0\" width=\"100%\"><b width=\"90%\">Text</b></span>"
(span {"border" 0, :width "100%"} (b {'width "90%"} "Text")))
"multiple attributes with content"))

(run-tests 'org.mired.bitchin.test)