capybara-accessible 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +3 -2
- data/.travis.yml +26 -0
- data/Gemfile +0 -1
- data/LICENSE +19 -0
- data/README.md +33 -40
- data/Rakefile +3 -8
- data/capybara-accessible.gemspec +16 -22
- data/lib/capybara/accessible.rb +34 -85
- data/lib/capybara/accessible/adapters/poltergeist.rb +18 -0
- data/lib/capybara/accessible/adapters/selenium.rb +20 -0
- data/lib/capybara/accessible/auditor.rb +27 -54
- data/lib/capybara/accessible/driver.rb +16 -0
- data/lib/capybara/accessible/extensions/driver.rb +28 -0
- data/lib/capybara/accessible/extensions/element.rb +19 -0
- data/lib/capybara/accessible/tasks.rb +0 -1
- data/lib/capybara/accessible/version.rb +1 -1
- data/lib/vendor/google/accessibility-developer-tools/axs_testing.js +2548 -1555
- data/spec/accessible_app.rb +2 -0
- data/spec/capybara_accessible_spec.rb +81 -0
- data/spec/spec_helper.rb +5 -10
- metadata +21 -66
- data/LICENSE.txt +0 -22
- data/lib/capybara/accessible/driver_extensions.rb +0 -6
- data/lib/capybara/accessible/element.rb +0 -10
- data/spec/poltergeist_driver_spec.rb +0 -56
- data/spec/selenium_driver_spec.rb +0 -70
- data/spec/webkit_driver_spec.rb +0 -71
- data/tddium.yml +0 -7
- data/travis.yml +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 44aeee6bf81a4a9249b1d99191de2f0f1a1f8260e5bd124f488d7d2b9cf59c4e
|
4
|
+
data.tar.gz: 6f10dd18c5978d5ceb5ce2f118b52856fd6381f27569845997e647675f82a46b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 22b5c864295928aa1315861d36d39db10d334b9f76edb2248b69374f4f1ba58f4f105b8c940e769b5b861ad95a27470daaf1d708bd1e777cbfea980f4684dc14
|
7
|
+
data.tar.gz: d124caccb286e9f84b5ce2c334012c3b3a621e6bcea243c3315f5f03385908ad694728a5162c4028777663b299e0826b4fd903f96d3581bdbd2253803e9373e3
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
language: ruby
|
2
|
+
sudo: required
|
3
|
+
dist: trusty
|
4
|
+
|
5
|
+
rvm:
|
6
|
+
- 2.3.7
|
7
|
+
- 2.4.4
|
8
|
+
- 2.5.3
|
9
|
+
- 2.6.1
|
10
|
+
|
11
|
+
before_install:
|
12
|
+
- sudo apt-get update
|
13
|
+
- sudo apt-get install chromium-chromedriver
|
14
|
+
- wget https://github.com/mozilla/geckodriver/releases/download/v0.24.0/geckodriver-v0.24.0-linux64.tar.gz
|
15
|
+
- mkdir geckodriver
|
16
|
+
- tar -xzf geckodriver-v0.24.0-linux64.tar.gz -C geckodriver
|
17
|
+
- export PATH=$PATH:$PWD/geckodriver
|
18
|
+
before_script:
|
19
|
+
- "export PATH=$PATH:/usr/lib/chromium-browser/"
|
20
|
+
- "export DISPLAY=:99.0"
|
21
|
+
- "sh -e /etc/init.d/xvfb start"
|
22
|
+
- sleep 3 # give xvfb some time to start
|
23
|
+
|
24
|
+
addons:
|
25
|
+
chrome: stable
|
26
|
+
firefox: latest-esr
|
data/Gemfile
CHANGED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2013-2016 Case Commons, Inc. <http://casecommons.org>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,60 +1,55 @@
|
|
1
|
-
# capybara-accessible
|
1
|
+
# [capybara-accessible](https://github.com/Casecommons/capybara-accessible)
|
2
2
|
|
3
|
-
|
3
|
+
[![Gem Version](https://img.shields.io/gem/v/capybara-accessible.svg?style=flat)](https://rubygems.org/gems/capybara-accessible)
|
4
|
+
[![Build Status](https://secure.travis-ci.org/Casecommons/capybara-accessible.svg?branch=master)](https://travis-ci.org/Casecommons/capybara-accessible)
|
4
5
|
|
5
|
-
capybara-accessible introduces accessibility tests into your [Rspec integration tests](https://www.relishapp.com/rspec/rspec-rails/docs/feature-specs/feature-spec),
|
6
|
-
helping you to capture existing failures and prevent future regressions.
|
6
|
+
capybara-accessible introduces accessibility tests into your [Rspec integration tests](https://www.relishapp.com/rspec/rspec-rails/docs/feature-specs/feature-spec) which use [Capybara](https://jnicklas.github.io/capybara/), helping you to capture existing failures and prevent future regressions.
|
7
7
|
|
8
|
-
It works by defining a custom webdriver that runs [Google's Accessibility Developer Tools](https://github.com/GoogleChrome/accessibility-developer-tools)
|
9
|
-
audits during each test run. Since the audits are invoked automatically on page load, you do not need to make explicit assertions on accessibility.
|
10
|
-
Instead, the test will simply fail with a message indicating the failures, like so:
|
8
|
+
It works by defining a custom webdriver that runs [Google's Accessibility Developer Tools](https://github.com/GoogleChrome/accessibility-developer-tools) audits during each test run. Since the audits are invoked automatically on page load, you do not need to make explicit assertions on accessibility. Instead, the test will simply fail with a message indicating the failures, like so:
|
11
9
|
|
12
|
-
![Error output from an Rspec failure](
|
10
|
+
![Error output from an Rspec failure](https://i.imgur.com/8RWEzzg.png)
|
13
11
|
|
14
12
|
Some of the audit rules that are included from Google's Accessibility Developer Tools:
|
15
|
-
* minimum color contrast
|
16
|
-
* label associations with inputs
|
17
|
-
* presence of alt attributes
|
18
|
-
* valid use of ARIA roles
|
19
13
|
|
20
|
-
|
21
|
-
|
14
|
+
- minimum color contrast
|
15
|
+
- label associations with inputs
|
16
|
+
- presence of alt attributes
|
17
|
+
- valid use of ARIA roles
|
22
18
|
|
23
|
-
|
24
|
-
we built capybara-accessible.
|
19
|
+
See the [Google Accessibility Developer Tools wiki](https://code.google.com/p/accessibility-developer-tools/wiki/AuditRules) for a full list of rules.
|
25
20
|
|
21
|
+
Visit the [capybara-accessible wiki](https://github.com/Casecommons/capybara-accessible/wiki) for background on why and how we built capybara-accessible.
|
26
22
|
|
27
23
|
## Installation
|
28
24
|
|
29
|
-
|
30
|
-
|
25
|
+
Install as usual: `gem install capybara-accessible` or add `gem 'capybara-accessible'` to your Gemfile. See `.travis.yml` for supported (tested) Ruby versions.
|
31
26
|
|
32
27
|
## Usage
|
33
28
|
|
34
|
-
|
35
|
-
|
29
|
+
**Attention: capybara-accessible stop supporting Capybara WebKit since version 0.3.0. You can only use 0.2.1 or pervious versions for Capybara WebKit.**
|
30
|
+
|
31
|
+
You can use capybara-accessible as a drop-in replacement for Rack::Test, Selenium(Firefox & Chrome), or Poltergeist drivers for Capybara. Simply set the driver in `spec/spec_helper.rb` or `features/support/env.rb`:
|
36
32
|
|
37
33
|
require 'capybara/rspec'
|
38
34
|
require 'capybara/accessible'
|
39
35
|
|
40
|
-
# For selenium integration
|
36
|
+
# For selenium firefox integration
|
41
37
|
Capybara.default_driver = :accessible_selenium
|
42
|
-
Capybara.javascript_driver =
|
38
|
+
Capybara.javascript_driver = Capybara.default_driver
|
43
39
|
|
44
|
-
# For
|
45
|
-
Capybara.default_driver = :
|
46
|
-
Capybara.javascript_driver =
|
40
|
+
# For selenium chrome integration
|
41
|
+
Capybara.default_driver = :accessible_selenium_chrome
|
42
|
+
Capybara.javascript_driver = Capybara.default_driver
|
47
43
|
|
48
44
|
# For poltergeist integration
|
49
45
|
Capybara.default_driver = :accessible_poltergeist
|
50
|
-
Capybara.javascript_driver =
|
46
|
+
Capybara.javascript_driver = Capybara.default_driver
|
51
47
|
|
52
48
|
|
53
|
-
We suggest that you use [pry-rescue with pry-stack_explorer](https://github.com/ConradIrwin/pry-rescue)
|
54
|
-
to debug the accessibility failures in the DOM. pry-rescue will open a debugging session at the first exception,
|
55
|
-
pausing the driver so that you can inspect the page.
|
49
|
+
We suggest that you use [pry-rescue with pry-stack_explorer](https://github.com/ConradIrwin/pry-rescue) to debug the accessibility failures in the DOM. pry-rescue will open a debugging session at the first exception, pausing the driver so that you can inspect the page.
|
56
50
|
|
57
51
|
### Disabling audits
|
52
|
+
|
58
53
|
You can disable audits on individual tests by tagging the example or group with `inaccessible`.
|
59
54
|
|
60
55
|
#### Rspec
|
@@ -96,25 +91,23 @@ You can disable audits on individual tests by tagging the example or group with
|
|
96
91
|
When I visit a page that is inaccessible
|
97
92
|
Then I should see the inaccessible image # this assertion will still be executed
|
98
93
|
|
99
|
-
|
100
94
|
### Changing the severity of audit rules
|
101
95
|
|
102
|
-
If you'd like to enforce certain rules and raise errors instead of showing them as warnings,
|
103
|
-
for example images should never have alt attributes, you can configure it as follows:
|
96
|
+
If you'd like to enforce certain rules and raise errors instead of showing them as warnings, for example images should never have alt attributes, you can configure it as follows:
|
104
97
|
|
105
98
|
Capybara::Accessible::Auditor.severe_rules = ['AX_TEXT_02']
|
106
99
|
|
107
|
-
|
108
100
|
## Support
|
109
101
|
|
110
|
-
If you think you've found a bug, or have installation questions or feature requests, please send a message
|
111
|
-
to the [mailing list](https://groups.google.com/forum/#!forum/capybara-accessible).
|
102
|
+
If you think you've found a bug, or have installation questions or feature requests, please send a message to the [mailing list](https://groups.google.com/forum/#!forum/capybara-accessible).
|
112
103
|
|
113
|
-
If you are commenting on the audit rules and failure messages, please check out the Google Accessibility Developer Tools
|
114
|
-
Project, and review their guidelines for reporting issues.
|
104
|
+
If you are commenting on the audit rules and failure messages, please check out the Google Accessibility Developer Tools Project, and review their guidelines for reporting issues.
|
115
105
|
|
116
106
|
## Contributing
|
117
107
|
|
118
|
-
NOTE: axs_testing.js is a generated file from
|
119
|
-
|
120
|
-
|
108
|
+
NOTE: axs_testing.js is a generated file from [Google's Accessibility Developer Tools](https://github.com/GoogleChrome/accessibility-developer-tools). If you'd like to contribute to the audit rules, please fork their Github project.
|
109
|
+
|
110
|
+
## License
|
111
|
+
|
112
|
+
Copyright © 2013–2016 [Case Commons, Inc](http://casecommons.org).
|
113
|
+
Licensed under the MIT license, see [LICENSE](/LICENSE) file.
|
data/Rakefile
CHANGED
@@ -1,11 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require 'bundler/gem_tasks'
|
2
2
|
require 'rspec/core/rake_task'
|
3
3
|
|
4
|
-
|
4
|
+
RSpec::Core::RakeTask.new
|
5
5
|
|
6
|
-
|
7
|
-
t.pattern = "spec/**/*_spec.rb"
|
8
|
-
end
|
9
|
-
|
10
|
-
desc "Default: run all specs"
|
11
|
-
task :default => [:spec]
|
6
|
+
task :default => :spec
|
data/capybara-accessible.gemspec
CHANGED
@@ -1,33 +1,27 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.push File.expand_path('../lib', __FILE__)
|
4
3
|
require 'capybara/accessible/version'
|
5
4
|
|
6
5
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
6
|
+
spec.name = 'capybara-accessible'
|
8
7
|
spec.version = Capybara::Accessible::VERSION
|
9
8
|
spec.authors = ["Case Commons"]
|
10
9
|
spec.email = ["accessibility@casecommons.org"]
|
11
|
-
spec.
|
12
|
-
spec.summary = %q{
|
13
|
-
spec.
|
14
|
-
spec.license =
|
10
|
+
spec.homepage = 'https://github.com/Casecommons/capybara-accessible'
|
11
|
+
spec.summary = %q{Capybara extension and webdriver for automated accessibility testing}
|
12
|
+
spec.description = %q{A Selenium based webdriver and Capybara extension that runs Google Accessibility Developer Tools auditing assertions on page visits.}
|
13
|
+
spec.license = 'MIT'
|
15
14
|
|
16
15
|
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ['lib']
|
17
19
|
|
18
|
-
spec.
|
19
|
-
spec.require_paths = ["lib"]
|
20
|
+
spec.add_dependency 'capybara', '>= 3.12.0'
|
20
21
|
|
21
|
-
spec.
|
22
|
-
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency "rake"
|
27
|
-
spec.add_development_dependency "rspec"
|
28
|
-
spec.add_development_dependency "pry"
|
29
|
-
spec.add_development_dependency "tddium"
|
30
|
-
|
31
|
-
# Sinatra is used by Capybara's TestApp
|
32
|
-
spec.add_development_dependency("sinatra")
|
22
|
+
spec.add_development_dependency 'poltergeist'
|
23
|
+
spec.add_development_dependency 'rake'
|
24
|
+
spec.add_development_dependency 'rspec'
|
25
|
+
spec.add_development_dependency 'selenium-webdriver', '>= 3.14.0'
|
26
|
+
spec.add_development_dependency 'sinatra'
|
33
27
|
end
|
data/lib/capybara/accessible.rb
CHANGED
@@ -1,72 +1,12 @@
|
|
1
1
|
require 'capybara'
|
2
2
|
require 'capybara/accessible/auditor'
|
3
|
-
require 'capybara/accessible/element'
|
4
|
-
require 'capybara/accessible/
|
5
|
-
require
|
6
|
-
require
|
3
|
+
require 'capybara/accessible/extensions/element'
|
4
|
+
require 'capybara/accessible/extensions/driver'
|
5
|
+
require 'capybara/accessible/version'
|
6
|
+
require 'capybara/accessible/railtie' if defined?(Rails)
|
7
7
|
|
8
8
|
module Capybara
|
9
9
|
module Accessible
|
10
|
-
class SeleniumDriverAdapter
|
11
|
-
def modal_dialog_present?(driver)
|
12
|
-
begin
|
13
|
-
driver.browser.switch_to.alert
|
14
|
-
true
|
15
|
-
rescue ::Selenium::WebDriver::Error::NoAlertOpenError, ::NoMethodError
|
16
|
-
false
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def failures_script
|
21
|
-
"return axs.Audit.auditResults(results).getErrors();"
|
22
|
-
end
|
23
|
-
|
24
|
-
def create_report_script
|
25
|
-
"return axs.Audit.createReport(results);"
|
26
|
-
end
|
27
|
-
|
28
|
-
def run_javascript(driver, script)
|
29
|
-
driver.execute_script(script)
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
class WebkitDriverAdapter
|
34
|
-
def modal_dialog_present?(driver)
|
35
|
-
driver.alert_messages.any?
|
36
|
-
end
|
37
|
-
|
38
|
-
def failures_script
|
39
|
-
"axs.Audit.auditResults(results).getErrors();"
|
40
|
-
end
|
41
|
-
|
42
|
-
def create_report_script
|
43
|
-
"axs.Audit.createReport(results);"
|
44
|
-
end
|
45
|
-
|
46
|
-
def run_javascript(driver, script)
|
47
|
-
driver.evaluate_script(script)
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
class PoltergeistDriverAdapter
|
52
|
-
def modal_dialog_present?(driver)
|
53
|
-
false
|
54
|
-
end
|
55
|
-
|
56
|
-
def failures_script
|
57
|
-
"return axs.Audit.auditResults(results).getErrors()"
|
58
|
-
end
|
59
|
-
|
60
|
-
def create_report_script
|
61
|
-
"return axs.Audit.createReport(results)"
|
62
|
-
end
|
63
|
-
|
64
|
-
def run_javascript(driver, script)
|
65
|
-
# Have to wrap in an anonymous function because of https://github.com/jonleighton/poltergeist/issues/198
|
66
|
-
driver.evaluate_script %{ (function() {#{script}})() }
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
10
|
class << self
|
71
11
|
def skip_audit
|
72
12
|
Capybara::Accessible::Auditor.disable
|
@@ -75,13 +15,10 @@ module Capybara
|
|
75
15
|
Capybara::Accessible::Auditor.enable
|
76
16
|
end
|
77
17
|
|
78
|
-
def
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
def setup(driver, adaptor)
|
83
|
-
@driver_adapter = adaptor
|
84
|
-
driver.extend(Capybara::Accessible::DriverExtensions)
|
18
|
+
def create_driver(base_driver, driver_adaptor, app, **options)
|
19
|
+
driver_class = Capybara::Accessible::Extensions::Driver.wrap(base_driver)
|
20
|
+
driver = driver_class.new(app, **options)
|
21
|
+
driver.accessible = driver_adaptor.new
|
85
22
|
driver
|
86
23
|
end
|
87
24
|
end
|
@@ -90,26 +27,38 @@ end
|
|
90
27
|
|
91
28
|
Capybara.register_driver :accessible do |app|
|
92
29
|
puts "DEPRECATED: Please register this driver as 'accessible_selenium'"
|
93
|
-
|
94
|
-
|
95
|
-
|
30
|
+
require 'capybara/accessible/adapters/selenium'
|
31
|
+
Capybara::Accessible.create_driver(
|
32
|
+
Capybara::Selenium::Driver,
|
33
|
+
Capybara::Accessible::Adapters::Selenium,
|
34
|
+
app,
|
35
|
+
)
|
96
36
|
end
|
97
37
|
|
98
|
-
|
99
38
|
Capybara.register_driver :accessible_selenium do |app|
|
100
|
-
|
101
|
-
|
102
|
-
|
39
|
+
require 'capybara/accessible/adapters/selenium'
|
40
|
+
Capybara::Accessible.create_driver(
|
41
|
+
Capybara::Selenium::Driver,
|
42
|
+
Capybara::Accessible::Adapters::Selenium,
|
43
|
+
app,
|
44
|
+
)
|
103
45
|
end
|
104
46
|
|
105
|
-
Capybara.register_driver :
|
106
|
-
|
107
|
-
|
108
|
-
|
47
|
+
Capybara.register_driver :accessible_selenium_chrome do |app|
|
48
|
+
require 'capybara/accessible/adapters/selenium'
|
49
|
+
Capybara::Accessible.create_driver(
|
50
|
+
Capybara::Selenium::Driver,
|
51
|
+
Capybara::Accessible::Adapters::Selenium,
|
52
|
+
app,
|
53
|
+
:browser => :chrome
|
54
|
+
)
|
109
55
|
end
|
110
56
|
|
111
57
|
Capybara.register_driver :accessible_poltergeist do |app|
|
112
|
-
|
113
|
-
|
114
|
-
|
58
|
+
require 'capybara/accessible/adapters/poltergeist'
|
59
|
+
Capybara::Accessible.create_driver(
|
60
|
+
Capybara::Poltergeist::Driver,
|
61
|
+
Capybara::Accessible::Adapters::Poltergeist,
|
62
|
+
app,
|
63
|
+
)
|
115
64
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Capybara::Accessible
|
2
|
+
module Adapters
|
3
|
+
class Poltergeist
|
4
|
+
def failures_script
|
5
|
+
'return axs.Audit.auditResults(results).getErrors()'
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_report_script
|
9
|
+
'return axs.Audit.createReport(results)'
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_javascript(driver, script)
|
13
|
+
# Have to wrap in an anonymous function because of https://github.com/jonleighton/poltergeist/issues/198
|
14
|
+
driver.evaluate_script %{ (function() {#{script}})() }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Capybara::Accessible
|
2
|
+
module Adapters
|
3
|
+
class Selenium
|
4
|
+
def failures_script
|
5
|
+
'return axs.Audit.auditResults(results).getErrors();'
|
6
|
+
end
|
7
|
+
|
8
|
+
def create_report_script
|
9
|
+
'return axs.Audit.createReport(results);'
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_javascript(driver, script)
|
13
|
+
driver.execute_script(script)
|
14
|
+
rescue ::Selenium::WebDriver::Error::UnhandledAlertError
|
15
|
+
puts 'Skipping accessibility audit: Modal dialog present'
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|