diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 8e21138f..69621d00 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -90,6 +90,7 @@ jobs: POSTGRES_PORT: 5432 INTERCOM_SECRET: secret BASE_URL: test.com + NODE_VERSION: 12.22.6 services: redis: @@ -111,6 +112,16 @@ jobs: steps: - uses: actions/checkout@v2.3.4 + - uses: actions/setup-node@v2 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Setup node + run: npm install -g npm@7.0.0 + working-directory: ./frontend + - run: npm install - name: Install PostgreSQL client run: | diff --git a/backend/Gemfile b/backend/Gemfile index cf488880..9886d03f 100644 --- a/backend/Gemfile +++ b/backend/Gemfile @@ -90,6 +90,8 @@ group :development do end group :test do + gem "capybara" + gem "cuprite" gem "mongoid-rspec" gem "shoulda-matchers" gem "vcr" diff --git a/backend/Gemfile.lock b/backend/Gemfile.lock index 77230109..1508a9f8 100644 --- a/backend/Gemfile.lock +++ b/backend/Gemfile.lock @@ -97,6 +97,15 @@ GEM cancancan (3.3.0) cancancan-mongoid (2.0.0) cancancan (>= 2.0, < 4) + capybara (3.39.2) + addressable + matrix + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (>= 1.5, < 3.0) + xpath (~> 3.2) coderay (1.1.3) coercible (1.0.0) descendants_tracker (~> 0.0.1) @@ -109,6 +118,9 @@ GEM crack (0.4.5) rexml crass (1.0.6) + cuprite (0.14.3) + capybara (~> 3.0) + ferrum (~> 0.13.0) database_cleaner (2.0.2) database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (2.1.0) @@ -170,6 +182,11 @@ GEM faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) + ferrum (0.13) + addressable (~> 2.5) + concurrent-ruby (~> 1.1) + webrick (~> 1.7) + websocket-driver (>= 0.6, < 0.8) ffaker (2.21.0) ffi (1.15.5-java) forecast_io (2.0.2) @@ -213,6 +230,7 @@ GEM net-pop net-smtp marcel (1.0.2) + matrix (0.4.2) method_source (1.0.0) mini_mime (1.1.2) mini_portile2 (2.8.4) @@ -446,11 +464,14 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + webrick (1.8.1) websocket-driver (0.7.6) websocket-extensions (>= 0.1.0) websocket-driver (0.7.6-java) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) + xpath (3.2.0) + nokogiri (~> 1.8) yard (0.9.34) zeitwerk (2.6.9) @@ -471,8 +492,10 @@ DEPENDENCIES byebug cancancan (~> 3.3.0) cancancan-mongoid (= 2.0.0) + capybara colored (~> 1.2) countries + cuprite database_cleaner database_cleaner-mongoid devise (= 4.8.0) diff --git a/backend/env-example b/backend/env-example index 96ab4a4b..dce94c15 100644 --- a/backend/env-example +++ b/backend/env-example @@ -55,3 +55,10 @@ FULLSTORY_ORG= # MongoDB MONGODB_HOST=localhost + +# System spec server ports +SYSTEM_SPEC_BACKEND_PORT=3003 +SYSTEM_SPEC_FRONTEND_PORT=4303 + +# Run system specs with a headless chrome instance +HEADLESS_CHROME=true diff --git a/backend/lib/tasks/spec.rake b/backend/lib/tasks/spec.rake new file mode 100644 index 00000000..2f93b347 --- /dev/null +++ b/backend/lib/tasks/spec.rake @@ -0,0 +1,17 @@ +require_relative "../../spec/system/support/frontend_app" + +namespace :spec do + namespace :system do + task :start_frontend do + frontend_app = SystemSpec::FrontendApp.instance + frontend_app.start $stdout + + trap "INT" do + frontend_app.stop + exit 1 + end + + frontend_app.wait_on + end + end +end diff --git a/backend/spec/system/signup_spec.rb b/backend/spec/system/signup_spec.rb new file mode 100644 index 00000000..2829e661 --- /dev/null +++ b/backend/spec/system/signup_spec.rb @@ -0,0 +1,8 @@ +require "system_spec_helper" + +RSpec.describe "signup flow" do + it "loads the root page" do + visit "/" + expect(page.title).to eq "Flaredown" + end +end diff --git a/backend/spec/system/support/capybara_config.rb b/backend/spec/system/support/capybara_config.rb new file mode 100644 index 00000000..00a417bc --- /dev/null +++ b/backend/spec/system/support/capybara_config.rb @@ -0,0 +1,19 @@ +# Usually, especially when using Selenium, developers tend to increase the max wait time. +# With Cuprite, there is no need for that. +# We use a Capybara default value here explicitly. +Capybara.default_max_wait_time = 2 + +# Normalize whitespaces when using `has_text?` and similar matchers, +# i.e., ignore newlines, trailing spaces, etc. +# That makes tests less dependent on slightly UI changes. +Capybara.default_normalize_ws = true + +# Where to store system tests artifacts (e.g. screenshots, downloaded files, etc.). +# It could be useful to be able to configure this path from the outside (e.g., on CI). +Capybara.save_path = ENV.fetch("CAPYBARA_ARTIFACTS", "./tmp/capybara") + +# Port on which to fire up the rails backend. +Capybara.server_port = ENV["SYSTEM_SPEC_BACKEND_PORT"] + +# Where to find frontend app; see ./frontend_app.rb +Capybara.app_host = "http://localhost:#{ENV["SYSTEM_SPEC_FRONTEND_PORT"]}" diff --git a/backend/spec/system/support/cuprite_config.rb b/backend/spec/system/support/cuprite_config.rb new file mode 100644 index 00000000..3f4e5e03 --- /dev/null +++ b/backend/spec/system/support/cuprite_config.rb @@ -0,0 +1,20 @@ +# First, load Cuprite Capybara integration +require "capybara/cuprite" + +# Then, we need to register our driver to be able to use it later +# with #driven_by method. +# NOTE: The name :cuprite is already registered by Rails. +# See https://github.com/rubycdp/cuprite/issues/180 +Capybara.register_driver(:better_cuprite) do |app| + Capybara::Cuprite::Driver.new( + app, + window_size: [1200, 800], + browser_options: {}, + process_timeout: 10, + inspector: true, + headless: ENV["HEADLESS_CHROME"] != "false" + ) +end + +# Configure Capybara to use :better_cuprite driver by default +Capybara.default_driver = Capybara.javascript_driver = :better_cuprite diff --git a/backend/spec/system/support/frontend_app.rb b/backend/spec/system/support/frontend_app.rb new file mode 100644 index 00000000..3d71431d --- /dev/null +++ b/backend/spec/system/support/frontend_app.rb @@ -0,0 +1,79 @@ +module SystemSpec + class FrontendApp + include Singleton + + def start(log = "/dev/null") + if test_connection_to_frontend_app + puts "Frontend app already running at #{frontend_app_url}" + return + end + + puts "Starting frontend app at #{frontend_app_url}" + puts `pwd` + puts "ls .." + puts `ls ..` + puts "ls ../frontend" + puts `ls ../frontend` + @pid ||= Process.spawn( + frontend_app_cmd, + [:out, :err] => log, + :chdir => "../frontend" + ) + + timeout = 15.seconds + unless test_connection_to_frontend_app(timeout) + fail "Frontend app did not start within #{timeout} seconds" + end + end + + def wait_on + return unless @pid + Process.wait @pid + end + + def stop + return unless @pid + Process.kill "QUIT", @pid + Process.wait @pid + end + + private + + def test_connection_to_frontend_app(timeout = 0) + system( + test_connection_cmd(timeout), + out: "/dev/null" + ) + end + + def test_connection_cmd(timeout) + [ + "curl --silent --head -X GET", + "--retry-connrefused", + "--retry", timeout.to_s, + "--retry-delay", "1", + frontend_app_url + ].join(" ") + end + + def frontend_app_cmd + [ + "./node_modules/.bin/ember", "serve", + "--port", frontend_port, + "--proxy", "http://localhost:#{backend_port}" + ].join(" ") + end + + def frontend_app_url + "http://localhost:#{frontend_port}" + end + + def frontend_port + ENV["SYSTEM_SPEC_FRONTEND_PORT"] + end + + def backend_port + ENV["SYSTEM_SPEC_BACKEND_PORT"] + end + end +end diff --git a/backend/spec/system_spec_helper.rb b/backend/spec/system_spec_helper.rb new file mode 100644 index 00000000..93c0e4bf --- /dev/null +++ b/backend/spec/system_spec_helper.rb @@ -0,0 +1,21 @@ +require "rails_helper" + +# Most of the system spec config was borrowed, with many thanks, from: +# https://evilmartians.com/chronicles/system-of-a-test-setting-up-end-to-end-rails-testing +require "system/support/frontend_app" +require "system/support/capybara_config" +require "system/support/cuprite_config" + +RSpec.configure do |config| + config.prepend_before(:each, type: :system) do + driven_by Capybara.javascript_driver + end + + config.before(:suite) do + SystemSpec::FrontendApp.instance.start + end + + config.after(:suite) do + SystemSpec::FrontendApp.instance.stop + end +end