Bluefox
The bluefox library lets you be notified when a DOM tree has reached a specific condition, using a convenient syntax. It has been designed for high accuracy and low overhead.
The functionality is similar to the wait functions found in many WebDriver/Selenium client libraries. Unlike most of those libraries, bluefox does not employ periodic polling. Also, instead of sending many commands over the network during the wait, the resolving of wait conditions in bluefox takes place entirely in the browser. This means that the moment the wait condition resolves is a lot closer to the actual change.
The overhead that this library introduces to the page being tested is kept as low as possible.
Examples
npm i bluefox
Webpack / Browserify / node.js (jsdom)
const Bluefox = require('bluefox');
const wait = new Bluefox().target(window);
wait.timeout('5s').selector('section#main > div.contactInformation > a.viewProfile').then(link => {
link.click();
}).catch(err => {
console.error('uh o', err);
});
HTML Document
<!DOCTYPE html>
<html>
<head>
<title>Hi!</title>
<script src="node_modules/bluefox/standalone.js"></script>
<script>
(async () => {
console.log(new Date(), 'Waiting...');
const wait = new Bluefox().target(window);
const element = await wait.timeout('5s').selector('#foo > strong');
element.textContent = 'wereld!!';
console.log(new Date(), 'Done!');
})();
</script>
</head>
<body>
<div id="foo">
Hello
</div>
<script>
setTimeout(() => {
foo.insertAdjacentHTML(
'beforeend',
'<strong>world</strong>'
)
}, 1000);
</script>
</body>
WebDriver
const bluefoxString = require('bluefox/standalone.string.js');
const wd = require('wd');
const browser = wd.promiseRemote('http://localhost:9515');
(async () => {
await browser.init({pageLoadStrategy: 'none'});
await browser.setAsyncScriptTimeout(30000);
await browser.get(`http://example.com`);
await browser.execute(bluefoxString);
await browser.execute(`wait = new Bluefox().target(window).timeout('10s')`);
const result = await browser.executeAsync(`
const resolve = arguments[arguments.length - 1];
wait.documentComplete().selector('p > a[href]')
.then(() => resolve({okay: true}))
.catch(err => resolve({error: err.toString()}))
`);
console.log('result:', result);
})();
API
First, this library must by instantiated by calling its constructor:
const Bluefox = require('bluefox');
const wait = new Bluefox();
Wait conditions are then defined by chaining together actions
to form an expression
. An expression
is executed by consuming it as a Promise (expression.then(...)
, await expression
, etc).
During execution the current value
is consumed and/or modified by the actions that make up the expression; the output of an action is used as the input of the next action. The current value
must be either:
null
- A
WindowProxy
instance - A
HTMLDocument
instance - An
Element
instance - An array of
WindowProxy
, HTMLDocument
, Element
instances
For example the expression await wait.target(document.body).selector('div').selector('p')
returns the first <p>
element that is a descendant of the first <div>
element that is the first descendant of the document's <body>
element.
Some actions may cause the execution to remain pending based on their input value. For example await wait.target(document).documentInteractive()
delays the fulfillment of the execution until the HTML of the document has been completely parsed (DOMContentLoaded).
An expression is immutable. This lets you execute it multiple times or base a new expression of an existing one.
Available actions
.timeout(duration)
This action sets the timeout for all subsequent actions. If the timeout duration is reached, the Promise of the execution
rejects with an Error
. If this action is not specified, a default timeout of 30 seconds is used.
await wait.target(document).documentComplete();
await wait.target(document).timeout('1s').documentComplete();
await wait.target(document).timeout(1500).documentComplete();
The error object contains the following properties:
const expression = await wait.target(document).timeout('1.2s').documentComplete();
const fullExpression = expression.selector('body');
try {
await fullExpression;
}
catch (err) {
console.log(err.name);
console.log(err.message);
console.log(err.actionFailure);
console.log(err.timeout);
console.log(err.expression === expression);
console.log(err.fullExpression === fullExpression);
}
.target(value)
The target action is used to simply set the current value
. It is almost always used as the first action in the chain.
await wait.target(window).selector('html')
await wait.target(document).documentInteractive()
await wait.target(document.body).check(body => body.classList.contains('foo'))
await wait.target([someElement, anotherElement]).selectorAll('p')
.amount(minimum, maximum)
This action causes the execution to remain pending if the current value
has less than minimum
or more than maximum
objects. If this action is not specified, the execution waits until current value
contains 1 or more objects. The current value
is not modified by this action.
await wait.target(window).selectorAll('img.thumbnail')
await wait.target(window).selectorAll('img.thumbnail').amount(1, Infinity)
await wait.target(window).selectorAll('img.thumbnail').amount(1)
await wait.target(window).selectorAll('img.thumbnail').amount(0)
await wait.target(window).selectorAll('img.thumbnail').amount(2, 4)
.selector(cssSelector)
Return the first Element
(or null
) that matches the given CSS selector and is a descendant of any objects in current value
.
const link = await wait.target(window).selector('a.someLink');
link.click();
const anotherLink = await wait.target([someElement, anotherElement]).selector('a.someLink');
anotherLink.click();
const needsSomeEscaping = '#"b[l]a'
await wait.target(window).selector`div[data-foo=${needsSomeEscaping}]`;
.selectorAll(cssSelector)
Return all Element
instances (as an array) that match the given CSS selector and are a descendant of any objects in current value
.
const links = await wait.target(window).selectorAll('.todoList a');
links.forEach(link => link.click());
const needsSomeEscaping = '#"b[l]a'
await wait.target(window).selectorAll`div[data-foo=${needsSomeEscaping}]`;
.xpath(expression)
Execute the given XPath expression
setting the current value
as the XPath context node
and return the first Element
instance that matches. A result that is not an Element
will result in an error.
await wait.target(window).xpath(`.//h1[./@id = 'introduction']`);
await wait.target(document.body).xpath(`./p`);
Note: XPath expressions often cause more overhead than CSS Selectors.
.xpathAll(expression)
Execute the given XPath expression
setting the current value
as the XPath context node
and return the all Element
instances that match. A result that is not an Element
will result in an error.
await wait.target(window).xpathAll(`.//a[@href]`).amount(10, Infinity);
await wait.target(document.body).xpathAll(`./p`).amount(0);
Note: XPath expressions often cause more overhead than CSS Selectors.
.documentInteractive()
This action causes the execution to remain pending if any of the HTMLDocument
instances in current value
have a readyState
that is not "interactive"
nor "complete"
. If the current value
contains any Element
instances, the check will be performed on their ownerDocument
. The current value
is not modified by this action.
window.addEventListener('DOMContentLoaded',
() => console.log('documentInteractive!')
);
await wait.target(document).documentInteractive();
await wait.target(document.body).documentInteractive();
.documentComplete()
This action causes the execution to remain pending if any of the HTMLDocument
instances in current value
have a readyState
that is not "complete"
. If the current value
contains any Element
instances, the check will be performed on their ownerDocument
. The current value
is not modified by this action.
window.addEventListener('load',
() => console.log('documentComplete!')
);
await wait.target(document).documentComplete();
await wait.target(document.body).documentComplete();
.delay(duration)
This action causes the execution to remain pending until the given duration
has passed (since the start of the execution).
await wait.delay('2s');
await wait.delay(2000);
.isDisplayed()
This action removes all elements from current value
that are not currently displayed on the page. An element is "displayed" if it influences the rendering of the page. (Specifically, element.getBoundingClientRect()
returns a non zero width
and height
).
await wait.selector('.submitButton').isDisplayed()
await wait.selectorAll('.gallery img').isDisplayed().amount(10, 50);
.check(callback)
This action calls the given callback
function for each item in current value
and removes all items for which the callback returns false
.
await wait.selector('p.introduction').check(n => /hello/.test(n.textContent))
await wait.selector('img').check(n => n.complete).amount(10, Infinity)
.containsText(text)
This action removes all elements from current value
for which the textContent
does not contain the given text
.
await wait.selector('p.introduction').containsText('hello')
await wait.selector('p.introduction').containsText(/hello/)
await wait.selectorAll('p').containsText('ipsum').amount(10, 50);
.first()
This action returns the first element (index 0) from current value
const oneImage = await wait.selectorAll('.gallery img').isDisplayed().first();
const button = await wait.selectorAll('button').check(n => /Confirm Order/i.test(n.textContent)).first()