Tuesday, June 30, 2009

Adding Items to the RSS Feed

‹prev | My Chain | next›

I pick up with adding the 10 most recent meals to the site's RSS feed. First, I have to pull back the list of the 10 most recent IDs from CouchDB. Thankfully, I did something similar for the homepage, so I can re-use here:
      it "should request the 10 most recent meals from CouchDB" do
RestClient.
should_receive(:get).
with(/by_date.+limit=10/).
and_return('{"rows": [] }')

get "/main.rss"
end
To implement, I add this to the get '/main.rss' Sinatra application:
  url = "#{@@db}/_design/meals/_view/by_date?limit=10&descending=true"
data = RestClient.get url
@meal_view = JSON.parse(data)['rows']
With that example safely passing, I need to ensure that the full details are pulled back for those 10 meals. Again, I copy from the site's home page example to stub out the RestClient call that pulls back the 10 meal IDs. Then I specify that those 10 IDs need to be used to look up the full meal details for each corresponding meal. This leads to something of a longish example:
      it "should pull back full details for the 10 meals" do
RestClient.
stub!(:get).
and_return('{"rows": [' +
'{"key":"2009-06-10","value":["2009-06-10","Foo"]},' +
'{"key":"2009-06-09","value":["2009-06-09","Foo"]},' +
'{"key":"2009-06-08","value":["2009-06-08","Foo"]},' +
'{"key":"2009-06-07","value":["2009-06-07","Foo"]},' +
'{"key":"2009-06-06","value":["2009-06-06","Foo"]},' +
'{"key":"2009-06-05","value":["2009-06-05","Foo"]},' +
'{"key":"2009-06-04","value":["2009-06-04","Foo"]},' +
'{"key":"2009-06-03","value":["2009-06-03","Foo"]},' +
'{"key":"2009-06-02","value":["2009-06-02","Foo"]},' +
'{"key":"2009-06-01","value":["2009-06-01","Foo"]}' +
']}')

RestClient.
should_receive(:get).
with(/2009-06-/).
exactly(10).times.
and_return('{"title":"foo",' +
'"date":"2009-06-17",' +
'"summary":"foo summary",' +
'"menu":[]}')

get "/main.rss"
end
end
To get that example to pass, still using earlier RSS-specific examples, I use this code:
get '/main.rss' do
content_type "application/rss+xml"

url = "#{@@db}/_design/meals/_view/by_date?limit=10&descending=true"
data = RestClient.get url
@meal_view = JSON.parse(data)['rows']

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Meals"
maker.channel.link = ROOT_URL
maker.channel.description = "Meals from a Family Cookbook"
@meal_view.each do |couch_rec|
data = RestClient.get "#{@@db}/#{couch_rec['key']}"
meal = JSON.parse(data)
maker.items.new_item do |item|
item.title = meal['title']
end
end

end

rss.to_s
end
Rather than driving the complete RSS implementation via RSpec examples, at this point I am going to move back to the Cucumber scenario so that I can use real data with the complete stack.

Recall the Cucumber scenario so far:



I implement the visit the feed URL with a simple Webrat visit:
When /^I access the meal RSS feed$/ do
visit('/main.rss')
response.should be_ok
end
That passes without any changes thanks to the RSpec driven work inside the Sinatra application.

To verify complete implementation of the next step, that the 10 most recent meals should be included in the RSS feed, I first check the old feed on the legacy Rails site:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>EEE Cooks: Meals</title>
<link>http://www.eeecooks.com/meals/rss</link>
<description>Meals from a Family Cookbook</description>
<language>en-us</language>
<item>
<title>Star Wars: The Dinner</title>
<description><p>Inspired by one of the many <a href="http://www.cooksillustrated.com/">magazines</a> that Robin reads, we do a little grilling today. It&#8217;s been a while, so we were pleasantly surprised that the grill even started up, let alone that we were able to cook up such yummy food.</p></description>
<pubDate>Sun, 13 Jul 2008 00:00:00 +0000</pubDate>
<link>http://www.eeecooks.com/meals/2008/07/13</link>
<guid>http://www.eeecooks.com/meals/2008/07/13</guid>
</item>
<item>
<title>You Each Take a Side</title>
...
First, up, there should be 10 <item> children of <channel>, each with a <title> child. I should also verify that the RSS item titles contain the expected values (e.g. "Meal 0", "Meal 1", etc). I can specify this in the example with:
Then /^I should see the 10 most recent meals$/ do
response.
should have_selector("channel > item > title",
:count => 10)
response.
should have_selector("channel > item > title",
:content => "Meal 0")

end
Next, I implement the see-the-summary-in-the-RSS feed step with:
Then /^I should see the summary of each meal$/ do
response.
should have_selector("channel > item > description",
:content => "meal summary")
end
That fails initially because I did not have that attribute added to the RSS::Maker call, so I add it:
  rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Meals"
maker.channel.link = ROOT_URL
maker.channel.description = "Meals from a Family Cookbook"
@meal_view.each do |couch_rec|
data = RestClient.get "#{@@db}/#{couch_rec['key']}"
meal = JSON.parse(data)
maker.items.new_item do |item|
item.title = meal['title']
item.description = meal['summary']
end
end
To get the remaining attributes added to the RSS feed, I add two steps:
    And I should see a meal link
And I should see a meal published date
I will get those last two steps working tomorrow.

Monday, June 29, 2009

Quick Start on RSS Feeds

‹prev | My Chain | next›

Moving on to the next Cucumber feature, I need to implement RSS feeds. First up is the RSS feed for the meals:



It is nice that, once a project has reached a certain point, one or two steps are already defined when starting on a new feature or scenario. The "Given" step of 20 yummy meals is already working, so I am able to get right down to business of accessing the RSS feed. Since I have yet to define that action in the Sinatra application, I am done with my outside, Cucumber work and am ready to dive into the inside, driven by RSpec work.

In the legacy Rails app (and the legacy baked-from-XML version before it), the RSS feed was named main.rss. To keep disruption to a minimum, I will keep that name, which means that I first drive RSS implementation with this example:
  describe "GET /main.rss" do
it "should respond OK" do
get "/main.rss"
last_response.should be_ok
end
end
Implementation of this simple example is predictably simple:
get '/main.rss' do
""
end
Before switching to a RSS builder library, I implement a simple string bare bones RSS feed. This is a simpler step than going straight for the RSS builder. Simpler, smaller steps make for fewer things that can go wrong moving to subsequent steps, making for a smoother overall development process.

The example that I use to drive the bare bones RSS feed is:
    it "should be the meals rss feed" do
get "/main.rss"
last_response.
should have_selector("channel title",
:content => "EEE Cooks: Meals")
end
And the bare bones RSS feed that implements this example is:
get '/main.rss' do
"<channel><title>EEE Cooks: Title</title></channel>"
end
With that example passing, I will give the built-in ruby library RSS:Maker a try for building the real RSS feed. I convert the bare bones string version to:
get '/main.rss' do
content_type "application/rss+xml"

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Title"
end

rss.to_s
end
When I run the spec, however, I get this failure:
2)
RSS::NotSetError in 'eee GET /main.rss should be the meals rss feed'
required variables of maker.channel are not set: link, description
./eee.rb:40:in `GET /main.rss'
OK, I guess I can understand that those two attributes are needed, so I add them:
get '/main.rss' do
content_type "application/rss+xml"

rss = RSS::Maker.make("2.0") do |maker|
maker.channel.title = "EEE Cooks: Title"
maker.channel.link = "http://www.eeecooks.com/"
maker.channel.description = "Meals from a Family Cookbook"
end

rss.to_s
end
Which makes the spec pass again.

That is a good stopping point for today. Tomorrow I will build up the items in the RSS feed by pulling them from CouchDB.

Sunday, June 28, 2009

Feedback on Meals & Recipes

‹prev | My Chain | next›

I start today with a note about yesterday's work: when URL encoding in Sinatra, use Rack::Utils.escape. It works and it will encode in the same way that Rack::Test would expect things to be encoded. Maybe an obvious suggestion, but with the myriad of URL encoding options available in the Ruby world, it seemed one worth noting.

I used Rack::Utils.escape in the meal feedback link:
%div
%a{:href => "/feedback?url=#{Rack::Utils.escape(@url)}&subject=#{Rack::Utils.escape("[Meal] " + @meal['title'])}"} Send us feedback on this meal
Yesterday, I used the subject and url query parameters in that link to pre-populate the feedback form. Today, I need to ensure that the url parameter, when submitted by the feedback form to the "send us email" action does something. Specifically, it ought to include the URL in the email that we receive.

Unfortunately, because the email body is only one attribute to the Pony.mail call, I have to specify the entire body of the email message. I can use RSpec's hash_including so that I do not have to specify all of the Pony.mail attributes, but I still need the entire message body:
    it "should include the URL (if supplied) in the email" do
message = <<"_EOM"
From: from
Email: email

Message

URL: http://example.org/
_EOM

Pony.
should_receive(:mail).
with(hash_including(:body => message))

post "/email",
:name => "from",
:email => "email",
:subject => "Subject",
:message => "Message",
:url => "http://example.org/"
end
I get that and the previous non-URL example passing with this email action:
post '/email' do
message = <<"_EOM"
From: #{params[:name]}
Email: #{params[:email]}

#{params[:message]}
_EOM

message << "\nURL: #{params[:url]}\n" if params[:url]

Pony.mail(:to => "us _at_ eeecooks.com".gsub(/\s*_at_\s*/, '@'),
:subject => params[:subject],
:body => message)

haml :email
end
With that, I am ready to work my way back out to the Cucumber scenario to mark it as complete:



I implement the last step, that the form's subject should be pre-filled, with:
Then /^I should see a subject of "([^\"]*)"$/ do |subject|
response.should have_selector("input",
:value => subject)
end
And now, I have the entire meal feedback scenario complete:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -n \
-s "Give feedback to the authors on a yummy meal"
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Site

So that I may explore many wonderful recipes and see the meals in which they were served
As someone interested in cooking
I want to be able to easily explore this awesome site

Scenario: Give feedback to the authors on a yummy meal
Given a "Yummy" meal enjoyed on 2009-06-27
When I view the "Yummy" meal
And I click "Send us feedback on this meal"
Then I should see a feedback form
And I should see a subject of "[Meal] Yummy"

1 scenario
5 passed steps
After driving-by-example a similar feedback link in the recipe.haml template, I can mark the recipe-feedback scenario as complete as well:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -n \
> -s "Send compliments to the chef on a delicious recipe"
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Site

So that I may explore many wonderful recipes and see the meals in which they were served
As someone interested in cooking
I want to be able to easily explore this awesome site

Scenario: Send compliments to the chef on a delicious recipe
Given a recipe for Curried Shrimp
When I view the recipe
And I click "Send us feedback on this recipe"
Then I should see a feedback form
And I should see a subject of "[Recipe] Curried Shrimp"

1 scenario
5 passed steps
Not bad—two scenarios done in one night. Up next: RSS Feeds.

Saturday, June 27, 2009

Only Slightly More Sophisticated Feedback

‹prev | My Chain | next›

Now that I have the simple feedback Cucumber scenario working, I need to complete two more scenarios:
  Scenario: Give feedback to the authors on a yummy meal

Scenario: Send compliments to the chef on a delicious recipe
Actually, I need to define these scenarios, before I can implement them. I would much prefer to re-use steps rather than adding new ones, so I grep through my Given steps to see what I might use:
cstrom@jaynestown:~/repos/eee-code$ grep Given features/step_definitions/*
features/step_definitions/meal_details.rb:Given /^a "([^\"]*)" meal enjoyed [io]n (.+)$/ do |title, date_str|
features/step_definitions/meal_details.rb:Given /^a "([^\"]*)" meal with the "([^\"]*)" recipe on the menu$/ do |title, recipe_title|
The date does not really matter, but I opt for the former since it reads OK in the scenario. After picking and choosing the remainder of the scenario steps, I end up with:
  Scenario: Give feedback to the authors on a yummy meal

Given a "Yummy" meal enjoyed on 2009-06-27
When I view the "Yummy" meal
And I click "Send us feedback on this meal"
Then I should see a feedback form
And I should see a subject of "[Meal] Yummy"
And something similar for the recipe feedback scenario:
  Scenario: Send compliments to the chef on a delicious recipe

Given a "Yummy" recipe from 2009-06-27
When I view the recipe
And I click "Send us feedback on this recipe"
Then I should see a feedback form
And I should see a subject of "[Recipe] Yummy"
One other thing that ought to happen in both scenarios is that the recipe/meal should supply the feedback form with its URL so that the email that we receive includes the URL. We prefer having the URL so that we can click it to fix any mistakes. I do not include that in the scenario since it is written from the user's perspective—mixing perspectives can lead to unnecessary conflicts. Instead I will try to remember to do this as I work inside the code.

First up, I run the scenario to see what else needs to be implemented:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -n \
-s "Give feedback to the authors on a yummy meal"
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Site

So that I may explore many wonderful recipes and see the meals in which they were served
As someone interested in cooking
I want to be able to easily explore this awesome site

Scenario: Give feedback to the authors on a yummy meal
Given a "Yummy" meal enjoyed on 2009-06-27
When I view the "Yummy" meal
And I click "Send us feedback on this meal"
Could not find link with text or title or id "Send us feedback on this meal" (Webrat::NotFoundError)
features/site.feature:60:in `And I click "Send us feedback on this meal"'
Then I should see a feedback form
And I should see a subject of "[Meal] Yummy"

1 scenario
1 failed step
1 skipped step
1 undefined step
2 passed steps

You can implement step definitions for missing steps with these snippets:

Then /^I should see a subject of "([^\"]*)"$/ do |arg1|
pending
end
Not too bad, I just need to get two step defined.

To drive the meal template's inclusion of the subject and URL I just this rather ugly bit of RSpec:
  it "should include the recipe's URL in the feedback link" do
render("/views/meal.haml")
response.should have_selector("a",
:href => "/feedback?url=http%3A%2F%2Fexample.org%2Frecipe-1&subject=%5BMeal%5D+Meal+Title",
:content => "Send us feedback on this meal")
end
I need to escape the URL and subject, so the example needs to account for this. Once that is implemented, I have only one more step to go:



I will pick up tomorrow with that last step and doing the same for recipes.

Friday, June 26, 2009

How Many Ways Can I Screw Up a Simple Form?

‹prev | My Chain | next›

Having built my feedback form and the action to which it submits, I am ready to work my way back out to the Cucumber scenario driving this particular feature:



Telling Webrat to click the "submit" button is not specific enough. Webrat needs a value or id attribute. So I will rewrite that step as:
When I click the "Send Comments" button
I can define that step with:
When /^click the "([^\"]+)" button$/ do |button_text|
click_button button_text
end
When I run this scenario, I find:
  Scenario: Give feedback to the authors of this fantastic site
Given 25 yummy meals
When I view the site's homepage
And I click "Send us comments"
Then I should see a feedback form
When I fill out the form with effusive praise
And click the "Send Comments" button
Could not find button "Send Comments" (Webrat::NotFoundError)
features/site.feature:53:in `And click the "Send Comments" button'
Then I should see a thank you note
Hunh. Cannot find the "Send Comments" button? I was sure I had that in the Haml template:
...
%label
Message
%textarea{:name => "message", :rows => 8, :cols => 55}
%input{:type => "submit", :name => "Send Comments"}
Ah. I gave the button a name, but not a value. Another obvious mistake on my part caught by Cucumber. I give the button a proper name:
...
%label
Message
%textarea{:name => "message", :rows => 8, :cols => 55}
%input{:type => "submit", :name => "b", :value => "Send Comments"}
And then I re-run the Cucumber scenario, only to find:
  Scenario: Give feedback to the authors of this fantastic site
Given 25 yummy meals
When I view the site's homepage
And I click "Send us comments"
Then I should see a feedback form
When I fill out the form with effusive praise
And I click the "Send Comments" button
PATH_INFO must start with / (Rack::Lint::LintError)
(eval):7:in `get'
features/site.feature:53:in `And I click the "Send Comments" button'
Then I should see a thank you note
This error I do not quite understand. If I am working in the top level URL space of a site (e.g. http://eeecooks.com/feedback), then I expect any form submitted without an absolute URL to be relative to the top-level namespace (i.e. email should be submitted to http://eeecooks.com/email). No matter, I change:
%form{:action => "email"}
to:
%form{:action => "/email"}
And finally that step is passing. The last step is to verify that the user now sees a "thanks for the feedback" message. It is a step so simple that it almost seems not worth bothering. Still, it cannot hurt to define, especially since the step is so easy to define:
Then /^I should see a thank you note$/ do
response.should have_selector("h1", :content => "Thank You")
end
Finally, I get to see my simple feedback scenario is all of its glory:
  Scenario: Give feedback to the authors of this fantastic site
Given 25 yummy meals
When I view the site's homepage
And I click "Send us comments"
Then I should see a feedback form
When I fill out the form with effusive praise
And I click the "Send Comments" button
Then I should see a thank you note
expected following output to contain a <h1>Thank You</h1> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html><body><h1>Not Found</h1></body></html>
(Spec::Expectations::ExpectationNotMetError)
features/site.feature:54:in `Then I should see a thank you note'
Aaarrrgh! Not found?! How can that be?

After much soul searching and questioning of my ability as a developer, I finally ask myself how could it not be found? It is clearly defined in my Sinatra application:
post '/email' do
message = <<"_EOM"
From: #{params[:name]}
Email: #{params[:email]}

#{params[:message]}
_EOM

Pony.mail(:to => "us _at_ eeecooks.com".gsub(/\s*_at_\s*/, '@'),
:subject => params[:subject],
:body => message)

haml :email
end
The answer, of course, is in the first word of that code snippet: post. The /email action is only defined for POST actions, not GETs. Changing the form action definition in the Haml template will resolve everything:
%form{:action => "/email", :method => "post"}
Finally, I get:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -n \
-s "Give feedback to the authors of this fantastic site"
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Site

So that I may explore many wonderful recipes and see the meals in which they were served
As someone interested in cooking
I want to be able to easily explore this awesome site

Scenario: Give feedback to the authors of this fantastic site
Given 25 yummy meals
When I view the site's homepage
And I click "Send us comments"
Then I should see a feedback form
When I fill out the form with effusive praise
sendmail: 553 jaynestown.mail.rr.com does not exist
And I click the "Send Comments" button
Then I should see a thank you note

1 scenario
7 passed steps
Oops. An actual email is trying to go out there. If I had sendmail configured correctly, I would be spamming myself right now.

Cucumber is an awesome full-stack testing tool, but, at least in this case, I do not want to test the entire stack. What I want to do is stub out the Pony.mail call right before the form is submitted:
When /^I click the "([^\"]*)" button$/ do |button_text|
# Don't send email in tests
Pony.stub!(:mail)
click_button button_text
end
That will not work because Cucumber don't do stubbing:
  Scenario: Give feedback to the authors of this fantastic site
Given 25 yummy meals
When I view the site's homepage
And I click "Send us comments"
Then I should see a feedback form
When I fill out the form with effusive praise
And I click the "Send Comments" button
undefined method `stub!' for Pony:Module (NoMethodError)
features/site.feature:53:in `And I click the "Send Comments" button'
Then I should see a thank you note
Cucumber really is a full stack testing framework. Fortunately, Bryan Helmkamp has written some nice instructions for including RSpec stubbing and mocking in Cucumber. Following those instructions, I add this to my Cucumber's support/env.rb:
# RSpec mocks / stubs
require 'spec/mocks'

Before do
$rspec_mocks ||= Spec::Mocks::Space.new
...
end

After do
begin
$rspec_mocks.verify_all
ensure
$rspec_mocks.reset_all
end
...
And now, I am done with this scenario:



Tomorrow, I will likely add another small feature to the feedback mechanism before moving onto RSS feeds.

Thursday, June 25, 2009

Ponies and Filling out Forms with Webrat

‹prev | My Chain | next›

Following up on yesterday's simple form-by-example, today I get to fill out that form with Webrat.

Yay! Good clean fun.

In the site feedback Cucumber scenario, I am seeing the feedback form. Next up is "When I fill out the form with effusive praise", which I can define with:
When /^I fill out the form with effusive praise$/ do
fill_in "Name", :with => "Bob"
fill_in "Email", :with => "foo@example.org"
fill_in "Subject", :with => "Your site is awesome!"
fill_in "Message", :with => "The recipes are delicious and your use of CouchDB is impressive."
end
That's it! Webrat is just awesome.



This works because I have wisely wrapped my <label> tags around the input fields:
  <form action='email'>
<label>
Name
<input name='name' size='30' type='text' />
</label>

<label>
Email
<input name='email' size='30' type='text' />
</label>
<label>
Subject
<input name='subject' size='50' type='text' />
</label>
<label>
Message
<textarea cols='55' name='message' rows='8'></textarea>

</label>
<input name='Send Comments' type='submit' />
</form>
For the uninitiated, <label> tags not only cue Webrat which form fields to complete, but also activate the HTML form field when the label text is clicked—especially handy when clicking those tiny radio buttons or checkboxes.

I could also have added id attributes to the various form fields (e.g. <input id="email-input" type="text"…/>) and then done the label with a for attribute (e.g. <label for="email-input">Email</label>). That would have the same effect in most browsers, but would have the added benefit of working in Internet Explorer (wrapping <label> tags does not work in Internet Explorer), but why make life easier for Internet Explorer users?

With that step marked as complete, it is time to do something in response to submitting the feedback form. I drive-by-example a simple "Thanks for the feedback" page. Before I am really done, I need to actually do something with the feedback. In the various legacy versions of EEE Cooks, all that we do with the feedback is send an email. According to the Sinatra FAQ, that means Pony:
cstrom@jaynestown:~/repos/eee-code$ gem install pony
Building native extensions. This could take a while...
Successfully installed tmail-1.2.3.1
Successfully installed pony-0.3
2 gems installed
Pony is a great little gem—it does just enough to make sending email simple. The RSpec example that I use to drive sending email is:
    it "should send us an email" do
Pony.
should_receive(:mail).
with(hash_including(:subject => "Subject"))

post "/email",
:name => "Bob",
:subject => "Subject",
:message => "Feedback message."
end
The actual implementation:
post '/email' do
message = <<"_EOM"
From: #{params[:name]}
Email: #{params[:email]}

#{params[:message]}
_EOM

Pony.mail(:to => "us _at_ eeecooks.com".gsub(/\s*_at_\s*/, '@'),
:subject => params[:subject],
:body => message)

haml :email
end
That is a good stopping point for today. Tomorrow, I will work my way back out to the Cucumber scenario.

Wednesday, June 24, 2009

Feedback

‹prev | My Chain | next›

The next Cucumber scenario to implement is a somewhat simple feedback form. The very first incarnation of the site was static HTML baked from XML (even many of the search pages were baked). As such, we could not easily do in-page comments (pre-dated prototype.js and jQuery).

Even today, we still prefer the feedback form rather than on-page comments. It is amazing how rude some people can be when commenting on a family cookbook.

The scenario so far:
cstrom@jaynestown:~/repos/eee-code$ cucumber features -n \
-s "Give feedback to the authors of this fantastic site"
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Site

So that I may explore many wonderful recipes and see the meals in which they were served
As someone interested in cooking
I want to be able to easily explore this awesome site

Scenario: Give feedback to the authors of this fantastic site
Given 25 yummy meals
When I view the site's homepage
And I click "Send us comments"
Could not find link with text or title or id "Send us comments" (Webrat::NotFoundError)
features/site.feature:50:in `And I click "Send us comments"'
Then I should see a feedback form
When I fill out the form with effusive praise
And click the submit button
Then I should see a thank you note

1 scenario
1 failed step
4 undefined steps
2 passed steps
There is nothing in the way of behavior in the addition of a link to a feedback form. Still there is some semantic information in how this should be added to the homepage, so an example is in order. The feedback link should be included as part of an "About Us" section in the right-hand column of the homepage:
  it "should have a recipe search section" do
render("/views/index.haml")
response.should have_selector(".rhc h1",
:content => "About Us")
end
I implement this, including the link to the feedback form with the following Haml:
  .home-section
%h1 About Us
%p We are a young family living in the Baltimore area. One day we decided to put our recipe box online. This is our family cookbook
%p
%a{:href => "/feedback"} Send us comments
That gets the "Send us comments" step passing:



The next steps in the Cucumber scenario describe the feeback page, which does not yet exist. That means it is time to start working my way back into the code. First, the Sinatra application needs to respond to a feedback URL:
  describe "GET /feedback" do
it "should respond OK" do
get "/feedback"
last_response.should be_ok
end
end
Initially, I implement that with an empty string response (as the simplest thing that can work):
get '/feedback' do
""
end
I quickly replace that with a call to haml :feedback, which will need to be driven by example. The first example requires a name field for the feedback form:
  it "should include a field for the user's name" do
render("/views/feedback.haml")
response.should have_selector("label",
:content => "Name")
end
The first time I run the spec, I get an error:
1)
Errno::ENOENT in 'feedback.haml should include a field for the user's name'
No such file or directory - ./views/feedback.haml
/home/cstrom/repos/eee-code/spec/spec_helper.rb:25:in `read'
/home/cstrom/repos/eee-code/spec/spec_helper.rb:25:in `render'
./spec/views/feedback.haml_spec.rb:5
I change the message by creating a feedback.haml template:
cstrom@jaynestown:~/repos/eee-code$ touch views/feedback.haml
cstrom@jaynestown:~/repos/eee-code$ spec ./spec/views/feedback.haml_spec.rb
F

1)
'feedback.haml should include a field for the user's name' FAILED
expected following output to contain a tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">

./spec/views/feedback.haml_spec.rb:6:

Finished in 0.008351 seconds
Finally, I am done changing the message, now I can make it pass:
%label
Name
%input{:type => "text", :name => "name"}
I follow a similar path to get email address, subject and message fields on this form.

I will call it a night at that point. I will pick up tomorrow implementing the form handler and moving closer to being complete with this scenario.

Before shutting down, I will mark my progress by defining the should-see-a-feedback-form step:
Then /^I should see a feedback form$/ do
response.should have_selector("h1", :content => "Feedback")
response.should have_selector("form")
end

Tuesday, June 23, 2009

Not Much of a Scenario

‹prev | My Chain | next›

With a little bit of cleanup behind, it's on to the next Cucumber scenario—exploring food categories from the homepage. The whole scenario:



Nice! Out of 12 steps in this scenario, 4 are already passing and 2 more are defined, but not yet reached.

The first undefined step in the scenario, clicking on the Italian category on the homepage, describes something missing on the homepage—category links. So, it is time for an RSpec example for the Haml template:
  it "should link to the listing of Italian recipes" do
render("/views/index.haml")
response.should have_selector("a",
:content => "Italian")
end
I implement that with a call to the categories helper, which links to the Italian category, as well as the other major categories on the site.

That is sufficient to implement the next step, "When I click the Italian category". Hey, that seems vaguely familiar. Have I already done something similar? In fact, I have:
When /^I click on the Italian category$/ do
click_link "Italian"
end
Hmm… I do not particularly care for the "on" that is currently in there. I think that it reads better without it, so I remove it from the text of an earlier scenario and from the step definition. While I am re-working that step anyway, I note that I will soon need to verify that the user can click the "Breakfast" category. To get the step passing for both categories, I use a Regexp matcher:
When /^I click the (\w+) category$/ do |category|
click_link category
end
With that step passing, I am up to the "Then I should see 20 recipes" step. Again, this seems familiar. Looking through the old recipe search feature step definitions, I find:
Then /^I should see (\d+) results$/ do |count|
response.should have_selector("table td a", :count => count.to_i)
end
That is just what I want! Changing the scenario text from "Then I should see 20 recipes" to "Then I should see 20 results", I find:
  Scenario: Exploring food categories (e.g. Italian) from the homepage # features/site.feature:31
Given 25 yummy meals # features/step_definitions/site.rb:1
And 50 Italian recipes # features/step_definitions/recipe_search.rb:119
And 10 Breakfast recipes # features/step_definitions/recipe_search.rb:119
When I view the site's homepage # features/step_definitions/site.rb:84
And I click the Italian category # features/step_definitions/site.rb:128
Then I should see 20 results # features/step_definitions/recipe_search.rb:241
expected following output to contain a <table td a/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head><title>EEE Cooks</title></head>
<html><body>
<div id="header">
<div id="eee-header-logo">
<a href="/">
<img alt="Home" src="/images/eee_corner.png"></a>
</div>
</div>
<ul id="eee-categories">
<li><a href="/recipes/search?q=category:italian">Italian</a></li>
<li><a href="/recipes/search?q=category:asian">Asian</a></li>
<li><a href="/recipes/search?q=category:latin">Latin</a></li>
<li><a href="/recipes/search?q=category:breakfast">Breakfast</a></li>
<li><a href="/recipes/search?q=category:chicken">Chicken</a></li>
<li><a href="/recipes/search?q=category:fish">Fish</a></li>
<li><a href="/recipes/search?q=category:meat">Meat</a></li>
<li><a href="/recipes/search?q=category:salad">Salad</a></li>
<li><a href="/recipes/search?q=category:vegetarian">Vegetarian</a></li>
<a>Recipes</a>
</ul>
<form action="/recipes/search" method="get">
<input maxlength="2048" name="query" size="31" type="text" value="category:italian"><input name="s" type="submit" value="Search">
</form>
<p class="no-results">
No results matched your search. Please refine your search
</p>
Aw nuts! And I was on such a roll…

Upon further inspection, I note that there were no matches on the Italian recipe category. The source of the trouble was the implementation of the "Given 50 Italian recipes" step. That step, as shown in the Cucumber output, was defined for the recipe search feature. It was defined without categories. The text after the number ("Italian") was used to build the recipe title, not to assign categories. Since that step made no use of the categories / tag_names, I am free to make use of it now:
Given /^(\d+) (.+) recipes$/ do |count, keyword|
date = Date.new(2009, 4, 22)

(1..count.to_i).each do |i|
permalink = "id-#{i}-#{keyword.gsub(/\W/, '-')}"

recipe = {
:title => "#{keyword} recipe #{i}",
:date => date,
:preparations => [
{ 'ingredient' => { 'name' => 'ingredient' } }
],
:tag_names => [keyword.downcase]

}

RestClient.put "#{@@db}/#{permalink}",
recipe.to_json,
:content_type => 'application/json'
end
end
After making similar adjustments for the next several steps, I am on the last step of this scenario—when I search for a category that only has 10 recipes (i.e. is less that the search page size), then I should see no more result pages. I define this step with the assumption that, in such a case, the "Next" link should be inactive:
Then /^I should see no more pages of results$/ do
response.should have_selector(".inactive", :content => "Next »")
end
And, just like that, I have another scenario complete:



There are still a few more scenarios in need of implementation, but I am getting very close to deploying.

Monday, June 22, 2009

More Affordances

‹prev | My Chain | next›

Today I continue working on affordances that I have built up in the legacy version of the site over the years. I made progress yesterday, but still have a few remaining:

  • Meals on odd numbered days have right-aligned thumbnails on the homepage (added "depth")

  • Text links to older meals at the bottom of the meals section of the homepage

  • Category links on all pages (I only have them on the meal and recipe pages)

Even/Odd Meal Thumbnail Alignment

I will accomplish the actual alignment via CSS (as opposed to an align attribute on the thumbnail's <img> tag). To get the odd/even day of the month to drive the alignment of the thumbnail, I only need a simple modulo 2 of the day:
    %div{:class => "meal meal#{date.day % 2}"}
No tests are needed for this—there is no semantic information or behavior being driven. Just a little something to break up the monotony of the homepage.

Older Meals

In addition to the 10 meal summaries on the homepage, we also provide 3 additional links to older meals. In the Sinatra application, I am already pulling back 13 meals:
  url = "#{@@db}/_design/meals/_view/by_date?limit=13&descending=true"
data = RestClient.get url
@meal_view = JSON.parse(data)['rows']
I had been ignoring the last three meals. Instead, I pull back all 13 meals and then slice the last 3 from the list:
  @older_meals = @meals.slice!(10,3) || []
With the Sinatra app pulling back the older meals, it is time to drive the homepage view. The @older_meals instance variable should hold meals such as:
      assigns[:older_meals] =
[{ "_id" => "2009-04-15",
"date" => "2009-04-15",
"title" => "Foo"
}] * 3
Given that, the homepage should include links to those meals:
    it "should link to the next 3" do
render("/views/index.haml")
response.should have_selector("a",
:href => "/meals/2009/04/15",
:content => "Foo")
end
I implement that in Haml with:
  .other-meals
%i Older meals:
- @older_meals.each do |meal|
- date = Date.parse(meal['date'])
%a{:href => date.strftime("/meals/%Y/%m/%d")}= meal["title"]
,

Universal Category Links

Last up today is adding category links to all pages other than the homepage. Happily, that is a simple matter of adding the categories helper to the top of a few Haml templates.

That does it for affordances for now. Tomorrow, it will be back onto the last few Cucumber scenarios.
(commit)

Sunday, June 21, 2009

Cucumber Don't Do Affordances

‹prev | My Chain | next›

Before moving onto the next Cucumber scenario, I take a moment to see if the previous scenario missed anything. I think it likely covered just about all of the necessary behavior needed. But the legacy Rails site (and the previous incarnation as an XML-based CMS) built up many affordances that I would prefer not to lose in translation.

The homepage in Webrat save_and_open_page form:



The homepage on the legacy Rails site:



Naked CSS and missing images aside, it looks as though I am missing:

  • The date of the meals

  • A "Read more..." link for the meal

  • Only linking to new recipes, not the entire menu

  • Meals on odd numbered days have right-aligned thumbnails (added "depth")

  • Text links to older meals at the bottom of the meals section

Working through that list one at a time, first up is this RSpec example:
  it "should include a pretty date for each meal" do
render("/views/index.haml")
response.should have_selector(".meals", :content => "May 15")
end
This can be implemented with:
    %div= date.strftime("%B %e")
The next affordance to be added is:
  it "should suggest that the user read more..." do
render("/views/index.haml")
response.should have_selector(".meals a",
:href => "/meals/2009/05/15",
:content => "Read more…")
end
A simple link will make that example pass.

Only including new recipes rather than the whole meal menu is a bit trickier. The examples require building up the menu, which makes for longer examples.

One or more new recipes (the meal was prepared on 2009-05-15, new recipes would be from the same day) should be comma separated:
  it "should include a comma separated list of menu items" do
stub!(:recipe_link).
and_return(%Q|<a href="/recipes/2009/05/15/recipe1">chips</a>|,
%Q|<a href="/recipes/2009/05/15/recipe2">salsa</a>|)

assigns[:meals][0]["menu"] << "[recipe:2009/05/15/recipe1]"
assigns[:meals][0]["menu"] << "[recipe:2009/05/15/recipe2]"

render("/views/index.haml")
response.should have_selector(".menu-items",
:content => "chips, salsa")
end
Old recipes and non-recipes should not be included on the homepage:
  it "should only include new recipe menu items" do
stub!(:recipe_link).
and_return(%Q|<a href="/recipes/2009/05/15/recipe1">chips</a>|)

assigns[:meals][0]["menu"] << "[recipe:2009/05/14/recipe1]"
assigns[:meals][0]["menu"] << "chili"
assigns[:meals][0]["menu"] << "[recipe:2009/05/15/recipe2]"

render("/views/index.haml")
response.should have_selector(".menu-items",
:content => "chips")
end
No new recipes should produce nothing:
  it "should not include a delimiter between \"Read more\" and new recipes when no new recipes" do
render("/views/index.haml")
response.should_not have_selector(".meals",
:content => "|")

end
I implement these three examples with this Haml code:
    - new_recipe_regexp = Regexp.new(date.strftime("%Y/%m/%d"))
- new_recipes = meal["menu"].select{ |r| r =~ new_recipe_regexp }
(
%a{:href => date.strftime("/meals/%Y/%m/%d")}
Read more&hellip;
- if new_recipes.length > 0
|
.menu-items= wiki(new_recipes.join(", "))
)
In that code snippet, I store a regular expression built from the meal's current date. I use that regular expression to select all recipes from the menu that match the current date. As long as there are new recipes, a "pipe" delimiter is inserted between the link to the current meal and to the new recipes. Finally, the wiki helper converts the wiki recipe text to HTML links.

I will stop there for today and pick up with the last two missing affordances tomorrow.

Saturday, June 20, 2009

There and Back Again: A User's Journey from the Homepage Complete

‹prev | My Chain | next›

The next step in the exploration-from-the-homepage Cucumber scenario is returning back to the homepage. Amazingly, I have made it through three and a half months of my chain without a site-wide layout. That will need to change in order to get this next step passing.

Haml layouts in Sinatra are designated by creating a layout.haml file. The following simple layout should do the trick:
%html
%title= ["EEE Cooks", @title].compact.join(": ")
%body
#header
#eee-header-logo
%a{:href => "/"}
%img{:alt => "Home", :src => "/images/eee_corner.png"}

= yield

#footer
Before defining the click-logo-to-return-home step, I re-run the whole Cucumber scenario to make sure the layout not break anything. Unfortunately, the earlier step that checked the number of meal thumbnail images on the homepage is now failing:
   When I view the site's homepage
Then I should see the 10 most recent meals prominently displayed
And the prominently displayed meals should include a thumbnail image
expected following output to contain a <img/> tag:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head><title>EEE Cooks</title></head>
<html><body>
<div id="header">
<div id="eee-header-logo">
<a href="/">
<img alt="Home" src="/images/eee_corner.png"></a>
</div>
</div>
<div class="meals">
<h1>Meals</h1>
<a href="/meals/2009/06/11">
<img height="150" width="200" src="/images/2009-06-11/meal0.jpg"></a>
<h2>
<a href="/meals/2009/06/11">Meal 0</a>
</h2>
<p>meal summary</p>
...
The thumbnails are still there, so what gives? The failing Cucumber scenario is currently defined as:
Then /^the prominently displayed meals should include a thumbnail image$/ do
response.should have_selector("img", :count => 10)
end
Ah, rather than verifying that there are 10 meal thumbnail images, I am trying to verify that there are 10 images on the entire homepage. The addition of the site logo has upped the number of images on the homepage to 11. A CSS selector can provide the necessary context to only check meal thumbnails:
Then /^the prominently displayed meals should include a thumbnail image$/ do
response.should have_selector(".meals img", :count => 10)
end
The additional context in the scenario step gets it passing, so now I can try defining the click-the-logo step:
When /^I click the site logo$/ do
click_link "/"
end
That link is now on the page, thanks to the newly added layout, so this step is passing.

The next step is simply verifying that the user is back on the homepage after clicking the site logo. At this point, the distinguishing characteristic of the homepage is the inclusion of 10 meal summaries. So I use that to verify this step:
Then /^I should see the homepage$/ do
response.should have_selector(".meals h2", :count => 10)
end
Now that the user has gone from homepage to meal, to recipe, and back to the homepage, the next step is clicking on the recipe text under the 6th meal on the homepage. The primary reason for choosing the 6th meal/recipe was that this was the first that was vegetarian (the first 5 were Italian). Subsequent steps will verify that it behaves like vegetarian recipes should (i.e. the vegetarian category link is highlighted).

In this scenario, each meal has exactly one menu item—a recipe created just for that meal. Recipe names follow an obvious convention such that the recipe for the 6th meal should be named "Recipe for Meal 5" (meal indexes start at zero). The user should be able to click on the 6th recipe by clicking that text:
When /^I click the recipe link under the 6th meal$/ do
click_link "Recipe for Meal 5"
end
The second to last step in this scenario—verifying that the user is now on the recipe page—was already defined. After a small tweak to ensure that it applies for all recipes, I am onto the final step in this scenario, verifying that the vegetarian category is highlighted on this recipe page.

For a previous recipe, I defined a should-have-the-italian-category scenario step:
Then /^the Italian category should be highlighted$/ do
response.should have_selector("a",
:class => "active",
:content => "Italian")
end
A quick Regexp rewrite will get that step working for both Italian and vegetarian recipes:
Then /^the (\w+) category should be highlighted$/ do |category|
response.should have_selector("a",
:class => "active",
:content => category)
end
And, with that, I am all done with another scenario:



(commit)

With the entire scenario green, I will likely spend some time refactoring before moving onto the next scenario. Tomorrow.

Friday, June 19, 2009

I Can't Believe that I Never Linked

‹prev | My Chain | next›

Today, I continue working my way through the navigation from the home page Cucumber scenario. So far, I have been able to verify that meals are listed on the homepage and that the user can click through the meals onto recipe pages. Next in the scenario is verifying that the user can click on some category links before returning to the home page.

To verify the ability of the user to click on the Italian category on the recipe page, I ought to be able to do something as simple as:
When /^I click on the Italian category$/ do
click_link "Italian"
end
I ought to be able to do something like that, but...
cucumber -n features \
-s "Quickly scanning meals and recipes accessible from the home page"
...
When I click on the recipe in the menu
Then I should see the recipe page
And the Italian category should be highlighted
When I click on the Italian category
Could not find link with text or title or id "Italian" (Webrat::NotFoundError)
features/site.feature:23:in `When I click on the Italian category'
...
Hunh. I just spent yesterday DRYing up the category links. Did I mess something up? To answer that question, I'll use Webrat's save_and_open_page helper method to dump a copy of the page to the browser. After adding this to the Cucumber step:
When /^I click on the Italian category$/ do
save_and_open_page()
click_link "Italian"
end
I find this in the browser:



Hmmm... the categories, including Italian, are there, but they do not look like normal hyperlinks. Viewing the source, I see:
<ul id="eee-categories">
<li><a class="active">Italian</a></li>
<li><a>Asian</a></li>
...
Ah, no href attribute. It would seem that Webrat does not view <a> tags without href attributes as links any more than Firefox does. Ah well, looks as though I have a little more work to do in the code before I can mark this step as complete.

When I click on a category link, such as "Italian", the user should be taken to the list of recipes that fall into that category. Fortunately, I already have a couchdb-lucene search results page that holds this information. To link the categories at the top of the pages to those search results, I start with this RSpec example:
  it "should link to the category search results" do
recipe_category_link({}, "Italian").
should have_selector("a",
:href => "/recipes/search?q=tag_names:italian")
end
To get that example to pass, I add href attributes to the recipe_category_link helper:
    def recipe_category_link(recipe, category)
recipes = recipe.is_a?(Array) ? recipe : [recipe]
href = "/recipes/search?q=tag_names:#{category.downcase}"
if recipes.any? { |r|
r['tag_names'] &&
r['tag_names'].include?(category.downcase)
}
%Q|<a class="active" href="#{href}">#{category}</a>|
else
%Q|<a href="#{href}">#{category}</a>|
end
end
With that implemented, I work my way back to the Cucumber scenario to find the click-the-category-link step is now passing:



The next step in the scenario verifies that the five Italian recipes created early in the scenario are displayed on the category results page. Something like this ought to suffice:
Then /^I should see 5 Italian recipes$/ do
# 5 result rows + 1 header row
response.should have_selector("tr",
:count => 6)
end
That scenario step failed at first because I had not been indexing the categories in couchdb-lucene. After addressing that deficiency, I am up to 16 of 21 steps passing:



Hopefully tomorrow, I can finish up the navigation from the home page scenario.

Thursday, June 18, 2009

DRYing up the Categories

‹prev | My Chain | next›

I'm on vacation, but the don't-break-the-chain gods recognize no holidays and never spend a day at the beach. Lest they gaze upon me with disfavor, I will continue to do at least a little work each day on my chain to stay in their good graces. I have no idea what the wrath of the don't-break-the-chain gods is like and I do not intend to find out.

I noted last night that the code for the category links at the top of most pages was starting to feel repetitive. On the legacy Rails site, the category links look like:



The "Vegetarian" category is in red to indicate that the user is on a vegetarian meal, recipe, or list.

The new implementation of recipe categories, using Haml, looks like:
%ul#eee-categories
%li= recipe_category_link(@recipe, 'Italian')
%li= recipe_category_link(@recipe, 'Asian')
%li= recipe_category_link(@recipe, 'Latin')
%li= recipe_category_link(@recipe, 'Breakfast')
%li= recipe_category_link(@recipe, 'Chicken')
%li= recipe_category_link(@recipe, 'Fish')
%li= recipe_category_link(@recipe, 'Meat')
%li= recipe_category_link(@recipe, 'Salad')
%li= recipe_category_link(@recipe, 'Vegetarian')
%li
%a Recipes
There is much repetition in there—9 calls of the recipe_category_link helper, which is responsible for highlighting the link when appropriate. What's worse is that the following is in the meal Haml template (the above is in the recipe template):
%ul#eee-categories
%li= recipe_category_link(@recipes, 'Italian')
%li= recipe_category_link(@recipes, 'Asian')
%li= recipe_category_link(@recipes, 'Latin')
%li= recipe_category_link(@recipes, 'Breakfast')
%li= recipe_category_link(@recipes, 'Chicken')
%li= recipe_category_link(@recipes, 'Fish')
%li= recipe_category_link(@recipes, 'Meat')
%li= recipe_category_link(@recipes, 'Salad')
%li= recipe_category_link(@recipes, 'Vegetarian')
%li
%a Recipes
I have definitely reached the refactor portion of the Red-Green-Refactor behavior driven development cycle. I could DRY the code up by either factoring the duplication out into a shared partial or into a helper. I opt for the helper since recipe_category_link is already a helper—might as well keep them close should any later work need to be done.

The specs for the categories helper end up reading:
categories
- should link to the italian category
- should be able to highlight the link to the italian category
- should link to the fish category
- should link to the vegetarian category
- should link to all recipes
Since there is so much repetition in the the category listing, the individual RSpec examples are all very much the same. The "vegetarian" example reads:
  it "should link to the vegetarian category" do
categories({}).
should have_selector("#eee-categories a", :content => "Vegetarian")
end
The other examples replace "vegetarian" with the appropriate other category.

The categories helper gets driven to:
    def categories(context)
categories = %w{Italian Asian Latin Breakfast Chicken Fish Meat Salad Vegetarian}

links = categories.map do |category|
%Q|<li>#{recipe_category_link(context, category)}</li>|
end

links << "<a>Recipes</a>"

%Q|<ul id="eee-categories">#{links}</ul>|
end
Finally, I replace the repetitive Haml code with a single call to the new categories helper. I make sure to run all of my specs and then call it a night.
(commit)

Wednesday, June 17, 2009

Marking Meal to Recipe Navigation as Complete

‹prev | My Chain | next›

Before moving on to "real" work, I run all of my tests only to find that I have several errors. There are many errors in the meal.haml_spec.rb specification:
11)
NoMethodError in 'meal.haml should wikify the meal's description'
undefined method `[]' for nil:NilClass
./helpers.rb:18:in `recipe_category_link'
(haml):39:in `any?'
./helpers.rb:17:in `each'
./helpers.rb:17:in `any?'
./helpers.rb:17:in `recipe_category_link'
(haml):2:in `render'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.0.9/lib/haml/engine.rb:149:in `render'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.0.9/lib/haml/engine.rb:149:in `instance_eval'
/home/cstrom/.gem/ruby/1.8/gems/haml-2.0.9/lib/haml/engine.rb:149:in `render'
/home/cstrom/repos/eee-code/spec/spec_helper.rb:27:in `render'
./spec/views/meal.haml_spec.rb:86:
All eleven errors are caused by the inclusion of the recipe_category_link helper last night. I am not able to stub out the method, so I seed some dummy data in the before(:each) block to eliminate the errors:
    assigns[:recipes] = []
This prevents the recipe_category_link from acting upon a nil @recipes array when building the category links:
%ul#eee-categories
%li= recipe_category_link(@recipes, 'Italian')
%li= recipe_category_link(@recipes, 'Asian')
...
Next up, it is back to the Cucumber scenario. Specifically, I need to mark a few scenario steps as complete in the navigation-from-the-homepage Cucumber scenario. Yesterday, I was able to verify that a user could navigate from the homepage to a recent meal. I also ensured that site-wide categories were displaying on the same meal page.

Now, I need to ensure that the user can navigate from the meal down to a recipe listed on the menu. Cucumber tells me that I can implement the missing steps with:
When /^I click on the recipe in the menu$/ do
pending
end

Then /^I should see the recipe page$/ do
pending
end
As mentioned, the scenario so far involves clicking on the most recently prepared meal, then clicking on the recipe on that meal's menu. The meals created for this scenario have titles: "Meal 0", "Meal 1", "Meal 2", etc. In turn, the recipes for each meal are named simply: "Recipe for Meal 0", "Recipe for Meal 1", "Recipe for Meal 2", etc. So, to click on the recipe for the first meal, I need to look for a link to "Recipe for Meal 0":
When /^I click on the recipe in the menu$/ do
click_link "Recipe for Meal 0"
end
Similarly, to verify that I am on the appropriate recipe page, I check the <h1> tag:
Then /^I should see the recipe page$/ do
response.should have_selector("h1",
:content => "Recipe for Meal 0")
end
The next step in the scenario, that the Italian category should be highlighted for this Italian recipe, was implemented last night. The step was written for the meal (the meal is Italian because it has an Italian recipe on the menu), but applies to category links on the recipe page just as it does on the meal page.

And just like that, I am done with 14 of the 21 steps in the scenario:



Before moving onto the next steps, I think there are some DRY violations in the category links that I need to address. Tomorrow.

Tuesday, June 16, 2009

Categorizing Meals

‹prev | My Chain | next›

The next couple of items in the navigation-from-the-homepage Cucumber involve clicking through to a meal:



The next undefined step is "When I click on the first meal". Hmm... I think I forgot to link to the meals from the homepage. Looking closer I find an example that superficially describes linking to meals, but actually only verifies that it includes the title text:
  it "should link to the meal titles" do
render("/views/index.haml")
response.should have_selector("h2", :content => "Bar")
end
I rename that example as "should include meal titles". Then I create the real example for linking to meals:
  it "should link to the the meal titles" do
render("/views/index.haml")
response.should have_selector("a",
:href => "/meals/2009/05/15",
:content => "Bar")
end
Once I get that passing by adding the appropriate %a Haml tag, it is time to move back to the Cucumber scenario. I can mark the next two steps as passing at this point. I can both click a meal link and verify that I am on the meal page:
When /^I click on the first meal$/ do
click_link "Meal 0"
end

Then /^I should see the meal page$/ do
response.should have_selector("h1",
:content => "Meal 0")
end
Those two steps pass, putting me half way through the exploration-from-the-homepage scenario:



The next "then" step states that "the Italian category should be highlighted". I already have something similar for the recipe page:
%ul#eee-categories
%li= recipe_category_link(@recipe, 'Italian')
%li= recipe_category_link(@recipe, 'Asian')
...
To determine if a meal is Italian, each of the recipes on the menu need to be checked. If any of them are Italian, then the meal will be considered Italian. So I'll need to load the recipes and I'll need to update the recipe_category_link to handle more than one recipe.

First up, loading a recipe from wiki text. For text similar to "[recipe:2009/06/16]", CouchDB should be queried and the results parsed:
describe "wiki_recipe" do
before(:each) do
@json = '{"_id":"2009-06-16-recipe","title":"Recipe for Foo"}'
end
it "should lookup a recipe from recipe wiki text" do
RestClient.
should_receive(:get).
with(/2009-06-16/).
and_return(@json)

wiki_recipe(" [recipe:2009/06/16]")
end
it "should return a recipe from recipe wiki text" do
RestClient.
stub!(:get).
and_return(@json)

wiki_recipe(" [recipe:2009/06/16]").
should == { "_id" => "2009-06-16-recipe",
"title" => "Recipe for Foo" }

end
it "should return nil for non-recipe wiki text" do
wiki_recipe("[rcip:2009/06/16]").should be_nil
end
end
I can make those three examples pass with:
    def wiki_recipe(text)
if text =~ /\[recipe:([-\/\w]+)/
permalink = $1.gsub(/\//, '-')
JSON.parse(RestClient.get("#{_db}/#{permalink}"))
end
end
When viewing a meal, I load all recipes into an instance variable in the Sinatra app:
  @recipes = @meal['menu'].map { |m| wiki_recipe(m) }.compact
With the Sinatra app using wiki_recipe to lookup recipes, I need to get recipe_category_link to use multiple recipes to determine whether or not to highlight category links. The example uses two "recipes", one with an "italian" category, which should highlight the link:
  it "should create an active link if any recipes include the category" do
recipes = [{ 'tag_names' => ['italian'] },
{ 'tag_names' => ['foo'] }]
recipe_category_link(recipes, 'Italian').
should have_selector("a", :class => "active")
end
I make that example pass (while keeping the existing, non-array examples passing) with:
    def recipe_category_link(recipe, category)
recipes = recipe.is_a?(Array) ? recipe : [recipe]
if recipes.any? { |r|
r['tag_names'] &&
r['tag_names'].include?(category.downcase)
}
%Q|<a class="active">#{category}</a>|
else
%Q|<a>#{category}</a>|
end
end
I then use the new recipe_category_link and the @recipes instance variable assigned in the Sinatra application to build category links in the meal Haml template:
%ul#eee-categories
%li= recipe_category_link(@recipes, 'Italian')
%li= recipe_category_link(@recipes, 'Asian')
%li= recipe_category_link(@recipes, 'Latin')
%li= recipe_category_link(@recipes, 'Breakfast')
%li= recipe_category_link(@recipes, 'Chicken')
%li= recipe_category_link(@recipes, 'Fish')
%li= recipe_category_link(@recipes, 'Meat')
%li= recipe_category_link(@recipes, 'Salad')
%li= recipe_category_link(@recipes, 'Vegetarian')
%li
%a Recipes
Finally, with that in place, I work back out to the Cucumber scenario. Implementing the Italian-category-should-be-highlighted step:
Then /^the Italian category should be highlighted$/ do
response.should have_selector("a",
:class => "active",
:content => "Italian")
end
And now, I am one step closer to being done with this scenario:


(commit)

Astute readers will note that the recipe_category_link omits the href attribute. It is not much of a link without that attribute. Fortunately, subsequent steps in this scenario cover this omission. Something for another day.

Monday, June 15, 2009

Homepage Navigation

‹prev | My Chain | next›

Today, I continue working on the navigation from the homepage Cucumber scenario. I implemented view code to present meal and recipe information last night. I was able to mark one more step in the scenario as complete. Tonight I need to ensure that the homepage is displaying recipe links (as it should after yesterday's effort). The step that will verify that things are working as desired:
Then /^the prominently displayed meals should include the recipe titles$/ do
response.should have_selector(".menu-items",
:content => "Recipe for Meal 1")
end
When I run the scenario, however, I find:
cstrom@jaynestown:~/repos/eee-code$ cucumber -n features \
-s "Quickly scanning meals and recipes accessible from the home page"
Sinatra::Test is deprecated; use Rack::Test instead.
Feature: Site

So that I may explore many wonderful recipes and see the meals in which they were served
As someone interested in cooking
I want to be able to easily explore this awesome site

Scenario: Quickly scanning meals and recipes accessible from the home page
Given 25 yummy meals
And 1 delicious recipe for each meal
And the first 5 recipes are Italian
And the second 10 recipes are Vegetarian
When I view the site's homepage
Resource not found (RestClient::ResourceNotFound)
/usr/lib/ruby/1.8/net/http.rb:543:in `start'
./helpers.rb:41:in `recipe_link'
./helpers.rb:27:in `wiki'
./helpers.rb:27:in `gsub!'
./helpers.rb:27:in `wiki'
/home/cstrom/repos/eee-code/views/index.haml:7:in `render'
/home/cstrom/repos/eee-code/views/index.haml:3:in `each'
/home/cstrom/repos/eee-code/views/index.haml:3:in `render'
./features/support/../../eee.rb:30:in `GET /'
(eval):7:in `get'
features/site.feature:13:in `When I view the site's homepage'
Hmm... A RestClient error—time to check the CouchDB log, where I see:
[info] [<0.24918.15>] 127.0.0.1 - - 'GET' /eee-test/2009/06/11/recipe 404
Ah, somehow I am requesting the wrong URL. I ought to be requesting the ID from the DB, not a nested resource as I have done here. Specifically, I should be looking for /eee-test/2009-06-11-recipe. So where am I going wrong?

The request is being made by the wiki helper, which, in turn, uses the recipe_link helper to convert wiki text like [recipe:2009/06/11/recipe] into links to the recipe page—by looking the recipe up in the CouchDB database. In the current Cucumber scenario, I am generating wiki text with slashes (as I think I did in the legacy application). I have the feeling that, when I coded the helper in the first place, I accounted only for explicit IDs (e.g. 2009-06-11-recipe).

So it is back into the helper specification to add this example:
  it "should be able to link with \"slash\" dates (e.g. 2009/06/11/recipe)" do
RestClient.
should_receive(:get).
with(/2009-06-11-recipe/).
and_return(@json)

recipe_link("2009/06/11/recipe")
end
I am testing implementation details here rather than behavior. Should I ever change from RestClient to CouchRest, I will have to return here to update the spec—even though no behavior will change. I accept this as the price to keeping my unit tests at the unit level. Hopefully I will not be changing CouchDB client libraries often.

At any rate, this example can drive change in the actual code. This version of the recipe_link helper will work with links with slashes or dashes:
    def recipe_link(link, title=nil)
permalink = link.gsub(/\//, '-')
recipe = JSON.parse(RestClient.get("#{_db}/#{permalink}"))
%Q|<a href="/recipes/#{recipe['_id']}">#{title || recipe['title']}</a>|
end
I can now move back out to the Cucumber scenario to see where I am at:



Happily, where I am at is done with another step. Up next, I will move onto the next step which moves off the homepage as the "user" explores the site.