diff --git a/.circleci/config.yml b/.circleci/config.yml index bab7de2c7..926275370 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,6 +22,12 @@ jobs: # This is the circleci provided Redis container. - image: circleci/redis:4.0.10 + + # localstack image + - image: localstack/localstack:latest + environment: + - SERVICES=sqs + parallelism: 1 resource_class: large steps: @@ -73,6 +79,18 @@ jobs: command: | ~/project/ci-bin/capture-log "make -f Makefile.example test" + - store_test_results: + name: Store test results as summary + path: ~/test-results + + - store_artifacts: + name: Store test results as artifact + path: ~/test-results + + - store_artifacts: + name: Store capybara screenshots + path: ~/project/tmp/capybara + - run: name: Lint command: | diff --git a/Gemfile b/Gemfile index b36f17f4e..20932393e 100644 --- a/Gemfile +++ b/Gemfile @@ -60,7 +60,8 @@ group :development, :test do end group :test do - gem "capybara", "2.6.2" + gem "capybara" + gem "capybara-screenshot" gem "database_cleaner" gem "launchy" gem "rspec" @@ -68,6 +69,7 @@ group :test do gem "simplecov" gem "sniffybara", git: "https://github.com/department-of-veterans-affairs/sniffybara.git" gem "timecop" + gem "webdrivers" gem "webmock" end diff --git a/Gemfile.lock b/Gemfile.lock index 8265399a3..cae229fe8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,9 +61,9 @@ GIT GIT remote: https://github.com/department-of-veterans-affairs/sniffybara.git - revision: d5f94213dcef08756f8d1622355015739e96ff4f + revision: 351560b5789ca638ba7c9b093c2bb1a9a6fda4b3 specs: - sniffybara (0.0.1) + sniffybara (1.1.0) rainbow selenium-webdriver @@ -115,8 +115,8 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - addressable (2.5.2) - public_suffix (>= 2.0.2, < 4.0) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) akami (1.3.1) gyoku (>= 0.4.0) nokogiri @@ -150,17 +150,20 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (10.0.0) - capybara (2.6.2) + capybara (3.29.0) addressable - mime-types (>= 1.16) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) + mini_mime (>= 0.1.3) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + regexp_parser (~> 1.5) + xpath (~> 3.2) + capybara-screenshot (1.0.23) + capybara (>= 1.0, < 4) + launchy case_transform (0.2) activesupport - childprocess (0.9.0) - ffi (~> 1.0, >= 1.0.11) + childprocess (3.0.0) coderay (1.1.2) coffee-rails (4.2.2) coffee-script (>= 2.2.0) @@ -193,7 +196,7 @@ GEM faraday (0.14.0) multipart-post (>= 1.2, < 3) fastercsv (1.5.5) - ffi (1.9.25) + ffi (1.11.3) globalid (0.4.2) activesupport (>= 4.2.0) gyoku (1.3.1) @@ -235,9 +238,9 @@ GEM makara (0.4.1) activerecord (>= 3.0.0) method_source (0.9.2) - mime-types (3.1) + mime-types (3.3) mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + mime-types-data (3.2019.1009) mini_magick (4.9.4) mini_mime (1.0.1) mini_portile2 (2.4.0) @@ -251,7 +254,7 @@ GEM thor (~> 0.19) newrelic_rpm (4.8.0.341) nio4r (2.3.1) - nokogiri (1.10.5) + nokogiri (1.10.7) mini_portile2 (~> 2.4.0) nori (2.6.0) omniauth (1.9.0) @@ -271,7 +274,7 @@ GEM byebug (~> 10.0) pry (~> 0.10) psych (3.1.0) - public_suffix (3.0.2) + public_suffix (4.0.1) puma (3.12.2) rack (2.0.7) rack-cors (1.1.0) @@ -331,6 +334,7 @@ GEM redis-store (1.4.1) redis (>= 2.2, < 5) ref (2.0.0) + regexp_parser (1.6.0) request_store (1.4.0) rack (>= 1.4) rspec (3.7.0) @@ -398,9 +402,9 @@ GEM sdoc (0.4.2) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) - selenium-webdriver (3.11.0) - childprocess (~> 0.5) - rubyzip (~> 1.2) + selenium-webdriver (3.142.6) + childprocess (>= 0.5, < 4.0) + rubyzip (>= 1.2.2) sentry-raven (2.7.2) faraday (>= 0.7.6, < 1.0) sexp_processor (4.10.1) @@ -448,6 +452,10 @@ GEM wasabi (3.5.0) httpi (~> 2.0) nokogiri (>= 1.4.2) + webdrivers (4.1.2) + nokogiri (~> 1.6) + rubyzip (~> 1.0) + selenium-webdriver (>= 3.0, < 4.0) webmock (3.6.2) addressable (>= 2.3.6) crack (>= 0.3.2) @@ -464,8 +472,8 @@ GEM xmlmapper (>= 0.7.3) xmlmapper (0.7.3) nokogiri (~> 1.5) - xpath (2.1.0) - nokogiri (~> 1.3) + xpath (3.2.0) + nokogiri (~> 1.8) zaru (0.2.0) zero_downtime_migrations (0.0.7) activerecord @@ -482,7 +490,8 @@ DEPENDENCIES brakeman (= 3.1.5) bundler-audit byebug - capybara (= 2.6.2) + capybara + capybara-screenshot caseflow! coffee-rails (> 4.1.0) connect_vbms! @@ -533,6 +542,7 @@ DEPENDENCIES uglifier (>= 1.3.0) uswds-rails! wannabe_bool + webdrivers webmock zaru zero_downtime_migrations diff --git a/Makefile.example b/Makefile.example index f7fb991e5..ed5de364c 100644 --- a/Makefile.example +++ b/Makefile.example @@ -17,7 +17,7 @@ run: up ## Start Rails foreman start test: clean ## Run the rspec suite - bundle exec rspec + CI=true bundle exec rspec clean: ## Remove old files rm -f log/test.log diff --git a/app/controllers/downloads_controller.rb b/app/controllers/downloads_controller.rb index 0dcfac10a..09d409980 100644 --- a/app/controllers/downloads_controller.rb +++ b/app/controllers/downloads_controller.rb @@ -1,4 +1,5 @@ class DownloadsController < ApplicationController + # :nocov: before_action :authorize rescue_from ActiveRecord::RecordNotFound, with: :record_not_found @@ -152,4 +153,5 @@ def current_document_status { "progress": 0, "completed": 1, "errored": 2 }[current_tab.to_sym] end helper_method :current_document_status + # :nocov: end diff --git a/app/models/fetcher.rb b/app/models/fetcher.rb index 6d98c2f73..8b5144961 100644 --- a/app/models/fetcher.rb +++ b/app/models/fetcher.rb @@ -13,7 +13,7 @@ def content(save_document_metadata: true) private def cached_content - @cached_content ||= MetricsService.record("S3: fetch content for: #{document.s3_filename}", + @cached_content ||= MetricsService.record("S3: Fetcher fetch content for: #{document.s3_filename}", service: :s3, name: "fetch_content") do S3Service.fetch_content(document.s3_filename) diff --git a/app/services/record_fetcher.rb b/app/services/record_fetcher.rb index 1a847a2d9..8bbf35631 100644 --- a/app/services/record_fetcher.rb +++ b/app/services/record_fetcher.rb @@ -9,6 +9,7 @@ class RecordFetcher def process s = Redis::Semaphore.new("record_#{record.id}".to_s, url: Rails.application.secrets.redis_url_cache, + stale_client_timeout: 5, expiration: SECONDS_TO_AUTO_UNLOCK) s.lock(SECONDS_TO_AUTO_UNLOCK) content_from_s3 || content_from_vbms @@ -28,7 +29,7 @@ def content_from_vbms end def content_from_s3 - @content_from_s3 ||= MetricsService.record("S3: fetch content for: #{record.s3_filename}", + @content_from_s3 ||= MetricsService.record("S3: RecordFetcher fetch content for: #{record.s3_filename}", service: :s3, name: "content_from_s3") do S3Service.fetch_content(record.s3_filename) diff --git a/app/views/gui/single_page_app.html.erb b/app/views/gui/single_page_app.html.erb index 03d8fd12b..b4911d5ed 100644 --- a/app/views/gui/single_page_app.html.erb +++ b/app/views/gui/single_page_app.html.erb @@ -1,4 +1,4 @@ - + eFolder Express diff --git a/app/views/help/show.html.erb b/app/views/help/show.html.erb index b8c0a4bbb..1309cbff5 100644 --- a/app/views/help/show.html.erb +++ b/app/views/help/show.html.erb @@ -11,21 +11,21 @@
- + Access and Login
Duration: 1:52

Learn how to gain access and login to eFolder Express

- + Navigating the Interface
Duration: 1:59

Learn how to use eFolder Express and improve your workflow.

- + Find an eFolder
Duration: 1:03

Learn how to search for the full contents of a veteran's eFolder

@@ -34,21 +34,21 @@
- + Downloading the eFolder
Duration: 2:16

Learn how to download the files contained in a Veteran's eFolder.

- + Managing Multiple Downloads
Duration: 0:59

Learn how to improve your workflow by downloading and managing multiple Veteran's eFolder at once.

- + Providing Feedback
Duration: 1:36

Learn how to give feedback and seek help directly from the Digital Service support team.

diff --git a/lib/fakes/test_auth_strategy.rb b/lib/fakes/test_auth_strategy.rb index d1cc2078d..294f9ae39 100644 --- a/lib/fakes/test_auth_strategy.rb +++ b/lib/fakes/test_auth_strategy.rb @@ -1,10 +1,29 @@ require "omniauth/strategies/developer" require "omniauth/form" +class EfolderAuthForm < OmniAuth::Form + def header(title, header_info) + @html << <<-HTML + + + + + #{title} + #{css} + #{header_info} + + +

#{title}

+
+ HTML + self + end +end + class OmniAuth::Strategies::TestAuthStrategy < OmniAuth::Strategies::Developer # custom form rendering def request_phase - form = OmniAuth::Form.new(title: "Test VA Saml", url: callback_path) + form = EfolderAuthForm.new(title: "Test VA Saml", url: callback_path) options.fields.each do |field| form.text_field field.to_s.capitalize.tr("_", " "), field.to_s end diff --git a/lib/tasks/spec_browsers.rake b/lib/tasks/spec_browsers.rake deleted file mode 100644 index 630ac21ff..000000000 --- a/lib/tasks/spec_browsers.rake +++ /dev/null @@ -1,16 +0,0 @@ -begin - require "rspec" - - namespace :spec do - desc "Run the feature specs on supported browsers" - - Rake::Task["assets:precompile"].execute - - RSpec::Core::RakeTask.new(:browsers) do |t| - t.pattern = "spec/feature/**/*_spec.rb" - end - end - # rubocop:disable Lint/HandleExceptions -rescue LoadError, NameError -end -# rubocop:enable Lint/HandleExceptions diff --git a/spec/features/download_spec.rb b/spec/features/download_spec.rb index ffe62652a..d1cf6c868 100644 --- a/spec/features/download_spec.rb +++ b/spec/features/download_spec.rb @@ -266,7 +266,7 @@ def assert_coachmark_does_not_exist end scenario "Download with no documents" do - download = @user_download.create(status: :no_documents) + download = @user_download.create(file_number: "666000", status: :no_documents) visit download_path(download) expect(page).to have_css ".cf-app-msg-screen", text: "No Documents in eFolder" @@ -465,7 +465,7 @@ def assert_coachmark_does_not_exist expect(page).to have_content("Something went wrong...") end - scenario "Completed download" do + scenario "Completed download", download: true do veteran_info = { "12" => { "veteran_first_name" => "Stan", diff --git a/spec/features/react_download_spec.rb b/spec/features/react_download_spec.rb index 25228836b..fbac0e845 100644 --- a/spec/features/react_download_spec.rb +++ b/spec/features/react_download_spec.rb @@ -87,7 +87,7 @@ expect(manifest.veteran_last_four_ssn).to eq("2222") end - scenario "Happy path, zip file is downloaded" do + scenario "Happy path, zip file is downloaded", download: true do perform_enqueued_jobs do visit "/" fill_in "Search for a Veteran ID number below to get started.", with: veteran_id @@ -141,7 +141,7 @@ end end - scenario "Loading bar appears when waiting for case to download" do + scenario "Loading bar appears when waiting for case to download", download: true do perform_enqueued_jobs do visit "/" fill_in "Search for a Veteran ID number below to get started.", with: veteran_id @@ -160,7 +160,7 @@ expect(page).to have_css ".progress-bar" end - scenario "Veteran ID does not persist in search bar after searching" do + scenario "Veteran ID does not persist in search bar after searching", download: true do perform_enqueued_jobs do visit "/" fill_in "Search for a Veteran ID number below to get started.", with: veteran_id @@ -227,15 +227,15 @@ expect(page).to have_css ".cf-tab.cf-active", text: "Progress (1)" expect(page).to have_content "1 of 3 files remaining" - expect(page).to have_content Caseflow::DocumentTypes::TYPES[records[0].type_id] + expect(page).to have_content Caseflow::DocumentTypes::TYPES[records[0].type_id.to_i] click_on "Completed (1)" expect(page).to have_css ".cf-tab.cf-active", text: "Completed (1)" - expect(page).to have_content Caseflow::DocumentTypes::TYPES[records[1].type_id] + expect(page).to have_content Caseflow::DocumentTypes::TYPES[records[1].type_id.to_i] click_on "Errors (1)" expect(page).to have_css ".cf-tab.cf-active", text: "Errors (1)" - expect(page).to have_content Caseflow::DocumentTypes::TYPES[records[2].type_id] + expect(page).to have_content Caseflow::DocumentTypes::TYPES[records[2].type_id.to_i] click_on "Start over" @@ -268,7 +268,7 @@ let(:veteran_id) { "808909111" } after { Timecop.return } - scenario "Viewing page for manifest with old zipfile shows search results page" do + scenario "Viewing page for manifest with old zipfile shows search results page", download: true do perform_enqueued_jobs do # Search for an efolder and start a download. visit "/" diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 22608e342..b5892e7c6 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -24,91 +24,13 @@ # Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } -require "selenium-webdriver" -require "capybara" +# The TZ variable controls the timezone of the browser in capybara tests, so we always define it. +# By default (esp for CI) we use Eastern time, so that it doesn't matter where the developer happens to sit. +#ENV["TZ"] ||= "America/New_York" -Sniffybara::Driver.path_exclusions << /samlva/ -Sniffybara::Driver.configuration_file = File.expand_path("../support/VA-axe-configuration.json", __FILE__) -Sniffybara::Driver.issue_id_exceptions += [] - -download_directory = Rails.root.join("tmp", "downloads_all") -cache_directory = Rails.root.join("tmp", "browser_cache_all") - -FileUtils.mkpath download_directory unless File.directory?(download_directory) -if File.directory?(cache_directory) - FileUtils.rm_r cache_directory -else - Dir.mkdir cache_directory -end - -Capybara.register_driver(:virtual_framebuffer_chrome) do |app| - chrome_options = ::Selenium::WebDriver::Chrome::Options.new - - chrome_options.add_preference(:download, - prompt_for_download: false, - default_directory: download_directory) - - chrome_options.add_preference(:browser, - disk_cache_dir: cache_directory) - - options = { - port: 51_674, - browser: :chrome, - options: chrome_options - } - - Sniffybara::Driver.current_driver = Sniffybara::Driver.new(app, options) -end -Capybara.default_driver = :virtual_framebuffer_chrome - -# Convenience methods for stubbing current user -module StubbableUser - module ClassMethods - def stub=(user) - @stub = user - end - - def authenticate!(options = {}) - css_id = options[:css_id] || "123123" - user_name = options[:user_name] || "first last" - Functions.grant!("System Admin", users: [css_id]) if options[:roles]&.include?("System Admin") - - self.stub = find_or_create_by(css_id: css_id, station_id: "116").tap do |u| - u.name = user_name - u.email = "test@gmail.com" - u.roles = options[:roles] || ["Download eFolder"] - u.save - RequestStore.store[:current_user] = u - end - end - - def tester!(options = {}) - self.stub = find_or_create_by(css_id: ENV["TEST_USER_ID"], station_id: "116").tap do |u| - u.name = "first last" - u.email = "test@gmail.com" - u.roles = options[:roles] || ["Download eFolder"] - u.save - end - end - - def unauthenticate! - Functions.delete_all_keys! - RequestStore.store[:current_user] = nil - self.stub = nil - end - - def from_session_and_request(session, request) - @stub || super(session, request) - end - end - - def self.prepended(base) - class << base - prepend ClassMethods - end - end -end -User.prepend(StubbableUser) +# Assume the browser and the server are in the same timezone for now. Eventually we should +# use something like https://github.com/alindeman/zonebie to exercise browsers in different timezones. +#Time.zone = ENV["TZ"] module RandomHelper def self.valid_document_id @@ -119,7 +41,7 @@ def self.valid_document_id # Wrap this around your test to run it many times and ensure that it passes consistently. # Note: do not merge to master like this, or the tests will be slow! Ha. def ensure_stable - repeat_count = ENV["TRAVIS"] ? 100 : 20 + repeat_count = ENV.fetch("ENSURE_STABLE", "10").to_i repeat_count.times do yield end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cac62d59b..a2d5dbd4c 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -39,6 +39,7 @@ def test_large_files? RSpec.configure do |config| config.filter_run focus: true config.run_all_when_everything_filtered = true + config.filter_run_excluding download: true config.filter_run_excluding :large_files unless test_large_files? diff --git a/spec/support/VA-axe-configuration.json b/spec/support/VA-axe-configuration.json deleted file mode 100644 index 04d5f2e94..000000000 --- a/spec/support/VA-axe-configuration.json +++ /dev/null @@ -1,2003 +0,0 @@ -{ - "rules": [ - { - "id": "accesskeys", - "selector": "[accesskey]", - "excludeHidden": false, - "enabled": true, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "accesskeys" - ], - "tags": [ - "wcag2a", - "wcag211", - "section508", - "section508.22.n" - ] - }, - { - "id": "area-alt", - "selector": "map area[href]", - "excludeHidden": false, - "enabled": true, - "pageLevel": false, - "any": [ - "non-empty-alt" - ], - "all": [], - "none": [], - "tags": [ - "wcag2a", - "wcag111", - "section508", - "section508.22.a" - ] - }, - { - "id": "audio-caption", - "selector": "audio", - "excludeHidden": false, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "caption" - ], - "tags": [ - "wcag2a", - "wcag122", - "section508", - "section508.22.a" - ] - }, - { - "id": "blink", - "selector": "blink", - "excludeHidden": false, - "enabled": true, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "is-on-screen" - ], - "tags": [ - "wcag2a", - "wcag222", - "section508", - "section508.22.j" - ] - }, - { - "id": "button-name", - "selector": "button, [role=\"button\"], input[type=\"button\"], input[type=\"submit\"], input[type=\"reset\"]", - "excludeHidden": true, - "enabled": true, - "pageLevel": false, - "any": [ - "non-empty-if-present", - "non-empty-value", - "button-has-visible-text" - ], - "all": [], - "none": [ - "focusable-no-name" - ], - "tags": [ - "wcag2a", - "wcag412", - "section508", - "section508.22.a" - ] - }, - { - "id": "bypass", - "selector": "html", - "excludeHidden": true, - "enabled": true, - "pageLevel": true, - "any": [ - "internal-link-present", - "header-present", - "landmark" - ], - "all": [], - "none": [], - "tags": [ - "wcag2a", - "wcag241", - "section508", - "section508.22.o" - ], - "matches": "function matches(node) {\n return !!node.querySelector('a[href]');\n }" - }, - { - "id": "frame-title", - "selector": "frame, iframe", - "excludeHidden": true, - "enabled": true, - "pageLevel": false, - "any": [ - "non-empty-title" - ], - "all": [], - "none": [], - "tags": [ - "wcag2a", - "wcag241", - "section508", - "section508.22.i" - ] - }, - { - "id": "image-alt", - "selector": "img", - "excludeHidden": true, - "enabled": false, - "pageLevel": false, - "any": [ - "has-alt", - "aria-label", - "aria-labelledby", - "non-empty-title", - "role-presentation", - "role-none" - ], - "all": [], - "none": [], - "tags": [ - "wcag2a", - "wcag111", - "section508", - "section508.22.a" - ] - }, - { - "id": "input-image-alt", - "selector": "input[type=\"image\"]", - "excludeHidden": true, - "enabled": true, - "pageLevel": false, - "any": [ - "non-empty-alt" - ], - "all": [], - "none": [], - "tags": [ - "wcag2a", - "wcag111", - "section508", - "section508.22.a" - ], - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/input-image-alt?application=fireeyesFirefox", - "description": "Ensures that the text equivalent for button exists and is not empty", - "help": "Text equivalent for button is missing or empty" - } - }, - { - "id": "label", - "selector": "input:not([type='hidden']):not([type='image']):not([type='button']):not([type='submit']):not([type='reset']), select, textarea", - "excludeHidden": true, - "enabled": true, - "pageLevel": false, - "any": [ - "explicit-fixed" - ], - "all": [], - "none": [ - "help-same-as-label", - "multiple-label" - ], - "tags": [ - "wcag2a", - "wcag332", - "wcag131", - "section508", - "section508.22.n" - ] - }, - { - "id": "layout-table", - "selector": "table", - "excludeHidden": true, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "has-th", - "has-caption", - "has-summary" - ], - "tags": [ - "wcag2a", - "wcag131", - "section508", - "section508.22.g", - "section508.22.h" - ], - "matches": "function matches(node) {\n return !axe.commons.table.isDataTable(node);\n }" - }, - { - "id": "link-name", - "selector": "a[href]:not([role=\"button\"]), [role=link][href]", - "excludeHidden": true, - "enabled": true, - "pageLevel": false, - "any": [ - "has-visible-text" - ], - "all": [], - "none": [ - "focusable-no-name" - ], - "tags": [ - "wcag2a", - "wcag111", - "wcag412", - "section508", - "section508.22.a" - ] - }, - { - "id": "object-alt", - "selector": "object", - "excludeHidden": true, - "enabled": false, - "pageLevel": false, - "any": [ - "has-visible-text" - ], - "all": [], - "none": [ - "invalid-image-alt-text", - "invalid-ascii-art" - ], - "tags": [ - "wcag2a", - "wcag111", - "section508", - "section508.22.a" - ], - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/object-alt?application=fireeyesFirefox", - "description": " elements must have a text alternative", - "help": " element does not have a text alternative" - } - }, - { - "id": "server-side-image-map", - "selector": "img[ismap]", - "excludeHidden": true, - "enabled": true, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "exists" - ], - "tags": [ - "wcag2a", - "wcag211", - "section508", - "section508.22.f" - ] - }, - { - "id": "table-duplicate-name", - "selector": "table", - "excludeHidden": true, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "same-caption-summary" - ], - "tags": [ - "section508", - "section508.22.g", - "section508.22.h" - ] - }, - { - "id": "table-fake-caption", - "selector": "table", - "excludeHidden": true, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [ - "caption-faked" - ], - "none": [], - "tags": [ - "experimental", - "wcag2a", - "wcag131", - "section508", - "section508.22.g" - ], - "matches": "function matches(node) {\n return axe.commons.table.isDataTable(node);\n }" - }, - { - "id": "td-has-header", - "selector": "table", - "excludeHidden": true, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [ - "td-has-header" - ], - "none": [], - "tags": [ - "wcag2a", - "wcag131", - "section508", - "section508.22.g" - ], - "matches": "function matches(node) {\n if (axe.commons.table.isDataTable(node)) {\n var tableArray = axe.commons.table.toArray(node);\n return tableArray.length >= 3 && tableArray[0].length >= 3 && tableArray[1].length >= 3 && tableArray[2].length >= 3;\n }\n return false;\n }" - }, - { - "id": "td-headers-attr", - "selector": "table", - "excludeHidden": true, - "enabled": true, - "pageLevel": false, - "any": [], - "all": [ - "td-headers-attr" - ], - "none": [], - "tags": [ - "wcag2a", - "wcag131", - "section508", - "section508.22.g" - ] - }, - { - "id": "th-has-data-cells", - "selector": "table", - "excludeHidden": true, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [ - "th-has-data-cells" - ], - "none": [], - "tags": [ - "wcag2a", - "wcag131", - "section508", - "section508.22.g" - ], - "matches": "function matches(node) {\n return axe.commons.table.isDataTable(node);\n }" - }, - { - "id": "video-caption", - "selector": "video", - "excludeHidden": false, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "caption" - ], - "tags": [ - "wcag2a", - "wcag122", - "wcag123", - "section508", - "section508.22.a" - ] - }, - { - "id": "video-description", - "selector": "video", - "excludeHidden": false, - "enabled": false, - "pageLevel": false, - "any": [], - "all": [], - "none": [ - "description" - ], - "tags": [ - "wcag2aa", - "wcag125", - "section508", - "section508.22.b" - ] - }, - { - "id": "aria-allowed-attr", - "enabled": false - }, - { - "id": "aria-required-attr", - "enabled": false - }, - { - "id": "aria-required-children", - "enabled": false - }, - { - "id": "aria-required-parent", - "enabled": false - }, - { - "id": "aria-roles", - "enabled": false - }, - { - "id": "aria-valid-attr-value", - "enabled": false - }, - { - "id": "aria-valid-attr", - "enabled": false - }, - { - "id": "checkboxgroup", - "enabled": false - }, - { - "id": "color-contrast", - "enabled": false - }, - { - "id": "definition-list", - "enabled": false - }, - { - "id": "dlitem", - "enabled": false - }, - { - "id": "document-title", - "enabled": false - }, - { - "id": "duplicate-id", - "enabled": false - }, - { - "id": "empty-heading", - "enabled": false - }, - { - "id": "frame-title-unique", - "enabled": false - }, - { - "id": "heading-order", - "enabled": false - }, - { - "id": "html-has-lang", - "enabled": false - }, - { - "id": "html-lang-valid", - "enabled": false - }, - { - "id": "image-redundant-alt", - "enabled": false - }, - { - "id": "label-title-only", - "enabled": false - }, - { - "id": "link-in-text-block", - "enabled": false - }, - { - "id": "list", - "enabled": false - }, - { - "id": "listitem", - "enabled": false - }, - { - "id": "marquee", - "enabled": false - }, - { - "id": "meta-refresh", - "enabled": false - }, - { - "id": "meta-viewport-large", - "enabled": false - }, - { - "id": "meta-viewport", - "enabled": false - }, - { - "id": "radiogroup", - "enabled": false - }, - { - "id": "region", - "enabled": false - }, - { - "id": "scope-attr-valid", - "enabled": false - }, - { - "id": "skip-link", - "enabled": false - }, - { - "id": "tabindex", - "enabled": false - }, - { - "id": "valid-lang", - "enabled": false - }, - { - "id": "active-embed", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/active-embed?application=fireeyesFirefox", - "description": "Raises a potential violation for all potentially active tags", - "help": "Check the media to ensure that it has captions" - }, - "tags": [ - "section508", - "section508.22.a.potential" - ], - "selector": "embed", - "any": [ - "active-embed" - ], - "all": [], - "none": [] - }, - { - "id": "applet-ascii-alt", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/applet-ascii-alt?application=fireeyesFirefox", - "description": "Ensures elements have a meaningful alternate text (not ascii art)", - "help": "Suspicious text equivalent for applet - could be ASCII art" - }, - "tags": [ - "section508", - "section50.22.a" - ], - "selector": "applet", - "any": [], - "all": [], - "none": [ - "invalid-ascii-art" - ] - }, - { - "id": "applet-meaningful-alt", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/applet-meaningful-alt?application=fireeyesFirefox", - "description": "Ensures elements have a meaningful alternate text", - "help": "When an alt is supplied on applet, it must be meaningful (i.e. not a file name, size or meaningless value)." - }, - "tags": [ - "section508", - "section50.22.a" - ], - "selector": "applet", - "any": [], - "all": [], - "none": [ - "invalid-image-alt-text", - "invalid-image-alt-filename", - "blank-alt" - ] - }, - { - "id": "applet", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/applet?application=fireeyesFirefox", - "description": "Ensures elements have alternate text", - "help": "Applet must have valid alt attribute or accessible HTML content" - }, - "tags": [ - "section508", - "section50.22.a" - ], - "selector": "applet", - "any": [ - "has-visible-text", - "non-empty-alt" - ], - "all": [] - }, - { - "id": "area-alt-meaningful", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/area-alt-meaningful?application=fireeyesFirefox", - "description": "Ensure that the alt attribute on area elements has a meaningful value", - "help": "When an alt is supplied on area, it must be meaningful (i.e. not a file name or meaningless value)" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "map area[href]", - "excludeHidden": false, - "any": [], - "all": [], - "none": [ - "invalid-image-alt-text", - "invalid-image-alt-filename" - ] - }, - { - "id": "area-alt-suspicious", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/area-alt-suspicious?application=fireeyesFirefox", - "description": "Ensure that text equivalent cannot be ASCII art", - "help": "Suspicious alt attribute for area - could be ASCII art" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "map area[href]", - "excludeHidden": false, - "any": [], - "all": [], - "none": [ - "invalid-ascii-art" - ] - }, - { - "id": "area-alt-unique", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/area-alt-unique?application=fireeyesFirefox", - "description": "Ensures image maps do not have duplicate alts", - "help": "Each alt text in an image map must be unique" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "img[usemap]", - "any": [ - "area-alt-unique" - ], - "all": [], - "none": [] - }, - { - "id": "color-contrast-critical", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/color-contrast?application=fireeyesFirefox", - "description": "Ensures the contrast between foreground and background colors is above 4:1", - "help": "Elements must have a contrast ratio of above 4.5:1" - }, - "tags": [ - "wcag2aa", - "wcag143", - "section508" - ], - "selector": "*", - "excludeHidden": false, - "any": [ - "color-contrast-critical" - ], - "all": [], - "none": [] - }, - { - "id": "color-contrast-minor", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/color-contrast?application=fireeyesFirefox", - "description": "Ensures the contrast between foreground and background colors is above 4.5:1", - "help": "Elements must have a contrast ratio of above 4.5:1" - }, - "tags": [ - "wcag2aa", - "wcag143", - "section508" - ], - "selector": "*", - "excludeHidden": false, - "any": [ - "color-contrast-minor" - ], - "all": [], - "none": [] - }, - { - "id": "data-table-caption", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/data-table-caption?application=fireeyesFirefox", - "description": "Ensures data tables have a ", - "help": "Data tables must have a " - }, - "tags": [ - "section508", - "section508.22.g", - "section508.22.h" - ], - "selector": "table", - "any": [], - "all": [ - "has-caption", - "caption-not-empty" - ], - "none": [], - "matches": "function matches(node) {\nreturn axe.commons.table.isDataTable(node);\n}\n" - }, - { - "id": "direct-media-link", - "enabled": false, - "metadata": { - "description": "Ensures media files have a text equivalent", - "help": "Media file may be missing a text equivalent" - }, - "tags": [ - "section508", - "section508.22.a", - "section508.22.b" - ], - "selector": "a[href]", - "any": [], - "all": [], - "none": [ - "direct-audio-href", - "direct-video-href" - ] - }, - { - "id": "fieldset-legend", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/fieldset-legend?application=fireeyesFirefox", - "description": "Ensures
elements have a non-empty as the first child", - "help": "
elements must have a non-empty element as the first child" - }, - "tags": [ - "section508", - "section508.22.n" - ], - "selector": "fieldset", - "any": [], - "all": [], - "none": [ - "fieldset-legend", - "fieldset-legend-text" - ] - }, - { - "id": "form-id-unique", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/form-id-unique?application=fireeyesFirefox", - "description": "Ensures every form element with an ID has a unique ID", - "help": "Form elements IDs must be unique" - }, - "tags": [ - "section508", - "section508.22.n" - ], - "selector": "input:not([type='hidden']), select, textarea, button", - "any": [], - "all": [], - "none": [ - "form-id-not-unique" - ] - }, - { - "id": "image-active-alt", - "enabled": false, - "metadata": { - "description": "Ensure that active image alts are meaningful", - "help": "Active images (images inside anchor tags) must have a non-empty alt attribute", - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/active-image-alt?application=fireeyesFirefox" - }, - "tags": [ - "section508", - "section50.22.a" - ], - "selector": "a img", - "any": [], - "all": [], - "none": [ - "invalid-active-image-alt-text" - ] - }, - { - "id": "image-alt-meaningful", - "enabled": false, - "metadata": { - "description": "Ensure that image alts are meaningful", - "help": "When an alt is supplied, it must be meaningful (i.e. not a file name, size or meaningless value)", - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/meaningful-image-alt?application=fireeyesFirefox" - }, - "tags": [ - "section508", - "section50.22.a" - ], - "selector": "img", - "all": [], - "none": [ - "invalid-image-alt-text", - "invalid-image-alt-filename", - "invalid-ascii-art", - "blank-alt" - ] - }, - { - "id": "image-large-empty-alt", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/large-image-empty-alt?application=fireeyesFirefox", - "description": "Ensures large images have a non-empty alt", - "help": "Ensure that informational images (larger than your specified threshold) have a meaningful alt value" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "img", - "any": [], - "all": [], - "none": [ - "large-image-empty-alt" - ], - "matches": "function matches(node) {\n// not-child-of-anchor.js\nvar anchor = axe.commons.dom.findUp(node, 'a');\n\nreturn !anchor;\n}\n" - }, - { - "id": "image-li-alt", - "enabled": true, - "metadata": { - "description": "Ensure that image alts in lists are meaningful", - "help": "Alt-text for bullet image prohibited as per user's test settings", - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/image-li-alt?application=fireeyesFirefox" - }, - "tags": [ - "section508", - "section50.22.a" - ], - "selector": "li img", - "any": [], - "all": [], - "none": [ - "invalid-image-li-alt-text" - ], - "matches": "function matches(node) {\n// not-child-of-anchor.js\nvar anchor = axe.commons.dom.findUp(node, 'a');\n\nreturn !anchor;\n}\n" - }, - { - "id": "image-long-alt", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/image-long-alt?application=fireeyesFirefox", - "description": "Ensures image text equivalents are short or longdesc is used", - "help": "When an alt is supplied, better to have it short (if needs to be long then use longdesc attribute instead)" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "img", - "any": [], - "all": [], - "none": [ - "image-long-alt" - ] - }, - { - "id": "image-title-only", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/image-title-only?application=fireeyesFirefox", - "description": "Ensures images do not only have a title attribute", - "help": "This image needs an alt attribute, not just a title" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "img[title]", - "any": [ - "has-alt" - ], - "all": [], - "none": [] - }, - { - "id": "inactive-image-alt", - "enabled": true, - "metadata": { - "description": "Ensure that inactive images have an alt", - "help": "Images must have an alt attribute", - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/inactive-image-alt?application=fireeyesFirefox" - }, - "tags": [ - "section508", - "section50.22.a" - ], - "selector": "img", - "any": [ - "has-alt" - ], - "all": [], - "none": [], - "matches": "function matches(node) {\n// not-child-of-anchor.js\nvar anchor = axe.commons.dom.findUp(node, 'a');\n\nreturn !anchor;\n}\n" - }, - { - "id": "input-image-alt-ascii", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/input-image-alt-ascii?application=fireeyesFirefox", - "description": "Ensure that the alt attribute on input elements of type=\"image\" is not ASCII art", - "help": "Suspicious text equivalent for button - could be ASCII art" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "input[type='image']", - "any": [], - "all": [], - "none": [ - "invalid-ascii-art" - ] - }, - { - "id": "input-image-alt-meaningful", - "enabled": true, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/input-image-alt-meaningful?application=fireeyesFirefox", - "description": "Ensure that the alt attribute on input elements of type=\"image\" has a meaningful value", - "help": "When an alt is supplied on button, it must be meaningful (i.e. not a file name, size or meaningless value)" - }, - "tags": [ - "section508", - "section508.22.a" - ], - "selector": "input[type='image']", - "any": [], - "all": [], - "none": [ - "invalid-image-alt-text", - "invalid-image-alt-filename" - ] - }, - { - "id": "select-onchange", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/select-onchange?application=fireeyesFirefox", - "description": "Raises a potential violation for element to ensure it does not create a change of context without informing the user" - }, - "tags": [ - "section508", - "section508.22.n.potential" - ], - "selector": "select", - "any": [ - "select-onchange" - ], - "all": [], - "none": [] - }, - { - "id": "smil-captions", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/smil-captions?application=fireeyesFirefox", - "description": "Ensures tags have a synchronized media file (SMIL)", - "help": " tags must have a synchronized media file (SMIL)" - }, - "tags": [ - "section508", - "section508.22.b" - ], - "selector": "object", - "any": [ - "smil-captions" - ], - "all": [], - "none": [] - }, - { - "id": "va-skip-link", - "enabled": false, - "metadata": { - "helpUrl": "https://dequeuniversity.com/rules/worldspace/2.0/va-skip-link?application=fireeyesFirefox", - "description": "Raises a violation if no link can be found with the text 'skip ' that points to a valid in-page target", - "help": "Create a link that allows a keyboard user to skip over repetitive navigation" - }, - "tags": [ - "section508", - "section508.22.o" - ], - "selector": "a", - "pageLevel": true, - "any": [ - "va-skip-link" - ], - "all": [], - "none": [] - } - ], - "checks": [ - { - "id": "accesskeys", - "evaluate": "function evaluate(node, options) {\n if (axe.commons.dom.isVisible(node, false)) {\n this.data(node.getAttribute('accesskey'));\n this.relatedNodes([ node ]);\n }\n return true;\n }", - "after": "function after(results, options) {\n var seen = {};\n return results.filter(function(r) {\n if (!r.data) {\n return false;\n }\n var key = r.data.toUpperCase();\n if (!seen[key]) {\n seen[key] = r;\n r.relatedNodes = [];\n return true;\n }\n seen[key].relatedNodes.push(r.relatedNodes[0]);\n return false;\n }).map(function(r) {\n r.result = !!r.relatedNodes.length;\n return r;\n });\n }", - "enabled": true - }, - { - "id": "non-empty-alt", - "evaluate": "function evaluate(node, options) {\n var label = node.getAttribute('alt');\n return !!(label ? axe.commons.text.sanitize(label).trim() : '');\n }", - "enabled": true - }, - { - "id": "non-empty-title", - "evaluate": "function evaluate(node, options) {\n var title = node.getAttribute('title');\n return !!(title ? axe.commons.text.sanitize(title).trim() : '');\n }", - "enabled": true - }, - { - "id": "aria-label", - "evaluate": "function evaluate(node, options) {\n var label = node.getAttribute('aria-label');\n return !!(label ? axe.commons.text.sanitize(label).trim() : '');\n }", - "enabled": true - }, - { - "id": "aria-labelledby", - "evaluate": "function evaluate(node, options) {\n var getIdRefs = axe.commons.dom.idrefs;\n return getIdRefs(node, 'aria-labelledby').some(function(elm) {\n return elm && axe.commons.text.accessibleText(elm, true);\n });\n }", - "enabled": true - }, - { - "id": "caption", - "evaluate": "function evaluate(node, options) {\n return !node.querySelector('track[kind=captions]');\n }", - "enabled": true - }, - { - "id": "is-on-screen", - "evaluate": "function evaluate(node, options) {\n // From a visual perspective\n return axe.commons.dom.isVisible(node, false) && !axe.commons.dom.isOffscreen(node);\n }", - "enabled": true - }, - { - "id": "non-empty-if-present", - "evaluate": "function evaluate(node, options) {\n // Check for 'default' names, which are given to reset and submit buttons\n var nodeName = node.nodeName.toUpperCase();\n var type = (node.getAttribute('type') || '').toLowerCase();\n var label = node.getAttribute('value');\n this.data(label);\n if (nodeName === 'INPUT' && [ 'submit', 'reset' ].indexOf(type) !== -1) {\n return label === null;\n }\n return false;\n }", - "enabled": true - }, - { - "id": "non-empty-value", - "evaluate": "function evaluate(node, options) {\n var label = node.getAttribute('value');\n return !!(label ? axe.commons.text.sanitize(label).trim() : '');\n }", - "enabled": true - }, - { - "id": "button-has-visible-text", - "evaluate": "function evaluate(node, options) {\n var nodeName = node.nodeName.toUpperCase();\n var role = node.getAttribute('role');\n var label = void 0;\n if (nodeName === 'BUTTON' || role === 'button' && nodeName !== 'INPUT') {\n label = axe.commons.text.accessibleText(node);\n this.data(label);\n return !!label;\n } else {\n return false;\n }\n }", - "enabled": true - }, - { - "id": "role-presentation", - "evaluate": "function evaluate(node, options) {\n return node.getAttribute('role') === 'presentation';\n }", - "enabled": true - }, - { - "id": "role-none", - "evaluate": "function evaluate(node, options) {\n return node.getAttribute('role') === 'none';\n }", - "enabled": true - }, - { - "id": "focusable-no-name", - "evaluate": "function evaluate(node, options) {\n var tabIndex = node.getAttribute('tabindex'), isFocusable = axe.commons.dom.isFocusable(node) && tabIndex > -1;\n if (!isFocusable) {\n return false;\n }\n return !axe.commons.text.accessibleText(node);\n }", - "enabled": true - }, - { - "id": "internal-link-present", - "evaluate": "function evaluate(node, options) {\n return !!node.querySelector('a[href^=\"#\"]');\n }", - "enabled": true - }, - { - "id": "header-present", - "evaluate": "function evaluate(node, options) {\n return !!node.querySelector('h1, h2, h3, h4, h5, h6, [role=\"heading\"]');\n }", - "enabled": true - }, - { - "id": "landmark", - "evaluate": "function evaluate (node, options) { return node.getElementsByTagName('main').length > 0 || !!node.querySelector('[role=\"main\"]') ;}", - "enabled": true - }, - { - "id": "has-alt", - "evaluate": "function evaluate(node, options) {\n return node.hasAttribute('alt');\n }", - "enabled": true - }, - { - "id": "implicit-label", - "evaluate": "function evaluate(node, options) {\n var label = axe.commons.dom.findUp(node, 'label');\n if (label) {\n return !!axe.commons.text.accessibleText(label);\n }\n return false;\n }", - "enabled": true - }, - { - "id": "explicit-label", - "evaluate": "function evaluate(node, options) {\n if (node.id) {\n var label = document.querySelector('label[for=\"' + axe.commons.utils.escapeSelector(node.id) + '\"]');\n if (label) {\n return !!axe.commons.text.accessibleText(label);\n }\n }\n return false;\n }", - "enabled": true - }, - { - "id": "help-same-as-label", - "enabled": false, - "evaluate": "function evaluate(node, options) {\n var labelText = axe.commons.text.label(node), check = node.getAttribute('title');\n if (!labelText) {\n return false;\n }\n if (!check) {\n check = '';\n if (node.getAttribute('aria-describedby')) {\n var ref = axe.commons.dom.idrefs(node, 'aria-describedby');\n check = ref.map(function(thing) {\n return thing ? axe.commons.text.accessibleText(thing) : '';\n }).join('');\n }\n }\n return axe.commons.text.sanitize(check) === axe.commons.text.sanitize(labelText);\n }" - }, - { - "id": "multiple-label", - "evaluate": "function evaluate(node, options) {\n var labels = [].slice.call(document.querySelectorAll('label[for=\"' + axe.commons.utils.escapeSelector(node.id) + '\"]')), parent = node.parentNode;\n while (parent) {\n if (parent.tagName === 'LABEL' && labels.indexOf(parent) === -1) {\n labels.push(parent);\n }\n parent = parent.parentNode;\n }\n this.relatedNodes(labels);\n return labels.length > 1;\n }", - "enabled": true - }, - { - "id": "has-th", - "evaluate": "function evaluate(node, options) {\n var row, cell, badCells = [];\n for (var rowIndex = 0, rowLength = node.rows.length; rowIndex < rowLength; rowIndex++) {\n row = node.rows[rowIndex];\n for (var cellIndex = 0, cellLength = row.cells.length; cellIndex < cellLength; cellIndex++) {\n cell = row.cells[cellIndex];\n if (cell.nodeName.toUpperCase() === 'TH' || [ 'rowheader', 'columnheader' ].indexOf(cell.getAttribute('role')) !== -1) {\n badCells.push(cell);\n }\n }\n }\n if (badCells.length) {\n this.relatedNodes(badCells);\n return true;\n }\n return false;\n }", - "enabled": true - }, - { - "id": "has-caption", - "evaluate": "function evaluate(node, options) {\n return !!node.caption;\n }", - "enabled": true - }, - { - "id": "has-summary", - "evaluate": "function evaluate(node, options) {\n return !!node.summary;\n }", - "enabled": true - }, - { - "id": "has-visible-text", - "evaluate": "function evaluate(node, options) {\n return axe.commons.text.accessibleText(node).length > 0;\n }", - "enabled": true - }, - { - "id": "exists", - "evaluate": "function evaluate(node, options) {\n return true;\n }", - "enabled": true - }, - { - "id": "same-caption-summary", - "evaluate": "function evaluate(node, options) {\n return !!(node.summary && node.caption) && node.summary === axe.commons.text.accessibleText(node.caption);\n }", - "enabled": true - }, - { - "id": "caption-faked", - "evaluate": "function evaluate(node, options) {\n var table = axe.commons.table.toGrid(node);\n var firstRow = table[0];\n if (table.length <= 1 || firstRow.length <= 1 || node.rows.length <= 1) {\n return true;\n }\n return firstRow.reduce(function(out, curr, i) {\n return out || curr !== firstRow[i + 1] && firstRow[i + 1] !== undefined;\n }, false);\n }", - "enabled": true - }, - { - "id": "td-has-header", - "evaluate": "function evaluate(node, options) {\n var tableUtils = axe.commons.table;\n var badCells = [];\n var cells = tableUtils.getAllCells(node);\n cells.forEach(function(cell) {\n // For each non-empty data cell that doesn't have an aria label\n if (cell.textContent.trim() !== '' && tableUtils.isDataCell(cell) && !axe.commons.aria.label(cell)) {\n // Check if it has any headers\n var hasHeaders = tableUtils.getHeaders(cell);\n hasHeaders = hasHeaders.reduce(function(hasHeaders, header) {\n return hasHeaders || header !== null && !!header.textContent.trim();\n }, false);\n // If no headers, put it on the naughty list\n if (!hasHeaders) {\n badCells.push(cell);\n }\n }\n });\n if (badCells.length) {\n this.relatedNodes(badCells);\n return false;\n }\n return true;\n }", - "enabled": true - }, - { - "id": "td-headers-attr", - "evaluate": "function evaluate(node, options) {\n var cells = [];\n for (var rowIndex = 0, rowLength = node.rows.length; rowIndex < rowLength; rowIndex++) {\n var row = node.rows[rowIndex];\n for (var cellIndex = 0, cellLength = row.cells.length; cellIndex < cellLength; cellIndex++) {\n cells.push(row.cells[cellIndex]);\n }\n }\n var ids = cells.reduce(function(ids, cell) {\n if (cell.id) {\n ids.push(cell.id);\n }\n return ids;\n }, []);\n var badCells = cells.reduce(function(badCells, cell) {\n var isSelf, notOfTable;\n // Get a list all the values of the headers attribute\n var headers = (cell.getAttribute('headers') || '').split(/\\s/).reduce(function(headers, header) {\n header = header.trim();\n if (header) {\n headers.push(header);\n }\n return headers;\n }, []);\n if (headers.length !== 0) {\n // Check if the cell's id is in this list\n if (cell.id) {\n isSelf = headers.indexOf(cell.id.trim()) !== -1;\n }\n // Check if the headers are of cells inside the table\n notOfTable = headers.reduce(function(fail, header) {\n return fail || ids.indexOf(header) === -1;\n }, false);\n if (isSelf || notOfTable) {\n badCells.push(cell);\n }\n }\n return badCells;\n }, []);\n if (badCells.length > 0) {\n this.relatedNodes(badCells);\n return false;\n } else {\n return true;\n }\n }", - "enabled": true - }, - { - "id": "th-has-data-cells", - "evaluate": "function evaluate(node, options) {\n var tableUtils = axe.commons.table;\n var cells = tableUtils.getAllCells(node);\n var checkResult = this;\n // Get a list of all headers reffed to in this rule\n var reffedHeaders = [];\n cells.forEach(function(cell) {\n var headers = cell.getAttribute('headers');\n if (headers) {\n reffedHeaders = reffedHeaders.concat(headers.split(/\\s+/));\n }\n var ariaLabel = cell.getAttribute('aria-labelledby');\n if (ariaLabel) {\n reffedHeaders = reffedHeaders.concat(ariaLabel.split(/\\s+/));\n }\n });\n // Get all the headers\n var headers = cells.filter(function(cell) {\n if (axe.commons.text.sanitize(cell.textContent) === '') {\n return false;\n }\n return cell.nodeName.toUpperCase() === 'TH' || [ 'rowheader', 'columnheader' ].indexOf(cell.getAttribute('role')) !== -1;\n });\n var tableGrid = tableUtils.toGrid(node);\n // Look for all the bad headers\n return headers.reduce(function(res, header) {\n if (header.id && reffedHeaders.indexOf(header.id) !== -1) {\n return !res ? res : true;\n }\n var hasCell = false;\n var pos = tableUtils.getCellPosition(header, tableGrid);\n // Look for any data cells or row headers that this might refer to\n if (tableUtils.isColumnHeader(header)) {\n hasCell = tableUtils.traverse('down', pos, tableGrid).reduce(function(out, cell) {\n return out || cell.textContent.trim() !== '' && !tableUtils.isColumnHeader(cell);\n }, false);\n }\n // Look for any data cells or column headers that this might refer to\n if (!hasCell && tableUtils.isRowHeader(header)) {\n hasCell = tableUtils.traverse('right', pos, tableGrid).reduce(function(out, cell) {\n return out || cell.textContent.trim() !== '' && !tableUtils.isRowHeader(cell);\n }, false);\n }\n // report the node as having failed\n if (!hasCell) {\n checkResult.relatedNodes(header);\n }\n return res && hasCell;\n }, true);\n }", - "enabled": true - }, - { - "id": "description", - "evaluate": "function evaluate(node, options) {\n return !node.querySelector('track[kind=descriptions]');\n }", - "enabled": true - }, - { - "id": "active-embed", - "enabled": true, - "metadata": { - "impact": "moderate", - "messages": { - "pass": " elements that are not part of a media element or an must be manually checked for captions", - "fail": " elements that are not part of a media element or an must be manually checked for captions" - } - }, - "evaluate": "function evaluate(node, options) {\nvar parents;\nfunction active_embed_get_parents(node) {\n\tvar parents = [];\n\tif (node.parentNode) {\n\t\tparents = active_embed_get_parents(node.parentNode);\n\t}\n\tparents.push(node.nodeName.toLowerCase());\n\treturn parents;\n}\nparents = active_embed_get_parents(node.parentNode);\nif (parents.indexOf('video') !== -1 || parents.indexOf('object') !== -1 ||\n\tparents.indexOf('audio') !== -1) {\n\treturn true; //pass\n}\nreturn false; //fail\n}\n" - }, - { - "id": "area-alt-unique", - "enabled": true, - "metadata": { - "impact": "serious", - "messages": { - "pass": "All the alts wthin each map are unique", - "fail": "One or more of the alts within the image map is duplicated" - } - }, - "evaluate": "function evaluate(node, options) {\n// unique-area-alt.js\nvar usemap = node.getAttribute('usemap');\nvar related = [];\nif (usemap) {\n\tvar map = document.querySelector('[name=\"' + usemap.substring(1) + '\"]');\n\tif (map) {\n\t\tvar areas = map.querySelectorAll('area');\n\t\tareas = Array.prototype.slice.call(areas);\n\t\tvar alts = {};\n\t\tareas.forEach(function (area) {\n\t\t\tvar text = axe.commons.text.accessibleText(area,\n\t\t\t\t\t\tarea.offsetHeight && area.offsetWidth).toLowerCase();\n\t\t\tif (typeof alts[text] === 'undefined') {\n\t\t\t\talts[text] = [area];\n\t\t\t} else {\n\t\t\t\talts[text].push(area);\n\t\t\t}\n\t\t});\n\t\tObject.keys(alts).forEach(function (key) {\n\t\t\tif (alts[key].length > 1) {\n\t\t\t\trelated = related.concat(alts[key]);\n\t\t\t}\n\t\t});\n\t\tif (related.length) {\n\t\t\tthis.relatedNodes(related);\n\t\t\treturn false; // fail\n\t\t}\n\t}\n}\nreturn true; // pass\n}\n" - }, - { - "id": "blank-alt", - "enabled": true, - "metadata": { - "impact": "moderate", - "messages": { - "pass": "Ensures that the alt text is not all whitespace", - "fail": "The alt text seems to consist only of whitespace, this is not allowed" - } - }, - "evaluate": "function evaluate(node, options) {\nvar alt = axe.commons.text.accessibleText(node).toLowerCase();\nif (node.nodeName === 'AREA' && alt === '' && node.offsetHeight && node.offsetWidth) {\n\t// visible AREA element, have to ignore hidden\n\talt = axe.commons.text.accessibleText(node, true).toLowerCase();\n}\nvar pass = !alt || (alt.length && (alt.replace(/\\r\\n/g, '\\n').replace(/\\u00A0/g, ' ').replace(/[\\s]{2,}/g, ' ').trim().length))\nreturn !pass;\n}\n" - }, - { - "id": "caption-not-empty", - "enabled": true, - "metadata": { - "impact": "moderate", - "messages": { - "pass": "Ensures that the element in a data table is not empty", - "fail": "The element on a data table must not be empty" - } - }, - "evaluate": "function evaluate(node, options) {\nvar text = axe.commons.text.accessibleText(node.caption);\nreturn (text && text.length);\n}\n" - }, - { - "id": "color-contrast-critical", - "enabled": true, - "metadata": { - "impact": "serious", - "messages": { - "pass": "Element has a color contrast ratio greater than 4:1", - "fail": "Element has a color contrast ratio of less than 4:1" - } - }, - "options": { - "startThreshold": 4, - "stopThreshold": 0 - }, - "evaluate": "function evaluate(node, options) {\nfunction matches (node) {\n\tvar nodeName = node.nodeName.toUpperCase(),\n\t\tnodeType = node.type,\n\t\tdoc = document;\n\n\tif (node.getAttribute('aria-disabled') === 'true') {\n\t\treturn false;\n\t}\n\n\tif (nodeName === 'INPUT') {\n\t\treturn ['hidden', 'range', 'color', 'checkbox', 'radio', 'image'].indexOf(nodeType) === -1 && !node.disabled;\n\t}\n\n\tif (nodeName === 'SELECT') {\n\t\treturn !!node.options.length && !node.disabled;\n\t}\n\n\tif (nodeName === 'TEXTAREA') {\n\t\treturn !node.disabled;\n\t}\n\n\tif (nodeName === 'OPTION') {\n\t\treturn false;\n\t}\n\n\tif (nodeName === 'BUTTON' && node.disabled) {\n\t\treturn false;\n\t}\n\n\t// check if the element is a label for a disabled control\n\tif (nodeName === 'LABEL') {\n\t\t// explicit label of disabled input\n\t\tvar candidate = node.htmlFor && doc.getElementById(node.htmlFor);\n\t\tif (candidate && candidate.disabled) {\n\t\t\treturn false;\n\t\t}\n\n\t\tvar candidate = node.querySelector('input:not([type=\"hidden\"]):not([type=\"image\"])' +\n\t\t\t':not([type=\"button\"]):not([type=\"submit\"]):not([type=\"reset\"]), select, textarea');\n\t\tif (candidate && candidate.disabled) {\n\t\t\treturn false;\n\t\t}\n\n\t}\n\n\t// label of disabled control associated w/ aria-labelledby\n\tif (node.id) {\n\t\tvar candidate = doc.querySelector('[aria-labelledby~=' + axe.commons.utils.escapeSelector(node.id) + ']');\n\t\tif (candidate && candidate.disabled) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (axe.commons.text.visible(node, false, true) === '') {\n\t\treturn false;\n\t}\n\n\tvar range = document.createRange(),\n\t\tchildNodes = node.childNodes,\n\t\tlength = childNodes.length,\n\t\tchild, index;\n\n\tfor (index = 0; index < length; index++) {\n\t\tchild = childNodes[index];\n\n\t\tif (child.nodeType === 3 && axe.commons.text.sanitize(child.nodeValue) !== '') {\n\t\t\trange.selectNodeContents(child);\n\t\t}\n\t}\n\n\tvar rects = range.getClientRects();\n\tlength = rects.length;\n\n\tfor (index = 0; index < length; index++) {\n\t\t//check to see if the rectangle impinges\n\t\tif (axe.commons.dom.visuallyOverlaps(rects[index], node)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\nif (!matches(node) || !axe.commons.dom.isVisible(node, false)) {\n\treturn true;\n}\n\nvar noScroll = !!(options || {}).noScroll;\nvar bgNodes = [],\n\tbgColor = axe.commons.color.getBackgroundColor(node, bgNodes, noScroll),\n\tfgColor = axe.commons.color.getForegroundColor(node, noScroll);\n\n//We don't know, so we'll pass it provisionally\nif (fgColor === null || bgColor === null) {\n\treturn true;\n}\n\nvar nodeStyle = window.getComputedStyle(node);\nvar fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));\nvar fontWeight = nodeStyle.getPropertyValue('font-weight');\nvar bold = (['bold', 'bolder', '600', '700', '800', '900'].indexOf(fontWeight) !== -1);\n\nvar cr = axe.commons.color.hasValidContrastRatio(bgColor, fgColor, fontSize, bold);\nif (options && !cr.isValid &&\n\t(cr.contrastRatio < options.stopThreshold || cr.contrastRatio >= options.startThreshold)) {\n\tcr.isValid = true; // override using the thresholds supplied\n}\n\nthis.data({\n\tfgColor: fgColor.toHexString(),\n\tbgColor: bgColor.toHexString(),\n\tcontrastRatio: cr.contrastRatio.toFixed(2),\n\tfontSize: (fontSize * 72 / 96).toFixed(1) + 'pt',\n\tfontWeight: bold ? 'bold' : 'normal',\n});\n\nif (!cr.isValid) {\n\tthis.relatedNodes(bgNodes);\n}\nreturn cr.isValid;\n\n}\n" - }, - { - "id": "color-contrast-minor", - "enabled": true, - "metadata": { - "impact": "minor", - "messages": { - "pass": "Element has a color contrast ratio greater than 4.5:1", - "fail": "Element has a color contrast ratio of >= 4.0:1 and < 4.5:1, it must be at least 4.5:1" - } - }, - "options": { - "startThreshold": 4.5, - "stopThreshold": 4 - }, - "evaluate": "function evaluate(node, options) {\nfunction matches (node) {\n\tvar nodeName = node.nodeName.toUpperCase(),\n\t\tnodeType = node.type,\n\t\tdoc = document;\n\n\tif (node.getAttribute('aria-disabled') === 'true') {\n\t\treturn false;\n\t}\n\n\tif (nodeName === 'INPUT') {\n\t\treturn ['hidden', 'range', 'color', 'checkbox', 'radio', 'image'].indexOf(nodeType) === -1 && !node.disabled;\n\t}\n\n\tif (nodeName === 'SELECT') {\n\t\treturn !!node.options.length && !node.disabled;\n\t}\n\n\tif (nodeName === 'TEXTAREA') {\n\t\treturn !node.disabled;\n\t}\n\n\tif (nodeName === 'OPTION') {\n\t\treturn false;\n\t}\n\n\tif (nodeName === 'BUTTON' && node.disabled) {\n\t\treturn false;\n\t}\n\n\t// check if the element is a label for a disabled control\n\tif (nodeName === 'LABEL') {\n\t\t// explicit label of disabled input\n\t\tvar candidate = node.htmlFor && doc.getElementById(node.htmlFor);\n\t\tif (candidate && candidate.disabled) {\n\t\t\treturn false;\n\t\t}\n\n\t\tvar candidate = node.querySelector('input:not([type=\"hidden\"]):not([type=\"image\"])' +\n\t\t\t':not([type=\"button\"]):not([type=\"submit\"]):not([type=\"reset\"]), select, textarea');\n\t\tif (candidate && candidate.disabled) {\n\t\t\treturn false;\n\t\t}\n\n\t}\n\n\t// label of disabled control associated w/ aria-labelledby\n\tif (node.id) {\n\t\tvar candidate = doc.querySelector('[aria-labelledby~=' + axe.commons.utils.escapeSelector(node.id) + ']');\n\t\tif (candidate && candidate.disabled) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tif (axe.commons.text.visible(node, false, true) === '') {\n\t\treturn false;\n\t}\n\n\tvar range = document.createRange(),\n\t\tchildNodes = node.childNodes,\n\t\tlength = childNodes.length,\n\t\tchild, index;\n\n\tfor (index = 0; index < length; index++) {\n\t\tchild = childNodes[index];\n\n\t\tif (child.nodeType === 3 && axe.commons.text.sanitize(child.nodeValue) !== '') {\n\t\t\trange.selectNodeContents(child);\n\t\t}\n\t}\n\n\tvar rects = range.getClientRects();\n\tlength = rects.length;\n\n\tfor (index = 0; index < length; index++) {\n\t\t//check to see if the rectangle impinges\n\t\tif (axe.commons.dom.visuallyOverlaps(rects[index], node)) {\n\t\t\treturn true;\n\t\t}\n\t}\n\n\treturn false;\n}\n\nif (!matches(node) || !axe.commons.dom.isVisible(node, false)) {\n\treturn true;\n}\n\nvar noScroll = !!(options || {}).noScroll;\nvar bgNodes = [],\n\tbgColor = axe.commons.color.getBackgroundColor(node, bgNodes, noScroll),\n\tfgColor = axe.commons.color.getForegroundColor(node, noScroll);\n\n//We don't know, so we'll pass it provisionally\nif (fgColor === null || bgColor === null) {\n\treturn true;\n}\n\nvar nodeStyle = window.getComputedStyle(node);\nvar fontSize = parseFloat(nodeStyle.getPropertyValue('font-size'));\nvar fontWeight = nodeStyle.getPropertyValue('font-weight');\nvar bold = (['bold', 'bolder', '600', '700', '800', '900'].indexOf(fontWeight) !== -1);\n\nvar cr = axe.commons.color.hasValidContrastRatio(bgColor, fgColor, fontSize, bold);\nif (options && !cr.isValid &&\n\t(cr.contrastRatio < options.stopThreshold || cr.contrastRatio >= options.startThreshold)) {\n\tcr.isValid = true; // override using the thresholds supplied\n}\n\nthis.data({\n\tfgColor: fgColor.toHexString(),\n\tbgColor: bgColor.toHexString(),\n\tcontrastRatio: cr.contrastRatio.toFixed(2),\n\tfontSize: (fontSize * 72 / 96).toFixed(1) + 'pt',\n\tfontWeight: bold ? 'bold' : 'normal',\n});\n\nif (!cr.isValid) {\n\tthis.relatedNodes(bgNodes);\n}\nreturn cr.isValid;\n\n}\n" - }, - { - "id": "direct-audio-href", - "enabled": true, - "metadata": { - "impact": "serious", - "messages": { - "pass": "Href appears to point to a valid file type", - "fail": "Href appears to point to an audio file" - } - }, - "options": { - "extensions": [ - "aif", - "au", - "dwd", - "iff", - "pcm", - "sam", - "smp", - "snd", - "svx", - "vce", - "voc", - "wav", - "aiff", - "aifc", - "ra", - "ram", - "rm", - "rpm", - "mid", - "midi", - "mod", - "m3u", - "mp3", - "mp2", - "mpa", - "mpga", - "sid", - "cht", - "dus", - "es", - "gsm", - "gsd", - "rmf", - "stream", - "tsi", - "vox", - "vqf", - "vql", - "wqe", - "wma", - "wtx" - ] - }, - "evaluate": "function evaluate(node, options) {\n\t// direct-audio-href\n\tvar href = node.href;\n\n\tvar dotIdx = href.lastIndexOf('.');\n\tif (dotIdx === -1) {\n\t\treturn false; //pass\n\t}\n\tvar ext = axe.commons.text.sanitize(href.substring(dotIdx+1));\n\tif (!ext) {\n\t\treturn false; //pass\n\t}\n\tif (options.extensions.indexOf(ext) === -1) {\n\t\treturn false; //pass\n\t}\n\n\tthis.data(ext);\n\treturn true; //fail\n\n}\n" - }, - { - "id": "direct-video-href", - "enabled": true, - "metadata": { - "impact": "serious", - "messages": { - "pass": "Href appears to point to a valid file type", - "fail": "Href appears to point to a media file" - } - }, - "options": { - "extensions": [ - "avi", - "movie", - "movi", - "mv", - "mpeg", - "mpg", - "mpe", - "qt", - "mov", - "wmv", - "m4v", - "wvx", - "mp4", - "webm", - "flv", - "vob", - "ogg", - "ogv", - "gifv", - "mng", - "yuv", - "rm", - "rmvb", - "asf", - "m4p", - "mp2", - "mpv", - "m2v", - "3gp", - "3g2", - "mxf", - "roq", - "nsv", - "f4v", - "f4p", - "f4a", - "f4b" - ] - }, - "evaluate": "function evaluate(node, options) {\n\t// direct-audio-href\n\tvar href = node.href;\n\n\tvar dotIdx = href.lastIndexOf('.');\n\tif (dotIdx === -1) {\n\t\treturn false; //pass\n\t}\n\tvar ext = axe.commons.text.sanitize(href.substring(dotIdx+1));\n\tif (!ext) {\n\t\treturn false; //pass\n\t}\n\tif (options.extensions.indexOf(ext) === -1) {\n\t\treturn false; //pass\n\t}\n\n\tthis.data(ext);\n\treturn true; //fail\n\n}\n" - }, - { - "id": "explicit-fixed", - "enabled": true, - "metadata": { - "impact": "critical", - "messages": { - "pass": "Form element has an explicit