bbc-a11y 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +1 -0
  3. data/CONTRIBUTING.md +19 -0
  4. data/Gemfile +2 -0
  5. data/LICENSE +21 -0
  6. data/README.md +54 -0
  7. data/Rakefile +12 -0
  8. data/bbc-a11y.gemspec +32 -0
  9. data/bin/a11y +5 -0
  10. data/example/.a11y.rb +64 -0
  11. data/example/Gemfile +4 -0
  12. data/example/Rakefile +3 -0
  13. data/example/config.ru +1 -0
  14. data/example/public/missing_header.html +13 -0
  15. data/example/public/perfect.html +14 -0
  16. data/example/readme.md +0 -0
  17. data/features/01_core-purpose.md +24 -0
  18. data/features/02_validation.feature +31 -0
  19. data/features/03_javascript.feature +40 -0
  20. data/features/04_language.feature +58 -0
  21. data/features/05_page_title.feature +45 -0
  22. data/features/06_main_landmark.feature +24 -0
  23. data/features/07_headings.feature +65 -0
  24. data/features/08_title_attribute.feature +71 -0
  25. data/features/09_tabindex.feature +38 -0
  26. data/features/10_form-labels.md +47 -0
  27. data/features/11_visible-on-focus.md +58 -0
  28. data/features/13_colour-contrast.md +27 -0
  29. data/features/14_colour-meaning.md +19 -0
  30. data/features/15_focusable-controls.md +45 -0
  31. data/features/16_table.md +109 -0
  32. data/features/17_control-styles.md +78 -0
  33. data/features/18_focus-styles.md +36 -0
  34. data/features/19_form-interactions.md +33 -0
  35. data/features/20_image-alt.md +34 -0
  36. data/features/21_min-font-sizes.md +64 -0
  37. data/features/22_resize-zoom.md +80 -0
  38. data/features/step_definitions/core_content_steps.rb +3 -0
  39. data/features/step_definitions/language_steps.rb +21 -0
  40. data/features/step_definitions/page_steps.rb +46 -0
  41. data/features/step_definitions/w3c_steps.rb +7 -0
  42. data/features/support/capybara.rb +38 -0
  43. data/features/support/skipper.rb +5 -0
  44. data/features/support/world.rb +3 -0
  45. data/features/support/world_extender.rb +5 -0
  46. data/lib/bbc/a11y/cli.rb +49 -0
  47. data/lib/bbc/a11y/configuration.rb +83 -0
  48. data/lib/bbc/a11y/cucumber_runner.rb +133 -0
  49. data/lib/bbc/a11y/cucumber_support/heading_hierarchy.rb +92 -0
  50. data/lib/bbc/a11y/cucumber_support/language_detector.rb +26 -0
  51. data/lib/bbc/a11y/cucumber_support/page.rb +81 -0
  52. data/lib/bbc/a11y/cucumber_support/per_page_checks.rb +28 -0
  53. data/lib/bbc/a11y/cucumber_support/w3c.rb +36 -0
  54. data/lib/bbc/a11y/cucumber_support.rb +49 -0
  55. data/lib/bbc/a11y/version +1 -0
  56. data/lib/bbc/a11y.rb +4 -0
  57. data/spec/bbc/a11y/cucumber_support/heading_hierarchy_spec.rb +123 -0
  58. data/spec/bbc/a11y/cucumber_support/page_spec.rb +130 -0
  59. metadata +274 -0
@@ -0,0 +1,3 @@
1
+ Then(/^all core content is available and functional$/) do
2
+ skip_this_scenario
3
+ end
@@ -0,0 +1,21 @@
1
+ Then(/^the <html> element must have a `lang` attribute$/) do
2
+ page.must_have_lang_attribute
3
+ end
4
+
5
+ Then(/^the main natural language of the page must match that attribute$/) do
6
+ expected_language = language.detect(page)
7
+ puts "Detected page language: #{expected_language}"
8
+ page.must_have_lang_attribute_of(expected_language)
9
+ end
10
+
11
+ Then(/^any other tags with a lang attribute must contain text of that language$/) do
12
+ pending # express the regexp above with the code you wish you had
13
+ end
14
+ Then(/^all elements with `lang` attribute must have content in that natural language$/) do
15
+ pending # express the regexp above with the code you wish you had
16
+ end
17
+
18
+ Then(/^any parts expressed in a natural language different to the main language of the page must have a matching `lang` attribute$/) do
19
+ pending # express the regexp above with the code you wish you had
20
+ end
21
+
@@ -0,0 +1,46 @@
1
+ When(/^I visit the page$/) do
2
+ browser.visit settings.url
3
+ end
4
+
5
+ When(/^I view the page with JavaScript and CSS disabled$/) do
6
+ disable_javascript_and_css
7
+ browser.visit settings.url
8
+ end
9
+
10
+ Then(/^the document should have a title$/) do
11
+ page.must_have_title
12
+ end
13
+
14
+ Then(/^the title should describe the primary content of the document$/) do
15
+ begin
16
+ page.must_have_title_that_contains_h1_text
17
+ rescue => error
18
+ assert_title_describes_primary_content_of_document(browser.title, browser)
19
+ end
20
+ end
21
+
22
+ Then(/^there should be exactly one element with `role="(.*?)"`$/) do |arg1|
23
+ page.must_have_one_main_element
24
+ end
25
+
26
+ Then(/^there must be exactly one h1 element$/) do
27
+ page.must_have_one_h1
28
+ end
29
+
30
+ Then(/^each heading must be followed by content or a heading of one level deeper \(h2\-h6\)$/) do
31
+ puts "Heading hierarchy:"
32
+ puts page.heading_hierarchy.to_s
33
+ page.must_have_correct_heading_hierarchy
34
+ end
35
+
36
+ Then(/^there must be no elements with a title attribute whose content is repeated within the element$/) do
37
+ page.must_have_no_elements_with_title_attribute_content_repeated_within
38
+ end
39
+
40
+ Then(/^any form fields with associated labels do not have a title attribute$/) do
41
+ page.must_have_no_form_fields_with_label_and_title
42
+ end
43
+
44
+ Then(/^there should be no elements with a tabindex attribte of 0 or greater$/) do
45
+ page.must_not_have_any_positive_tabindex_values
46
+ end
@@ -0,0 +1,7 @@
1
+ When(/^I submit the page to the W3C Markup Validation Service$/) do
2
+ w3c.validate settings.url
3
+ end
4
+
5
+ Then(/^there should be no errors$/) do
6
+ expect(w3c.errors).to be_empty
7
+ end
@@ -0,0 +1,38 @@
1
+ require 'capybara'
2
+ require 'capybara/dsl'
3
+ require 'selenium/webdriver'
4
+
5
+ Capybara.register_driver(:without_javascript_or_css) do |app|
6
+ profile = Selenium::WebDriver::Firefox::Profile.new
7
+ profile['permissions.default.stylesheet'] = 2
8
+ profile['javascript.enabled'] = false
9
+ Capybara::Selenium::Driver.new(app, profile: profile)
10
+ end
11
+
12
+ Capybara.default_driver = :selenium
13
+
14
+ Before do
15
+ Capybara.use_default_driver
16
+ end
17
+
18
+ After do
19
+ Capybara.reset_sessions!
20
+ end
21
+
22
+ module BBC
23
+ module A11y
24
+
25
+ module Browser
26
+ def browser
27
+ Capybara.current_session
28
+ end
29
+
30
+ def disable_javascript_and_css
31
+ Capybara.current_driver = :without_javascript_or_css
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+
38
+ World(BBC::A11y::Browser)
@@ -0,0 +1,5 @@
1
+ Before do |test_case|
2
+ if settings.scenarios_to_skip.any? { |name| test_case.name.match name }
3
+ skip_this_scenario
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'bbc/a11y/cucumber_support'
2
+
3
+ World(BBC::A11y::CucumberSupport)
@@ -0,0 +1,5 @@
1
+ Before do |test_case|
2
+ settings.world_extensions.each do |extension_module|
3
+ extend extension_module
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ require 'bbc/a11y/cucumber_runner'
2
+ require 'bbc/a11y/configuration'
3
+
4
+ module BBC
5
+ module A11y
6
+
7
+ # A very thin wrapper around Cucumber which takes settings on the command-line,
8
+ # stores them somewhere our automation code will be able to find it, and then
9
+ # runs Cucumber
10
+ class CLI
11
+ MissingArgument = Class.new(StandardError)
12
+
13
+ def initialize(stdin, stdout, stderr, args)
14
+ @stdin, @stdout, @stderr, @args = stdin, stdout, stderr, args
15
+ end
16
+
17
+ def call
18
+ CucumberRunner.new(settings, cucumber_args).call
19
+ rescue MissingArgument => error
20
+ stderr.puts "You missed an argument: #{error.message}"
21
+ stderr.puts HELP
22
+ raise SystemExit
23
+ end
24
+
25
+ private
26
+
27
+ def settings
28
+ Configuration.parse(File.expand_path(".a11y.rb"))
29
+ end
30
+
31
+ def cucumber_args
32
+ return unless args.include?('--')
33
+ args[args.find_index('--')+1..-1].join(' ')
34
+ end
35
+
36
+ attr_reader :stdin, :stderr, :stdout, :args
37
+ private :stdin, :stderr, :stdout, :args
38
+
39
+ HELP = %{
40
+ Usage: a11y url-to-test [-- cucumber-args]
41
+
42
+ url-to-test - URL of the page to run the full set of accessiblity tests against
43
+ cucumber-args - Arguments to pass to Cucumber when running the tests. See cucumber --help
44
+ for details.
45
+ }
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,83 @@
1
+ module BBC
2
+ module A11y
3
+
4
+ def self.configure(&block)
5
+ @settings = Configuration::DSL.new(block).settings
6
+ end
7
+
8
+ def self.configuration
9
+ @settings ||= Configuration::Settings.new
10
+ end
11
+
12
+ module Configuration
13
+ def self.parse(file)
14
+ require file
15
+ # the file should call BBC::A11y.configure
16
+ BBC::A11y.configuration
17
+ end
18
+
19
+ class Settings
20
+ attr_reader :before_all_hooks,
21
+ :after_all_hooks,
22
+ :pages
23
+
24
+ def initialize
25
+ @before_all_hooks = []
26
+ @after_all_hooks = []
27
+ @pages = []
28
+ freeze
29
+ end
30
+ end
31
+
32
+ class PageSettings
33
+ attr_reader :url, :scenarios_to_skip, :world_extensions
34
+
35
+ def initialize(url)
36
+ @url = url
37
+ @scenarios_to_skip = []
38
+ @world_extensions = []
39
+ freeze
40
+ end
41
+ end
42
+
43
+ class DSL
44
+ attr_reader :settings
45
+ def initialize(block)
46
+ @settings = Settings.new
47
+ instance_eval &block
48
+ end
49
+
50
+ def before_all(&block)
51
+ settings.before_all_hooks << block
52
+ end
53
+
54
+ def after_all(&block)
55
+ settings.after_all_hooks << block
56
+ end
57
+
58
+ def page(url, &block)
59
+ settings.pages << PageDSL.new(url, block).settings
60
+ end
61
+ end
62
+
63
+ class PageDSL
64
+ attr_reader :settings
65
+
66
+ def initialize(url, block)
67
+ @settings = PageSettings.new(url)
68
+ instance_eval &block if block
69
+ end
70
+
71
+ def skip_scenario(name)
72
+ @settings.scenarios_to_skip << name
73
+ end
74
+
75
+ def customize_world(&block)
76
+ @settings.world_extensions << Module.new(&block)
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+ end
@@ -0,0 +1,133 @@
1
+ require 'forwardable'
2
+ require 'cucumber'
3
+ require 'bbc/a11y/cucumber_support'
4
+ require 'cucumber/core/filter'
5
+ require 'colorize'
6
+
7
+ module BBC
8
+ module A11y
9
+ class CucumberFormatter
10
+ def initialize(*args)
11
+ @current_feature = nil
12
+ end
13
+
14
+ def before_test_case(test_case)
15
+ on_new_feature(test_case) do |feature|
16
+ puts
17
+ puts "#{feature.name}"
18
+ puts "#{"-" * feature.name.length}"
19
+ puts
20
+ end
21
+ print " - #{test_case.name} "
22
+ end
23
+
24
+ def after_test_case(test_case, result)
25
+ print colour(result)
26
+ if result.failed? || result.pending?
27
+ puts
28
+ puts result.exception.message.to_s.red
29
+ puts result.exception.backtrace.join("\n").red
30
+ end
31
+ puts
32
+ end
33
+
34
+ private
35
+
36
+ def colour(result)
37
+ ResultColour.new(result).apply_to(result.to_s)
38
+ end
39
+
40
+ def on_new_feature(test_case)
41
+ feature = test_case.source.first
42
+ if feature != @current_feature
43
+ @current_feature = feature
44
+ yield feature
45
+ end
46
+ end
47
+
48
+ class ResultColour
49
+ def initialize(result)
50
+ @color = :white
51
+ result.describe_to(self)
52
+ end
53
+
54
+ def passed
55
+ @color = :green
56
+ end
57
+
58
+ def skipped
59
+ @color = :blue
60
+ end
61
+
62
+ def pending(*)
63
+ @color = :yellow
64
+ end
65
+
66
+ def failed
67
+ @color = :red
68
+ end
69
+
70
+ def duration(*)
71
+ end
72
+
73
+ def exception(*)
74
+ end
75
+
76
+ def apply_to(string)
77
+ string.send(@color)
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ class CucumberRunner
84
+ def initialize(settings, cucumber_args)
85
+ @settings = settings
86
+ end
87
+
88
+ def call
89
+ runtime = Cucumber::Runtime.new(configuration)
90
+ run_before_all_hooks
91
+ @settings.pages.each do |page_settings|
92
+ # stash the settings where the support code will find them
93
+ BBC::A11y::CucumberSupport.current_page_settings = page_settings
94
+ print_page_header page_settings
95
+ runtime.run!
96
+ end
97
+ ensure
98
+ run_after_all_hooks
99
+ end
100
+
101
+ private
102
+
103
+ def configuration
104
+ return @configuration if @configuration
105
+ features_path = File.expand_path(File.dirname(__FILE__) + "/../../../features")
106
+ @configuration = Cucumber::Cli::Configuration.new
107
+ # This is ugly, but until Cucumber offers a better API, we have to pass in our settings as though
108
+ # they were CLI arguments
109
+ @configuration.parse!([
110
+ features_path,
111
+ "--format", "BBC::A11y::CucumberFormatter"])
112
+ @configuration
113
+ end
114
+
115
+ def run_before_all_hooks
116
+ @settings.before_all_hooks.each &:call
117
+ end
118
+
119
+ def run_after_all_hooks
120
+ @settings.after_all_hooks.each &:call
121
+ end
122
+
123
+ def print_page_header(page_settings)
124
+ puts
125
+ puts
126
+ puts "BBC Accesibility: #{page_settings.url}"
127
+ puts "=" * "BBC Accesibility: #{page_settings.url}".length
128
+ end
129
+
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,92 @@
1
+ module BBC
2
+ module A11y
3
+ module CucumberSupport
4
+
5
+ class HeadingHierarchy
6
+ include RSpec::Matchers
7
+
8
+ def initialize(page)
9
+ @page = page
10
+ end
11
+
12
+ def validate
13
+ return self unless headings.count > 1
14
+ adjacent_pairs = headings.zip(headings[1..-1])[0..-2]
15
+ errors = adjacent_pairs.reduce([]) { |errors, pair|
16
+ previous, current = *pair
17
+ if current > previous
18
+ delta = current.number - previous.number
19
+ if delta != 1
20
+ errors << current
21
+ end
22
+ end
23
+ errors
24
+ }
25
+ expect(errors).to be_empty, error_message(errors)
26
+ self
27
+ end
28
+
29
+ def to_s
30
+ headings.map { |h|
31
+ indent = " " * (h.number - 1)
32
+ indent + h.to_s
33
+ }.join("\n")
34
+ end
35
+
36
+ private
37
+
38
+ def error_message(errors)
39
+ "Headings were not in order: " +
40
+ headings.map { |h| errors.include?(h) ? "**#{h}**" : h }.
41
+ join(", ")
42
+ end
43
+
44
+ def headings
45
+ heading_elements.map { |h| Heading.new(h) }
46
+ end
47
+
48
+ def heading_elements
49
+ # can't work out how to get Capybara to do this reliably, so we'll reach down to the XML parser
50
+ xml = Nokogiri::XML.parse(page.source).remove_namespaces!
51
+ header_xml_nodes = xml.xpath('//*[substring(name(),1,1) = "h" and number(substring(name(),2,1))]')
52
+ header_xml_nodes.map(&:path).map { |xpath|
53
+ page.find(:xpath, xpath, visible: false)
54
+ }
55
+ end
56
+
57
+ attr_reader :page
58
+ private :page
59
+
60
+ class Heading
61
+ attr_reader :number
62
+
63
+ def initialize(element)
64
+ @element = element
65
+ @tag_name = element.tag_name
66
+ @number = @tag_name[1].to_i
67
+ end
68
+
69
+ def > (other)
70
+ number > other.number
71
+ end
72
+
73
+ def == (other)
74
+ begin
75
+ other.element.path == @element.path
76
+ rescue NotSupportedByDriverError
77
+ other.element == @element
78
+ end
79
+ end
80
+
81
+ def to_s
82
+ @tag_name
83
+ end
84
+
85
+ attr_reader :element
86
+ protected :element
87
+ end
88
+ end
89
+
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,26 @@
1
+ module BBC
2
+ module A11y
3
+ module CucumberSupport
4
+
5
+ module LanguageDetector
6
+ # factory for language detector, allows us to use different mechanisms (e.g. a hard-coded language passed from settings)
7
+ def self.new
8
+ CLDLanguageDetector.new
9
+ end
10
+
11
+ require 'cld'
12
+ class CLDLanguageDetector
13
+ InsufficientConfidence = Class.new(StandardError)
14
+
15
+ # returns the code of the language, or raises an error if insufficient confidence
16
+ def detect(text)
17
+ detected_language = CLD.detect_language(text.to_s)
18
+ raise InsufficientConfidence unless detected_language[:reliable]
19
+ detected_language[:code]
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
1
+ require 'rspec/expectations'
2
+ require 'capybara/rspec/matchers'
3
+ require 'bbc/a11y/cucumber_support/heading_hierarchy'
4
+
5
+ module BBC
6
+ module A11y
7
+ module CucumberSupport
8
+
9
+ class Page
10
+
11
+ include RSpec::Matchers
12
+ include Capybara::RSpecMatchers
13
+
14
+ def initialize(browser)
15
+ @browser = browser
16
+ end
17
+
18
+ def title
19
+ browser.title
20
+ end
21
+
22
+ def must_have_lang_attribute
23
+ expect(browser).to have_css('html[lang]')
24
+ end
25
+
26
+ def must_have_lang_attribute_of(expected_code)
27
+ expect(browser.find('html')['lang'].split('-')[0]).to eq expected_code
28
+ end
29
+
30
+ def must_have_title
31
+ expect(browser.title).not_to be_empty
32
+ end
33
+
34
+ def must_have_title_that_contains_h1_text
35
+ expect(browser.title).to include(browser.find('h1').text)
36
+ end
37
+
38
+ def must_have_one_main_element
39
+ expect(browser.all('[role="main"]').length).to eq 1
40
+ end
41
+
42
+ def must_have_one_h1
43
+ expect(browser.all('h1', visible: false).length).to eq 1
44
+ end
45
+
46
+ def must_have_correct_heading_hierarchy
47
+ heading_hierarchy.validate
48
+ end
49
+
50
+ def must_have_no_elements_with_title_attribute_content_repeated_within
51
+ bad_nodes = browser.all('[title]').select { |node| node.text.include? node['title'] }
52
+ expect(bad_nodes).to be_empty
53
+ end
54
+
55
+ def must_have_no_form_fields_with_label_and_title
56
+ bad_nodes = browser.all('form *[id][title]').select { |node| browser.has_css?("label[for='#{node['id']}']") }
57
+ expect(bad_nodes).to be_empty
58
+ end
59
+
60
+ def must_not_have_any_positive_tabindex_values
61
+ bad_nodes = browser.all('[tabindex]').select { |node| node['tabindex'].to_i >= 0 }
62
+ expect(bad_nodes).to be_empty
63
+ end
64
+
65
+ def to_s
66
+ browser.text
67
+ end
68
+
69
+ def heading_hierarchy
70
+ HeadingHierarchy.new(browser)
71
+ end
72
+
73
+ private
74
+
75
+ attr_reader :browser
76
+ private :browser
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,28 @@
1
+ module BBC
2
+ module A11y
3
+ module CucumberSupport
4
+ module PerPageChecks
5
+ def assert_title_describes_primary_content_of_document(title, page)
6
+ pending <<-ERROR
7
+ Because the title did not contain the header text, you need to write a custom
8
+ method to define how to make this check.
9
+
10
+ In your .a11y.rb file, add the following code:
11
+
12
+ BBC::A11y.configure do
13
+ page "my_page.html" do
14
+
15
+ customize_world do
16
+ def assert_title_describes_primary_content_of_document(title, page)
17
+ # TODO: add your custom code here to make the check
18
+ end
19
+ end
20
+
21
+ end
22
+ end
23
+ ERROR
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ require 'w3c_validators'
2
+
3
+ module BBC
4
+ module A11y
5
+ module CucumberSupport
6
+
7
+ class W3C
8
+ include W3CValidators
9
+
10
+ def initialize
11
+ @validator = MarkupValidator.new(options)
12
+ end
13
+
14
+ def validate(url)
15
+ @results = @validator.validate_uri(url)
16
+ end
17
+
18
+ def errors
19
+ @results.errors
20
+ end
21
+
22
+ private
23
+
24
+ def options
25
+ return {} unless proxy
26
+ { proxy_server: proxy, proxy_port: 80 }
27
+ end
28
+
29
+ def proxy
30
+ @proxy ||= ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY'].map { |key| ENV[key] }.compact.first
31
+ end
32
+ end
33
+
34
+ end
35
+ end
36
+ end