bbc-a11y 0.0.12 → 0.0.13
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.
- checksums.yaml +4 -4
- data/.ruby-version +1 -0
- data/README.md +9 -11
- data/Rakefile +2 -2
- data/a11y.rb +5 -0
- data/bbc-a11y.gemspec +3 -5
- data/bin/a11y +2 -2
- data/examples/bbc-pages/a11y.rb +2 -6
- data/examples/local-web-app/Gemfile +1 -1
- data/examples/local-web-app/a11y.rb +10 -22
- data/features/check_standards/focusable_controls.feature +62 -0
- data/features/check_standards/form_interactions.feature +45 -0
- data/features/check_standards/form_labels.feature +55 -0
- data/features/check_standards/headings.feature +154 -0
- data/features/check_standards/image_alt.feature +39 -0
- data/features/check_standards/language.feature +46 -0
- data/features/check_standards/main_landmark.feature +39 -0
- data/features/check_standards/tab_index.feature +54 -0
- data/features/cli/display_failing_result.feature +10 -0
- data/features/{exit_status.feature → cli/exit_status.feature} +1 -2
- data/features/cli/provide_muting_tips.feature +25 -0
- data/features/cli/report_configuration_errors.feature +43 -0
- data/features/cli/skipping_standards.feature +15 -0
- data/features/cli/specify_url.feature +9 -0
- data/features/cli/specify_url_via_config.feature +13 -0
- data/features/mute_errors.feature +118 -0
- data/features/step_definitions/steps.rb +25 -1
- data/lib/bbc/a11y/cli.rb +32 -44
- data/lib/bbc/a11y/configuration.rb +56 -22
- data/lib/bbc/a11y/linter.rb +42 -0
- data/lib/bbc/a11y/standards.rb +43 -0
- data/lib/bbc/a11y/standards/anchor_hrefs.rb +18 -0
- data/lib/bbc/a11y/standards/content_follows_headings.rb +22 -0
- data/lib/bbc/a11y/standards/exactly_one_main_heading.rb +20 -0
- data/lib/bbc/a11y/standards/exactly_one_main_landmark.rb +20 -0
- data/lib/bbc/a11y/standards/form_labels.rb +39 -0
- data/lib/bbc/a11y/standards/form_submit_buttons.rb +21 -0
- data/lib/bbc/a11y/standards/heading_hierarchy.rb +34 -0
- data/lib/bbc/a11y/standards/image_alt.rb +18 -0
- data/lib/bbc/a11y/standards/language_attribute.rb +19 -0
- data/lib/bbc/a11y/standards/tab_index.rb +22 -0
- data/lib/bbc/a11y/version +1 -1
- data/spec/bbc/a11y/cli_spec.rb +22 -15
- data/spec/bbc/a11y/configuration_spec.rb +15 -40
- data/standards/support/capybara.rb +1 -2
- metadata +62 -81
- data/features/specify_url_via_cli.feature +0 -10
- data/features/specify_url_via_config.feature +0 -16
- data/lib/bbc/a11y.rb +0 -17
- data/lib/bbc/a11y/cucumber_runner.rb +0 -208
- data/lib/bbc/a11y/cucumber_support.rb +0 -56
- data/lib/bbc/a11y/cucumber_support/disabled_w3c.rb +0 -37
- data/lib/bbc/a11y/cucumber_support/heading_hierarchy.rb +0 -94
- data/lib/bbc/a11y/cucumber_support/language_detector.rb +0 -26
- data/lib/bbc/a11y/cucumber_support/matchers.rb +0 -21
- data/lib/bbc/a11y/cucumber_support/page.rb +0 -94
- data/lib/bbc/a11y/cucumber_support/per_page_checks.rb +0 -28
- data/lib/bbc/a11y/cucumber_support/w3c.rb +0 -36
- data/spec/bbc/a11y/cucumber_support/heading_hierarchy_spec.rb +0 -162
- data/spec/bbc/a11y/cucumber_support/matchers_spec.rb +0 -52
- data/spec/bbc/a11y/cucumber_support/page_spec.rb +0 -197
data/lib/bbc/a11y/cli.rb
CHANGED
@@ -1,72 +1,60 @@
|
|
1
1
|
require 'bbc/a11y/configuration'
|
2
|
-
require 'bbc/a11y'
|
2
|
+
require 'bbc/a11y/linter'
|
3
|
+
require 'open-uri'
|
4
|
+
require 'capybara'
|
5
|
+
require 'colorize'
|
3
6
|
|
4
7
|
module BBC
|
5
8
|
module A11y
|
6
9
|
|
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
10
|
class CLI
|
11
|
-
MissingArgument = Class.new(StandardError)
|
12
|
-
|
13
11
|
def initialize(stdin, stdout, stderr, args)
|
14
12
|
@stdin, @stdout, @stderr, @args = stdin, stdout, stderr, args
|
15
13
|
end
|
16
14
|
|
17
|
-
def call
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
A11y.until_version('0.1.0') do
|
31
|
-
exit_with_message "Please rename your .a11y.rb configuration file to a11y.rb" if File.exist?(".a11y.rb")
|
15
|
+
def call
|
16
|
+
all_errors = []
|
17
|
+
settings.pages.each do |page_settings|
|
18
|
+
errors = check_standards_for(page_settings)
|
19
|
+
if errors.empty?
|
20
|
+
stdout.puts "✓ #{page_settings.url}".colorize(:green)
|
21
|
+
else
|
22
|
+
stdout.puts "✗ #{page_settings.url}".colorize(:red)
|
23
|
+
stdout.puts errors.map { |error|
|
24
|
+
" - #{error}"
|
25
|
+
}.join("\n")
|
26
|
+
end
|
27
|
+
all_errors += errors
|
32
28
|
end
|
33
|
-
|
29
|
+
exit 1 unless all_errors.empty?
|
30
|
+
rescue Configuration::ParseError => error
|
31
|
+
exit_with_message error.message
|
34
32
|
end
|
35
33
|
|
36
|
-
|
37
|
-
if args.find_index('--')
|
38
|
-
args[0..(args.find_index('--') - 1)]
|
39
|
-
else
|
40
|
-
args
|
41
|
-
end
|
42
|
-
end
|
34
|
+
private
|
43
35
|
|
44
|
-
def
|
45
|
-
|
46
|
-
|
36
|
+
def check_standards_for(page_settings)
|
37
|
+
standards = Standards.for(page_settings)
|
38
|
+
html = open(page_settings.url).read
|
39
|
+
Linter.new(Capybara.string(html), standards).run.errors.to_a
|
47
40
|
end
|
48
41
|
|
49
|
-
def
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
STDERR.puts "\nExiting... Interrupt again to exit immediately."
|
54
|
-
end
|
42
|
+
def settings
|
43
|
+
return Configuration.for_urls(@args) if @args.any?
|
44
|
+
configuration_file = File.expand_path("a11y.rb")
|
45
|
+
Configuration.parse(configuration_file)
|
55
46
|
end
|
56
47
|
|
57
48
|
def exit_with_message(*messages)
|
58
|
-
messages.each { |message| puts message }
|
59
|
-
|
49
|
+
messages.each { |message| stderr.puts message }
|
50
|
+
exit 1
|
60
51
|
end
|
61
52
|
|
62
53
|
attr_reader :stdin, :stderr, :stdout, :args
|
63
54
|
private :stdin, :stderr, :stdout, :args
|
64
55
|
|
65
56
|
HELP = %{
|
66
|
-
Usage: a11y [
|
67
|
-
|
68
|
-
cucumber-args - Arguments to pass to Cucumber when running the tests. See cucumber --help
|
69
|
-
for details.
|
57
|
+
Usage: a11y [url]
|
70
58
|
}
|
71
59
|
|
72
60
|
end
|
@@ -1,19 +1,20 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
1
3
|
module BBC
|
2
4
|
module A11y
|
3
5
|
|
4
6
|
def self.configure(&block)
|
5
|
-
|
7
|
+
Configuration::DSL.new(block).settings
|
6
8
|
end
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
# TODO: add tests for this?
|
11
|
+
def self.configure_from_file(filename)
|
12
|
+
Configuration::DSL.new(File.read(filename), filename).settings
|
10
13
|
end
|
11
14
|
|
12
15
|
module Configuration
|
13
|
-
def self.parse(
|
14
|
-
|
15
|
-
# the file should call BBC::A11y.configure
|
16
|
-
BBC::A11y.configuration
|
16
|
+
def self.parse(filename)
|
17
|
+
BBC::A11y.configure_from_file(filename)
|
17
18
|
end
|
18
19
|
|
19
20
|
def self.for_urls(urls)
|
@@ -21,6 +22,28 @@ module BBC
|
|
21
22
|
Settings.new.with_pages(page_settings)
|
22
23
|
end
|
23
24
|
|
25
|
+
ParseError = Class.new(StandardError) do
|
26
|
+
def message
|
27
|
+
file, line = backtrace.first.split(":")[0..1]
|
28
|
+
file = Pathname.new(file).relative_path_from(Pathname.new(Dir.pwd))
|
29
|
+
line = line.to_i
|
30
|
+
source_snippet = File.read(file).lines.each_with_index.map { |content, index|
|
31
|
+
indent = (index == line - 1) ? "=> " : " "
|
32
|
+
indent + content
|
33
|
+
}[line - 2..line]
|
34
|
+
|
35
|
+
[
|
36
|
+
"There was an error reading your configuration file at line #{line} of '#{file}'",
|
37
|
+
"",
|
38
|
+
source_snippet,
|
39
|
+
"",
|
40
|
+
super,
|
41
|
+
"",
|
42
|
+
"For help learning the configuration DSL, please visit https://github.com/cucumber-ltd/bbc-a11y"
|
43
|
+
]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
24
47
|
class Settings
|
25
48
|
attr_reader :before_all_hooks,
|
26
49
|
:after_all_hooks,
|
@@ -39,30 +62,44 @@ module BBC
|
|
39
62
|
end
|
40
63
|
|
41
64
|
class PageSettings
|
42
|
-
attr_reader :url
|
65
|
+
attr_reader :url
|
66
|
+
attr_reader :skipped_standards
|
43
67
|
|
44
|
-
def initialize(url,
|
68
|
+
def initialize(url, skipped_standards=[])
|
45
69
|
@url = url
|
46
|
-
@
|
47
|
-
@world_extensions = world_extensions
|
70
|
+
@skipped_standards = skipped_standards
|
48
71
|
freeze
|
49
72
|
end
|
50
73
|
|
51
|
-
def
|
52
|
-
|
74
|
+
def merge(other)
|
75
|
+
self.class.new(url, skipped_standards + other.skipped_standards)
|
53
76
|
end
|
54
77
|
|
55
|
-
def
|
56
|
-
|
78
|
+
def skip_standard?(standard)
|
79
|
+
@skipped_standards.any? { |pattern|
|
80
|
+
pattern.match(standard.name)
|
81
|
+
}
|
57
82
|
end
|
58
83
|
end
|
59
84
|
|
60
85
|
class DSL
|
61
86
|
attr_reader :settings
|
62
|
-
|
87
|
+
|
88
|
+
def initialize(config, config_filename = nil)
|
63
89
|
@settings = Settings.new
|
64
90
|
@general_page_settings = []
|
65
|
-
|
91
|
+
begin
|
92
|
+
if config_filename
|
93
|
+
instance_eval config, config_filename
|
94
|
+
else
|
95
|
+
instance_eval &config
|
96
|
+
end
|
97
|
+
rescue NoMethodError => error
|
98
|
+
method_name = error.message.scan(/\`(.*)'/)[0][0]
|
99
|
+
raise Configuration::ParseError, "`#{method_name}` is not part of the configuration language", error.backtrace
|
100
|
+
rescue => error
|
101
|
+
raise Configuration::ParseError, error.message, error.backtrace
|
102
|
+
end
|
66
103
|
@settings = settings.with_pages(apply_general_settings(settings.pages))
|
67
104
|
end
|
68
105
|
|
@@ -102,13 +139,10 @@ module BBC
|
|
102
139
|
instance_eval &block if block
|
103
140
|
end
|
104
141
|
|
105
|
-
def
|
106
|
-
@settings.
|
142
|
+
def skip_standard(pattern)
|
143
|
+
@settings.skipped_standards << pattern
|
107
144
|
end
|
108
145
|
|
109
|
-
def customize_world(&block)
|
110
|
-
@settings.world_extensions << Module.new(&block)
|
111
|
-
end
|
112
146
|
end
|
113
147
|
|
114
148
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'bbc/a11y/standards'
|
2
|
+
|
3
|
+
module BBC
|
4
|
+
module A11y
|
5
|
+
|
6
|
+
class Linter
|
7
|
+
def initialize(page, standards=Standards.all)
|
8
|
+
@page = page
|
9
|
+
@standards = standards
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
errors = []
|
14
|
+
@standards.each do |standard|
|
15
|
+
standard.new(@page).call(errors)
|
16
|
+
end
|
17
|
+
LintResult.new(errors)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class LintResult
|
22
|
+
def initialize(errors)
|
23
|
+
@errors = errors
|
24
|
+
end
|
25
|
+
|
26
|
+
def passed?
|
27
|
+
@errors.empty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def failed?
|
31
|
+
!passed?
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
@errors.map(&:to_s).join("\n")
|
36
|
+
end
|
37
|
+
|
38
|
+
attr_reader :errors
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'bbc/a11y/standards/anchor_hrefs'
|
2
|
+
require 'bbc/a11y/standards/content_follows_headings'
|
3
|
+
require 'bbc/a11y/standards/exactly_one_main_heading'
|
4
|
+
require 'bbc/a11y/standards/exactly_one_main_landmark'
|
5
|
+
require 'bbc/a11y/standards/form_labels'
|
6
|
+
require 'bbc/a11y/standards/form_submit_buttons'
|
7
|
+
require 'bbc/a11y/standards/heading_hierarchy'
|
8
|
+
require 'bbc/a11y/standards/image_alt'
|
9
|
+
require 'bbc/a11y/standards/language_attribute'
|
10
|
+
require 'bbc/a11y/standards/tab_index'
|
11
|
+
|
12
|
+
module BBC
|
13
|
+
module A11y
|
14
|
+
module Standards
|
15
|
+
def self.for(page_settings)
|
16
|
+
all.reject { |standard|
|
17
|
+
page_settings.skip_standard?(standard)
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.matching(name)
|
22
|
+
all.select { |standard|
|
23
|
+
name.match(standard.name)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.all
|
28
|
+
[
|
29
|
+
AnchorHrefs,
|
30
|
+
ContentFollowsHeadings,
|
31
|
+
FormLabels,
|
32
|
+
FormSubmitButtons,
|
33
|
+
HeadingHierarchy,
|
34
|
+
ImageAlt,
|
35
|
+
ExactlyOneMainHeading,
|
36
|
+
ExactlyOneMainLandmark,
|
37
|
+
LanguageAttribute,
|
38
|
+
TabIndex
|
39
|
+
]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module BBC
|
2
|
+
module A11y
|
3
|
+
module Standards
|
4
|
+
class AnchorHrefs
|
5
|
+
def initialize(page)
|
6
|
+
@page = page
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(errors)
|
10
|
+
@page.all("a:not([href])").each do |anchor|
|
11
|
+
errors << "Anchor has no href attribute: #{anchor.path}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module BBC
|
2
|
+
module A11y
|
3
|
+
module Standards
|
4
|
+
|
5
|
+
class ContentFollowsHeadings
|
6
|
+
def initialize(page)
|
7
|
+
@page = page
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(errors)
|
11
|
+
["h1", "h2", "h3", "h4", "h5", "h6"].each do |h|
|
12
|
+
if @page.all("#{h} + #{h}").any?
|
13
|
+
errors << "Heading elements must be followed by content. " +
|
14
|
+
"No content follows a #{h}."
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module BBC
|
2
|
+
module A11y
|
3
|
+
module Standards
|
4
|
+
class ExactlyOneMainHeading
|
5
|
+
def initialize(page)
|
6
|
+
@page = page
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(errors)
|
10
|
+
count = @page.all('h1').size
|
11
|
+
if count != 1
|
12
|
+
errors << "A document must have exactly one heading." +
|
13
|
+
" Found #{count} h1 elements."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module BBC
|
2
|
+
module A11y
|
3
|
+
module Standards
|
4
|
+
class ExactlyOneMainLandmark
|
5
|
+
def initialize(page)
|
6
|
+
@page = page
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(errors)
|
10
|
+
count = @page.all('*[role=main]').size
|
11
|
+
if count != 1
|
12
|
+
errors << "A document must have exactly one main landmark." +
|
13
|
+
" Found #{count} elements with role=\"main\"."
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module BBC
|
2
|
+
module A11y
|
3
|
+
module Standards
|
4
|
+
class FormLabels
|
5
|
+
def initialize(page)
|
6
|
+
@page = page
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(errors)
|
10
|
+
fields = @page.all(potential_offenders)
|
11
|
+
fields.each do |field|
|
12
|
+
if offender? field
|
13
|
+
errors << "Field has no label or title attribute: #{field.path}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def potential_offenders
|
21
|
+
"input:not([title]), textarea:not([title]), select:not([title])"
|
22
|
+
end
|
23
|
+
|
24
|
+
def offender?(field)
|
25
|
+
(missing_id? field) or (missing_label? field)
|
26
|
+
end
|
27
|
+
|
28
|
+
def missing_id?(field)
|
29
|
+
field['id'] == nil or field['id'] == ''
|
30
|
+
end
|
31
|
+
|
32
|
+
def missing_label?(field)
|
33
|
+
@page.all("label[for='#{field['id']}']").empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module BBC
|
2
|
+
module A11y
|
3
|
+
module Standards
|
4
|
+
class FormSubmitButtons
|
5
|
+
def initialize(page)
|
6
|
+
@page = page
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(errors)
|
10
|
+
@page.all("form").each do |form|
|
11
|
+
submits = form.all("input[type=submit]")
|
12
|
+
if submits.empty?
|
13
|
+
errors << "Form has no submit button: #{form.path}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|