Search code examples
ruby-on-railsrubycapybarahotwire-railsturbo

How do I make assertions consistently wait until a turbo frame has been updated?


My system tests are experiencing a race condition leading to inconsistent test results. Sometimes the turbo frame is updated before my assertion (test passes) and sometimes afterward (test fails).

View:

<div data-controller="filter">
  <%= form_with url: root_path, method: :get, data: { turbo_frame: "intakes", filter_target: "form", action: "change->filter#submit" } do %>
      <%= select_tag "country", options_for_select(@countries) %>
  <% end %>

  <%= turbo_frame_tag "intakes" do %>
      <table>
        <% @family_intakes.each do |family_intake| %>
          <tr>
            <td><%= family_intake.full_name %></td>
          </tr>
        <% end %>
      </table>
  <% end %>
</div>

This Stimulus controller submits my form on change event (ultimately updating the turbo frame):

export default class extends Controller {
  static targets = ["form"];

  submit(event) {
    this.formTarget.requestSubmit();
  }
}

Test:

class FamilyIntakesTest < ApplicationSystemTestCase
  test "Only intakes from selected country are displayed" do
    login
    select "Afghanistan", from: "country"
    assert_selector "td", text: family_intakes(:manizha).first
    assert_selector "td", text: family_intakes(:sayed).first
    refute_selector "td", text: family_intakes(:mohammad).first
  end
end

Error:

Failure:
FamilyIntakesTest#test_Only_intakes_from_selected_country_are_displayed [/home/eric/<redacted>/test/system/family_intakes_test.rb:31]:
expected not to find visible css "td" with text "Mohammad", found 1 match: "Mohammad Ahmad". Also found "Manizha Ahmadi", "Sayed Shinwari", which matched the selector but not all filters.

Solution

  • My current solution is to add a turbo-response class to the table being updated inside my turbo frame and to scope my test assertions to that css class. That way I can't get a false negative if my assertion runs before before the frame is updated.

    View update

    <table class="<%= "turbo-response" if turbo_frame_request? %>">
    

    Helper:

    module ApplicationHelper
      def turbo_frame_request?
        request.headers["Turbo-Frame"]
      end
    end
    

    Test:

    class FamilyIntakesTest < ApplicationSystemTestCase
      test "Only intakes from selected country are displayed" do
        login
        select "Afghanistan", from: "country"
        within('.turbo-response') do
          assert_selector "td", text: family_intakes(:manizha).first
          assert_selector "td", text: family_intakes(:sayed).first
          refute_selector "td", text: family_intakes(:mohammad).first
        end
      end
    end
    

    Also, during the debugging process, to prevent any "accidental" success of my tests caused by the turbo frame just "happening" to load first, I intentionally slowed down my JavaScript form submission:

    submit(event) {
      setTimeout(() => this.formTarget.requestSubmit(), 1500);
    }
    

    The slowdown should be less than Capybara.default_max_wait_time which is typically 2 seconds or else Capybara really will give up. I went with 1.5 seconds or 1500 milliseconds. Just make sure to remove the setTimout later! Getting my tests to pass with or without that slowdown gives me way more confidence that they are passing for the right reasons!