everydayrails / everydayrails-rspec-2017

Sample source for the 2017 edition of Everyday Rails Testing with RSpec.
http://rspectutorial.com
312 stars 247 forks source link

Chapter 6: Capybara initial test fails #92

Open kkerley opened 6 years ago

kkerley commented 6 years ago

I am completely baffled by this and the fact I cannot find any information about how to do this is downright infuriating.

Using the following scenario from pages 95-96:

  scenario "user creates a new project" do
    user = FactoryBot.create(:user)

    visit root_path
    click_link "Sign in"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
    click_button "Log in"

    expect {
      click_link "New Project"
      save_and_open_page
      fill_in "Name", with: "Test Project"
      fill_in "Description", with: "Trying out Capybara"
      click_button "Create Project"

      expect(page).to have_content "Project was successfully created"
      expect(page).to have_content "Test Project"
      expect(page).to have_content "Owner: #{user.name}"
    }.to change(user.projects, :count).by(1)
  end # scenario "user creates a new project"

I repeatedly get:

  Failure/Error: fill_in "Name", with: "Test Project"

     Capybara::ElementNotFound:
       Unable to find visible field "Name" that is not disabled

As you can see above, I moved a little further along in the chapter and added in the save_and_open_page code after click_link "New Project" to see what is going on. And this is the HTML that's output on the page:

  <div class="form-group">
    <label>Name</label>
    <input class="form-control" id="project_name" type="text" name="project[name]">
  </div>

  <div class="form-group">
    <label>Description</label>
    <textarea class="form-control" id="project_description" name="project[description]"></textarea>
  </div>

So the label and field are there as expected. And I then looked at the project/_form.html.erb file to confirm that it does, in fact, associate the labels and fields:

  <div class="form-group">
    <%= f.label :name %>
    <%= f.text_field :name, class: "form-control", id: :project_name %>
  </div>

  <div class="form-group">
    <%= f.label :description %>
    <%= f.text_area :description, class: "form-control", id: :project_description %>
  </div>

Yet, there's no for attribute on either rendered HTML label associating the label with the field the label is getting its text value from. And I can find absolutely nothing explaining what magic is needed to make that happen. Rails docs show examples of fields setup the exact same way with the corresponding for attribute output on the label yet not on this form. And when I move the save_and_open_page line up under click_link "Sign in" (since there's no issue getting that form filled in and moving on), I see this rendered HTML:

<div class="row">
    <div class="form-group col-xs-12 col-md-4">
      <label for="user_email">Email</label><br>
      <input autofocus="autofocus" class="form-control" type="email" value="" name="user[email]" id="user_email">
    </div>
  </div>

  <div class="row">
    <div class="form-group col-xs-12 col-md-4">
      <label for="user_password">Password</label><br>
      <input autocomplete="off" class="form-control" type="password" name="user[password]" id="user_password">
    </div>
  </div>

which comes from this code found in views/devise/sessions/new.html.erb:

  <div class="row">
    <div class="form-group col-xs-12 col-md-4">
      <%= f.label :email %><br />
      <%= f.email_field :email, autofocus: true, class: "form-control" %>
    </div>
  </div>

  <div class="row">
    <div class="form-group col-xs-12 col-md-4">
      <%= f.label :password %><br />
      <%= f.password_field :password, autocomplete: "off", class: "form-control" %>
    </div>
  </div>

So somehow, basically identical code is outputting differently for no reason I can discern. And because of this, it's making the provided test scenario in the book impossible to follow because it doesn't behave as expected.

Given that on page 101 under the Testing JavaScript interactions header it says "So we’ve verified, with a passing spec, that our user interface for adding projects is working as planned." either something is totally different with how Rails works since the book was last updated, or something in the code or book or both necessary to make this work as presented in the book is missing.

I've found through Stack Overflow that changing:

fill_in "Name", with: "Test Project"
fill_in "Description", with: "Trying out Capybara"

to:

fill_in "project[name]", with: "Test Project"
fill_in "project[description]", with: "Trying out Capybara"

Works and I've confirmed it does and the tests pass since those are the actual names of the fields, but that's not the point: I shouldn't have to do that because these labels should have a proper for attribute on them, yet they do not and it's seemingly impossible to get that attribute on the Rails-generated label.

This is the exact kind of stuff that has happened every single time I've attempted to learn TDD and is the exact reason I ultimately gave up on Rails four years ago after using it for 9 years. Simple stuff that shouldn't even be a thought doesn't work the way the book/guide/video I'm following says it should and instead of writing tests that are actually confirming functionality and providing peace of mind, I'm trying to figure out why something so painfully basic and completely unrelated to the test isn't happening and thus causing the test to fail.

ruralocity commented 6 years ago

Could you share a copy of your Gemfile.lock as it exists to this point, along with your Ruby version (ruby --version)? Are you on Windows/Mac/Linux/something else? Maybe there's a discrepancy somewhere.

kkerley commented 6 years ago

I'm on macOS 10.13.6 using Ruby 2.5.1p57 through RVM.

Gemfile.lock:

GIT
  remote: https://github.com/thoughtbot/shoulda-matchers.git
  revision: 4b160bd19ecca7f97d7ac22dccd5fde9b0da5a9f
  branch: rails-5
  specs:
    shoulda-matchers (3.1.2)
      activesupport (>= 4.2.0)

GEM
  remote: https://rubygems.org/
  specs:
    actioncable (5.2.1)
      actionpack (= 5.2.1)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)
    actionmailer (5.2.1)
      actionpack (= 5.2.1)
      actionview (= 5.2.1)
      activejob (= 5.2.1)
      mail (~> 2.5, >= 2.5.4)
      rails-dom-testing (~> 2.0)
    actionpack (5.2.1)
      actionview (= 5.2.1)
      activesupport (= 5.2.1)
      rack (~> 2.0)
      rack-test (>= 0.6.3)
      rails-dom-testing (~> 2.0)
      rails-html-sanitizer (~> 1.0, >= 1.0.2)
    actionview (5.2.1)
      activesupport (= 5.2.1)
      builder (~> 3.1)
      erubi (~> 1.4)
      rails-dom-testing (~> 2.0)
      rails-html-sanitizer (~> 1.0, >= 1.0.3)
    activejob (5.2.1)
      activesupport (= 5.2.1)
      globalid (>= 0.3.6)
    activemodel (5.2.1)
      activesupport (= 5.2.1)
    activerecord (5.2.1)
      activemodel (= 5.2.1)
      activesupport (= 5.2.1)
      arel (>= 9.0)
    activestorage (5.2.1)
      actionpack (= 5.2.1)
      activerecord (= 5.2.1)
      marcel (~> 0.3.1)
    activesupport (5.2.1)
      concurrent-ruby (~> 1.0, >= 1.0.2)
      i18n (>= 0.7, < 2)
      minitest (~> 5.1)
      tzinfo (~> 1.1)
    addressable (2.5.2)
      public_suffix (>= 2.0.2, < 4.0)
    archive-zip (0.11.0)
      io-like (~> 0.3.0)
    arel (9.0.0)
    autoprefixer-rails (9.1.4)
      execjs
    bcrypt (3.1.12)
    bindex (0.5.0)
    bootstrap-sass (3.3.7)
      autoprefixer-rails (>= 5.2.1)
      sass (>= 3.3.4)
    builder (3.2.3)
    byebug (10.0.2)
    capybara (2.15.4)
      addressable
      mini_mime (>= 0.1.3)
      nokogiri (>= 1.3.3)
      rack (>= 1.0.0)
      rack-test (>= 0.5.4)
      xpath (~> 2.0)
    childprocess (0.9.0)
      ffi (~> 1.0, >= 1.0.11)
    chromedriver-helper (2.0.0)
      archive-zip (~> 0.10)
      nokogiri (~> 1.8)
    climate_control (0.2.0)
    coffee-rails (4.2.2)
      coffee-script (>= 2.2.0)
      railties (>= 4.0.0)
    coffee-script (2.4.1)
      coffee-script-source
      execjs
    coffee-script-source (1.12.2)
    concurrent-ruby (1.0.5)
    crack (0.4.3)
      safe_yaml (~> 1.0.0)
    crass (1.0.4)
    devise (4.5.0)
      bcrypt (~> 3.0)
      orm_adapter (~> 0.1)
      railties (>= 4.1.0, < 6.0)
      responders
      warden (~> 1.2.3)
    diff-lcs (1.3)
    erubi (1.7.1)
    execjs (2.7.0)
    factory_bot (4.10.0)
      activesupport (>= 3.0.0)
    factory_bot_rails (4.10.0)
      factory_bot (~> 4.10.0)
      railties (>= 3.0.0)
    faker (1.9.1)
      i18n (>= 0.7)
    ffi (1.9.25)
    geocoder (1.5.0)
    globalid (0.4.1)
      activesupport (>= 4.2.0)
    hashdiff (0.3.7)
    i18n (1.1.0)
      concurrent-ruby (~> 1.0)
    io-like (0.3.0)
    jbuilder (2.7.0)
      activesupport (>= 4.2.0)
      multi_json (>= 1.2)
    jquery-rails (4.3.3)
      rails-dom-testing (>= 1, < 3)
      railties (>= 4.2.0)
      thor (>= 0.14, < 2.0)
    launchy (2.4.3)
      addressable (~> 2.3)
    listen (3.1.5)
      rb-fsevent (~> 0.9, >= 0.9.4)
      rb-inotify (~> 0.9, >= 0.9.7)
      ruby_dep (~> 1.2)
    loofah (2.2.2)
      crass (~> 1.0.2)
      nokogiri (>= 1.5.9)
    mail (2.7.0)
      mini_mime (>= 0.1.1)
    marcel (0.3.3)
      mimemagic (~> 0.3.2)
    method_source (0.9.0)
    mime-types (3.2.2)
      mime-types-data (~> 3.2015)
    mime-types-data (3.2018.0812)
    mimemagic (0.3.2)
    mini_mime (1.0.1)
    mini_portile2 (2.3.0)
    minitest (5.11.3)
    multi_json (1.13.1)
    nio4r (2.3.1)
    nokogiri (1.8.4)
      mini_portile2 (~> 2.3.0)
    orm_adapter (0.5.0)
    paperclip (6.1.0)
      activemodel (>= 4.2.0)
      activesupport (>= 4.2.0)
      mime-types
      mimemagic (~> 0.3.0)
      terrapin (~> 0.6.0)
    public_suffix (3.0.3)
    puma (3.12.0)
    rack (2.0.5)
    rack-test (1.1.0)
      rack (>= 1.0, < 3)
    rails (5.2.1)
      actioncable (= 5.2.1)
      actionmailer (= 5.2.1)
      actionpack (= 5.2.1)
      actionview (= 5.2.1)
      activejob (= 5.2.1)
      activemodel (= 5.2.1)
      activerecord (= 5.2.1)
      activestorage (= 5.2.1)
      activesupport (= 5.2.1)
      bundler (>= 1.3.0)
      railties (= 5.2.1)
      sprockets-rails (>= 2.0.0)
    rails-dom-testing (2.0.3)
      activesupport (>= 4.2.0)
      nokogiri (>= 1.6)
    rails-html-sanitizer (1.0.4)
      loofah (~> 2.2, >= 2.2.2)
    railties (5.2.1)
      actionpack (= 5.2.1)
      activesupport (= 5.2.1)
      method_source
      rake (>= 0.8.7)
      thor (>= 0.19.0, < 2.0)
    rake (12.3.1)
    rb-fsevent (0.10.3)
    rb-inotify (0.9.10)
      ffi (>= 0.5.0, < 2)
    responders (2.4.0)
      actionpack (>= 4.2.0, < 5.3)
      railties (>= 4.2.0, < 5.3)
    rspec-core (3.8.0)
      rspec-support (~> 3.8.0)
    rspec-expectations (3.8.1)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.8.0)
    rspec-mocks (3.8.0)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.8.0)
    rspec-rails (3.8.0)
      actionpack (>= 3.0)
      activesupport (>= 3.0)
      railties (>= 3.0)
      rspec-core (~> 3.8.0)
      rspec-expectations (~> 3.8.0)
      rspec-mocks (~> 3.8.0)
      rspec-support (~> 3.8.0)
    rspec-support (3.8.0)
    ruby_dep (1.5.0)
    rubyzip (1.2.2)
    safe_yaml (1.0.4)
    sass (3.5.7)
      sass-listen (~> 4.0.0)
    sass-listen (4.0.0)
      rb-fsevent (~> 0.9, >= 0.9.4)
      rb-inotify (~> 0.9, >= 0.9.7)
    sass-rails (5.0.7)
      railties (>= 4.0.0, < 6)
      sass (~> 3.1)
      sprockets (>= 2.8, < 4.0)
      sprockets-rails (>= 2.0, < 4.0)
      tilt (>= 1.1, < 3)
    selenium-webdriver (3.14.0)
      childprocess (~> 0.5)
      rubyzip (~> 1.2)
    spring (2.0.2)
      activesupport (>= 4.2)
    spring-commands-rspec (1.0.4)
      spring (>= 0.9.1)
    spring-watcher-listen (2.0.1)
      listen (>= 2.7, < 4.0)
      spring (>= 1.2, < 3.0)
    sprockets (3.7.2)
      concurrent-ruby (~> 1.0)
      rack (> 1, < 3)
    sprockets-rails (3.2.1)
      actionpack (>= 4.0)
      activesupport (>= 4.0)
      sprockets (>= 3.0.0)
    sqlite3 (1.3.13)
    terrapin (0.6.0)
      climate_control (>= 0.0.3, < 1.0)
    thor (0.20.0)
    thread_safe (0.3.6)
    tilt (2.0.8)
    turbolinks (5.2.0)
      turbolinks-source (~> 5.2)
    turbolinks-source (5.2.0)
    tzinfo (1.2.5)
      thread_safe (~> 0.1)
    uglifier (4.1.19)
      execjs (>= 0.3.0, < 3)
    vcr (4.0.0)
    warden (1.2.7)
      rack (>= 1.0)
    web-console (3.7.0)
      actionview (>= 5.0)
      activemodel (>= 5.0)
      bindex (>= 0.4.0)
      railties (>= 5.0)
    webmock (3.4.2)
      addressable (>= 2.3.6)
      crack (>= 0.3.2)
      hashdiff
    websocket-driver (0.7.0)
      websocket-extensions (>= 0.1.0)
    websocket-extensions (0.1.3)
    xpath (2.1.0)
      nokogiri (~> 1.3)

PLATFORMS
  ruby

DEPENDENCIES
  bootstrap-sass
  byebug
  capybara (~> 2.15.4)
  chromedriver-helper
  coffee-rails (~> 4.2)
  devise
  factory_bot_rails (~> 4.10.0)
  faker
  geocoder
  jbuilder (~> 2.5)
  jquery-rails
  launchy (~> 2.4.3)
  listen (>= 3.0.5, < 3.2)
  paperclip
  puma (~> 3.7)
  rails (~> 5.2.1)
  rspec-rails (~> 3.8.0)
  sass-rails (~> 5.0)
  selenium-webdriver
  shoulda-matchers!
  spring
  spring-commands-rspec
  spring-watcher-listen (~> 2.0.0)
  sqlite3
  turbolinks (~> 5)
  tzinfo-data
  uglifier (>= 1.3.0)
  vcr
  web-console (>= 3.3.0)
  webmock

BUNDLED WITH
   1.16.4

And I used this branch: https://github.com/everydayrails/everydayrails-rspec-2017/tree/01-untested as my starting point so I'd be the one writing most of the code rather than looking at completed code. I believe a few gems have been updated from the versions in the book, but that doesn't seem like anything that should be affecting Rails outputting for attributes on labels to properly associate the label and input.

kkerley commented 5 years ago

It's happening again with the subsequent test to check a task and see that it completed and then unchecking to see that it's back to be incomplete (pages 101-103).

scenario "user toggles a task", js: true do
    user = FactoryBot.create(:user)
    project = FactoryBot.create(:project, name: "RSpec tutorial", owner: user)
    task = project.tasks.create!(name: "Finish RSpec tutorial")

    visit root_path
    click_link "Sign in"
    fill_in "Email", with: user.email
    fill_in "Password", with: user.password
    click_button "Log in"

    click_link "RSpec tutorial"
    # check "Finish RSpec tutorial"
    check "completed"

    save_and_open_page
    expect(page).to have_css "label#task_#{task.id}.completed"
    expect(task.reload).to be_completed

    # uncheck "Finish RSPec tutorial"
    uncheck "completed"

    expect(page).to_not have_css "label#task_#{task.id}.completed"
    expect(task.reload).to_not be_completed
  end # scenario "user toggles a task"

I've done some subsequent research and it appears this is an issue with form_with (and the Devise login form uses form_for which is outputting correctly. I found this thread discussing the same basic issues I'm having and it links to other Github threads but if there was a resolution, it doesn't seem to be working in 5.2.1. I haven't used Rails since 3.2 so this is all very new to me and I'm not sure I understand the differences between form_for and form_with, nor am I able to successfully change over the forms from the Everyday Rails app.

So in the code above, I changed the check and uncheck commands to use "completed" since that's the name of the checkbox, but my concern regarding both this book and my own applications after I finish this is that I'm going to end up in a much more complex/complicated situation with multiple elements that have the same names because it's a SPA, and tests are going to fail endlessly because I can't get more specific.

I guess that's where the scoping could come into play but given how weird form_with behaves, what if there aren't unique IDs I can use to get more specific? Having to manually test in the browser in the dev environment defeats the entire purpose of doing automated TDD like this.

ruralocity commented 5 years ago

I was planning to dig into this over the weekend, but at what point did you switch to 5.2? The app is built and tested on 5.1. I always recommend having a test suite in place before upgrading Rails versions.

kkerley commented 5 years ago

I finished the chapter and am curious about the output from adding additional tasks to this feature spec for completing/un-completing tasks:

I made four tasks total:

task1 = project.tasks.create!(name: "Finish RSpec tutorial")
task2 = project.tasks.create!(name: "Add additional tasks")
task3 = project.tasks.create!(name: "Make sure additional tasks can be completed")
task4 = project.tasks.create!(name: "Use Capybara context")

And all are being output as expected:

<tbody>
    <tr class="task">
  <td>
    <label id="task_1" class="">
      <input type="checkbox" name="completed" id="completed" value="1" data-remote="true" data-url="/projects/1/tasks/1/toggle" data-method="post" style="font-size: 300%">
      Finish RSpec tutorial
    </label>
  </td>
  <td class="task-actions">
    <a class="btn btn-xs btn-default" href="/projects/1/tasks/1/edit">
      <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
      Edit
</a>    <a data-confirm="Are you sure?" class="btn btn-default btn-xs" rel="nofollow" data-method="delete" href="/projects/1/tasks/1">
      <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
      Delete
</a>  </td>
</tr>
<tr class="task">
  <td>
    <label id="task_2" class="">
      <input type="checkbox" name="completed" id="completed" value="2" data-remote="true" data-url="/projects/1/tasks/2/toggle" data-method="post" style="font-size: 300%">
      Add additional tasks
    </label>
  </td>
  <td class="task-actions">
    <a class="btn btn-xs btn-default" href="/projects/1/tasks/2/edit">
      <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
      Edit
</a>    <a data-confirm="Are you sure?" class="btn btn-default btn-xs" rel="nofollow" data-method="delete" href="/projects/1/tasks/2">
      <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
      Delete
</a>  </td>
</tr>
<tr class="task">
  <td>
    <label id="task_3" class="">
      <input type="checkbox" name="completed" id="completed" value="3" data-remote="true" data-url="/projects/1/tasks/3/toggle" data-method="post" style="font-size: 300%">
      Make sure additional tasks can be completed
    </label>
  </td>
  <td class="task-actions">
    <a class="btn btn-xs btn-default" href="/projects/1/tasks/3/edit">
      <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
      Edit
</a>    <a data-confirm="Are you sure?" class="btn btn-default btn-xs" rel="nofollow" data-method="delete" href="/projects/1/tasks/3">
      <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
      Delete
</a>  </td>
</tr>
<tr class="task">
  <td>
    <label id="task_4" class="">
      <input type="checkbox" name="completed" id="completed" value="4" data-remote="true" data-url="/projects/1/tasks/4/toggle" data-method="post" style="font-size: 300%">
      Use Capybara context
    </label>
  </td>
  <td class="task-actions">
    <a class="btn btn-xs btn-default" href="/projects/1/tasks/4/edit">
      <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>
      Edit
</a>    <a data-confirm="Are you sure?" class="btn btn-default btn-xs" rel="nofollow" data-method="delete" href="/projects/1/tasks/4">
      <span class="glyphicon glyphicon-trash" aria-hidden="true"></span>
      Delete
</a>  </td>
</tr>
  </tbody>

But each task has the same name="completed" and id="completed" attributes. I know I can use context to specify by within "#task_1" do ... but that just seems bad to have the same name and id for each one of them. I looked at /views/tasks/_task.html.erb and see:

<label id="<%= dom_id(task) %>" class="<%= task.completed? ? 'completed' : nil %>">
    <%= check_box_tag 'completed', task.id, task.completed,
        data: {
          remote: true,
          url: toggle_project_task_path(task.project, task),
          method: :post
        },
        style: "font-size: 300%"
    %>
    <%= task.name %>
</label>

It's never a good idea to have duplicate ids on a page and when I try to change _task.html.erb to:

<%= check_box_tag "task_#{task.id}_completed", task.id, task.completed,
          data: {
            remote: true,
            url: toggle_project_task_path(task.project, task),
            method: :post
          },
          style: "font-size: 300%"
%>

The test always fails because it can't find tasks using that ID structure. When I try to save_and_open_page to see what's wrong and what's being output, there are no checkboxes on the page at all.

So we really have to have the same id for every checkbox?

kkerley commented 5 years ago

I was planning to dig into this over the weekend, but at what point did you switch to 5.2? The app is built and tested on 5.1. I always recommend having a test suite in place before upgrading Rails versions.

I've been using Rails 5.2 the entire time.

ruralocity commented 5 years ago

For the purposes of this book, I think you'll have a better time with it if you stick to gem versions as defined in the sample code's Gemfile.lock, including Rails 5.1. My approach to learning TDD is to write. tests for code that's already been browser-tested. Once you're more comfortable writing tests, flip it around to a test-first approach that typically comes with TDD (see chapter 11).

I can't guarantee that newer versions of Rails or any other dependencies will work with the sample app. Due to the process and time commitment involved with building up the sample app and tests chapter by chapter, I don't usually redo everything for every minor-level Rails release.

But once a test suite is in place for an earlier version of Rails, it can help with the upgrade process for Rails or any other dependency. This is the approach I take with real-world, production apps.

So we really have to have the same id for every checkbox?

You're right, this is an issue with the code that something in my version of the dependencies doesn't catch, but maybe something in Rails 5.2 is less permissive. (I first suspected a difference in Capybara versions, but that doesn't appear to be the case.)

You could fix the application code with something like

diff --git a/app/views/tasks/_task.html.erb b/app/views/tasks/_task.html.erb
index 0b420ae..c63897e 100644
--- a/app/views/tasks/_task.html.erb
+++ b/app/views/tasks/_task.html.erb
@@ -1,7 +1,7 @@
 <tr class="task">
   <td>
     <label id="<%= dom_id(task) %>" class="<%= task.completed? ? 'completed' : nil %>">
-      <%= check_box_tag 'completed', task.id, task.completed,
+      <%= check_box_tag dom_id(task), task.id, task.completed,
           data: {
             remote: true,
             url: toggle_project_task_path(task.project, task),

But I would also be curious to see how things behave by basing the sample app on Rails 5.1.

kkerley commented 5 years ago

For the sake of completing the book, I've just moved forward by using the Rails-generated names of fields instead of trying to find them by their label text value. Such an odd discrepancy in Rails.

The rest of the book has been great and extremely helpful. I stopped getting hung up on the Capybara stuff because I remembered I'll be doing the front end of any projects in React, so the Capybara functionality would be skipped in favor of React testing via Jest and Enzyme.

For what it's worth, I've made a few changes to the test suites to account for the output differences between 5.2 and 5.1 (the original functions can be found on page 142 and 143):

def complete_task(labelID, name)
    within labelID do
      check name
    end
  end # complete_task

  def undo_complete_task(labelID, name)
    within labelID do
      uncheck name
    end
  end # undo_complete_task

  def expect_complete_task(labelCSS, task)
    aggregate_failures do
      expect(page).to have_css labelCSS
      expect(task.reload).to be_completed
    end
  end # expect_complete_task

  def expect_incomplete_task(labelCSS, task)
    aggregate_failures do
      expect(page).to_not have_css labelCSS
      expect(task.reload).to_not be_completed
    end
  end # expect_incomplete_task

And then the corresponding scenarios were changed to:

scenario "user toggles task1", js: true do
    # using our custom login helper:
    # sign_in_as user
    # or the one provided by Devise:
    sign_in user
    go_to_project "RSpec tutorial"

    complete_task("label#task_1", "completed")
    expect_complete_task("label#task_1.completed", task1)

    undo_complete_task("label#task_1", "completed")
    expect_incomplete_task("label#task_1.completed", task1)
  end # scenario "user toggles task1"

  scenario "user completes task4", js: true do
    # using our custom login helper:
    # sign_in_as user
    # or the one provided by Devise:
    sign_in user
    go_to_project "RSpec tutorial"

    complete_task("label#task_4", "completed")
    expect_complete_task("label#task_4.completed", task4)
  end # scenario "user toggles task4"

  scenario "user toggles task3", js: true do
    # using our custom login helper:
    # sign_in_as user
    # or the one provided by Devise:
    sign_in user
    go_to_project "RSpec tutorial"

    complete_task("label#task_3", "completed")
    expect_complete_task("label#task_3.completed", task3)

    undo_complete_task("label#task_3", "completed")
    expect_incomplete_task("label#task_3.completed", task3)
  end # scenario "user toggles task3"

(I expanded on the book examples by adding 3 more tasks and updating their variable names accordingly).

This works and hopefully it can be helpful for someone else in the future should they get stuck on this.

Thanks for writing the book and finally making TDD make sense!

garth-bldup commented 3 years ago

For anyone googling: the way i solved this problem is by passing for: :my-desired-id as part of the options hash to the call to f.label. so if you were doing this example, you'd write:

<div class="form-group">
    <%= f.label :name, for: :project_name %>
    <%= f.text_field :name, class: "form-control", id: :project_name %>
  </div>