diff --git a/.gitignore b/.gitignore
index 955852e2..665cbb82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,4 @@ extension/Handlebar.js
upload.py
gen/
node_modules/
+.tmp/
diff --git a/Changelog.md b/Changelog.md
new file mode 100644
index 00000000..e1290cb4
--- /dev/null
+++ b/Changelog.md
@@ -0,0 +1,66 @@
+### Enhancements:
+* Rework findTextAlternatives not to return non-exposed text alternatives.
+* Add Bower config (#157)
+
+### Bug fixes:
+* Check for any text alternatives when assessing unlabeled images (#154).
+
+## 2.7.0 - 2015-05-15
+
+### New rules
+* This element does not support ARIA roles, states and properties (`src/audits/AriaOnReservedElement.js`)
+* aria-owns should not be used if ownership is implicit in the DOM (`src/audits/AriaOwnsDescendant.js`)
+* Elements with ARIA roles must be in the correct scope (`src/audits/AriaRoleNotScoped.js`)
+* An element's ID must be unique in the DOM (`src/audits/DuplicateId.js`)
+* The web page should have the content's human language indicated in the markup (`src/audits/HumanLangMissing.js`)
+* An element's ID must not be present in more that one aria-owns attribute at any time (`src/audits/MultipleAriaOwners.js`)
+* ARIA attributes which refer to other elements by ID should refer to elements which exist in the DOM (`src/audits/NonExistentAriaRelatedElement.js` - previously `src/audits/NonExistentAriaLabeledBy.js`)
+* Elements with ARIA roles must ensure required owned elements are present (`src/audits/RequiredOwnedAriaRoleMissing.js`)
+* Avoid positive integer values for tabIndex (`src/audits/TabIndexGreaterThanZero.js`)
+* This element has an unsupported ARIA attribute (`src/audits/UnsupportedAriaAttribute.js`)
+
+### Enhancements:
+* Add configurable blacklist phrases and stop words to LinkWithUnclearPurpose (#99)
+* Detect and warn if we reuse the same code for more than one rule. (#133)
+* Force focus before testing visibility on focusable elements. (#65)
+* Use getDistributedNodes to get nodes distributed into shadowRoots (#128)
+* Add section to Audit Rules page for HumanLangMissing and link to it from rule (#119)
+* Reference "applied role" in axs.utils.getRoles enhancement (#130)
+* Add warning that AX_FOCUS_02 is not available from axs.Audit.run() (#85)
+
+### Bug fixes:
+* Incorrect use of nth-of-type against className in utils.getQuerySelectorText (#87)
+* AX_TEXT_01 Accessibility Audit test should probably ignore role=presentation elements (#97)
+* Fix path to audit rules in phantomjs runner (#108)
+* Label audit should fail if form fields lack a label, even with placeholder text (#81)
+* False positives for controls without labels with role=presentation (#23)
+* Fix "valid" flag on return value of axs.utils.getRoles (#131)
+
+Note: this version number is somewhat arbitrary - just bringing it vaguely in line with [the extension](https://github.com/GoogleChrome/accessibility-developer-tools-extension) since that's where the library originated - but will use semver for version bumps going forward from here.
+
+## 0.0.5 - 2014-02-04
+
+### Enhancements:
+* overlapping elements detection code made more sophisticated
+* axs.properties.getFocusProperties() returns more information about visibility
+* new axs.properties.hasDirectTextDescendant() method with more sophisticated detection of text content
+
+### Bug fixes:
+* FocusableElementNotVisibleAndNotAriaHidden audit passes on elements which are brought onscreen on focus
+* UnfocusableElementsWithOnclick checks for element.disabled
+* Fix infinite loop when getting descendant text content of a label containing an input
+* Detect elements which are out of scroll area of any parent element, not just the document scroll area
+* findTextAlternatives doesn't throw TypeError if used on a HTMLSelectElement
+
+## 0.0.4 - 2013-10-03
+
+### Enhancements:
+
+* axs.AuditRule.run() has a new signature: it now takes an options object. Please see method documentation for details.
+* Audit Rule severity can be overridden (per Audit Rule) in AuditConfig.
+
+### Bug fixes:
+
+* axs.utils.isLowContrast() now rounds to the nearest 0.1 before checking (so `#777` is now a passing value)
+* MainRoleOnInappropriateElement was always failing due to accessible name calculation taking the main role into account and not descending into content (now just gets descendant content directly)
+* UnfocusableElementsWithOnClick had a dangling if-statement causing very noisy false positives
diff --git a/Gruntfile.js b/Gruntfile.js
index bd90d175..e13ff852 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -1,63 +1,205 @@
-'use strict';
-
module.exports = function(grunt) {
+ 'use strict';
+
+ require('load-grunt-tasks')(grunt);
+
grunt.initConfig({
- 'git-describe': {
- options: {
+ pkg: grunt.file.readJSON('package.json'),
+ changelog: 'Changelog.md',
+
+ 'gh-release': {},
- },
- 'run': {}
- },
closurecompiler: {
minify: {
requiresConfig: 'git-revision',
files: {
- "gen/axs_testing.js": [
- "./lib/closure-library/closure/goog/base.js",
- "./src/js/axs.js",
- "./src/js/BrowserUtils.js",
- "./src/js/Constants.js",
- "./src/js/AccessibilityUtils.js",
- "./src/js/Properties.js",
- "./src/js/AuditRule.js",
- "./src/js/AuditRules.js",
- "./src/js/AuditResults.js",
- "./src/js/Audit.js",
- "./src/audits/*"
+ '.tmp/build/axs_testing.js': [
+ './lib/closure-library/closure/goog/base.js',
+ './src/js/axs.js',
+ './src/js/BrowserUtils.js',
+ './src/js/Constants.js',
+ './src/js/AccessibilityUtils.js',
+ './src/js/Properties.js',
+ './src/js/AuditRule.js',
+ './src/js/AuditRules.js',
+ './src/js/AuditResults.js',
+ './src/js/Audit.js',
+ './src/audits/*'
]
},
options: {
- "language_in": "ECMASCRIPT5",
- "formatting": "PRETTY_PRINT",
- "summary_detail_level": 3,
- "warning_level": "VERBOSE",
- "compilation_level": "SIMPLE_OPTIMIZATIONS",
- "output_wrapper": "<%= grunt.file.read('scripts/output_wrapper.txt') %>",
- "externs": "./src/js/externs/externs.js"
+ 'language_in': 'ECMASCRIPT5',
+ 'formatting': 'PRETTY_PRINT',
+ 'summary_detail_level': 3,
+ 'warning_level': 'VERBOSE',
+ 'compilation_level': 'SIMPLE_OPTIMIZATIONS',
+ 'output_wrapper': "<%= grunt.file.read('scripts/output_wrapper.txt') %>",
+ 'externs': './src/js/externs/externs.js'
}
}
},
+
qunit: {
all: ['test/index.html']
+ },
+
+ copy: {
+ dist: {
+ expand: true,
+ cwd: '.tmp/build',
+ src: '**/*',
+ dest: 'dist/js'
+ }
+ },
+
+ clean: {
+ all: ['.tmp', 'dist']
+ },
+
+ bump: {
+ options: {
+ prereleaseName: 'rc',
+ files: ['package.json', 'bower.json'],
+ updateConfigs: ['pkg'],
+ pushTo: "<%= grunt.config.get('gh-release.remote') %>",
+ commitFiles: ['package.json', "<%= grunt.config.get('changelog') %>", 'bower.json', 'dist']
+ }
+ },
+
+ coffee: {
+ compile: {
+ files: {
+ '.tmp/util/gh_repo.js': 'src/util/gh_repo.coffee'
+ }
+ }
+ },
+
+ prompt: {
+ 'gh-release': {
+ options: {
+ questions: [
+ {
+ config: 'gh-release.remote',
+ type: 'input',
+ message: 'Git Remote (usually upstream or origin)',
+ default: 'upstream',
+ validate: function(val) {
+ return (grunt.util._.size(val) > 0);
+ }
+ },
+ {
+ config: 'gh-release.repo',
+ type: 'input',
+ message: 'Github Repository',
+ default: 'GoogleChrome/accessibility-developer-tools',
+ validate: function(val) {
+ return (grunt.util._.size(val) > 0);
+ }
+ },
+ {
+ config: 'gh-release.username',
+ type: 'input',
+ message: 'Github Username',
+ validate: function(val) {
+ return (grunt.util._.size(val) > 0);
+ }
+ },
+ {
+ config: 'gh-release.password',
+ type: 'password',
+ message: 'Github Password or Token',
+ validate: function(val) {
+ return (grunt.util._.size(val) > 0);
+ }
+ }
+ ]
+ }
+ }
}
});
- grunt.loadNpmTasks('grunt-closurecompiler');
- grunt.loadNpmTasks('grunt-contrib-qunit');
+ grunt.registerTask('changelog', function(type) {
+ grunt.task.requires('bump-only:' + type);
- grunt.registerTask('git-describe', function() {
- var _spawn = require("grunt-util-spawn")(grunt);
+ var config = {
+ data: {
+ version: grunt.config.get('pkg.version'),
+ releaseDate: grunt.template.today("yyyy-mm-dd")
+ }
+ };
+
+ var stopRegex = /^\#\#\ [0-9]+.*$/m;
+ var stopIndex = 0;
+ var releaseNotes = '';
+ var dest = grunt.config.get('changelog');
+ var contents = grunt.file.read(dest);
+ var headerTpl = "## <%= version %> - <%= releaseDate %>\n\n";
+ var header = grunt.template.process(headerTpl, config);
+
+ if (contents.length > 0) {
+ if ((stopIndex = contents.search(stopRegex)) !== -1) {
+ releaseNotes = contents.slice(0, stopIndex);
+ }
+ }
+
+ grunt.config.set("gh-release.release-notes", releaseNotes);
+
+ grunt.file.write(dest, "" + header + contents);
+ grunt.log.ok("Changelog updated, and release notes extracted.");
+ });
+
+ grunt.registerTask('gh-release', function() {
+ // Compile and load GH Repo manager.
+ grunt.task.requires('coffee:compile');
+ var GHRepo = require('./.tmp/util/gh_repo');
+
+ var done = this.async();
+ var config = grunt.config.get('gh-release');
+ var pkg = grunt.config.get('pkg');
+ var currentRelease = 'v' + pkg.version;
+ var nextRelease = currentRelease.replace(/-rc\.[0-9]+/, '');
+ var repo = new GHRepo(config);
+ repo.log = function() { grunt.log.writeln.apply(grunt, arguments); };
+ var payload = {
+ tag_name: currentRelease,
+ name: nextRelease,
+ body: config['release-notes'],
+ draft: true
+ };
+
+ grunt.log.writeln("Searching for existing GH release:", nextRelease);
+ repo.getReleaseByName(nextRelease)
+ .then(function(release) {
+ if (release) {
+ payload.body += "\n" + release.body;
+ repo.updateRelease(release, payload).then(function() {
+ grunt.log.ok('Github release ' + nextRelease + ' updated successfully.');
+ done();
+ });
+ } else {
+ repo.createRelease(payload).then(function() {
+ grunt.log.ok('Github release ' + nextRelease + ' created successfully');
+ done();
+ });
+ }
+ })
+ .catch(function(err) {
+ throw err;
+ });
+ });
+
+ grunt.registerTask('git-describe', function() {
// Start async task
var done = this.async();
- _spawn({
- "cmd" : "git",
- "args" : [ "rev-parse", "HEAD" ],
- "opts" : {
- "cwd" : "."
+ grunt.util.spawn({
+ 'cmd' : 'git',
+ 'args' : [ 'rev-parse', 'HEAD' ],
+ 'opts' : {
+ 'cwd' : '.'
}
- }, function(err, result) {
+ }, function(err, result) {
if (err) {
grunt.log.error(err).verbose.error(result);
done();
@@ -69,20 +211,35 @@ module.exports = function(grunt) {
});
});
+ grunt.registerTask('release', function(releaseType) {
+ if (typeof releaseType === 'undefined' || releaseType === null) {
+ grunt.fail.fatal('You must specify a release type. i.e. grunt release:prerelease');
+ }
+
+ grunt.task.run([
+ 'prompt:gh-release',
+ 'build',
+ 'test:unit',
+ 'copy:dist',
+ 'bump-only:' + releaseType,
+ 'changelog:' + releaseType,
+ 'bump-commit',
+ 'coffee:compile',
+ 'gh-release'
+ ]);
+ });
+
grunt.registerTask('save-revision', function() {
grunt.event.once('git-describe', function (rev) {
- grunt.log.writeln("Git Revision: " + rev);
+ grunt.log.writeln('Git Revision: ' + rev);
grunt.config.set('git-revision', rev);
});
grunt.task.run('git-describe');
});
- grunt.registerTask('copy-dist', function() {
- grunt.file.copy('gen/axs_testing.js', 'dist/js/axs_testing.js');
- });
-
- grunt.registerTask('default', ['save-revision', 'closurecompiler:minify', 'qunit']);
- grunt.registerTask('build', ['default', 'copy-dist']);
- grunt.registerTask('travis', ['closurecompiler:minify', 'qunit']);
+ grunt.registerTask('build', ['clean:all', 'save-revision', 'closurecompiler:minify']);
+ grunt.registerTask('test:unit', ['qunit']);
+ grunt.registerTask('dist', ['build', 'copy:dist']);
+ grunt.registerTask('travis', ['closurecompiler:minify', 'test:unit']);
+ grunt.registerTask('default', ['build', 'test:unit']);
};
-
diff --git a/bower.json b/bower.json
new file mode 100644
index 00000000..2611fe5a
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,29 @@
+{
+ "name": "accessibility-developer-tools",
+ "version": "2.7.0",
+ "homepage": "https://github.com/GoogleChrome/accessibility-developer-tools",
+ "authors": [
+ "Google"
+ ],
+ "description": "This is a library of accessibility-related testing and utility code.",
+ "main": "dist/js/axs_testing.js",
+ "moduleType": [
+ "globals"
+ ],
+ "keywords": [
+ "accessibility",
+ "testing",
+ "WCAG"
+ ],
+ "license": "Apache License 2.0",
+ "ignore": [
+ "**/.*",
+ "lib",
+ "scripts",
+ "src",
+ "test",
+ "tools",
+ "Gruntfile.js",
+ "package.json"
+ ]
+}
diff --git a/dist/js/.gitkeep b/dist/js/.gitkeep
deleted file mode 100644
index e69de29b..00000000
diff --git a/dist/js/axs_testing.js b/dist/js/axs_testing.js
index 1fe815bc..d8fbf82b 100644
--- a/dist/js/axs_testing.js
+++ b/dist/js/axs_testing.js
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*
- * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/08b3be1e095d0203a094b370c27f77446a6c69e9
+ * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/a33b34feb4bf5c6990c9d88f98c3c8e3115168ab
*
* See project README for build steps.
*/
@@ -1536,20 +1536,19 @@ axs.AuditRule.collectMatchingElements = function(a, b, c, d) {
}
}
if (e && "content" == e.localName) {
- for (var f = e.getDistributedNodes(), g = 0;g < f.length;g++) {
- axs.AuditRule.collectMatchingElements(f[g], b, c, d);
+ for (e = e.getDistributedNodes(), f = 0;f < e.length;f++) {
+ axs.AuditRule.collectMatchingElements(e[f], b, c, d);
}
} else {
if (e && "shadow" == e.localName) {
if (f = e, d) {
- for (f = f.getDistributedNodes(), g = 0;g < f.length;g++) {
- axs.AuditRule.collectMatchingElements(f[g], b, c, d);
+ for (e = f.getDistributedNodes(), f = 0;f < e.length;f++) {
+ axs.AuditRule.collectMatchingElements(e[f], b, c, d);
}
- } else {
+ } else {
console.warn("ShadowRoot not provided for", e);
}
}
- e && "iframe" == e.localName && e.contentDocument && axs.AuditRule.collectMatchingElements(e.contentDocument, b, c, d);
for (a = a.firstChild;null != a;) {
axs.AuditRule.collectMatchingElements(a, b, c, d), a = a.nextSibling;
}
@@ -1591,7 +1590,7 @@ axs.AuditRules = {};
}
if (c.name in a) {
throw Error('Can not add audit rule with same name: "' + c.name + '"');
- }
+ }
a[c.name] = b[c.code] = c;
};
axs.AuditRules.getRule = function(c) {
@@ -1804,7 +1803,7 @@ axs.AuditRules.addRule({name:"audioWithoutControls", heading:"Audio elements sho
if (a.test(e) && (e = e.replace(a, ""), !axs.constants.ARIA_PROPERTIES.hasOwnProperty(e))) {
return !0;
}
- }
+ }
return !1;
}, code:"AX_ARIA_11"});
})();
@@ -1866,8 +1865,7 @@ axs.AuditRules.addRule({name:"focusableElementNotVisibleAndNotAriaHidden", headi
return !1;
}
}
- a = axs.properties.findTextAlternatives(a, {});
- return null == a || "" === a.trim() ? !1 : !0;
+ return "" === axs.properties.findTextAlternatives(a, {}).trim() ? !1 : !0;
}, test:function(a) {
if (axs.utils.isElementOrAncestorHidden(a)) {
return !1;
diff --git a/package.json b/package.json
index 9557995e..c06f6ff5 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,23 @@
{
"name": "accessibility-developer-tools",
- "version": "2.6.0",
+ "version": "2.7.0",
"repository": {
"type": "git",
"url": "http://github.com/GoogleChrome/accessibility-developer-tools"
},
"devDependencies": {
+ "bluebird": "^2.9.27",
"grunt": "^0.4.5",
+ "grunt-bump": "^0.3.1",
"grunt-cli": "^0.1.13",
"grunt-closurecompiler": "^0.9.9",
- "grunt-contrib-qunit": "^0.2.2",
- "grunt-util-spawn": "^0.0.2"
+ "grunt-contrib-clean": "^0.6.0",
+ "grunt-contrib-coffee": "^0.13.0",
+ "grunt-contrib-copy": "^0.8.0",
+ "grunt-contrib-qunit": "^0.7.0",
+ "grunt-prompt": "^1.3.0",
+ "load-grunt-tasks": "^3.2.0",
+ "superagent": "^1.2.0"
},
"scripts": {
"test": "grunt travis --verbose"
diff --git a/src/audits/ImageWithoutAltText.js b/src/audits/ImageWithoutAltText.js
index 9ec511f6..91c95dcb 100644
--- a/src/audits/ImageWithoutAltText.js
+++ b/src/audits/ImageWithoutAltText.js
@@ -15,11 +15,12 @@
goog.require('axs.AuditRules');
goog.require('axs.browserUtils');
goog.require('axs.constants.Severity');
+goog.require('axs.properties');
goog.require('axs.utils');
axs.AuditRules.addRule({
name: 'imagesWithoutAltText',
- heading: 'Images should have an alt attribute',
+ heading: 'Images should have a text alternative or presentational role',
url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_text_02',
severity: axs.constants.Severity.WARNING,
relevantElementMatcher: function(element) {
@@ -27,7 +28,15 @@ axs.AuditRules.addRule({
!axs.utils.isElementOrAncestorHidden(element);
},
test: function(image) {
- return (!image.hasAttribute('alt') && image.getAttribute('role') != 'presentation');
+ var imageIsPresentational = (image.hasAttribute('alt') && image.alt == '') || image.getAttribute('role') == 'presentation';
+ if (imageIsPresentational)
+ return false;
+ var textAlternatives = {};
+ axs.properties.findTextAlternatives(image, textAlternatives);
+ var numTextAlternatives = Object.keys(textAlternatives).length;
+ if (numTextAlternatives == 0)
+ return true;
+ return false;
},
code: 'AX_TEXT_02'
});
diff --git a/src/js/Properties.js b/src/js/Properties.js
index 9dac1a83..a6a40ed9 100644
--- a/src/js/Properties.js
+++ b/src/js/Properties.js
@@ -494,31 +494,16 @@ axs.properties.getTextFromHostLanguageAttributes = function(element,
existingComputedname,
recursive) {
var computedName = existingComputedname;
- if (axs.browserUtils.matchSelector(element, 'img')) {
- if (element.hasAttribute('alt')) {
- var altValue = {};
- altValue.type = 'string';
- altValue.valid = true;
- altValue.text = element.getAttribute('alt');
- if (computedName)
- altValue.unused = true;
- else
- computedName = altValue.text;
- textAlternatives['alt'] = altValue;
- } else {
- var altValue = {};
- altValue.valid = false;
- altValue.errorMessage = 'No alt value provided';
- textAlternatives['alt'] = altValue;
- var src = element.src;
- if (typeof(src) == 'string') {
- var parts = src.split('/');
- var filename = parts.pop();
- var filenameValue = { text: filename };
- textAlternatives['filename'] = filenameValue;
- computedName = filename;
- }
- }
+ if (axs.browserUtils.matchSelector(element, 'img') && element.hasAttribute('alt')) {
+ var altValue = {};
+ altValue.type = 'string';
+ altValue.valid = true;
+ altValue.text = element.getAttribute('alt');
+ if (computedName)
+ altValue.unused = true;
+ else
+ computedName = altValue.text;
+ textAlternatives['alt'] = altValue;
}
var controlsSelector = ['input:not([type="hidden"]):not([disabled])',
@@ -611,13 +596,28 @@ axs.properties.getTextProperties = function(node) {
var computedName = axs.properties.findTextAlternatives(node, textProperties, false, true);
if (Object.keys(textProperties).length == 0) {
+ /** @type {Element} */ var element = axs.utils.asElement(node);
+ if (element && axs.browserUtils.matchSelector(element, 'img')) {
+ var altValue = {};
+ altValue.valid = false;
+ altValue.errorMessage = 'No alt value provided';
+ textProperties['alt'] = altValue;
+
+ var src = element.src;
+ if (typeof(src) == 'string') {
+ var parts = src.split('/');
+ var filename = parts.pop();
+ var filenameValue = { text: filename };
+ textProperties['filename'] = filenameValue;
+ computedName = filename;
+ }
+ }
+
if (!computedName)
return null;
- textProperties.hasProperties = false;
- } else {
- textProperties.hasProperties = true;
}
+ textProperties.hasProperties = Boolean(Object.keys(textProperties).length);
textProperties.computedText = computedName;
textProperties.lastWord = axs.properties.getLastWord(computedName);
return textProperties;
diff --git a/src/util/gh_repo.coffee b/src/util/gh_repo.coffee
new file mode 100644
index 00000000..a4844cc4
--- /dev/null
+++ b/src/util/gh_repo.coffee
@@ -0,0 +1,69 @@
+request = require 'superagent'
+Promise = require 'bluebird'
+
+# Small utility class to interact with the Github v3 releases API.
+module.exports = class GHRepo
+ constructor: (@config = {}) ->
+ @baseUrl = "https://api.github.com/repos/#{@config.repo}"
+
+ _buildRequest: (req) ->
+ req
+ .auth @config.username, @config.password
+ .set 'Accept', 'application/vnd.github.v3'
+ .set 'User-Agent', 'grunt'
+
+ log: -> console.log.apply console, arguments
+
+ getReleaseByTagName: (tag) ->
+ # GET /repos/:owner/:repo/releases/tags/:tag
+ new Promise (resolve, reject) =>
+ @log 'GET', "#{@baseUrl}/releases/tags/#{tag}"
+ @_buildRequest(request.get "#{@baseUrl}/releases/tags/#{tag}")
+ .end (err, res) ->
+ return resolve() if res.statusCode is 404
+ return reject(err) if err?
+ return reject("Request failed") if res.statusCode isnt 200
+ resolve res.body
+
+ getReleases: (tag) ->
+ # GET /repos/:owner/:repo/releases
+ new Promise (resolve, reject) =>
+ @log 'GET', "#{@baseUrl}/releases"
+ @_buildRequest(request.get "#{@baseUrl}/releases")
+ .end (err, res) ->
+ return resolve() if res.statusCode is 404
+ return reject(err) if err?
+ return reject("Request failed") if res.statusCode isnt 200
+ resolve res.body
+
+ updateRelease: (release, payload) ->
+ # PATCH /repos/:owner/:repo/releases/:id
+ new Promise (resolve, reject) =>
+ @log 'PATCH', "#{@baseUrl}/releases/#{release.id}"
+ @_buildRequest(request.patch "#{@baseUrl}/releases/#{release.id}")
+ .send payload
+ .end (err, res) ->
+ return reject(err) if err?
+ return reject("Request failed") if res.statusCode isnt 200
+ resolve res.body
+
+ createRelease: (payload) ->
+ # POST /repos/:owner/:repo/releases
+ new Promise (resolve, reject) =>
+ @log 'POST', "#{@baseUrl}/releases"
+ @_buildRequest(request.post "#{@baseUrl}/releases")
+ .send payload
+ .end (err, res) ->
+ return reject(err) if err?
+ return reject("Request failed") if res.statusCode isnt 201
+ resolve res.body
+
+ getReleaseByName: (name) ->
+ new Promise (resolve, reject) =>
+ @getReleases().then (releases = []) ->
+ for release in releases
+ return resolve(release) if release.name is name
+
+ return resolve()
+ .catch (err) ->
+ reject "Unable to fetch project releases."
diff --git a/test/audits/image-without-alt-text-test.js b/test/audits/image-without-alt-text-test.js
new file mode 100644
index 00000000..be25802c
--- /dev/null
+++ b/test/audits/image-without-alt-text-test.js
@@ -0,0 +1,83 @@
+// Copyright 2015 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+(function() {
+ module('ImageWithoutAltText');
+ var rule = axs.AuditRules.getRule('imagesWithoutAltText');
+
+ test('Image with no text alternative', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ var expected = { elements: [img], result: axs.constants.AuditResult.FAIL };
+ deepEqual(rule.run({ scope: fixture }), expected, 'Image has no text alternative');
+ });
+
+ test('Image with no text alternative and presentational role', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.setAttribute('role', 'presentation');
+ var expected = { elements: [], result: axs.constants.AuditResult.PASS };
+ deepEqual(rule.run({ scope: fixture }), expected, 'Image has presentational role');
+ });
+
+ test('Image with alt text', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.alt = 'Smile!';
+ var expected = { elements: [], result: axs.constants.AuditResult.PASS };
+ deepEqual(rule.run({ scope: fixture }), expected, 'Image has alt text');
+ });
+
+ test('Image with empty alt text', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.alt = '';
+ var expected = { elements: [], result: axs.constants.AuditResult.PASS };
+ deepEqual(rule.run({ scope: fixture }), expected, 'Image has empty alt text');
+ });
+
+ test('Image with aria label', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.setAttribute('aria-label', 'Smile!');
+ var expected = { elements: [], result: axs.constants.AuditResult.PASS };
+ deepEqual(rule.run({ scope: fixture }), expected, 'Image has aria label');
+ });
+
+ test('Image with aria labelledby', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ var label = fixture.appendChild(document.createElement('div'));
+ label.textContent = 'Smile!';
+ label.id = 'label';
+ img.setAttribute('aria-labelledby', 'label');
+ var expected = { elements: [], result: axs.constants.AuditResult.PASS };
+ deepEqual(rule.run({ scope: fixture }), expected, 'Image has aria labelledby');
+ });
+
+ test('Image with title', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.setAttribute('title', 'Smile!');
+ var expected = { elements: [], result: axs.constants.AuditResult.PASS };
+ deepEqual(rule.run({ scope: fixture }), expected, 'Image has title');
+ });
+})();
diff --git a/test/index.html b/test/index.html
index e00f854f..d142dc11 100644
--- a/test/index.html
+++ b/test/index.html
@@ -56,6 +56,7 @@
+
diff --git a/test/js/properties-test.js b/test/js/properties-test.js
index c96b59d1..f023bd6f 100644
--- a/test/js/properties-test.js
+++ b/test/js/properties-test.js
@@ -32,6 +32,66 @@ test('returns the calculated text alternative for the given element', function()
ok(false, 'Threw exception');
}
});
+test('Image with no text alternative', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ var textAlternatives = {};
+ axs.properties.findTextAlternatives(img, textAlternatives);
+ equal(Object.keys(textAlternatives).length, 0, 'Image has no text alternative');
+});
+
+test('Image with alt text', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.alt = 'Smile!';
+ var textAlternatives = {};
+ axs.properties.findTextAlternatives(img, textAlternatives);
+ equal(Object.keys(textAlternatives).length, 1, 'exactly one text alternative');
+ equal('alt' in textAlternatives, true, 'alt in textAlternatives');
+ equal('Smile!', textAlternatives.alt.text);
+});
+
+test('Image with aria label', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.setAttribute('aria-label', 'Smile!');
+ var textAlternatives = {};
+ axs.properties.findTextAlternatives(img, textAlternatives);
+ equal(Object.keys(textAlternatives).length, 1, 'exactly one text alternative');
+ equal('ariaLabel' in textAlternatives, true, 'ariaLabel in textAlternatives');
+ equal('Smile!', textAlternatives.ariaLabel.text);
+});
+
+test('Image with aria labelledby', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ var label = fixture.appendChild(document.createElement('div'));
+ label.textContent = 'Smile!';
+ label.id = 'label';
+ img.setAttribute('aria-labelledby', 'label');
+ var textAlternatives = {};
+ axs.properties.findTextAlternatives(img, textAlternatives);
+ equal(Object.keys(textAlternatives).length, 1, 'exactly one text alternative');
+ equal('ariaLabelledby' in textAlternatives, true, 'ariaLabelledby in textAlternatives');
+ equal('Smile!', textAlternatives.ariaLabelledby.text);
+});
+
+test('Image with title', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ img.setAttribute('title', 'Smile!');
+ var textAlternatives = {};
+ axs.properties.findTextAlternatives(img, textAlternatives);
+ equal(Object.keys(textAlternatives).length, 1, 'exactly one text alternative');
+ equal('title' in textAlternatives, true, 'title in textAlternatives');
+ equal('Smile!', textAlternatives.title.text);
+});
+
module('getTextFromHostLanguageAttributes', {
setup: function () {
@@ -154,3 +214,17 @@ test('get implicit role for li descendant of ul', function() {
var actual = axs.properties.getImplicitRole(element);
strictEqual(actual, '');
});
+
+module('getTextProperties', {});
+test('Image with no text alternative', function() {
+ var fixture = document.getElementById('qunit-fixture');
+ var img = fixture.appendChild(document.createElement('img'));
+ img.src = 'smile.jpg';
+ var textProperties = axs.properties.getTextProperties(img);
+ equal('alt' in textProperties, true, 'alt in textProperties');
+ equal(textProperties.alt.valid, false, 'alt is not valid');
+ equal('filename' in textProperties, true, 'filename in textProperties');
+ equal(textProperties.filename.text, 'smile.jpg');
+ equal('computedText' in textProperties, true, 'computedText in textProperties');
+ equal(textProperties.computedText, 'smile.jpg');
+});