{"id":8602,"date":"2021-07-28T11:44:59","date_gmt":"2021-07-28T15:44:59","guid":{"rendered":"https:\/\/blogs.library.duke.edu\/bitstreams\/?p=8602"},"modified":"2021-07-29T23:48:50","modified_gmt":"2021-07-30T03:48:50","slug":"automated-accessibility-testing-and-continuous-integration","status":"publish","type":"post","link":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/","title":{"rendered":"Automated Accessibility Testing and Continuous Integration"},"content":{"rendered":"<p>We use <a href=\"https:\/\/blogs.library.duke.edu\/bitstreams\/2019\/05\/10\/web-accessibility-values-and-vigilance\/\">several different tools and strategies<\/a> at the Duke Libraries to ensure that our web interfaces are accessible. Our aim is to<a href=\"https:\/\/web.accessibility.duke.edu\/duke-guidelines\"> comply with WCAG 2.0 AA and Section 508<\/a> guidelines.<\/p>\n<p>One of our favorite accessibility checking tools in our toolbox is the <a href=\"https:\/\/www.deque.com\/axe\/browser-extensions\/\">axe DevTools Browser Extension<\/a> by Deque Systems. It&#8217;s easy to use, whether on a live site, or in our own local development environments while we&#8217;re working on building new features for our applications. Simply open your browser&#8217;s Developer Tools (<code>F12<\/code>), click the <code>Scan<\/code> button, and get an instant report of any violations, complete with recommendations about how to fix them.<\/p>\n<figure id=\"attachment_8603\" aria-describedby=\"caption-attachment-8603\" style=\"width: 2450px\" class=\"wp-caption aligncenter\"><a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-8603\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM.png\" alt=\"screenshot of axe DevTools browser extension in action\" width=\"2450\" height=\"1484\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM.png 2450w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM-300x182.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM-1024x620.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM-768x465.png 768w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM-1536x930.png 1536w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-10.42.35-AM-2048x1241.png 2048w\" sizes=\"auto, (max-width: 2450px) 100vw, 2450px\" \/><\/a><figcaption id=\"caption-attachment-8603\" class=\"wp-caption-text\">An axe DevTools test result for our archival finding aids homepage.<\/figcaption><\/figure>\n<p>Now, it&#8217;s one thing to make software compliant; it&#8217;s another to keep it that way. Duke&#8217;s <a href=\"https:\/\/web.accessibility.duke.edu\/get-started\/self-sufficiency\">Web Accessibility office astutely notes<\/a>:<\/p>\n<blockquote><p>Keeping a website compliant is a continuous effort. Websites are living things. Content changes, features are added. When a site becomes compliant it does not stay compliant.<\/p><\/blockquote>\n<p>One of the goals we set out to accomplish in 2021 was to figure out how to add <em>automated, continuous\u00a0accessibility testing<\/em> for our <a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\">ArcLight software<\/a>, which powers our <a href=\"https:\/\/archives.lib.duke.edu\/\">archival finding aids<\/a> search and discovery application. We got it implemented successfully a few months ago and we&#8217;re pleased with how it has been working so far.<\/p>\n<p>The solution: using the Deque Systems <strong><a href=\"https:\/\/github.com\/dequelabs\/axe-core-gems\/blob\/develop\/packages\/axe-core-rspec\/README.md\">Axe Core RSpec gem<\/a><\/strong>, along with a <a href=\"https:\/\/github.com\/SeleniumHQ\/docker-selenium#standalone\">Selenium Standalone Chrome<\/a> <a href=\"https:\/\/hub.docker.com\/r\/selenium\/standalone-chrome\">Docker image<\/a>, in our GitLab CI pipeline.<\/p>\n<p><em>Say what?<\/em><\/p>\n<p>Let me back up a bit and give some background on the concepts and tools upon which this solution depends. Namely: Continuous Integration, RSpec, Capybara, Selenium, and Docker.<\/p>\n<h2>Continuous Integration<\/h2>\n<p>For any given software project, we may have several developers making multiple changes to a shared codebase in the same day. <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/introduction\/index.html#continuous-integration\">Continuous Integration<\/a> is the practice of ensuring that these code changes 1) don&#8217;t break any existing functionality, and 2) comply with established guidelines for code quality.<\/p>\n<p>At Duke Libraries, we use <a href=\"https:\/\/about.gitlab.com\/\">GitLab<\/a> to host code repositories for most of our software (e.g., our <a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\">ArcLight application<\/a>). It&#8217;s <a href=\"https:\/\/about.gitlab.com\/devops-tools\/github-vs-gitlab\/\">similar to GitHub, with built-in tooling for DevOps<\/a>. One such tool is <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/\">GitLab CI\/CD<\/a>, which we use to manage continuous integration. It&#8217;s common for us to <a href=\"https:\/\/docs.gitlab.com\/ee\/ci\/pipelines\/\">configure pipelines<\/a> to run automated tasks for each new code branch or tag, in at least few stages, e.g.: <code>build<\/code> &gt; <code>test<\/code>\u00a0&gt; <code>deploy<\/code>.<\/p>\n<figure id=\"attachment_8606\" aria-describedby=\"caption-attachment-8606\" style=\"width: 899px\" class=\"wp-caption aligncenter\"><a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-1.03.27-PM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-8606\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-1.03.27-PM.png\" alt=\"screenshot of GitLab CI pipeline stages\" width=\"899\" height=\"777\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-1.03.27-PM.png 899w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-1.03.27-PM-300x259.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-26-at-1.03.27-PM-768x664.png 768w\" sizes=\"auto, (max-width: 899px) 100vw, 899px\" \/><\/a><figcaption id=\"caption-attachment-8606\" class=\"wp-caption-text\">Evidence of a GitLab CI pipeline that has run &#8212; and passed all stages &#8212; for a code change.<\/figcaption><\/figure>\n<p>For accessibility testing, we knew we needed to add something to our existing CI pipeline that would check for compliance and flag any issues.<\/p>\n<h2>RSpec Testing Framework<\/h2>\n<p>Many of our applications are built using Ruby on Rails. We write tests for our code using <a href=\"https:\/\/rspec.info\/\">RSpec<\/a>, a popular testing framework for Rails applications. Developers write tests (using the framework&#8217;s DSL \/ domain-specific language) to accompany their code changes. Those tests all execute as part of our CI pipeline (see above). Any failing tests will prevent code from being merged or getting deployed to production.<\/p>\n<p><a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/rspec-logo.jpeg\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-8605\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/rspec-logo.jpeg\" alt=\"RSpec logo\" width=\"291\" height=\"115\" \/><\/a><\/p>\n<p>There are <a href=\"https:\/\/thoughtbot.com\/blog\/rails-test-types-and-the-testing-pyramid\">many different types of tests<\/a>.\u00a0 On one end of the spectrum, there are &#8220;unit tests,&#8221; which verify that one small piece of code (e.g., one method) returns what we expect it to when it is given different inputs. On the other, there are &#8220;feature tests,&#8221; which typically verify that several pieces of code are working together as intended in different conditions. This often simulates the use of a feature by a person (e.g., when a user clicks this button, test that they get to this page and verify this link gets rendered, etc.). Feature tests might alternatively be called &#8220;integration tests,&#8221; &#8220;acceptance tests,&#8221; or even &#8220;system tests&#8221; &#8212; the terminology is both squishy and evolving.<\/p>\n<p>At any rate, accessibility testing is a specific kind of feature test.<\/p>\n<h2>Capybara<\/h2>\n<p>On its own, RSpec unfortunately doesn&#8217;t natively support feature tests. It requires a companion piece of software called <a href=\"http:\/\/teamcapybara.github.io\/capybara\/\">Capybara<\/a>, which can simulate a user interacting with a web interface. Capybara brings with it a DSL to visit pages, fill out forms, or click on elements within RSpec tests, and special matchers to check that the page is behaving as intended.<\/p>\n<figure id=\"attachment_8610\" aria-describedby=\"caption-attachment-8610\" style=\"width: 1306px\" class=\"wp-caption aligncenter\"><a href=\"http:\/\/teamcapybara.github.io\/capybara\/\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-8610\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/capybara-homepage.png\" alt=\"screenshot of Capybara homepage\" width=\"1306\" height=\"813\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/capybara-homepage.png 1306w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/capybara-homepage-300x187.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/capybara-homepage-1024x637.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/capybara-homepage-768x478.png 768w\" sizes=\"auto, (max-width: 1306px) 100vw, 1306px\" \/><\/a><figcaption id=\"caption-attachment-8610\" class=\"wp-caption-text\">Homepage for Capybara, featuring an actual capybara.<\/figcaption><\/figure>\n<p>When configuring Capybara, you <a href=\"https:\/\/github.com\/teamcapybara\/capybara#drivers\">set up the driver<\/a> you want it to use when running different kinds of tests. Its default driver is <code>RackTest<\/code>, which is fast but it can&#8217;t execute JavaScript like a real web browser can. Our <a href=\"http:\/\/We use several different tools and strategies at the Duke Libraries to ensure that our web interfaces are accessible. Our aim is to comply with WCAG 2.0 AA and Section 508 guidelines. One of our favorite accessibility checking tools in our toolbox is the axe DevTools Browser Extension by Deque Systems. It's easy to use, whether on a live site, or in our own local development environments while we're working on building new features for our applications. Click the scan button and get an instant report of any violations, complete with the steps to take in order to fix them. An axe DevTools test result for our archival finding aids homepage. Now, it's one thing to make software compliant; it's another to keep it that way. Duke's Web Accessibility office astutely notes: Keeping a website compliant is a continuous effort. Websites are living things. Content changes, features are added. When a site becomes compliant it does not stay compliant. One of the goals we set out to accomplish in 2021 was to figure out how to add automated, continuous\u00a0accessibility testing for our ArcLight software, which powers our archival finding aids search and discovery application. We got it implemented successfully a few months ago and we're pleased with how it has been working so far. The solution: using the Deque Systems Axe Core RSpec gem, along with a Selenium Standalone Chrome Docker image, in our GitLab CI pipeline. Say what? Let me back up a bit and give some background on the concepts and tools upon which this solution depends. Namely: Continuous Integration, RSpec, Capybara, Selenium, and Docker. Continuous Integration For any given software project, we may have several developers making multiple changes to a shared codebase in the same day. Continuous Integration is the practice of ensuring that these code changes 1) don't break any existing functionality, and 2) comply with established guidelines for code quality. At Duke Libraries, we use GitLab to host code repositories for most of our software (e.g., our ArcLight application). It's similar to GitHub, but with better built-in tooling for DevOps. One such tool is GitLab CI\/CD, which we use to manage continuous integration. It's common for us to configure pipelines to run automated tasks for each new code branch or tag, in at least few stages, e.g.: build &gt; test\u00a0&gt; deploy. Evidence of a GitLab CI pipeline that has run -- and passed all stages -- for a code change. For accessibility testing, we knew we needed to add something to our existing CI pipeline that would check for compliance and flag any issues. RSpec Testing Framework Many of our applications are built using Ruby on Rails. We write tests for our code using RSpec, a popular testing framework for Rails applications. Developers write tests (using the framework's DSL \/ domain-specific language) to accompany their code changes. Those tests all execute as part of our CI pipeline (see above). Any failing tests will prevent code from being merged or getting deployed to production. There are many different types of tests.\u00a0 On one end of the spectrum, there are &quot;unit tests,&quot; which verify that one small piece of code (e.g., one method) returns what we expect it to when it is given different inputs. On the other, there are &quot;feature tests,&quot; which typically verify that several pieces of code are working together as intended in different conditions. This often simulates the use of a feature by a person (e.g., when a user clicks this button, test that they get to this page and verify this link gets rendered, etc.). Feature tests might alternatively be called &quot;integration tests,&quot; &quot;acceptance tests,&quot; or even &quot;system tests&quot; -- the terminology is both squishy and evolving. At any rate, accessibility testing is a specific kind of feature test. Capybara On its own, RSpec unfortunately doesn't natively support feature tests. It requires a companion piece of software called Capybara, which can simulate a user interacting with a web interface. Capybara brings with it a DSL to visit pages, fill out forms, or click on elements within RSpec tests, and special matchers to check that the page is behaving as intended. Homepage for Capybara, featuring an actual capybara. When configuring Capybara, you set up the driver you want it to use when running different kinds of tests. Its default driver is RackTest, which is fast but it can't execute JavaScript like a real web browser can. Our ArcLight UI, for instance, uses a bunch of JavaScript. So we knew that any accessibility tests would have to be performed using a driver for an actual browser; the default Capybara alone wouldn't cut it. Selenium Writing code to drive various browsers was probably a nightmare until Selenium came along. Selenium is an &quot;umbrella project for a range of tools and libraries that enable and support the automation of web browsers.&quot; Its WebDriver platform gives you a language-agnostic coding interface that is now compatible with all the major web browsers. That makes it a valuable component in automated browser testing. Documentation for Selenium WebDriver for browser automation The best way we could find to get Capybara to control a real browser in an RSpec test was to add\u00a0the selenium-webdriver gem to our project's Gemfile. Docker Over the past few years, we have evolved our DevOps practice and embraced containerizing our applications using Docker. Complex applications that have a lot of interwoven infrastructure dependencies used to be quite onerous to build, run, and share.\u00a0 Getting one's local development environment into shape to successfully run an application used be a whole-day affair. Worse still, the infrastructure on the production server used to bear little resemblance to what developers were working with on their local machines. A systems diagram depicting the various services that run to support our ArcLight app. Docker helps a dev team configure in their codebase all of these system dependencies in a way that that's easily reproducible in any environment. It builds a network of &quot;containers&quot; on a single host, each running a service that's crucial to the application.\u00a0 Now a developer can simply check out the code, run a couple commands, wait a few minutes, and they're good to go. The same basic setup also applies in a production environment (with a few easily-configurable differences). And that simplicity also carries over to the CI environment where the test suite will run. Orange boxes depict each service \/ container we have defined in our Docker configuration. So what we needed to do was add another container to our existing Docker configuration that would be dedicated to running any JavaScript-dependent feature tests -- including accessibility tests -- in a browser controlled by Selenium WebDriver. Keeping this part containerized would hopefully ensure that when a developer runs the tests in their local environment, the exact same browser version and drivers get used in the CI pipeline. We can steer clear of any &quot;well, it worked on my machine&quot; issues. Putting it All Together Phew. OK, with all of that out of the way, let's look closer at how we put all of these puzzle pieces together. Gems We had to add the following three gems to our Gemfile's test group: group :test do \u00a0gem 'axe-core-rspec' # accessibility testing \u00a0gem 'capybara' \u00a0gem 'selenium-webdriver' end A Docker Container for Selenium The folks at Selenium HQ host &quot;standalone&quot; browser Docker images in Docker Hub, complete with the browser software and accompanying drivers. We found a tagged version of their Standalone Chrome image that worked well, and pull that into our newly-defined &quot;selenium&quot; container for our test environments. In docker-compose.test.yml services: selenium: image: selenium\/standalone-chrome:3.141.59-xenon ports: - 4444:4444 volumes: - \/dev\/shm:\/dev\/shm environment: - JAVA_OPTS=-Dwebdriver.chrome.whitelistedIps= - START_XVFB=false [...] Since this is new territory for us, and already fairly complex, we're starting with just one browser: Chrome. We may be able to add more in the future. Capybara Driver Configuration Next thing we needed to do was tell Capybara that whenever it encounters any javascript-dependent feature tests, it should run them in our standalone Chrome container (using a &quot;remote&quot; driver).\u00a0 The Selenium WebDriver gem lets us set some options for how we want Chrome to run. The key setting here is &quot;headless&quot; -- that is, run Chrome, but more efficiently, without all the fancy GUI stuff that a real user might see. In spec_helper.rb Capybara.javascript_driver = :selenium_remote Capybara.register_driver :selenium_remote do |app| capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( chromeOptions: { args: [ 'headless', 'no-sandbox', 'disable-gpu', 'disable-infobars', 'window-size=1400,1000', 'enable-features=NetworkService,NetworkServiceInProcess' ] } ) Capybara::Selenium::Driver.new(app, browser: :remote, desired_capabilities: capabilities, url: 'http:\/\/selenium:4444\/wd\/hub') end That last URL http:\/\/selenium:4444\/wd\/hub is the location of our Chrome driver within our selenium container. There are a few other important Capybara settings configured in spec_helper.rb that are needed in order to get our app and seleniumcontainers to play nicely together. Capybara.server = :puma, { Threads: '1:1' } Capybara.server_port = '3002' Capybara.server_host = '0.0.0.0' Capybara.app_host = &quot;http:\/\/app:#{Capybara.server_port}&quot; Capybara.always_include_port = true Capybara.default_max_wait_time = 30 # our ajax responses are sometimes slow Capybara.enable_aria_label = true [...] The server_port, server_host and app_host variables are the keys here. Basically, we're saying: Capybara (which runs in our app container) should start up Puma to run the test app, listening on http:\/\/0.0.0.0:3002\u00a0for requests beyond the current host during a test. The selenium container (where the Chrome browser resides) should access the application under test at http:\/\/app:3002 (since it\u2019s in the app container). Some Actual RSpec Accessibility Tests Here's the fun part, where we actually get to write the accessibility tests. The axe-core-rspec gem makes it a breeze. The be_axe_clean matcher ensures that if we have a WCAG 2.0 AA or Section 508 violation, it'll trip the wire and report a failing test. In accessibility_spec.rb require 'spec_helper' require 'axe-rspec' RSpec.describe 'Accessibility (WCAG, 508, Best Practices)', type: :feature, js: true, accessibility: true do describe 'homepage' do it 'is accessible' do visit '\/' expect(page).to be_axe_clean end end [...] end With type: :feature and js: true we signal to RSpec that this block of tests should be handled by Capybara and must be run in our headless Chrome Selenium container. The example above is the simplest case: visit the homepage and do an Axe check. We also make sure to test several different kinds of pages, and with some different variations on UI interactions. E.g., test after clicking to open the Advanced Search modal. The CI Pipeline We started out having accessibility tests run along with all our other RSpec tests during the test stage in our GitLab CI pipeline. But we eventually determined it was better to keep accessibility tests isolated in a separate job, one that would not block a code merge or deployment in the event of a failure. We use the accessibility: true tag in\u00a0our RSpec accessibility test blocks (see the above example) to distinguish them from other feature tests. No, we don't condone pushing inaccessible code to production! It's just that we sometimes get false positives --violations are reported where there are none -- particularly in our most Javascript-heavy pages. There are likely some timing issues there that we'll work to refine with more configuration. Output from a successful automated accessibility test job run in a GitLab CI pipeline. Here's a completed job in our CI pipeline logs where the accessibility tests all passed, see especially lines 395 - end. Concluding Thoughts Automation and accessibility testing are both rapidly evolving areas, and the setup that's working for our ArcLight app today might look considerably different within the next several months. Still, I thought it'd be useful to pause and reflect on the steps we took to get automated accessibility testing up and running. This strategy would be reasonably reproducible for many other applications we support. A lot of what I have outlined could also be accomplished with variations in tooling. Don't use GitLab CI? No problem -- just substitute your own CI platform. The most important takeaways are: Accessibility testing is important to do, continually Continuous integration helpfully automates testing that used to be manual Containerizing helps streamline continuous integration You can run automated browser-based tests in a ready-made container Capybara and Selenium\u00a0 Deque Systems provides useful open source Axe software that plugs into your test framework\u00a0 Many thanks to David Chandek-Stark (Duke) for architecting a large portion of this work. Thanks also to Simon Choy (Duke), Dann Bohn (Penn St.), and Adam Wead (Penn St.) for their assistance helping us troubleshoot and understand how these pieces fit together.\">ArcLight UI<\/a>, for instance, uses a bunch of JavaScript. So we knew that any accessibility tests would have to be performed using a driver for an actual browser; the default Capybara alone wouldn&#8217;t cut it.<\/p>\n<h2>Selenium<\/h2>\n<p>Writing code to drive various browsers was probably a nightmare until Selenium came along. Selenium is an &#8220;<a href=\"https:\/\/github.com\/SeleniumHQ\/selenium#readme\">umbrella project for a range of tools and libraries that enable and support the automation of web browsers<\/a>.&#8221; Its <a href=\"https:\/\/www.selenium.dev\/documentation\/en\/webdriver\/\">WebDriver<\/a> platform gives you a language-agnostic coding interface that is now compatible with all the major web browsers. That makes it a valuable component in automated browser testing.<\/p>\n<figure id=\"attachment_8611\" aria-describedby=\"caption-attachment-8611\" style=\"width: 1325px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.selenium.dev\/documentation\/en\/webdriver\/\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-8611\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/selenium-webdriver-docs.png\" alt=\"screenshot of Selenium WebDriver website\" width=\"1325\" height=\"667\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/selenium-webdriver-docs.png 1325w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/selenium-webdriver-docs-300x151.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/selenium-webdriver-docs-1024x515.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/selenium-webdriver-docs-768x387.png 768w\" sizes=\"auto, (max-width: 1325px) 100vw, 1325px\" \/><\/a><figcaption id=\"caption-attachment-8611\" class=\"wp-caption-text\">Documentation for Selenium WebDriver for browser automation<\/figcaption><\/figure>\n<p>The best way we could find to get Capybara to control a real browser in an RSpec test was to add\u00a0the <a href=\"https:\/\/rubygems.org\/gems\/selenium-webdriver\/\">selenium-webdriver<\/a> gem to our project&#8217;s <code>Gemfile<\/code>.<\/p>\n<h2>Docker<\/h2>\n<p>Over the past few years, we have evolved our <a href=\"https:\/\/en.wikipedia.org\/wiki\/DevOps\">DevOps<\/a> practice and embraced containerizing our applications using <a href=\"https:\/\/www.docker.com\/why-docker\">Docker<\/a>. Complex applications that have a lot of interwoven infrastructure dependencies used to be quite onerous to build, run, and share.\u00a0 Getting one&#8217;s local development environment into shape to successfully run an application used be a whole-day affair. Worse still, the infrastructure on the production server used to bear little resemblance to what developers were working with on their local machines.<\/p>\n<figure id=\"attachment_8619\" aria-describedby=\"caption-attachment-8619\" style=\"width: 1792px\" class=\"wp-caption aligncenter\"><a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.27.59-AM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-8619\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.27.59-AM.png\" alt=\"systems diagram for ArcLight infrastructure\" width=\"1792\" height=\"1010\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.27.59-AM.png 1792w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.27.59-AM-300x169.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.27.59-AM-1024x577.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.27.59-AM-768x433.png 768w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.27.59-AM-1536x866.png 1536w\" sizes=\"auto, (max-width: 1792px) 100vw, 1792px\" \/><\/a><figcaption id=\"caption-attachment-8619\" class=\"wp-caption-text\">A systems diagram depicting the various services that run to support our ArcLight app.<\/figcaption><\/figure>\n<p>Docker helps a dev team configure in their codebase all of these system dependencies in a way that that&#8217;s easily reproducible in any environment. It builds a network of &#8220;containers&#8221; on a single host, each running a service that&#8217;s crucial to the application.\u00a0 Now a developer can simply check out the code, <a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/tree\/develop\/.docker\">run a couple commands<\/a>, wait a few minutes, and they&#8217;re good to go.<\/p>\n<p>The same basic setup also applies in a production environment (with a few easily-configurable differences). And that simplicity also carries over to the CI environment where the test suite will run.<\/p>\n<figure id=\"attachment_8620\" aria-describedby=\"caption-attachment-8620\" style=\"width: 1788px\" class=\"wp-caption aligncenter\"><a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.37.48-AM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-8620\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.37.48-AM.png\" alt=\"Systems diagram for ArcLight, highlighting containerized components of the infrastructure\" width=\"1788\" height=\"1008\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.37.48-AM.png 1788w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.37.48-AM-300x169.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.37.48-AM-1024x577.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.37.48-AM-768x433.png 768w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-28-at-8.37.48-AM-1536x866.png 1536w\" sizes=\"auto, (max-width: 1788px) 100vw, 1788px\" \/><\/a><figcaption id=\"caption-attachment-8620\" class=\"wp-caption-text\">Orange boxes depict each service \/ container we have defined in our Docker configuration.<\/figcaption><\/figure>\n<p>So what we needed to do was add another container to our existing Docker configuration that would be dedicated to running any JavaScript-dependent feature tests &#8212; <em>including accessibility tests<\/em> &#8212; in a browser controlled by Selenium WebDriver.<\/p>\n<p>Keeping this part containerized would hopefully ensure that when a developer runs the tests in their local environment, the exact same browser version and drivers get used in the CI pipeline. We can steer clear of any &#8220;well, it worked on <em>my<\/em> machine&#8221; issues.<\/p>\n<h2>Putting it All Together<\/h2>\n<p>Phew. OK, with all of that background out of the way, let&#8217;s look closer at how we put all of these puzzle pieces together.<\/p>\n<h3>Gems<\/h3>\n<p>We had to add the following three gems to our <a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/blob\/develop\/Gemfile#L46-50\"><strong>Gemfile<\/strong>&#8216;s test group<\/a>:<\/p>\n<pre>group :test do\r\n \u00a0gem '<a href=\"https:\/\/rubygems.org\/gems\/axe-core-rspec\">axe-core-rspec<\/a>' # accessibility testing\r\n \u00a0gem '<a href=\"https:\/\/rubygems.org\/gems\/capybara\">capybara<\/a>'\r\n \u00a0gem '<a href=\"https:\/\/rubygems.org\/gems\/selenium-webdriver\">selenium-webdriver<\/a>'\r\nend<\/pre>\n<h3>A Docker Container for Selenium<\/h3>\n<p>The folks at Selenium HQ host <a href=\"https:\/\/github.com\/SeleniumHQ\/docker-selenium#standalone\">&#8220;standalone&#8221; browser Docker images<\/a> in <a href=\"https:\/\/hub.docker.com\/\">Docker Hub<\/a>, complete with the browser software and accompanying drivers. We found a tagged version of their Standalone Chrome image that worked well, and pull that into our newly-defined &#8220;<code>selenium<\/code>&#8221; container for our test environments.<\/p>\n<p>In <strong><a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/blob\/develop\/.docker\/docker-compose.test.yml#L4-19\">docker-compose.test.yml<\/a><\/strong><\/p>\n<pre class=\"code highlight\" lang=\"yaml\"><span id=\"LC3\" class=\"line hll\" lang=\"yaml\"><span class=\"na\">services<\/span><span class=\"pi\">:<\/span><\/span>\r\n<span id=\"LC4\" class=\"line hll\" lang=\"yaml\">  <span class=\"na\">selenium<\/span><span class=\"pi\">:<\/span><\/span>\r\n<strong><span id=\"LC9\" class=\"line hll\" lang=\"yaml\">    <span class=\"na\">image<\/span><span class=\"pi\">:<\/span> <span class=\"s\">selenium\/standalone-chrome:3.141.59-xenon<\/span><\/span><\/strong>\r\n<span id=\"LC10\" class=\"line hll\" lang=\"yaml\">    <span class=\"na\">ports<\/span><span class=\"pi\">:<\/span><\/span>\r\n<span id=\"LC11\" class=\"line hll\" lang=\"yaml\">      <span class=\"pi\">-<\/span> <span class=\"s\">4444:4444<\/span><\/span>\r\n<span id=\"LC12\" class=\"line hll\" lang=\"yaml\">    <span class=\"na\">volumes<\/span><span class=\"pi\">:<\/span><\/span>\r\n<span id=\"LC13\" class=\"line hll\" lang=\"yaml\">      <span class=\"pi\">-<\/span> <span class=\"s\">\/dev\/shm:\/dev\/shm<\/span><\/span>\r\n<span id=\"LC14\" class=\"line hll\" lang=\"yaml\">    <span class=\"na\">environment<\/span><span class=\"pi\">:<\/span><\/span>\r\n<span id=\"LC16\" class=\"line hll\" lang=\"yaml\">      <span class=\"pi\">-<\/span> <span class=\"s\">JAVA_OPTS=-Dwebdriver.chrome.whitelistedIps=<\/span><\/span>\r\n<span id=\"LC18\" class=\"line hll\" lang=\"yaml\">      <span class=\"pi\">-<\/span> <span class=\"s\">START_XVFB=false\r\n<\/span><\/span>[...]<\/pre>\n<p>Since this is new territory for us, and already fairly complex, we&#8217;re starting with just one browser: Chrome. We may be able to add more in the future.<\/p>\n<h3>Capybara Driver Configuration<\/h3>\n<p>Next thing we needed to do was tell Capybara that whenever it encounters any javascript-dependent feature tests, it should run them in our standalone Chrome container (using a &#8220;remote&#8221; driver).\u00a0 The Selenium WebDriver gem lets us set some options for how we want Chrome to run.<\/p>\n<p>The key setting here is &#8220;<code>headless<\/code>&#8221; &#8212; that is, run Chrome, but more efficiently, <a href=\"https:\/\/developers.google.com\/web\/updates\/2017\/04\/headless-chrome\">without all the fancy GUI stuff<\/a> that a real user might see.<\/p>\n<p>In <strong><a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/blob\/develop\/spec\/spec_helper.rb#L16-37\">spec_helper.rb<\/a><\/strong><\/p>\n<pre class=\"code highlight\" lang=\"ruby\"><span id=\"LC16\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">javascript_driver<\/span> <span class=\"o\">=<\/span> <span class=\"ss\">:selenium_remote<\/span><\/span>\r\n<span id=\"LC17\" class=\"line hll\" lang=\"ruby\"><\/span>\r\n<span id=\"LC18\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">register_driver<\/span> <span class=\"ss\">:selenium_remote<\/span> <span class=\"k\">do<\/span> <span class=\"o\">|<\/span><span class=\"n\">app<\/span><span class=\"o\">|<\/span><\/span>\r\n<span id=\"LC19\" class=\"line hll\" lang=\"ruby\">  <span class=\"n\">capabilities<\/span> <span class=\"o\">=<\/span> <span class=\"no\">Selenium<\/span><span class=\"o\">::<\/span><span class=\"no\">WebDriver<\/span><span class=\"o\">::<\/span><span class=\"no\">Remote<\/span><span class=\"o\">::<\/span><span class=\"no\">Capabilities<\/span><span class=\"p\">.<\/span><span class=\"nf\">chrome<\/span><span class=\"p\">(<\/span><\/span>\r\n<span id=\"LC23\" class=\"line hll\" lang=\"ruby\">    <span class=\"ss\">chromeOptions: <\/span><span class=\"p\">{<\/span> <span class=\"ss\">args: <\/span><span class=\"p\">[<\/span><\/span>\r\n<span id=\"LC24\" class=\"line hll\" lang=\"ruby\">      <span class=\"s1\">'<strong>headless<\/strong>'<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC25\" class=\"line hll\" lang=\"ruby\">      <span class=\"s1\">'no-sandbox'<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC26\" class=\"line hll\" lang=\"ruby\">      <span class=\"s1\">'disable-gpu'<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC27\" class=\"line hll\" lang=\"ruby\">      <span class=\"s1\">'disable-infobars'<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC28\" class=\"line hll\" lang=\"ruby\">      <span class=\"s1\">'window-size=1400,1000'<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC29\" class=\"line hll\" lang=\"ruby\">      <span class=\"s1\">'enable-features=NetworkService,NetworkServiceInProcess'<\/span><\/span>\r\n<span id=\"LC30\" class=\"line hll\" lang=\"ruby\">    <span class=\"p\">]<\/span> <span class=\"p\">}<\/span><\/span>\r\n<span id=\"LC31\" class=\"line hll\" lang=\"ruby\">  <span class=\"p\">)<\/span><\/span>\r\n<span id=\"LC32\" class=\"line hll\" lang=\"ruby\"><\/span>\r\n<span id=\"LC33\" class=\"line hll\" lang=\"ruby\">  <span class=\"no\">Capybara<\/span><span class=\"o\">::<\/span><span class=\"no\">Selenium<\/span><span class=\"o\">::<\/span><span class=\"no\">Driver<\/span><span class=\"p\">.<\/span><span class=\"nf\">new<\/span><span class=\"p\">(<\/span><span class=\"n\">app<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC34\" class=\"line hll\" lang=\"ruby\">                                 <span class=\"ss\">browser: :remote<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC35\" class=\"line hll\" lang=\"ruby\">                                 <span class=\"ss\">desired_capabilities: <\/span><span class=\"n\">capabilities<\/span><span class=\"p\">,<\/span><\/span>\r\n<span id=\"LC36\" class=\"line hll\" lang=\"ruby\">                                 <span class=\"ss\">url: <\/span><span class=\"s1\">'<strong>http:\/\/selenium:4444\/wd\/hub<\/strong>'<\/span><span class=\"p\">)<\/span><\/span>\r\n<span id=\"LC37\" class=\"line hll\" lang=\"ruby\"><span class=\"k\">end<\/span><\/span>\r\n<\/pre>\n<p>That last URL <code>http:\/\/selenium:4444\/wd\/hub<\/code> is the location of our Chrome driver within our selenium container.<\/p>\n<p>There are a few other important Capybara settings configured in <strong><a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/blob\/develop\/spec\/spec_helper.rb#L39-50\">spec_helper.rb<\/a> <\/strong>that are needed in order to get our <code>app<\/code> and <code>selenium<\/code>containers to play nicely together.<\/p>\n<pre class=\"code highlight\" lang=\"ruby\"><span id=\"LC41\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">server<\/span> <span class=\"o\">=<\/span> <span class=\"ss\">:puma<\/span><span class=\"p\">,<\/span> <span class=\"p\">{<\/span> <span class=\"no\">Threads<\/span><span class=\"p\">:<\/span> <span class=\"s1\">'1:1'<\/span> <span class=\"p\">}<\/span><\/span>\r\n<span id=\"LC43\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">server_port<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">'<strong>3002<\/strong>'<\/span><\/span>\r\n<span id=\"LC44\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">server_host<\/span> <span class=\"o\">=<\/span> <span class=\"s1\">'<strong>0.0.0.0<\/strong>'<\/span><\/span>\r\n<span id=\"LC45\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">app_host<\/span> <span class=\"o\">=<\/span> <span class=\"s2\">\"<strong>http:\/\/app:<\/strong><\/span><strong><span class=\"si\">#{<\/span><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">server_port<\/span><span class=\"si\">}<\/span><\/strong><span class=\"s2\">\"<\/span><\/span>\r\n<span id=\"LC47\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">always_include_port<\/span> <span class=\"o\">=<\/span> <span class=\"kp\">true<\/span><\/span>\r\n<span id=\"LC48\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">default_max_wait_time<\/span> <span class=\"o\">=<\/span> <span class=\"mi\">30<\/span> <span class=\"c1\"># our ajax responses are sometimes slow<\/span><\/span>\r\n<span id=\"LC50\" class=\"line hll\" lang=\"ruby\"><span class=\"no\">Capybara<\/span><span class=\"p\">.<\/span><span class=\"nf\">enable_aria_label<\/span> <span class=\"o\">=<\/span> <span class=\"kp\">true\r\n[...]<\/span><\/span>\r\n<\/pre>\n<p>The <code>server_port<\/code>, <code>server_host<\/code> and <code>app_host<\/code> variables are the keys here. Basically, we&#8217;re saying:<\/p>\n<ul>\n<li>\n<div>Capybara (which runs in our <code>app<\/code> container) should start up Puma to run the test app, listening on <a href=\"tcp:\/\/0.0.0.0:3002\">http:\/\/0.0.0.0:3002<\/a>\u00a0for requests beyond the current host during a test.<\/div>\n<\/li>\n<li>\n<div>The <code>selenium<\/code> container (where the Chrome browser resides) should access the application under test at <a href=\"http:\/\/app:3002\/\">http:\/\/app:3002<\/a> (since it\u2019s in the <code>app<\/code> container).<\/div>\n<\/li>\n<\/ul>\n<h3>Some Actual RSpec Accessibility Tests<\/h3>\n<p>Here&#8217;s the fun part, where we actually get to write the accessibility tests. The <a href=\"https:\/\/github.com\/dequelabs\/axe-core-gems\/blob\/develop\/packages\/axe-core-rspec\/README.md\"><strong>axe-core-rspec<\/strong><\/a> gem makes it a breeze. The <code>be_axe_clean<\/code> matcher ensures that if we have a WCAG 2.0 AA or Section 508 violation, it&#8217;ll trip the wire and report a failing test.<\/p>\n<p>In <strong><a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/blob\/develop\/spec\/features\/accessibility_spec.rb\">accessibility_spec.rb<\/a><\/strong><\/p>\n<pre class=\"code highlight\" lang=\"ruby\"><span id=\"LC17\" class=\"line\" lang=\"ruby\"><span class=\"nb\">require<\/span> <span class=\"s1\">'spec_helper'<\/span><\/span>\r\n<span id=\"LC18\" class=\"line\" lang=\"ruby\"><span class=\"nb\">require<\/span> <span class=\"s1\">'axe-rspec'<\/span><\/span>\r\n<span id=\"LC19\" class=\"line\" lang=\"ruby\"><\/span>\r\n<span id=\"LC20\" class=\"line\" lang=\"ruby\"><span class=\"no\">RSpec<\/span><span class=\"p\">.<\/span><span class=\"nf\">describe<\/span> <span class=\"s1\">'Accessibility (WCAG, 508, Best Practices)'<\/span><span class=\"p\">,<\/span> <strong><span class=\"ss\">type: :feature<\/span><\/strong><span class=\"p\">,<\/span> <strong><span class=\"ss\">js: <\/span><span class=\"kp\">true<\/span><\/strong><span class=\"p\">,<\/span> <strong><span class=\"ss\">accessibility: <\/span><span class=\"kp\">true<\/span><\/strong> <span class=\"k\">do<\/span><\/span>\r\n<span id=\"LC21\" class=\"line\" lang=\"ruby\">  <span class=\"n\">describe<\/span> <span class=\"s1\">'homepage'<\/span> <span class=\"k\">do<\/span><\/span>\r\n<span id=\"LC22\" class=\"line\" lang=\"ruby\">    <span class=\"n\">it<\/span> <span class=\"s1\">'is accessible'<\/span> <span class=\"k\">do<\/span><\/span>\r\n<span id=\"LC23\" class=\"line\" lang=\"ruby\">      <span class=\"n\">visit<\/span> <span class=\"s1\">'\/'<\/span><\/span>\r\n<span id=\"LC24\" class=\"line\" lang=\"ruby\">      <span class=\"n\">expect<\/span><span class=\"p\">(<\/span><span class=\"n\">page<\/span><span class=\"p\">).<\/span><span class=\"nf\">to<\/span> <strong><span class=\"n\">be_axe_clean<\/span><\/strong><\/span>\r\n<span id=\"LC25\" class=\"line\" lang=\"ruby\">    <span class=\"k\">end<\/span><\/span>\r\n<span id=\"LC26\" class=\"line\" lang=\"ruby\">  <span class=\"k\">end<\/span><\/span>\r\n\r\n  [...]\r\nend<\/pre>\n<p>With <code>type: :feature<\/code> and <code>js: true<\/code> we signal to RSpec that this block of tests should be handled by Capybara and must be run in our headless Chrome Selenium container.<\/p>\n<p>The example above is the simplest case: 1) visit the homepage and 2) do an Axe check. We also make sure to test several different kinds of pages, and with some different variations on UI interactions. E.g., test after clicking to open the Advanced Search modal.<\/p>\n<h3>The CI Pipeline<\/h3>\n<p>We started out having accessibility tests run along with all our other RSpec tests during the <code>test<\/code> stage in our GitLab CI pipeline. But we eventually determined it was better to <a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/blob\/develop\/.gitlab-ci.yml#L84-90\">keep accessibility tests isolated in a separate job<\/a>, one that would not block a code merge or deployment in the event of a failure. We use the <span id=\"LC20\" class=\"line\" lang=\"ruby\"><span class=\"kp\"><code>accessibility: true<\/code> tag in\u00a0our RSpec accessibility test blocks (<em>see the above example<\/em>) to distinguish them from other feature tests.<\/span><\/span><\/p>\n<p>No, we don&#8217;t condone pushing inaccessible code to production! It&#8217;s just that we sometimes get false positives &#8212; violations are reported where there are none &#8212; particularly in Javascript-heavy pages. There are likely some timing issues there that we&#8217;ll work to refine with more configuration.<\/p>\n<h3>A Successful Accessibility Test<\/h3>\n<p>Here&#8217;s a completed job in our CI pipeline logs where the accessibility tests all passed:<\/p>\n<p><a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-11.18.05-PM.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-8615\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-11.18.05-PM.png\" alt=\"Screenshot of passing CI pipeline, including accessibility\" width=\"1560\" height=\"258\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-11.18.05-PM.png 1560w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-11.18.05-PM-300x50.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-11.18.05-PM-1024x169.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-11.18.05-PM-768x127.png 768w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-11.18.05-PM-1536x254.png 1536w\" sizes=\"auto, (max-width: 1560px) 100vw, 1560px\" \/><\/a><\/p>\n<figure id=\"attachment_8614\" aria-describedby=\"caption-attachment-8614\" style=\"width: 2878px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/gitlab.oit.duke.edu\/dul-its\/dul-arclight\/-\/jobs\/750622\"><img loading=\"lazy\" decoding=\"async\" class=\"size-full wp-image-8614\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-10.48.36-PM.png\" alt=\"Screenshot from successful accessibility test job in GitLab CI\" width=\"2878\" height=\"1484\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-10.48.36-PM.png 2878w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-10.48.36-PM-300x155.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-10.48.36-PM-1024x528.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-10.48.36-PM-768x396.png 768w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-10.48.36-PM-1536x792.png 1536w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/Screen-Shot-2021-07-27-at-10.48.36-PM-2048x1056.png 2048w\" sizes=\"auto, (max-width: 2878px) 100vw, 2878px\" \/><\/a><figcaption id=\"caption-attachment-8614\" class=\"wp-caption-text\">Output from a successful automated accessibility test job run in a GitLab CI pipeline.<\/figcaption><\/figure>\n<p>Our GitLab CI logs are not publicly available, so here&#8217;s a <a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/gitlab-ci-accessibility-test-success.pdf\">brief snippet<\/a> from a successful test.<\/p>\n<h3>An Accessibility Test With Failures<\/h3>\n<p><a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/accessibility-test-failure.png\"><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-full wp-image-8631\" src=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/accessibility-test-failure.png\" alt=\"Screenshot displaying a failed accessibility test job\" width=\"1660\" height=\"418\" srcset=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/accessibility-test-failure.png 1660w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/accessibility-test-failure-300x76.png 300w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/accessibility-test-failure-1024x258.png 1024w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/accessibility-test-failure-768x193.png 768w, https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/accessibility-test-failure-1536x387.png 1536w\" sizes=\"auto, (max-width: 1660px) 100vw, 1660px\" \/><\/a>Here&#8217;s a CI pipeline for a code branch that adds two buttons to the homepage with color contrast and <code>aria-label<\/code> violations. The axe tests flag the issues as <code>FAILED<\/code> and recommend revisions (<a href=\"http:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/gitlab-ci-accessibility-test-failure.pdf\">see snippet from the logs<\/a>).<\/p>\n<h3>Concluding Thoughts<\/h3>\n<p>Automation and accessibility testing are both rapidly evolving areas, and the setup that&#8217;s working for our ArcLight app today might look considerably different within the next several months. Still, I thought it&#8217;d be useful to pause and reflect on the steps we took to get automated accessibility testing up and running. This strategy would be reasonably reproducible for many other applications we support.<\/p>\n<p>A lot of what I have outlined could also be accomplished with variations in tooling. Don&#8217;t use GitLab CI? No problem &#8212; just substitute your own CI platform. The five most important takeaways here are:<\/p>\n<ol>\n<li>Accessibility testing is important to do, <em>continually<\/em><\/li>\n<li>Use continuous integration to automate testing that used to be manual<\/li>\n<li>Containerizing helps streamline continuous integration, including testing<\/li>\n<li>You can run automated browser-based tests in a ready-made container<\/li>\n<li>Deque&#8217;s open source Axe testing tools are easy to use and pluggable into your existing test framework<\/li>\n<\/ol>\n<hr \/>\n<p><em>Many thanks to David Chandek-Stark (Duke) for architecting a large portion of this work. Thanks also to Simon Choy (Duke), Dann Bohn (Penn St.), and Adam Wead (Penn St.) for their assistance helping us troubleshoot and understand how these pieces fit together.<\/em><\/p>\n<hr \/>\n<p><em>The banner image in this post uses three icons from the <a href=\"https:\/\/fontawesome.com\/v5\/changelog\/latest\">FontAwesome Free 5<\/a> icon set, unchanged, and licensed under a <a href=\"https:\/\/creativecommons.org\/licenses\/by\/4.0\/\">CC-BY 4.0 license<\/a>.<\/em><\/p>\n<hr \/>\n<p><em>REVISION 7\/29\/21: This post was updated, adding links to snippets from CI logs that demonstrate successful accessibility tests vs. those that reveal violations.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>We use several different tools and strategies at the Duke Libraries to ensure that our web interfaces are accessible. Our aim is to comply with WCAG 2.0 AA and Section 508 guidelines. One of our favorite accessibility checking tools in our toolbox is the axe DevTools Browser Extension by Deque Systems. It&#8217;s easy to use, &hellip; <a href=\"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">Automated Accessibility Testing and Continuous Integration<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":103,"featured_media":8625,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12,38],"tags":[],"class_list":["post-8602","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tech","category-user-experience"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.2 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Automated Accessibility Testing and Continuous Integration - Bitstreams: The Digital Collections Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Automated Accessibility Testing and Continuous Integration - Bitstreams: The Digital Collections Blog\" \/>\n<meta property=\"og:description\" content=\"We use several different tools and strategies at the Duke Libraries to ensure that our web interfaces are accessible. Our aim is to comply with WCAG 2.0 AA and Section 508 guidelines. One of our favorite accessibility checking tools in our toolbox is the axe DevTools Browser Extension by Deque Systems. It&#8217;s easy to use, &hellip; Continue reading Automated Accessibility Testing and Continuous Integration &rarr;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/\" \/>\n<meta property=\"og:site_name\" content=\"Bitstreams: The Digital Collections Blog\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/dukedigitalcoll\" \/>\n<meta property=\"article:published_time\" content=\"2021-07-28T15:44:59+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2021-07-30T03:48:50+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/automated-accessibility-checks-2.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1600\" \/>\n\t<meta property=\"og:image:height\" content=\"978\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Sean Aery\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@dukedigitalcoll\" \/>\n<meta name=\"twitter:site\" content=\"@dukedigitalcoll\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Sean Aery\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"12 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/\"},\"author\":{\"name\":\"Sean Aery\",\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/#\\\/schema\\\/person\\\/b1cdd20f3c85d271cfb87386a1dd28fe\"},\"headline\":\"Automated Accessibility Testing and Continuous Integration\",\"datePublished\":\"2021-07-28T15:44:59+00:00\",\"dateModified\":\"2021-07-30T03:48:50+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/\"},\"wordCount\":2114,\"commentCount\":0,\"image\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/files\\\/2021\\\/07\\\/automated-accessibility-checks-2.png\",\"articleSection\":[\"Technology\",\"User Experience\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/\",\"url\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/\",\"name\":\"Automated Accessibility Testing and Continuous Integration - Bitstreams: The Digital Collections Blog\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/files\\\/2021\\\/07\\\/automated-accessibility-checks-2.png\",\"datePublished\":\"2021-07-28T15:44:59+00:00\",\"dateModified\":\"2021-07-30T03:48:50+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/#\\\/schema\\\/person\\\/b1cdd20f3c85d271cfb87386a1dd28fe\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#primaryimage\",\"url\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/files\\\/2021\\\/07\\\/automated-accessibility-checks-2.png\",\"contentUrl\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/files\\\/2021\\\/07\\\/automated-accessibility-checks-2.png\",\"width\":1600,\"height\":978},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/2021\\\/07\\\/28\\\/automated-accessibility-testing-and-continuous-integration\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Automated Accessibility Testing and Continuous Integration\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/#website\",\"url\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/\",\"name\":\"Bitstreams: The Digital Collections Blog\",\"description\":\"Notes from the Duke University Libraries Digital Projects Team\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/#\\\/schema\\\/person\\\/b1cdd20f3c85d271cfb87386a1dd28fe\",\"name\":\"Sean Aery\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/de490821e24542e00ba40994017cf2414ed2510c15138b471561acc5fdff47f6?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/de490821e24542e00ba40994017cf2414ed2510c15138b471561acc5fdff47f6?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/de490821e24542e00ba40994017cf2414ed2510c15138b471561acc5fdff47f6?s=96&d=mm&r=g\",\"caption\":\"Sean Aery\"},\"url\":\"https:\\\/\\\/blogs.library.duke.edu\\\/bitstreams\\\/author\\\/seanaeryduke-edu\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Automated Accessibility Testing and Continuous Integration - Bitstreams: The Digital Collections Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/","og_locale":"en_US","og_type":"article","og_title":"Automated Accessibility Testing and Continuous Integration - Bitstreams: The Digital Collections Blog","og_description":"We use several different tools and strategies at the Duke Libraries to ensure that our web interfaces are accessible. Our aim is to comply with WCAG 2.0 AA and Section 508 guidelines. One of our favorite accessibility checking tools in our toolbox is the axe DevTools Browser Extension by Deque Systems. It&#8217;s easy to use, &hellip; Continue reading Automated Accessibility Testing and Continuous Integration &rarr;","og_url":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/","og_site_name":"Bitstreams: The Digital Collections Blog","article_publisher":"https:\/\/www.facebook.com\/dukedigitalcoll","article_published_time":"2021-07-28T15:44:59+00:00","article_modified_time":"2021-07-30T03:48:50+00:00","og_image":[{"width":1600,"height":978,"url":"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/automated-accessibility-checks-2.png","type":"image\/png"}],"author":"Sean Aery","twitter_card":"summary_large_image","twitter_creator":"@dukedigitalcoll","twitter_site":"@dukedigitalcoll","twitter_misc":{"Written by":"Sean Aery","Est. reading time":"12 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#article","isPartOf":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/"},"author":{"name":"Sean Aery","@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/#\/schema\/person\/b1cdd20f3c85d271cfb87386a1dd28fe"},"headline":"Automated Accessibility Testing and Continuous Integration","datePublished":"2021-07-28T15:44:59+00:00","dateModified":"2021-07-30T03:48:50+00:00","mainEntityOfPage":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/"},"wordCount":2114,"commentCount":0,"image":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#primaryimage"},"thumbnailUrl":"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/automated-accessibility-checks-2.png","articleSection":["Technology","User Experience"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/","url":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/","name":"Automated Accessibility Testing and Continuous Integration - Bitstreams: The Digital Collections Blog","isPartOf":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/#website"},"primaryImageOfPage":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#primaryimage"},"image":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#primaryimage"},"thumbnailUrl":"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/automated-accessibility-checks-2.png","datePublished":"2021-07-28T15:44:59+00:00","dateModified":"2021-07-30T03:48:50+00:00","author":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/#\/schema\/person\/b1cdd20f3c85d271cfb87386a1dd28fe"},"breadcrumb":{"@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#primaryimage","url":"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/automated-accessibility-checks-2.png","contentUrl":"https:\/\/blogs.library.duke.edu\/bitstreams\/files\/2021\/07\/automated-accessibility-checks-2.png","width":1600,"height":978},{"@type":"BreadcrumbList","@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/2021\/07\/28\/automated-accessibility-testing-and-continuous-integration\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/blogs.library.duke.edu\/bitstreams\/"},{"@type":"ListItem","position":2,"name":"Automated Accessibility Testing and Continuous Integration"}]},{"@type":"WebSite","@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/#website","url":"https:\/\/blogs.library.duke.edu\/bitstreams\/","name":"Bitstreams: The Digital Collections Blog","description":"Notes from the Duke University Libraries Digital Projects Team","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/blogs.library.duke.edu\/bitstreams\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/blogs.library.duke.edu\/bitstreams\/#\/schema\/person\/b1cdd20f3c85d271cfb87386a1dd28fe","name":"Sean Aery","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/de490821e24542e00ba40994017cf2414ed2510c15138b471561acc5fdff47f6?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/de490821e24542e00ba40994017cf2414ed2510c15138b471561acc5fdff47f6?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/de490821e24542e00ba40994017cf2414ed2510c15138b471561acc5fdff47f6?s=96&d=mm&r=g","caption":"Sean Aery"},"url":"https:\/\/blogs.library.duke.edu\/bitstreams\/author\/seanaeryduke-edu\/"}]}},"_links":{"self":[{"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/posts\/8602","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/users\/103"}],"replies":[{"embeddable":true,"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/comments?post=8602"}],"version-history":[{"count":18,"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/posts\/8602\/revisions"}],"predecessor-version":[{"id":8634,"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/posts\/8602\/revisions\/8634"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/media\/8625"}],"wp:attachment":[{"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/media?parent=8602"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/categories?post=8602"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blogs.library.duke.edu\/bitstreams\/wp-json\/wp\/v2\/tags?post=8602"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}