[007.3] Implementing the Checklist screen from our acceptance tests

Using acceptance tests to implement features

Subscribe now

Implementing the Checklist screen from our acceptance tests [12.06.2017]

Today we will implement some functionalities in our Checklist screen. In this screen we will have the Checklists and the Checklist Items. Our code will be guided by the acceptance tests we created on Monday. This is a good practice because someone else could create the acceptances tests without you. Now, everything we need to do is only implement and we have a guide on what we should do. It makes the development part easier when you have that.

We will start this episode using the tag dailydrip/produciton.com after episode_007.2.

Viewing a Checklist

Our first scenario is to view a checklist.

  scenario 'viewing a checklist' do
    visit checklist_path(ruby)

    expect(page).to have_title("#{ruby} | Produciton")
    expect(page).to have_incompleted_checklist_item(configure_ssl)
    expect(page).to have_incompleted_checklist_item(set_up_cdn)
  end

In this test, we are checking if the page title has the checklist title and Produciton on it. Also, we are checking if it contains two incomplete checklist items.

We don't have the show html page yet, and we didn't implement the controller method.

We will create the show page for checklists.

vim app/views/checklists/show.html.slim

And we will add the content for and pass the title.

- content_for(:title, "#{@checklist.title} | Produciton")

Using content_for is a good practice. It allows us to change the markup page from the application layout.

Our application layout doesn't have the content for yet and it is not using slim. We will change that to slim, and add the content_for to use the title.

Changing it to slim. And if we don't pass the content_for for title, we will just show Produciton.

doctype html
html
  head
    title
     = content_for?(:title) ? yield(:title) : "Produciton"
    = csrf_meta_tags
    = stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload'
    = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
  body
    = yield

And now our controller method. In the controller method, we will do a simple find by getting the checklist id from params.

vim app/controllers/checklists_controller.rb
  def show
    @checklist = Checklist.find(params[:id])
  end

With this, the first part of our viewing acceptance test should pass. And It passes. It seems it gets the correct title.

  scenario 'viewing a checklist' do
    visit checklist_path(ruby)

    expect(page).to have_title("#{ruby.title} | Produciton")
   # expect(page).to have_uncompleted_checklist_item(configure_ssl)
   # expect(page).to have_uncompleted_checklist_item(set_up_cdn)
  end

Now, let's focus on having the uncompleted checklist items. This matcher does not exist yet. Going to our custom matchers, we will create.

RSpec::Matchers.define :have_uncompleted_checklist_item do |expected|
  match do |actual|
    selector = 'ul.checklist-list > li.uncompleted > a'
    expect(actual).to have_selector(selector, text: expected.title.to_s)
  end

  description do
    "have an uncompleted checklist item link for #{expected}"
  end
end

In the matcher, we are checking if it has the correct css class for the list and the text.

In our show view, we need to implement this, having exactly the same classes we used.

vim app/views/checklists/show.html.slim

Here we are iterating over the checklist items and creating the class. To show the correct checklist item class, we will use a helper.

- content_for(:title, "#{@checklist.title} | Produciton")

ul class="checklist-list"
  - @checklist.checklist_items.each do |item|
    li class="#{checklist_item_class(item)}"
      = link_to item.title, '#'
vim app/helpers/application_helper.rb

And our helper will show the completed string if the checklist item is completed and uncompleted, otherwise.

...
  def checklist_item_class(item)
    item.completed ? 'completed' : 'uncompleted'
  end
...

With this, our test should pass. And It passes. We have used the matcher and the good thing is because we can replicate this idea in other parts of our code.

We can go ahead and implement the completed matcher. We will change the class from li to completed. When we change the class, we will add in the css a property to make a strikethrough in the text.

RSpec::Matchers.define :have_completed_checklist_item do |expected|
  match do |actual|
    selector = 'ul.checklist-list > li.completed > a'
    expect(actual).to have_selector(selector, text: expected.title.to_s)
  end

  description do
    "have a completed checklist item link for #{expected}"
  end
end

And now let's add the property for completed in our css.

vim app/assets/stylesheets/application.css
.completed {
  text-decoration: line-through;
}

We will show all the checklist items, and when they are completed, there will be a strikethrough the text.

When all the items are completed, we will show a message Everything's finished!. Let's change the view to have this.

- content_for(:title, "#{@checklist.title} | Produciton")

ul class="checklist-list"
  - @checklist.checklist_items.each do |item|
    li class="#{checklist_item_class(item)}"
      = link_to item.title, '#'
  - if @checklist.all_done?
    h3 Everything's finished!

Here we are using a method for the Checklist instance and it gets all the items for a given Checklist and it checks if all these items are completed. Let's implement this.

vim app/models/checklist.rb

And the implementation is simple. We will just check the completed attribute for all the checklist items.

  def all_done?
    checklist_items.pluck(:completed).all?
  end

Completing Checklists

Coming back to our acceptance tests. We need to implement a method to click and mark the checklist item as complete. We can create this method in the support files.

vim spec/support/checklists.rb

In this method, we will look for an id, and the id of the button contains the checklist item id. And then we will click on it.

def complete_checklist_item(item)
  find(:css, "#complete-checklist-item-#{item.id}").click
end

Our ChecklistItem should have this id. We will implement a form_for to be used with the checklist item. This is a common way we can do this in Rails, by passing the correct parameters for our model, and it knows which controller method to call, in this case the POST update method, which is represented by the update method in the controller.

In this form, we update the check list item and submit it to the ChecklistItemsController which we don't have yet.

- content_for(:title, "#{@checklist.title} | Produciton")

ul class="checklist-list"
    - @checklist.checklist_items.each do |item|
      li class="#{checklist_item_class(item)}"
        = link_to item.title, '#'
        = form_for item do |f|
          = f.hidden_field :completed, {value: !item.completed}
          = f.submit checklist_item_button(item), id: "complete-checklist-item-#{item.id}"
  - if @checklist.all_done?
    h3 Everything's finished!

The value of completed is the negative of the current value. And in the submit button, the id will be the id we have just used in our support file.

We are using a helper to show the button message. We implement this in our ApplicationHelper.

  def checklist_item_button(item)
    item.completed ? 'Mark as Uncompleted' : 'Mark as Completed'
  end

We need to create the ChecklistItemsController.

vim app/controllers/checklist_items_controller.rb

In the ChecklistItemsController we will implement only the method update as we saw before, since it is the only one we are using.

class ChecklistItemsController < ApplicationController
  def update
    @checklist_item = ChecklistItem.find(params[:id])
    if @checklist_item.update(checklist_item_params)
      flash[:success] = 'Item updated!'
    else
      flash[:error] = 'Failed to update the item.'
    end
    redirect_to @checklist_item.checklist
  end

  private

  def checklist_item_params
    params.require(:checklist_item).permit(:title, :completed)
  end
end

The update method will be simple. We get the checklist_item parameters and we update the checklist_item. If it succeeds, we show a flash message, saying the item is updated, and an error message, otherwise. In both cases, we redirect to the corresponding checklist from the checklist_item.

Let's also add the flash messages in our application layout.

vim  app/views/layouts/application.html.slim
  body
    - flash.each do |key, value|
      div{class= "alert alert-#{key}"}= value
    = yield
...

And we need to add the checklist_items controller resource into our routes file. We will use the only option here and pass only the update, to not create routes we are not using.

  resources :checklist_items, only: [:update]

What we did

With this, we are able to run the acceptance tests related to completing action for checklist. Let's run those tests.

One test is for completing a checklist item and then we have a completed and a not-completed one. The other test is for completing all the checklist items and then we can see the string Everything's finished.

Running these tests and they pass.

Summary

We could see today how the acceptance tests helped us on how to make our code. The tests were the light to our steps, and this was really nice because we knew what our next step was. In tomorrow’s episode we will keep implementing more of the code guided by the acceptance tests we created on Monday. See you tomorrow!

Resources