Skip to content

Commit

Permalink
Implement E2E tests with Capybara
Browse files Browse the repository at this point in the history
This is a spike of a couple hours' work to show what E2E tests with
Capybara could look like. This implements "Task 1" of the tech spec
discussed on 9/27.

These tests won't run during CI, but maybe they will be useful in our
manual testing. Future plans are to stub out Pinwheel and then enable
these in CI.
  • Loading branch information
tdooner committed Oct 9, 2024
1 parent 4b7718d commit 4435278
Show file tree
Hide file tree
Showing 9 changed files with 221 additions and 2 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,10 @@ prefix ends with `_html`.

# Testing

## Running tests
## Running tests (in the `app` subdirectory)

* Tests: `bundle exec rake spec`
* Tests: `bundle exec rspec`
* E2E tests: `RUN_E2E_TESTS=1 bundle exec rspec spec/e2e/`
* Ruby linter: `bundle exec rake standard`
* Accessibility scan: `./bin/pa11y-scan`
* Dynamic security scan: `./bin/owasp-scan`
Expand Down
1 change: 1 addition & 0 deletions app/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ group :development, :test do
gem "rubocop"
gem "rubocop-rspec"
gem "rubocop-rails-omakase"
gem "selenium-webdriver"
gem "standard", "~> 1.7"
gem "timecop"
gem "wkhtmltopdf-binary"
Expand Down
10 changes: 10 additions & 0 deletions app/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ GEM
base64
language_server-protocol (3.17.0.3)
lint_roller (1.1.0)
logger (1.6.1)
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
Expand Down Expand Up @@ -448,6 +449,13 @@ GEM
rexml
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
rubyzip (2.3.2)
selenium-webdriver (4.25.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
Expand Down Expand Up @@ -511,6 +519,7 @@ GEM
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.8.2)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
Expand Down Expand Up @@ -567,6 +576,7 @@ DEPENDENCIES
rubocop
rubocop-rails-omakase
rubocop-rspec
selenium-webdriver
sidekiq (~> 6.4)
sprockets-rails
standard (~> 1.7)
Expand Down
4 changes: 4 additions & 0 deletions app/app/services/pinwheel_webhook_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ def create_subscription_if_necessary(tunnel_url, name)
if existing_subscription
puts " Existing Pinwheel webhook subscription found in Pinwheel #{@sandbox_config.pinwheel_environment}: #{existing_subscription["url"]}"
remove_subscriptions(subscriptions.excluding(existing_subscription))

existing_subscription["id"]
else
remove_subscriptions(subscriptions)

puts " Registering Pinwheel webhooks for Ngrok tunnel in Pinwheel #{@sandbox_config.pinwheel_environment}..."
response = @pinwheel.create_webhook_subscription(WEBHOOK_EVENTS, receiver_url)
new_webhook_subscription_id = response["data"]["id"]
puts " ✅ Set up Pinwheel webhook: #{new_webhook_subscription_id}"

new_webhook_subscription_id
end
end

Expand Down
117 changes: 117 additions & 0 deletions app/spec/e2e/cbv_flow_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
require "rails_helper"

RSpec.describe "e2e CBV flow test", type: :feature, js: true do
include E2eTestHelpers
include_context "with_ngrok_tunnel"

let(:cbv_flow_invitation) { create(:cbv_flow_invitation) }

before(:all, js: true) do
unless ENV.fetch("PINWHEEL_API_TOKEN_SANDBOX", "").length == 64
raise "You need to set a PINWHEEL_API_TOKEN_SANDBOX in .env.test.local in order for this test to succeed"
end
unless ENV.fetch("USER", "").length > 0
raise "You need to set a USER environment variable"
end

# TODO: Remove this when we stub out Pinwheel usage:
# (We will have to allow access to the capybara server URL.)
WebMock.allow_net_connect!

# Register Ngrok with Pinwheel
capybara_server_url = URI(page.server_url)
@ngrok.start_tunnel(capybara_server_url.port)
puts "Found ngrok tunnel at #{@ngrok.tunnel_url}!"
@subscription_id = PinwheelWebhookManager.new.create_subscription_if_necessary(
@ngrok.tunnel_url,
ENV["USER"]
)
end

after(:all, js: true) do
if @subscription_id
puts "[PINWHEEL] Deleting webhook subscription id: #{@subscription_id}"
PinwheelService.new("sandbox").delete_webhook_subscription(@subscription_id)
end

# TODO: Remove these when we stub out Pinwheel usage:
page.quit
WebMock.disable_net_connect!
end

shared_examples "proceeding through the flow normally" do
it "completes the flow" do
# /cbv/entry
visit URI(cbv_flow_invitation.to_url).request_uri
verify_page(page, title: I18n.t("cbv.entries.show.header.default", agency_acronym: "CBV"))
click_button I18n.t("cbv.entries.show.get_started")

# /cbv/agreement
verify_page(page, title: I18n.t("cbv.agreements.show.header"))
find("label", text: I18n.t("cbv.agreements.show.checkbox.default", agency_full_name: I18n.t("shared.agency_full_name.sandbox"))).click
click_button I18n.t("cbv.agreements.show.continue")

# /cbv/employer_search
verify_page(page, title: I18n.t("cbv.employer_searches.show.header"))
fill_in name: "query", with: "foo"
click_button I18n.t("cbv.employer_searches.show.search")
expect(page).to have_content("McKee Foods")
find("div.usa-card__container", text: "McKee Foods").click_button(I18n.t("cbv.employer_searches.show.select"))

# Pinwheel modal
pinwheel_modal = page.find("iframe.pinwheel-modal-show")
page.within_frame(pinwheel_modal) do
debugger
if I18n.locale == :en
fill_in "Username", with: "user_good", wait: 10
fill_in "Workday Password", with: "pass_good"
click_button "Continue"
elsif I18n.locale == :es
fill_in "Nombre de usuario", with: "user_good", wait: 10
fill_in "Contraseña de Workday", with: "pass_good"
click_button "Continuar"
else
raise "Unknown locale: #{I18n.locale}"
end
end

# /cbv/synchronizations
verify_page(page, title: I18n.t("cbv.synchronizations.show.header"), wait: 15)

# All the pinwheel webhooks occur here!

# /cbv/payment_details
verify_page(page, title: I18n.t("cbv.payment_details.show.header", employer_name: "Acme Corporation"), wait: 60)
fill_in "cbv_flow[additional_information]", with: "Some kind of additional information"
click_button I18n.t("cbv.payment_details.show.continue")

# /cbv/add_job
verify_page(page, title: I18n.t("cbv.add_jobs.show.header"))
find("label", text: I18n.t("cbv.add_jobs.show.no_radio")).click
click_button I18n.t("cbv.add_jobs.show.continue")

# /cbv/summary
verify_page(page, title: I18n.t("cbv.summaries.show.header"))
find(:css, "label[for=cbv_flow_consent_to_authorized_use]").click
click_button I18n.t("cbv.summaries.show.send_report", agency_acronym: "CBV")

# TODO: Test PDF rendering by writing it to a file
end
end

context "in english" do
it_behaves_like "proceeding through the flow normally"
end

context "in spanish" do
before do
cbv_flow_invitation.update(language: "es")
end

around do |ex|
I18n.with_locale("es", &ex)
end

it_behaves_like "proceeding through the flow normally"
end
end
3 changes: 3 additions & 0 deletions app/spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
require "view_component/test_helpers"
require "support/context/gpg_setup"
require "view_component/system_test_helpers"

require "capybara/rspec"
Capybara.default_driver = Capybara.javascript_driver

# Add additional requires below this line. Rails is not loaded until this point!

# Requires supporting ruby files with custom matchers and macros, etc, in
Expand Down
5 changes: 5 additions & 0 deletions app/spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@
# metadata: `fit`, `fdescribe` and `fcontext`, respectively.
config.filter_run_when_matching :focus

# Don't run E2E tests with JS for now
if ENV["RUN_E2E_TESTS"].nil?
config.filter_run_excluding js: true
end

# Allows RSpec to persist some state between runs in order to support
# the `--only-failures` and `--next-failure` CLI options. We recommend
# you configure your source control system to ignore this file.
Expand Down
63 changes: 63 additions & 0 deletions app/spec/support/context/with_ngrok_tunnel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
require "open3"

# This shared context handles the lifecycle of an ngrok subprocess.
#
# To use it in your specs:
#
# include_context "with_ngrok_tunnel"
#
# and then in your before/after blocks:
#
# before(:all) do
# @ngrok.start_tunnel(3000) # Create tunnel to port 3000
#
# # Do something with the Ngrok tunnel
# puts "Ngrok is running at #{@ngrok.tunnel_url}"
# end
RSpec.shared_context "with_ngrok_tunnel" do
class NgrokManager
def initialize
@thread = nil
@tunnel_url = nil
end

def start_tunnel(destination_port)
@thread = Thread.new do |t|
begin
_stdin, stdout, _stderr, wait_thr = Open3.popen3("ngrok http #{destination_port} --log stdout")
puts "[NGROK] Started with pid #{wait_thr.pid} to local port #{destination_port}"
stdout.each_line do |log|
if log.include?('msg="started tunnel"')
@tunnel_url ||= log.match(/url=([^ ]+)/)[1].strip
end
end
rescue => e
puts "[NGROK] Fatal error: #{e}"
ensure
puts "[NGROK] Killing pid #{wait_thr.pid}"
Process.kill("TERM", wait_thr.pid) if wait_thr.alive?
end
end
end

def tunnel_url
Timeout.timeout(5) { sleep 0.1 while @tunnel_url.nil? }

@tunnel_url
rescue Timeout::Error => ex
raise "[NGROK] Timed out waiting for ngrok to initialize. Make sure `ngrok http 3000 --log stdout` works locally?"
end

def kill
@thread.kill if @thread
end
end

before(:all) do
@ngrok = NgrokManager.new
end

after(:all) do
@ngrok.kill if @ngrok
end
end
15 changes: 15 additions & 0 deletions app/spec/support/e2e_test_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module E2eTestHelpers
def verify_page(page, title:, wait: Capybara.default_max_wait_time)
expect(page).to have_content(title, wait: wait)

# Verify page has no missing translations
Capybara.using_wait_time(0) do
missing_translations = page.all("span", class: "translation_missing")
raise(<<~ERROR) if missing_translations.any?
E2E test failed on #{page.current_url}:
#{missing_translations.map { |el| el["title"] }}
ERROR
end
end
end

0 comments on commit 4435278

Please sign in to comment.