Automatizar pruebas de accesibilidad con axe-core y Azure DevOps

Si bien es cierto que sólo entre el 20 y el 50 por cierto de los problemas de accesibilidad pueden ser detectados de manera automática, en la mayoría de los sitios ni siquiera a estos les estamos poniendo solución, y me incluyo en este saco. Es por ello que en este artículo quiero contarte cómo he automatizado lo que sí podemos detectar con la herramienta axe-core y Azure DevOps con el único objetivo de mejorar, de lo que sí está en nuestras manos, la vida de aquellas personas que seguro que lo agradecerán.

¿Qué es axe-core?

Existen diferentes herramientas que nos pueden ayudar en esta tarea. En algunas compañías utilizan Lighthouse, la cual no es específica de accesibilidad (por lo que aporta poca información), accesibility-checker de bbc, que lleva tres años sin actualizarse, o incluso algunas herramientas que podemos utilizar de forma manual como WebAIM Contrast Check o auditorias de consultoras externas que hace el trabajo sea un poco más largo en el tiempo, de tal manera que arreglarlo nos supone un sobreesfuerzo adicional. El objetivo es que esto sea parte de nuestro día a día.
Axe es una herramienta de Deque considerada la más completa hoy en día para detectar todo lo que algunas de las anteriores solo hacen parcialmente. Es posible utilizarla de manera programática, a través de su módulo de Node.js, o bien a través de la línea de comandos, que es lo que he utilizado para esta automatización con Azure DevOps. El tipo de resultados que devuelve es como el siguiente:

Resultados de una ejecución de axe-core
Resultados de una ejecución de axe-core

Uno de los inconvenientes de utilizar Axe a través de la línea de comandos es que deberías ir pasándole todas aquellas URLs que tienes interés en analizar. Es por ello que he montado un par de pipelines que automatizan el trabajo por nosotros.

Pipelines de Azure DevOps

El primer ejemplo es el siguiente:

trigger:
  branches:
    include:
      - main
  paths:
    include:
      - axe-core-with-sitemap.yml 
jobs:
  - job:
    timeoutInMinutes: 240
    pool:
      vmImage: ubuntu-latest
    steps:
        - task: [email protected]
          displayName: Update Chrome
          inputs:
            targetType: 'inline'
            script: |
              google-chrome --version              
              sudo apt-get install -y libxml2-utils
              wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
              sudo dpkg -i google-chrome*.deb  # Might show "errors", fixed by next line
              sudo apt-get install -f -y
              google-chrome --version
        - task: [email protected]
          displayName: Install Node.js
          inputs:
            versionSpec: '14.x'
            checkLatest: true
        - task: [email protected]
          displayName: Execute axe-core
          inputs:
            targetType: 'inline'
            script: |          
              npm i @axe-core/[email protected] -g
              
              curl -s $(SITEMAP) | xmllint --format - | grep "<loc>" | awk -F"<loc>" '{print $2} ' | awk -F"</loc>" '{print $1}' > urls.txt
              echo $(wc -l urls.txt)
              mkdir -p $(Build.SourcesDirectory)/results              
              # loop every line in urls.txt file
              while read p; do
                echo "$p"
                axe $p --show-errors --timer --dir results
              done <urls.txt
              for file in results/*.json; do
                echo $file                
                npx axe-sarif-converter --input-files $file --output-file $file.sarif
              done            
        - task: [email protected]
          displayName: publish SARIF results
          inputs:
            # The exact name "CodeAnalysisLogs" is required for the Sarif Results Viewer Extension for Azure Pipelines https://marketplace.visualstudio.com/items?itemName=sariftools.scans
            # to find the .sarif files our accessibility test produces.
            artifactName: "CodeAnalysisLogs"            
            pathtoPublish: "$(Build.SourcesDirectory)/results"
          condition: succeededOrFailed()    

En este caso utilizo el archivo sitemap.xml, pasado como la variable $(SITEMAP) a la pipeline, para que recupere todas las URLs que encuentre en esta y utilice la herramienta por todas ellas. Antes de hacer este proceso actualizo a la última versión el navegador Chrome, que es el que utiliza por defecto, ya que puede ocurrir que el agente no la tenga, instalo Node.js (por si estás utilizando un agente gestionado por ti) y también la librería libxml2-utils, de la cuál utilizo el comando xmllint para formatear el sitemap, en el caso de que no venga de esta manera, ya que sino la expresión con awk no me funcionará para obtener todas las URLs y guardarlas en un archivo llamado urls.txt. Una vez que tengo todo esto, itero este archivo y paso la herramienta. En este ejemplo, para no tener que instalar versiones anteriores de Chrome he instalado @axe-core/[email protected], que próximamente será la más reciente estable.

Una vez que ya tengo todos los resultados, estos están en formato JSON, pero me gustaría visualizarlos de forma sencilla a través de Azure DevOps, por lo que he utilizado un módulo desarrollado por Microsoft llamado axe-sarif-converter, que justamente me va a permitir convertir estos de JSON a SARIF. ¿Por qué hago esto? Porque hay una extensión llamada SARIF SAST Scans Tab que si alojamos todos estos archivos en un artefacto llamado CodeAnalysisLogs será capaz de pintarlos como resultado en una pestaña que llamará Scans. Si echamos un vistazo a una ejecución ya finalizada el resultado sería como el siguiente:

Resultados de axe-core en Azure DevOps con al extensión SARIF SAST Scans Tab
Resultados de axe-core en Azure DevOps

Dependiendo de cuántas URLs sean necesarias escanear, junto con el tiempo de carga de las mismas, el tamaño que tengan, etcétera el proceso puede ser más o menos largo. Si es mayor a una hora necesitarás usar un agente hospedado por ti para que no de time out, ya que puedes regular este (en el ejemplo está configurado a 240 minutos). Además, si se generan demasiados resultados es posible que la extensión no sea capaz de pintarlos todos y de un error de carga. Para que te hagas una idea: 182 URLs me han tardado 18 minutos y carga los resultados sin problemas, 353 URLs 31 minutos y carga los resultados sin problemas, 832 URLs 2 horas y 6 minutos y tenía problemas a la hora de cargar los resultados. Es por ello que otra alternativa podría ser, en lugar de usar el sitemap, elegir qué URLs queremos analizar. La pipeline cambiaría ligeramente a esta:

trigger:
  branches:
    include:
      - main
  paths:
    include:
      - axe-core-with-list-of-urls.yml 
jobs:
  - job:
    timeoutInMinutes: 90
    pool:
      name: default      
    steps:
        - task: [email protected]
          displayName: Update Chrome
          inputs:
            targetType: 'inline'
            script: |
              google-chrome --version
              wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
              sudo dpkg -i google-chrome*.deb  # Might show "errors", fixed by next line
              sudo apt-get install -f -y
              google-chrome --version
        - task: [email protected]
          displayName: Install Node.js
          inputs:
            versionSpec: '14.x'
            checkLatest: true
        - task: [email protected]
          displayName: Execute axe
          inputs:
            targetType: 'inline'
            script: |          
              npm i @axe-core/[email protected] -g
              echo "https://URL_1" >> urls.txt
              echo "https://URL_2" >> urls.txt
              echo "https://URL_3" >> urls.txt
              echo "https://URL_4" >> urls.txt
              echo "https://URL_5" >> urls.txt
              echo "https://URL_6" >> urls.txt              
              mkdir -p $(Build.SourcesDirectory)/results              
              # loop every line in urls.txt file
              while read p; do
                echo "$p"
                axe $p --show-errors --timer --dir results
              done <urls.txt
              for file in results/*.json; do
                echo $file                
                npx axe-sarif-converter --input-files $file --output-file $file.sarif
              done
            
        - task: [email protected]
          displayName: publish SARIF results
          inputs:
            # The exact name "CodeAnalysisLogs" is required for the Sarif Results Viewer Extension for Azure Pipelines https://marketplace.visualstudio.com/items?itemName=sariftools.scans
            # to find the .sarif files our accessibility test produces.
            artifactName: "CodeAnalysisLogs"
            pathtoPublish: "$(Build.SourcesDirectory)/results"
          condition: succeededOrFailed()    

Por supuesto, el archivo urls.txt podría generado previamente y añadido como parte del código fuente en lugar de crearlo durante la pipeline, pero para que te hagas una idea 😃

¡Saludos!