Skip to content

Instantly share code, notes, and snippets.

@Lissy93
Last active November 11, 2019 18:15
Show Gist options
  • Save Lissy93/b50d6cf7d641c8532d8696523898eb9b to your computer and use it in GitHub Desktop.
Save Lissy93/b50d6cf7d641c8532d8696523898eb9b to your computer and use it in GitHub Desktop.
An automated testing script, that checks for accessibility issues. This file can just be dropped into your Protractor, Chai and Cucumber test environment.
// Copyright Alicia Sykes <https://aliciasykes.com>. Licensed under MIT X11: https://git.io/Jew4i
import { binding, when } from "cucumber-tsflow";
import { browser, by, element, protractor } from "protractor";
import { expect } from "chai";
import { Config } from "../utils/config";
import { DomUtil } from "../utils/domUtil";
@binding()
class AccessibilitySteps {
private arrayOfFails = [];
private config = new Config();
private domUtil = new DomUtil();
private seoKey = "seoKey";
private pointer = 0;
private accessibilityArrayTags = [];
/**
* The Start function, called from the Cucumber script
* This function calls all the other accessibility tests
* @param callback
*/
@when(/^I check for accessibility and SEO health$/)
runAllAccessibilityTests (callback) {
this.iTestAltTags().then(() => {
return this.iTestForBackgroundImages();
})
.then(() => {
return this.iTestTitleAttributes();
})
.then(() => {
return this.iTestTitleAttributesOnBUttons();
})
.then(() => {
return this.iTestTitleTag();
})
.then(() => {
return this.iTestForExactlyOneTag("h1");
})
.then(() => {
return this.iTestForLabelsInFormElements();
})
.then(() => {
if (this.arrayOfFails.length > 0){
browser.getCurrentUrl().then((theUrl) => {
let combined = this.config.get(this.seoKey);
combined += "\n" + this.formatErrorArr(this.arrayOfFails, theUrl);
this.config.set(this.seoKey, combined);
this.arrayOfFails = [];
});
}
}).then(
callback
);
}
/**
* this function takes in a string as a parameter of comma seperated values
* checking "header" "footer" "main" "nav" "kf-section"
* it is on the tester to provided the parameters for the specific page
*/
@when(/^I check "([^"]*)" tags appear$/)
testTagsAppear(arg1: string, callback)
{
let tagsString = arg1;
this.accessibilityArrayTags = tagsString.split(",");
this.pointer = 0;
for ( let i = 0; i < this.accessibilityArrayTags.length; i++){
this.accessibilityArrayTags[i] = this.accessibilityArrayTags[i].replace(/ /g, "");
}
this.iterateThroughAccessibilityTagsArray(callback);
}
iterateThroughAccessibilityTagsArray(callback){
if (this.pointer === this.accessibilityArrayTags.length){
this.accessibilityArrayTags = [];
callback();
}
switch (this.accessibilityArrayTags[this.pointer]){
case "header":
this.iTestForNumTagsInARangeForIteration("header", 0, 2, callback);
break;
case "footer":
this.iTestForNumTagsInARangeForIteration("footer", 0, 2, callback);
break;
case "nav":
this.iTestForNumTagsInARangeForIteration("nav", 0, 2, callback);
break;
case "kf-section":
this.iTestForMoreThanXtagsForIteration("kf-section", 0 , callback);
break;
case "main":
this.iTestForNumTagsInARangeForIteration("main", 0, 2, callback);
break;
default:
}
}
iTestForNumTagsInARangeForIteration(tag, lower, upper, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.above(lower).and.below(upper);
this.pointer++;
}).then(() => {this.iterateThroughAccessibilityTagsArray(callback); });
}
iTestForMoreThanXtagsForIteration(tag, lower, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.above(lower);
this.pointer++;
}).then(() => {this.iterateThroughAccessibilityTagsArray(callback); });
}
iTestForLessThanXtagsForIteration(tag, upper, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.below(upper);
this.pointer++;
}).then(() => {this.iterateThroughAccessibilityTagsArray(callback); });
}
iTestForZeroOccurencesForIteration(tag, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.equal(0);
this.pointer++;
}).then(() => {this.iterateThroughAccessibilityTagsArray(callback); });
}
@when(/^I check "([^"]*)" tags appear more than (\d+) times and less than (\d+) times$/)
iTestIfTheNumberOfTagsAreInARange(arg1, arg2, arg3, callback){
this.iTestForNumTagsInARange(arg1, arg2, arg3, callback);
}
@when(/^I check the "([^"]*)" tag appears exactly one time$/)
iTestIfTheTagAppearsExactlyOnce(arg1, callback){
this.iTestForNumTagsInARange(arg1, 0, 2, callback);
}
@when(/^I check the "([^"]*)" tag appears more than (\d+) times$/)
iTestIfTheTagMoreThan(arg1, arg2, callback){
this.iTestForMoreThanXtags(arg1, arg2, callback);
}
@when(/^I check the "([^"]*)" tag appears less than (\d+) times$/)
iTestIfTheTagLessThan(arg1, arg2, callback){
this.iTestForLessThanXtags(arg1, arg2, callback);
}
@when(/^I check the "([^"]*)" tag does not appears on the page$/)
iTestIfTheTagAppearsZeroTimes(arg1, callback){
this.iTestForZeroOccurences(arg1, callback);
}
@when(/^I check the "([^"]*)" tag appears on the page$/)
iTestIfTheTagIsOnThePage(arg1, callback){
this.iTestForMoreThanXtags(arg1, 0, callback);
}
/*
* Check for the occurences of a tag
*/
iTestForNumTagsInARange(tag, lower, upper, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.above(lower).and.below(upper);
}).then(callback);
}
iTestForMoreThanXtags(tag, lower, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.above(lower);
}).then(callback);
}
iTestForLessThanXtags(tag, upper, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.below(upper);
}).then(callback);
}
iTestForZeroOccurences(tag, callback): void{
let elements = element.all(by.css(tag));
elements.count().then((count) => {
expect(count).to.be.equal(0);
}).then(callback);
}
/**
* This function checks if a page has a <main> element
* stub for now
* @param callback
*/
@when(/^I check for a main element$/)
checkMainElement (callback) {
// expect(myElement.isPresent('main')).toBeTruthy();
}
/**
* The Start function, called from the Cucumber script
* This function calls all the other accessibility tests
* @param callback
*/
@when(/^I check for site SEO health$/)
runSiteSEOTests(callback) {
this.iTestSiteMap().then(() => {
return this.iTestRobots();
}).then(callback);
}
/**
* Checks if a sitemap exists
*/
private iTestSiteMap(): Promise<void> {
return this.domUtil.doesPageExist("sitemap.xml").then((success) => {
if (!success) {
console.warn("Sitemap doesnt exist");
}
});
}
/**
* Checks if a robot.txt exists
*/
private iTestRobots(): Promise<void> {
return this.domUtil.doesPageExist("robot.txt").then((success) => {
if (!success) {
console.warn("Robots doesnt exist");
}
});
}
/**
* Generates a string containing the list of accessibility errors
* and the URL of the page they occurred on to print out to developer
* @param errorsArr
* @param url
* @returns {string}
*/
private formatErrorArr(errorsArr, url){
const numErrs = errorsArr.length;
let errors =
`\n${numErrs} ACCESSIBILITY FAIL${numErrs !== 1 ? "s" : ""} ` +
`found on Page: '${url}':\n${Array(60).join("-")}\n`;
errorsArr.forEach((errorObject, index) => {
errors += `(${index + 1}) ${errorObject}\n`;
});
errors += `${Array(60).join("-")}\n`;
return errors;
}
/**
* Reusable function for checking attributes on elements
* Determines if ever element (of type elem)
* contains the attribute (of type attr)
* @param elem
* @param attr
* @param failMsg
* @returns {Promise<T>}
*/
private checkAttrOnElement(elem, attr, failMsg){
let defer = protractor.promise.defer(); // to be deferred, and returned
element.all(by.css(elem)).map( (element) => {
return element.getAttribute(attr);
}).then((results) => {
results.forEach((altValue) => {
if (altValue === ""){
this.arrayOfFails.push(failMsg);
}
});
defer.fulfill();
});
return defer.promise;
}
/**
* Function should ensure that all img elements have
* a valid (and not empty or missing) alt attribute
* @returns {Promise<T>}
*/
iTestAltTags () {
return this.checkAttrOnElement("img", "alt", "Missing Alt Attribute on Image");
}
/**
* Tests that none of the images have been done
* with divs with background-image property
* @returns {Promise<T>}
*/
iTestForBackgroundImages (){
let defer = protractor.promise.defer();
let promises = [];
element.all(by.css("div")).getCssValue("background-image").then((killerImg) => {
for (let i = 0; i < killerImg.length; ++i) {
promises.push(killerImg[i]);
}
});
protractor.promise.all(promises).then(() => {
promises.forEach((backgroundProperty) => {
if (backgroundProperty !== "none") {
let evilImg = backgroundProperty.substr(backgroundProperty.lastIndexOf("/") + 1).replace(/[^\w.]/gi, "");
this.arrayOfFails.push(`Images should not be defined using background-img (${evilImg})`);
}
});
defer.fulfill();
});
return defer.promise;
}
/**
* Tests that all a tags have a title
* @returns {Promise<T>}
*/
iTestTitleAttributes () {
return this.checkAttrOnElement("a", "title", "Missing title Attribute on Anchor tag");
}
/**
* Tests that all a tags have a title
* @returns {Promise<T>}
*/
iTestTitleAttributesOnBUttons () {
return this.checkAttrOnElement("button", "title", "Missing title Attribute on Button");
}
/**
* Function should ensure that the page has a
* title element, with valid contents
*/
iTestTitleTag () {
let defer = protractor.promise.defer();
let elements = element.all(by.css("title"));
// Check for empty title
elements.count().then((count) => {
if (count < 1) {
this.arrayOfFails.push("Missing title tag");
defer.fulfill();
}
});
// Check for URL title (fallback for some browsers)
browser.getTitle().then((browserTitle) => {
if (browserTitle.match(/^s?https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#]+$/i)){
this.arrayOfFails.push("Browser title must not be a URL");
}
defer.fulfill();
});
return defer.promise;
}
@when(/^I check title is set to "([^"]*)"$/)
iCheckTheTitleIsSetToTheCorrectValue(arg1, callback) {
expect(browser.getTitle()).to.eventually.equal(arg1).notify(callback);
}
/*
* check that a exactly one tag for x tag exist,
*
*/
iTestForExactlyOneTag(arg1) {
let defer = protractor.promise.defer();
let elements = element.all(by.css(arg1));
elements.count().then((count) => {
if (count < 1) {
this.arrayOfFails.push("Missing " + arg1 + "tag");
defer.fulfill();
}
else if (count > 1){
this.arrayOfFails.push("Too Many " + arg1 + " tag");
defer.fulfill();
}
else{
defer.fulfill();
}
});
return defer.promise;
}
/**
* Tests that headings are correctly nested
* e.g. H2's must be under a H1, H3's must
* be under a H2, and so on...
*/
iTestSubHeadings () {
// Hmmmmmm o.O TODO determine exactly how this should be checked (see file for options)
}
/**
* Tests that every form input has a label
* element associated with it
*/
iTestForLabelsInFormElements () {
let defer = protractor.promise.defer();
let promises = [];
element.all(by.css("label")).then((labels) => {
for (let i = 0; i < labels.length; ++i) {
promises.push(labels[i].getAttribute("for"));
}
});
protractor.promise.all(promises).then((arr) => {
if (arr.length === 0) {
defer.fulfill();
} else {
for (let labelFor in arr) {
if (labelFor === ""){
this.arrayOfFails.push("Label element missing for attribute");
}
}
defer.fulfill();
}
});
return defer.promise;
}
}
export = AccessibilitySteps;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment