It's a keyword-driven framework, and the test data is kept in a CSV file. The test data look like this:
This is the classic Google Search example from the Watir documentation.
I used CSV because it's the simplest implementation. This is, after all, a toy. In the real world this might be HTML tables or wiki tables or a spreadsheet. What we want is for anyone to be able to write a test quickly and easily after a little bit of training on FireBug or the IE Developer Toolbar. (Test design is a different issue. Let's not talk about that now.)
The framework grabs the test data and interprets each row in order. A simple dispatcher invokes the correct execution method depending on what the first element in the row of test data is. This has two advantages.
For one thing, there are a finite number of types of page elements to deal with, so our methods to manipulate them will be limited in number.
For another thing, we can write custom test fixtures using our own special keywords, and make our own little DSL, like I did with the final row above that starts with "match".
Reporting pass/fail status is left as an exercise for the reader.
This simple framework could easily be the start of a real test automation project. It's robust, it scales well, and it can easily be customized, improved, and refactored over time.
[UPDATE: the comment below about leaking memory is interesting. I didn't say it explicitly in the original post, but I think test data (CSV files) should represent fewer than about 200 individual steps. My vision is that this script is invoked for a series of CSV files all testing different paths through the application, and each CSV file is handled by a new process. Paths through the application with more than about 200 steps make it much harder to analyze failures. The fewer the number of test steps, the better.]
class ExampleTest < Test::Unit::TestCase
@ie = Watir::IE.new
@command_array = 
File.open('watir_keywords.csv') do |file|
while line = file.gets
@command_array << line
@command_array.each do |comm|
args = comm.split(',')
#arguably 'case' would be nicer here than 'if', but this gets the job done.
if args == "goto"
elsif args == "text_field"
elsif args == "button"
else args == "match"