Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

pa11y

Package Overview
Dependencies
Maintainers
6
Versions
103
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pa11y - npm Package Compare versions

Comparing version 4.11.0 to 4.12.0

9

CHANGELOG.md
# Changelog
## 4.12.0 (2017-08-23)
* Add the `wait-for-element-state` action
* Update the `wait-for-url` action to support waiting for hostname
* Update dependencies
* HTML CodeSniffer: 2.0.7 to 2.1.0
* Lots of updates to the README and trouble-shooting guide
* Support Node.js 8.x
## 4.11.0 (2017-06-02)

@@ -5,0 +14,0 @@

71

lib/action.js

@@ -139,3 +139,3 @@ 'use strict';

name: 'wait-for-url',
match: /^wait for (fragment|hash|path|url)( to (not )?be)? (.+)$/i,
match: /^wait for (fragment|hash|path|url|host)( to (not )?be)? (.+)$/i,
build: function(browser, page, options, matches) {

@@ -163,2 +163,5 @@ var actionOptions = {

break;
case 'host':
value = window.location.host;
break;
default:

@@ -185,4 +188,68 @@ // no default behaviour

}
},
// Action which waits for an element to be added, removed, visible, or hidden
// E.g. "wait for element .foo to be added"
// E.g. "wait for .foo .bar to be visible"
{
name: 'wait-for-element-state',
match: /^wait for( element)? (.+)( to be) (added|removed|visible|hidden)$/i,
build: function(browser, page, options, matches) {
var maxRetries = 10;
var retryCount = 0;
var actionOptions = {
selector: matches[2],
state: matches[4]
};
function waitForState(state, done) {
page.evaluate(function(actionOptions) {
var target = document.querySelector(actionOptions.selector);
/* Validate target that should exist. Syntax is like below but shorter */
if (!target && ['removed', 'hidden'].indexOf(actionOptions.state) !== -1) {
return false;
}
/* Check current state */
if (actionOptions.state === 'added' || actionOptions.state === 'removed') {
return Boolean(target);
}
/* Check if the target is visible */
return Boolean(
target.offsetWidth ||
target.offsetHeight ||
target.getClientRects().length
);
}, actionOptions, function(error, result) {
var wait = result;
var errorMessage = 'Failed action: element "' + actionOptions.selector;
errorMessage += '" failed to be ' + actionOptions.state;
options.log.debug(' … waiting ("' + result + '")');
if (actionOptions.state === 'added' || actionOptions.state === 'visible') {
// Don't wait when it needs to be visible or added and it's there already
wait = !result;
}
if (wait) {
retryCount += 1;
if (retryCount > maxRetries) {
return done(new Error(errorMessage));
}
setTimeout(function() {
waitForState(actionOptions.state, done);
}, 200);
} else {
done();
}
});
}
return function(done) {
waitForState(actionOptions.state, done);
};
}
}
];

2

package.json
{
"name": "pa11y",
"version": "4.11.0",
"version": "4.12.0",
"description": "Pa11y is your automated accessibility testing pal",

@@ -5,0 +5,0 @@ "keywords": [

@@ -31,4 +31,10 @@

Need a GUI? Try [Koa11y](https://open-indy.github.io/Koa11y/)!
---
## Latest news from Pa11y
💭 We'd like to find out how you use Pa11y and what you think about it. Please [fill in our survey][survey] to let us know your thoughts!
✨ 🔜 ✨ The Pa11y team is very excited to announce plans for the successor to Pa11y Dashboard and Pa11y Webservice, codename "Sidekick". Help us define the features that you want to see by visiting the [proposal][sidekick-proposal]. ✨

@@ -49,2 +55,3 @@

- [Common Questions and Troubleshooting](#common-questions-and-troubleshooting)
- [Tutorials and articles](#tutorials-and-articles)
- [Contributing](#contributing)

@@ -190,3 +197,3 @@ - [Support and Migration](#support-and-migration)

Pa11y can also ignore notices, warnings, and errors up to a threshold number. This might be useful if you're using CI and don't want to break your build. The following example will return exit code 0 on a page with 9 errors, and return exit code 2 on a page with 11 errors.
Pa11y can also ignore notices, warnings, and errors up to a threshold number. This might be useful if you're using CI and don't want to break your build. The following example will return exit code 0 on a page with 9 errors, and return exit code 2 on a page with 10 or more errors.

@@ -647,2 +654,3 @@ ```

'click element #tab-1',
'wait for element #tab-1-content to be visible',
'set field #fullname to John Doe',

@@ -714,2 +722,3 @@ 'check field #terms-and-conditions',

actions: [
'click element #login-link',
'wait for path to be /login'

@@ -720,3 +729,23 @@ ]

### Wait For Element State
This allows you to pause the test until an element on the page (matching a CSS selector) is either added, removed, visible, or hidden. This will wait until Pa11y times out so it should be used after another action that would trigger the change in state. This action takes one of the forms:
- `wait for element <selector> to be added`
- `wait for element <selector> to be removed`
- `wait for element <selector> to be visible`
- `wait for element <selector> to be hidden`
E.g.
```js
pa11y({
actions: [
'click element #tab-2',
'wait for element #tab-1 to be hidden'
]
});
```
Examples

@@ -764,2 +793,12 @@ --------

Tutorials and articles
------------------------------------
Here are some useful articles written by Pa11y users and contributors:
- [Accessibility Testing with Pa11y](https://bitsofco.de/pa11y/)
- [Using actions in Pa11y](http://hollsk.co.uk/posts/view/using-actions-in-pa11y)
- [Introduction to Accessibility Testing With Pa11y](http://cruft.io/posts/accessibility-testing-with-pa11y/)
Contributing

@@ -828,2 +867,3 @@ ------------

[sniff-issue]: https://github.com/squizlabs/HTML_CodeSniffer/issues/109
[survey]: https://goo.gl/forms/AiMDJR2IuaqX4iD03
[windows-install]: https://github.com/TooTallNate/node-gyp#installation

@@ -830,0 +870,0 @@

@@ -47,6 +47,6 @@ 'use strict';

assert.deepEqual(this.lastJsonResponse[1], {
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.B',
context: '<b>World</b>',
message: 'Semantic markup should be used to mark emphasised or special text so that it can be programmatically determined.',
selector: 'html > body > p > b',
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H42',
context: '<p><b>Hello World!</b></p>',
message: 'Heading markup should be used if this content is intended as a heading.',
selector: 'html > body > p',
type: 'warning',

@@ -85,6 +85,6 @@ typeCode: 2

assert.deepEqual(this.lastJsonResponse[2], {
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.B',
context: '<b>World</b>',
message: 'Semantic markup should be used to mark emphasised or special text so that it can be programmatically determined.',
selector: 'html > body > p > b',
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H42',
context: '<p><b>Hello World!</b></p>',
message: 'Heading markup should be used if this content is intended as a heading.',
selector: 'html > body > p',
type: 'warning',

@@ -91,0 +91,0 @@ typeCode: 2

@@ -47,6 +47,6 @@ 'use strict';

assert.deepEqual(this.lastJsonResponse[1], {
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.B',
context: '<b>World</b>',
message: 'Semantic markup should be used to mark emphasised or special text so that it can be programmatically determined.',
selector: 'html > body > p > b',
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H42',
context: '<p><b>Hello World!</b></p>',
message: 'Heading markup should be used if this content is intended as a heading.',
selector: 'html > body > p',
type: 'warning',

@@ -85,6 +85,6 @@ typeCode: 2

assert.deepEqual(this.lastJsonResponse[2], {
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.B',
context: '<b>World</b>',
message: 'Semantic markup should be used to mark emphasised or special text so that it can be programmatically determined.',
selector: 'html > body > p > b',
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H42',
context: '<p><b>Hello World!</b></p>',
message: 'Heading markup should be used if this content is intended as a heading.',
selector: 'html > body > p',
type: 'warning',

@@ -91,0 +91,0 @@ typeCode: 2

@@ -34,6 +34,6 @@ 'use strict';

assert.deepEqual(this.lastJsonResponse[2], {
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.B',
context: '<b>World</b>',
message: 'Semantic markup should be used to mark emphasised or special text so that it can be programmatically determined.',
selector: 'html > body > p:nth-child(1) > b',
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H42',
context: '<p><b>Hello World!</b></p>',
message: 'Heading markup should be used if this content is intended as a heading.',
selector: 'html > body > p:nth-child(1)',
type: 'warning',

@@ -43,6 +43,6 @@ typeCode: 2

assert.deepEqual(this.lastJsonResponse[3], {
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.B',
context: '<b>foo</b>',
message: 'Semantic markup should be used to mark emphasised or special text so that it can be programmatically determined.',
selector: '#foo > b',
code: 'WCAG2AA.Principle1.Guideline1_1.1_1_1.H67.2',
context: '<img alt="">',
message: 'Img element is marked so that it is ignored by Assistive Technology.',
selector: '#foo > img',
type: 'warning',

@@ -52,8 +52,8 @@ typeCode: 2

assert.deepEqual(this.lastJsonResponse[4], {
code: 'WCAG2AA.Principle1.Guideline1_3.1_3_1.H49.B',
context: '<b id="bar">bar</b>',
message: 'Semantic markup should be used to mark emphasised or special text so that it can be programmatically determined.',
selector: '#bar',
type: 'warning',
typeCode: 2
code: 'WCAG2AA.Principle1.Guideline1_1.1_1_1.G73,G74',
context: '<img alt="">',
message: 'If this image cannot be fully described in a short text alternative, ensure a long text alternative is also available, such as in the body text or through a link.',
selector: '#foo > img',
type: 'notice',
typeCode: 3
});

@@ -60,0 +60,0 @@ });

@@ -716,2 +716,446 @@ 'use strict';

describe('wait-for-element-state action', function() {
var action;
beforeEach(function() {
action = buildAction.allowedActions.find(function(allowedAction) {
return allowedAction.name === 'wait-for-element-state';
});
});
it('should have a name property', function() {
assert.strictEqual(action.name, 'wait-for-element-state');
});
it('should have a match property', function() {
assert.instanceOf(action.match, RegExp);
});
describe('.match', function() {
it('should match all of the expected action strings', function() {
assert.deepEqual('wait for .foo to be added'.match(action.match), [
'wait for .foo to be added',
undefined,
'.foo',
' to be',
'added'
]);
assert.deepEqual('wait for element .foo to be added'.match(action.match), [
'wait for element .foo to be added',
' element',
'.foo',
' to be',
'added'
]);
assert.deepEqual('wait for element .foo .bar to be added'.match(action.match), [
'wait for element .foo .bar to be added',
' element',
'.foo .bar',
' to be',
'added'
]);
assert.deepEqual('wait for .foo to be removed'.match(action.match), [
'wait for .foo to be removed',
undefined,
'.foo',
' to be',
'removed'
]);
assert.deepEqual('wait for element .foo to be removed'.match(action.match), [
'wait for element .foo to be removed',
' element',
'.foo',
' to be',
'removed'
]);
assert.deepEqual('wait for element .foo .bar to be removed'.match(action.match), [
'wait for element .foo .bar to be removed',
' element',
'.foo .bar',
' to be',
'removed'
]);
assert.deepEqual('wait for .foo to be visible'.match(action.match), [
'wait for .foo to be visible',
undefined,
'.foo',
' to be',
'visible'
]);
assert.deepEqual('wait for element .foo to be visible'.match(action.match), [
'wait for element .foo to be visible',
' element',
'.foo',
' to be',
'visible'
]);
assert.deepEqual('wait for element .foo .bar to be visible'.match(action.match), [
'wait for element .foo .bar to be visible',
' element',
'.foo .bar',
' to be',
'visible'
]);
assert.deepEqual('wait for .foo to be hidden'.match(action.match), [
'wait for .foo to be hidden',
undefined,
'.foo',
' to be',
'hidden'
]);
assert.deepEqual('wait for element .foo to be hidden'.match(action.match), [
'wait for element .foo to be hidden',
' element',
'.foo',
' to be',
'hidden'
]);
assert.deepEqual('wait for element .foo .bar to be hidden'.match(action.match), [
'wait for element .foo .bar to be hidden',
' element',
'.foo .bar',
' to be',
'hidden'
]);
});
});
it('should have a build method', function() {
assert.isFunction(action.build);
});
describe('.build(browser, page, options, matches)', function() {
var matches;
var options;
var page;
var returnedValue;
beforeEach(function() {
options = {
log: {
debug: sinon.spy()
}
};
page = {
evaluate: sinon.stub().callsArgWithAsync(2, null, true)
};
matches = 'wait for element .foo to be added'.match(action.match);
returnedValue = action.build({}, page, options, matches);
});
it('returns a function', function() {
assert.isFunction(returnedValue);
});
describe('returned function', function() {
beforeEach(function(done) {
returnedValue(done);
});
it('calls `page.evaluate` with a function and some action options', function() {
assert.calledOnce(page.evaluate);
assert.isFunction(page.evaluate.firstCall.args[0]);
assert.deepEqual(page.evaluate.firstCall.args[1], {
state: matches[4],
selector: matches[2]
});
});
it('logs that the program is waiting', function() {
assert.calledWith(options.log.debug, ' … waiting ("true")');
});
describe('evaluate function', function() {
var element;
var evaluateFunction;
beforeEach(function() {
evaluateFunction = page.evaluate.firstCall.args[0];
element = {};
global.document = {
querySelector: sinon.stub().returns(element)
};
});
afterEach(function() {
delete global.window;
});
describe('when the state action option is "added"', function() {
beforeEach(function() {
returnedValue = evaluateFunction({
state: 'added',
selector: '.foo'
});
});
afterEach(function() {
delete global.document;
});
it('selects an element with the given selector', function() {
assert.calledOnce(document.querySelector);
assert.calledWithExactly(document.querySelector, '.foo');
});
it('returns `true`', function() {
assert.isTrue(returnedValue);
});
});
describe('when the state action option is "removed"', function() {
beforeEach(function() {
returnedValue = evaluateFunction({
state: 'removed',
selector: '.foo'
});
});
afterEach(function() {
delete global.document;
});
it('selects an element with the given selector', function() {
assert.calledOnce(document.querySelector);
assert.calledWithExactly(document.querySelector, '.foo');
});
it('returns `true`', function() {
assert.isTrue(returnedValue);
});
});
describe('when the state action option is "added" but without element', function() {
beforeEach(function() {
element = false;
global.document = {
querySelector: sinon.stub().returns(element)
};
returnedValue = evaluateFunction({
state: 'added',
selector: '.foo'
});
});
afterEach(function() {
delete global.document;
});
it('selects an element with the given selector', function() {
assert.calledOnce(document.querySelector);
assert.calledWithExactly(document.querySelector, '.foo');
});
it('returns `false`', function() {
assert.isFalse(returnedValue);
});
});
describe('when the state action option is "removed" but without element', function() {
beforeEach(function() {
element = null;
global.document = {
querySelector: sinon.stub().returns(element)
};
returnedValue = evaluateFunction({
state: 'removed',
selector: '.foo'
});
});
afterEach(function() {
delete global.document;
});
it('selects an element with the given selector', function() {
assert.calledOnce(document.querySelector);
assert.calledWithExactly(document.querySelector, '.foo');
assert.isFalse(returnedValue);
});
});
describe('when the state action option is visible', function() {
beforeEach(function() {
element = {
offsetWidth: 10
};
global.document = {
querySelector: sinon.stub().returns(element)
};
returnedValue = evaluateFunction({
state: 'visible',
selector: '.foo'
});
});
afterEach(function() {
delete global.document;
});
it('selects an element with the given selector', function() {
assert.calledOnce(document.querySelector);
assert.calledWithExactly(document.querySelector, '.foo');
assert.isTrue(returnedValue);
});
});
describe('when the state action option is hidden and element exists', function() {
beforeEach(function() {
element = {
offsetWidth: 0,
getClientRects: sinon.stub().returns([{
bottom: 61,
height: 17,
left: 835.03125,
right: 849.609375,
top: 44,
width: 14.578125
}])
};
global.document = {
querySelector: sinon.stub().returns(element)
};
returnedValue = evaluateFunction({
state: 'hidden',
selector: '.foo'
});
});
afterEach(function() {
delete global.document;
});
it('selects an element with the given selector', function() {
assert.calledOnce(document.querySelector);
assert.calledWithExactly(document.querySelector, '.foo');
assert.isTrue(returnedValue);
});
});
describe('when the state action option is hidden and element is not visible', function() {
beforeEach(function() {
element = {
offsetWidth: 0,
getClientRects: sinon.stub().returns([])
};
global.document = {
querySelector: sinon.stub().returns(element)
};
returnedValue = evaluateFunction({
state: 'hidden',
selector: '.foo'
});
});
afterEach(function() {
delete global.document;
});
it('selects an element with the given selector', function() {
assert.calledOnce(document.querySelector);
assert.calledWithExactly(document.querySelector, '.foo');
assert.isFalse(returnedValue);
});
});
});
});
describe('when `page.evaluate` calls back with a result that doesn\'t match the expected value', function() {
beforeEach(function(done) {
sinon.stub(global, 'setTimeout').yieldsAsync();
page.evaluate.callsArgWithAsync(2, null, false);
page.evaluate.onCall(2).callsArgWithAsync(2, null, true);
returnedValue(done);
});
afterEach(function() {
global.setTimeout.restore();
});
it('sets a timeout', function() {
assert.called(global.setTimeout);
assert.isFunction(global.setTimeout.firstCall.args[0]);
assert.strictEqual(global.setTimeout.firstCall.args[1], 200);
});
it('calls page.evaluate until it calls back with the expected value', function() {
assert.calledThrice(page.evaluate);
assert.calledThrice(options.log.debug);
assert.calledWith(options.log.debug.firstCall, ' … waiting ("false")');
assert.calledWith(options.log.debug.secondCall, ' … waiting ("false")');
assert.calledWith(options.log.debug.thirdCall, ' … waiting ("true")');
});
});
describe('when `page.evaluate` times out', function() {
var caughtError;
beforeEach(function(done) {
sinon.stub(global, 'setTimeout').yieldsAsync();
page.evaluate.callsArgWithAsync(2, null, false);
page.evaluate.onCall(11).callsArgWithAsync(2, null, true);
returnedValue(function(error) {
caughtError = error;
done();
});
});
afterEach(function() {
global.setTimeout.restore();
});
it('throws an error after 10 retries', function() {
assert.strictEqual(caughtError.message, 'Failed action: element ".foo" failed to be added');
});
});
});
describe('.build(browser, page, options, matches)', function() {
var matches;
var options;
var page;
var returnedValue;
beforeEach(function() {
options = {
log: {
debug: sinon.spy()
}
};
page = {
evaluate: sinon.stub().callsArgWithAsync(2, null, true)
};
matches = 'wait for element .foo to be visible'.match(action.match);
returnedValue = action.build({}, page, options, matches);
});
describe('when `page.evaluate` is called and state is added and result is true', function() {
beforeEach(function(done) {
sinon.stub(global, 'setTimeout').yieldsAsync();
page.evaluate.callsArgWithAsync(2, null, true);
returnedValue(done);
});
afterEach(function() {
global.setTimeout.restore();
});
it('does not wait', function() {
assert.isFalse(global.setTimeout.called);
});
});
});
});
describe('wait-for-url action', function() {

@@ -765,2 +1209,9 @@ var action;

]);
assert.deepEqual('wait for host to be example.com'.match(action.match), [
'wait for host to be example.com',
'host',
' to be',
undefined,
'example.com'
]);
assert.deepEqual('wait for url to be https://example.com/'.match(action.match), [

@@ -801,2 +1252,9 @@ 'wait for url to be https://example.com/',

]);
assert.deepEqual('wait for host to not be example.com'.match(action.match), [
'wait for host to not be example.com',
'host',
' to not be',
'not ',
'example.com'
]);
});

@@ -927,2 +1385,16 @@

describe('when the subject action option is "host"', function() {
beforeEach(function() {
returnedValue = evaluateFunction({
subject: 'host'
});
});
it('returns `window.location.host`', function() {
assert.strictEqual(returnedValue, global.window.location.host);
});
});
});

@@ -929,0 +1401,0 @@

@@ -29,3 +29,14 @@ Troubleshooting

### PhantomJS fails to run and returns exit code 127
Pa11y spins up a PhantomJS instance to render the page, then injects HTML_Codesniffer into it and returns a report based on its findings. If PhantomJS cannot run due to broken executable, missing libraries or dependencies, or any other internal error that prevents it from running at all, it will return an exit code of 127.
If this happens, running the phantomjs executable will usually provide more useful information, for example:
```
./node_modules/phantomjs/lib/phantom/bin/phantomjs: error while loading shared libraries: libfontconfig.so.1: cannot open shared object file: No such file or directory
```
We don't maintain PhantomJS, but there's a good chance someone has found the same problem before, so you can try to search in the PhantomJS repo for a solution. For example, for the error above, the fix would be to install the missing library, as shown in [phantomjs#10904](https://github.com/ariya/phantomjs/issues/10904) and [phantomjs#13597](https://github.com/ariya/phantomjs/issues/13597).
Common Questions

@@ -32,0 +43,0 @@ ----

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc