首页 > 解决方案 > 在使用 Vue 和 Rails 时如何避免闪烁的规范?

问题描述

我们最近一直在将 Vue 集成到我们的 Rails 应用程序中,但这导致了很多闪烁的 Capybara/rspec 测试,据我所知,这些测试来自竞争条件,其中部分或全部 Vue 元素在下一个 Capybara 动作/预期开始,或者可能正在渲染,但不知何故模糊了元素。

我出现的错误类型如下:

element click intercepted: Element is not clickable at point (392, 641)
    (Session info: headless chrome=77.0.3865.90)
    (Driver info: chromedriver=77.0.3865.40 (f484704e052e0b556f8030b65b953dce96503217-refs/branch-heads/3865@{#442}),platform=Mac OS X 10.14.6 x86_64) (Selenium::WebDriver::Error::ElementClickInterceptedError)

还有这些:

element click intercepted: Element <input type="submit" name="commit" value="Sign In" data-disable-with="Sign In" class="btn -light -full -solid"> is not clickable at point (272, 37). Other element would receive the click: <div class="site-header__menu__container | container gutter-lg">...</div>
    (Session info: headless chrome=77.0.3865.90)
    (Driver info: chromedriver=77.0.3865.40 (f484704e052e0b556f8030b65b953dce96503217-refs/branch-heads/3865@{#442}),platform=Mac OS X 10.14.6 x86_64) (Selenium::WebDriver::Error::ElementClickInterceptedError)

(可能值得一提的是,这些测试失败时给出的坐标因一次失败而异)

鉴于 Vue 元素以 0 高度标签开始,可能位于我们的站点菜单下方(本身就是一个 Vue 组件),我认为这两个错误具有相同的原因 - 即我们的目标元素没有(完全?)呈现但是,为了简洁起见,我将只显示其中的代码 - 尽管如果您认为我错了,请告诉我,我将编辑或创建另一个帖子。

我已经尝试了处理此类问题的两篇文章中的方法 - https://engineering.gusto.com/eliminating-flaky-ruby-tests/https://thoughtbot.com/blog/write -reliable-asynchronous-integration-tests-with-capybara - 但两者似乎都没有太大效果(虽然我不知道该怎么做来代替第一个谴责的“访问”方法,这是核心大多数功能测试)。

这是第一个的相关功能代码:

@vcr
@javascript
Scenario: Successfully pledging after something like confirming my account
  Given something like I am on the pledge form
  When I submit a valid pledge
  Then I should land on the HelloSign signature page

如此实施:

Given("something like I am on the pledge form") do
  FactoryBot.create(:user, email: test_user_email, password: test_user_password)
  test_user.confirm
  step 'something like I have signed up and confirmed'
  user = User.last
  user.confirm
  login_as(user, scope: :user)
  visit new_pledge_path
end

When("I submit a valid pledge") do
  fill_in('pledge_pledgor_home_postcode', with: 'Up a tree, cutting mistletoe')
  fill_in('First name', with: "Asterix")
  fill_in('Surname', with: "deGaulle")
  fill_in('Phone number', with: '2345678')
  fill_in('Home address', with: 'A quiet village near the fortified Roman camp')
  fill_in('City', with: 'Totorum')

  fill_in('pledge_companies_attributes_0_name', with: 'Circvmbendibvs Wheels')
  fill_in('pledge_companies_attributes_0_number', with: '1')

  # Minor hack to be able to select options given the JS Choices library's obfuscation:
  find('#country-code-select-wrapper .choices').click
  find('#choices--pledge_pledgor_phone_code-item-choice-3').click
  find('#country-select-wrapper .choices').click
  find('#choices--pledge_pledgor_home_country-item-choice-10').click

  # Defocus the dropdowns before submitting:
  find("body").click

  # Need to activate this wrapping block, run once, then remove it when
  # refreshing the cassette. This seems dumb - would be nice to find a better solution
  # accept_alert do

  click_button('Review & Submit') ## unless ENV['IS_CIRCLE'].present?

  # end
end

按钮本身不是 Vue 元素,但在页面上它上面还有各种其他元素:

<%= form_for @pledge, html: { class: "standard-form | standard-form-base", id: 'pledge-form' } do |f| %>
  <!-- pledge errors - plain html
  <%= render "layouts/components/form_feedback" %>

  <!--  Sub Section Header - again just html -->
  <%= render "layouts/components/sub-section-header.html", content: @new_pledge_page.about_you %>

  <!--  @new_pledge_page is a Contentful Model object that -->
  <%= render "pledge_fields", content: @new_pledge_page.pledge_fields, f: f %>
<% end %>

而 _pledge_fields.html.erb 部分:

<!--  Group  -->
<div class="form-group">
  <div class="field-group">
    <%= f.label :pledgor_first_name, content.first_name, class: "standard-label" %>
    <%= f.text_field :pledgor_first_name, value: @pf_presenter.forenames_estimate %>
  </div>
</div>

<!--  Group  -->
<div class="form-group">
  <div class="field-group">
    <%= f.label :pledgor_surname, content.surname, class: "standard-label" %>
    <%= f.text_field :pledgor_surname, value: @pf_presenter.surname_estimate %>
  </div>
</div>

<!--  Group  -->
<div class="form-group">
  <div class="field-group | w-2/5" id="country-code-select-wrapper">
    <label class="standard-label" for="country">
      <%= content.phone_code %>
    </label>
    <select_box :opt="{ variant: '-standard -md', id: 'pledge_pledgor_phone_code', name: 'pledge[pledgor_phone_code]', value: '<%= f.object.pledgor_phone_code.presence || "1" %>' }">
      <%= @pf_presenter.country_phone_code_options %>
    </select_box>
  </div>
  <div class="field-group | w-3/5">
    <%= f.label :pledgor_phone_number, content.phone_number, class: "standard-label" %>
    <%= f.text_field :pledgor_phone_number, placeholder: "(000) 000-0000" %>
  </div>
</div>

<!--  Group  -->
<div class="form-group">
  <div class="field-group" id="country-select-wrapper">
    <label class="standard-label" for="country">
      <%= content.country %>
    </label>
    <select_box :opt="{ variant: '-standard -md', id: 'pledge_pledgor_home_country', name: 'pledge[pledgor_home_country]', value: '<%= f.object.pledgor_home_country.presence || "US" %>' }">
      <%= @pf_presenter.country_options(pledge: f.object) %>
    </select_box>
  </div>
</div>

<!--  Group  -->
<div class="form-group">
  <div class="field-group">
    <%= f.label :pledgor_home_address, content.home_address, class: "standard-label" %>
    <%= f.text_field :pledgor_home_address %>
  </div>
</div>

<!--  Group  -->
<div class="form-group">
  <div class="field-group | w-1/3">
    <%= f.label :pledgor_home_city, content.city, class: "standard-label" %>
    <%= f.text_field :pledgor_home_city %>
  </div>
  <div class="field-group | w-2/3">
    <% if @pf_presenter.probably_based_in?('us') %>
    <%= f.label :pledgor_home_postcode, content.zip_code, class: "standard-label" %>
    <% else %>
    <%= f.label :pledgor_home_postcode, content.postcode, class: "standard-label" %>
    <% end %>
    <%= f.text_field :pledgor_home_postcode %>
  </div>
</div>

<% if !@user.campaign %>
  <!--  Sub Section Header  -->
  <%= render "layouts/components/sub-section-header.html", content: content.company_details_subheader %>

  <%= render "company_fields", content: content.company_details, f: f %>
<% end %>

<!--  Sub Section Header  -->
<%= render "layouts/components/sub-section-header.html", content: content.how_much_subheader %>

<!-- :id="pledge_percentage" -->
<!--  Range Slider  -->
<range_slider :opt="{ id: 'pledge_percentage', name: 'pledge[percentage]' }" :min="2" :start="<%= f.object.percentage || 40 %>" :max="100" class="form-block">
</range_slider>

<!--  Sub Section Header  -->
<%= render "layouts/components/sub-section-header.html", content: content.confirmation_subheader %>

<% if !current_user.try :OptedIntoComms__c %>
<div class="<%= 'hidden' if !@pf_presenter.assumed_in_eu? %>" id="js-gdpr-input">
  <div class="form-group">
    <div class="field-group">
      <%= f.check_box :receive_comms, checked: false, disabled: !@pf_presenter.assumed_in_eu? %>
      <%= f.label :receive_comms, "#{content.gdpr_label}", class: "standard-label -checkbox" %>
    </div>
  </div>
</div>
<% else %>
  <%= f.hidden_field :receive_comms, value: true %>
<% end %>

<%= f.hidden_field :id %>

<%= f.submit content.review_submit, class: "btn -light -full -solid", id: "submit-pledge" %>

我们的 Gemfile 中的一些可能相关的规格:

ruby "2.6.1"
gem "rails", "5.1.6.2"
gem "rspec-rails", "~> 3.7.2"
gem 'webdrivers', '~> 4.1.2'
gem 'selenium-webdriver', '~> 3.142.4'
gem "capybara", "~> 3.22.0"

标签: vue.jsrspeccucumberruby-on-rails-5capybara

解决方案


从您提到的情况来看,您似乎在运行时 + 编译器模式下使用 Vue。

因此,HTML 元素将在 Vue 能够编译它们并使其交互之前可用。

您可以尝试使用v-cloak,该指令将在元素编译后被删除。

v-cloak然后,在 Capybara 中,您可以通过从结果中排除元素来确保等到元素编译完成。

:element使用选择器的示例:

find(:element, 'select_box', 'v-cloak' => nil)

如果您需要经常这样做,您可以扩展 Capybara 的选择器以接收可选vue_loaded: true属性:

Capybara::Selector.all.each_value do |selector|
  selector.instance_eval do
    expression_filter(:vue_loaded) do |expression|
      builder(expression).add_attribute_conditions('v-cloak': nil)
    end
    describe_expression_filters do |**options|
      if (value = options[:vue_loaded])
        " that was already compiled"
      end
    end
  end
end

接着:

find('range_slider', vue_loaded: true)

推荐阅读