From 45d73955c908f7e8fe8b97793bd003501d74e083 Mon Sep 17 00:00:00 2001 From: kosiakkatrina <54268893+kosiakkatrina@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:50:24 +0100 Subject: [PATCH] CLDC-759 Add accessibility testing (#2529) * Add accessibility tests * Add accessibiliy tests to CI * Lint and refactor * Adjust and update paths * Add some more page tests --- .github/workflows/staging_pipeline.yml | 61 +++++++- Gemfile | 1 + Gemfile.lock | 23 +++ config/routes.rb | 2 - spec/features/accessibility_spec.rb | 194 +++++++++++++++++++++++++ spec/rails_helper.rb | 4 + 6 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 spec/features/accessibility_spec.rb diff --git a/.github/workflows/staging_pipeline.yml b/.github/workflows/staging_pipeline.yml index 0f1352ffb..415a943d1 100644 --- a/.github/workflows/staging_pipeline.yml +++ b/.github/workflows/staging_pipeline.yml @@ -130,7 +130,7 @@ jobs: - name: Run tests run: | - bundle exec rspec spec/features --fail-fast + bundle exec rspec spec/features --fail-fast --exclude-pattern "spec/features/accessibility_spec.rb" model_test: name: Model tests @@ -249,6 +249,65 @@ jobs: run: | bundle exec rake parallel:spec['spec/requests'] + accessibility_test: + name: Accessibility tests + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:13.5 + env: + POSTGRES_PASSWORD: password + POSTGRES_USER: postgres + POSTGRES_DB: data_collector + ports: + - 5432:5432 + # Needed because the Postgres container does not provide a health check + # tmpfs makes database faster by using RAM + options: >- + --mount type=tmpfs,destination=/var/lib/postgresql/data + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + RAILS_ENV: test + GEMFILE_RUBY_VERSION: 3.1.1 + DB_HOST: localhost + DB_DATABASE: data_collector + DB_USERNAME: postgres + DB_PASSWORD: password + RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }} + PARALLEL_TEST_PROCESSORS: 4 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + cache: yarn + node-version: 20 + + - name: Create database + run: | + bundle exec rake parallel:setup + + - name: Compile assets + run: | + bundle exec rake assets:precompile + + - name: Run tests + run: | + bundle exec rspec spec/features/accessibility_spec.rb --fail-fast + lint: name: Lint runs-on: ubuntu-latest diff --git a/Gemfile b/Gemfile index f43a1b7d9..332359f98 100644 --- a/Gemfile +++ b/Gemfile @@ -97,6 +97,7 @@ group :development do end group :test do + gem "axe-core-rspec" gem "capybara", require: false gem "capybara-lockstep" gem "capybara-screenshot" diff --git a/Gemfile.lock b/Gemfile.lock index fa09aeb53..c8d1edd1a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,6 +91,17 @@ GEM aws-sigv4 (~> 1.8) aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) + axe-core-api (4.9.1) + dumb_delegator + virtus + axe-core-rspec (4.9.1) + axe-core-api (= 4.9.1) + dumb_delegator + virtus + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) base64 (0.2.0) bcrypt (3.1.20) better_html (2.0.2) @@ -128,6 +139,8 @@ GEM launchy childprocess (5.0.0) coderay (1.1.3) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) concurrent-ruby (1.3.3) connection_pool (2.4.1) crack (1.0.0) @@ -137,6 +150,8 @@ GEM cssbundling-rails (1.4.0) railties (>= 6.0.0) date (3.3.4) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) devise (4.9.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -155,6 +170,7 @@ GEM dotenv-rails (3.0.2) dotenv (= 3.0.2) railties (>= 6.1) + dumb_delegator (1.0.0) encryptor (3.0.0) erb_lint (0.5.0) activesupport @@ -204,6 +220,7 @@ GEM activesupport (>= 6.1.4.4) i18n (1.14.5) concurrent-ruby (~> 1.0) + ice_nine (0.11.2) iniparse (1.5.0) jmespath (1.6.2) jsbundling-rails (1.3.0) @@ -450,6 +467,7 @@ GEM railties (>= 6.0.0) strscan (3.1.0) thor (1.3.1) + thread_safe (0.3.6) timecop (0.9.8) timeout (0.4.1) turbo-rails (1.5.0) @@ -467,6 +485,10 @@ GEM activesupport (>= 5.2.0, < 8.0) concurrent-ruby (~> 1.0) method_source (~> 1.0) + virtus (2.0.0) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) warden (1.2.9) rack (>= 2.0.9) web-console (4.2.1) @@ -499,6 +521,7 @@ PLATFORMS DEPENDENCIES auto_strip_attributes aws-sdk-s3 + axe-core-rspec bootsnap (>= 1.4.4) bundler-audit byebug diff --git a/config/routes.rb b/config/routes.rb index a6de59a65..e0d9631e9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -72,7 +72,6 @@ Rails.application.routes.draw do get "details", to: "schemes#details" get "check-answers", to: "schemes#check_answers" get "edit-name", to: "schemes#edit_name" - get "support-services-provider", to: "schemes#support_services_provider" get "new-deactivation", to: "schemes#new_deactivation" get "deactivate-confirm", to: "schemes#deactivate_confirm" get "reactivate", to: "schemes#reactivate" @@ -148,7 +147,6 @@ Rails.application.routes.draw do post "data-sharing-agreement", to: "organisations#confirm_data_sharing_agreement" get "users", to: "organisations#users" - get "users/invite", to: "users/account#new" get "lettings-logs", to: "organisations#lettings_logs" get "delete-lettings-logs", to: "delete_logs#delete_lettings_logs_for_organisation" post "delete-lettings-logs", to: "delete_logs#delete_lettings_logs_for_organisation_with_selected_ids" diff --git a/spec/features/accessibility_spec.rb b/spec/features/accessibility_spec.rb new file mode 100644 index 000000000..d0c218445 --- /dev/null +++ b/spec/features/accessibility_spec.rb @@ -0,0 +1,194 @@ +require "rails_helper" + +RSpec.describe "Accessibility", js: true do + let(:user) { create(:user, :support) } + let!(:other_user) { create(:user, name: "new user", organisation: user.organisation, email: "new_user@example.com", confirmation_token: "abc") } + + def find_routes(type, resource, subresource) + routes = Rails.application.routes.routes.select do |route| + route.verb == "GET" && route.path.spec.to_s.start_with?("/#{type}") + end + + routes.map do |route| + route_path = route.path.spec.to_s + route_path + .gsub("/#{type}s/:id", "/#{type}s/#{resource.id}") + .gsub(":#{type.underscore}_id", resource.id.to_s) + .gsub(":id", subresource.id.to_s) + .gsub("(.:format)", "") + end + end + + before do + allow(user).to receive(:need_two_factor_authentication?).and_return(false) + sign_in(user) + end + + context "when viewing user pages" do + let(:user_paths) do + Rails.application.routes.routes.select { |route| route.verb == "GET" && route.path.spec.to_s.start_with?("/user") }.map { |route| + route_path = route.path.spec.to_s + route_path.gsub(":id", other_user.id.to_s).gsub(":user_id", other_user.id.to_s).gsub("(.:format)", "") + }.uniq + end + + it "is has accessible pages" do + user_paths.each do |path| + visit(path) + expect(page).to have_current_path(path) + expect(page).to be_axe_clean.according_to :wcag2aa + end + end + end + + context "when viewing organisation pages" do + let(:parent_relationship) { create(:organisation_relationship, parent_organisation: other_user.organisation) } + let(:child_relationship) { create(:organisation_relationship, child_organisation: other_user.organisation) } + let(:organisation_paths) do + routes = find_routes("organisation", other_user.organisation, other_user.organisation).reject do |route| + route.match?(/\A\/organisations\/#{other_user.organisation_id}\z/) || + route.include?("filters/update") + end + routes << "/organisations/#{other_user.organisation_id}/details" + route_mappings = { + "/schemes/csv-download" => "?download_type=combined", + "logs/csv-download" => "?codes_only=false&years[]=2024", + "filters/update" => "?codes_only=false", + "stock-owners/remove" => "?target_organisation_id=#{child_relationship.parent_organisation.id}", + "managing-agents/remove" => "?target_organisation_id=#{parent_relationship.child_organisation.id}", + } + + routes.map do |route| + additional_params = route_mappings.find { |pattern, _| route.include?(pattern) }&.last + route += additional_params if additional_params + route + end + end + + it "is has accessible pages" do + organisation_paths.each do |path| + visit(path) + expect(page).to have_current_path(path) + expect(page).to be_axe_clean.according_to :wcag2aa + end + end + end + + context "when viewing lettings log pages" do + let(:bulk_upload) { create(:bulk_upload, user:) } + let(:lettings_log) { create(:lettings_log, :completed, assigned_to: other_user, bulk_upload_id: bulk_upload.id) } + let(:organisation_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) } + + let(:lettings_log_paths) do + routes = find_routes("lettings-log", lettings_log, bulk_upload) + all_page_ids = FormHandler.instance.lettings_forms.values.flat_map(&:pages).map(&:id).uniq + lettings_log_pages = lettings_log.form.pages + other_form_page_ids = all_page_ids - lettings_log_pages.map(&:id) + + routes.reject { |path| + path.include?("/edit") || path.include?("/new") || path.include?("*page") || path.include?("filters/update") || + path.include?("local-authority/check-answers") || path.include?("declaration/check-answers") || + path.include?("/lettings-logs/bulk-upload-logs/#{bulk_upload.id}") || + path.include?("bulk-upload-soft-validations-check") || + path == "/lettings-logs/bulk-upload-resume/#{bulk_upload.id}" || + other_form_page_ids.any? { |page_id| path.include?(page_id.dasherize) } || + lettings_log_pages.any? { |page| path.include?(page.id.dasherize) && !page.routed_to?(lettings_log, user) } + }.uniq + end + + before do + lettings_log.dup.tap do |log| + log.save(validate: false) + end + allow(FormHandler.instance).to receive(:in_crossover_period?).and_return(true) + end + + it "is has accessible pages" do + lettings_log_paths.each do |path| + path += "?original_log_id=#{lettings_log.id}" if path.include?("duplicate") + path += "?codes_only=true&years[]=2024" if path.include?("csv") + path.gsub!("/start", "/prepare-your-file?form[year]=2024") if path.include?("bulk-upload-logs/start") + path.gsub!("/start", "/fix-choice") if path.include?("/bulk-upload-resume/#{bulk_upload.id}/start") + visit(path) + expect(page).to have_current_path(path) + expect(page).to be_axe_clean.according_to :wcag2aa + end + end + end + + context "when viewing sales log pages" do + let(:bulk_upload) { create(:bulk_upload, user:) } + let(:sales_log) { create(:sales_log, :completed, assigned_to: other_user, bulk_upload_id: bulk_upload.id) } + let(:organisation_relationship) { create(:organisation_relationship, parent_organisation: user.organisation) } + + let(:sales_log_paths) do + all_page_ids = FormHandler.instance.sales_forms.values.flat_map(&:pages).map(&:id).uniq + sales_log_pages = sales_log.form.pages + other_form_page_ids = all_page_ids - sales_log_pages.map(&:id) + + routes = find_routes("sales-log", sales_log, bulk_upload) + + routes.reject { |path| + path.include?("/edit") || path.include?("/new") || path.include?("*page") || + path.include?("/sales-logs/bulk-upload-logs/#{bulk_upload.id}") || + path.include?("bulk-upload-soft-validations-check") || path.include?("filters/update") || + path == "/sales-logs/bulk-upload-resume/#{bulk_upload.id}" || + path == "/sales-logs/bulk-upload-logs" || + other_form_page_ids.any? { |page_id| path.include?(page_id.dasherize) } || + sales_log_pages.any? { |page| path.include?(page.id.dasherize) && !page.routed_to?(sales_log, user) } + }.uniq + end + + it "is has accessible pages" do + sales_log_paths.each do |path| + path += "?original_log_id=#{sales_log.id}" if path.include?("duplicate") + path += "?codes_only=true&years[]=2024" if path.include?("csv") + path.gsub!("/start", "/prepare-your-file?form[year]=2024") if path.include?("bulk-upload-logs/start") + path.gsub!("/start", "/fix-choice") if path.include?("/bulk-upload-resume/#{bulk_upload.id}/start") + + visit(path) + expect(page).to have_current_path(path) + expect(page).to be_axe_clean.according_to :wcag2aa + end + end + end + + context "when viewing scheme pages" do + let(:scheme) { create(:scheme, owning_organisation: other_user.organisation) } + let!(:location) { create(:location, scheme:) } + let(:scheme_paths) do + routes = find_routes("scheme", scheme, location) + + routes.reject { |path| + path.include?("/edit") || path.include?("/new") || path.include?("*page") || + path.include?("reactivate") || path.include?("deactivate") + }.uniq + end + + before do + allow(FormHandler.instance).to receive(:in_crossover_period?).and_return(true) + end + + it "is has accessible pages" do + scheme_paths.each do |path| + visit(path) + expect(page).to have_current_path(path) + expect(page).to be_axe_clean.according_to :wcag2aa + end + end + end + + context "when viewing other pages" do + [{ path: "/", title: "homepage" }, + { path: "/guidance", title: "guidance" }, + { path: "/privacy-notice", title: "privacy notice" }, + { path: "/lettings-logs/bulk-upload-logs/guidance?form[year]=2024&referrer=home", title: "lettings BU guidance" }, + { path: "/sales-logs/bulk-upload-logs/guidance?form[year]=2024&referrer=home", title: "sales BU guidance" }].each do |test_case| + it "is has accessible #{test_case[:title]} page" do + visit(test_case[:path]) + expect(page).to have_current_path(test_case[:path]) + expect(page).to be_axe_clean.according_to :wcag2aa + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 36cf81b99..5f7529554 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -10,10 +10,14 @@ require "capybara-screenshot/rspec" require "selenium-webdriver" require "view_component/test_helpers" require "pundit/rspec" +require "axe-rspec" Capybara.register_driver :headless do |app| options = Selenium::WebDriver::Firefox::Options.new options.add_argument("--headless") + options.add_argument("--no-sandbox") + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--window-size=1400,1400") Capybara::Selenium::Driver.new(app, browser: :firefox, options:) end