-
Notifications
You must be signed in to change notification settings - Fork 5
Scripting guide (with examples)
Before you start, have a look at the page with common scripting mistakes. More knowledge, less frustration!
- Setting a timeout for the entire script
- Logging
- Transactions
- Tabs
- Assertions
- Events
- Wait
- Simulating input events (mouse / keyboard)
- Blocking HTTP requests
- Modifying HTTP Requests
As a fail safe, each script has a default timeout of 60 seconds. (note that a lot of commands such as navigate
and wait
also have their own timeouts)
'Openrunner-Script: v1';
'Openrunner-Script-Timeout: 120s';
The normal javascript logging functions can be used to help debug your script. You can find these messages in "Tools" -> "Web Developer" -> "Browser Console" (⌘⇧J).
'Openrunner-Script: v1';
console.log('Hello!');
A transaction block is used in a script to record a single "stopwatch" measurement. After a script run, each transaction reports a start time and a duration. The script commands that are placed within a transaction block effect the duration of that transaction. A transaction might also report an error (when one of the script commands fails).
Each transaction must be assigned an unique identification string (for example, these unique ID's could be used to store all of the measurements in a database). This ID should usually not be changed if you update your script, unless the body of your transaction has been changed so much that it could be considered a different kind of measurement. Instead, a title
may optionally be set and continuously modified. Reporting tools that integrate with Openrunner should use the id
to store measurements in their databases and use the title
to represent the transaction to the end-user.
The result of all transactions end up in the script result json document.
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
const tab = await tabs.create();
// In this example, "HomePage" is the transaction ID
await transaction('HomePage', async t => {
t.title = '00 Open the home page';
await tab.navigate('http://computest.nl/', {timeout: '10s'});
await tab.wait(async () => {
await wait.documentComplete()
.selector('#layerslider_1 p.slider-text')
.isDisplayed();
});
});
// By default, if an error is encountered during a transaction (for
// example navigating to an url times out), your script will stop.
// By setting `eatError` to true, only the transaction itself will
// fail and the script will continue with the next transactions:
await transaction('TrySomething', async t => {
t.eatError = true;
await tab.navigate('http://badserver.example', {timeout: '2s'});
});
// This transaction will always run, even if 'TrySomething' fails:
await transaction('NextTransaction', async t => {
await tab.navigate('http://computest.nl', {timeout: '2s'});
});
The "tabs" module is used to create new tabs, to navigate them to an URL, to run scripts within the tab and to wait until the content of a tab has reached a certain condition.
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
// tabs.create():
// Create a new tab (another tab may be created by calling this function a
// second time)
const tab = await tabs.create();
// tab.navigate():
// Navigate the tab to the given URL, as if a user entered something in the
// address bar. This function will wait until the HTML begins to load, if
// this takes longer than 10 seconds, an error is thrown. (the default
// timeout is 30s)
await tab.navigate('http://computest.nl/', {timeout: '10s'});
// tab.run():
// Run the given function within the content of the tab and wait until the
// function is done. Note: if the tab navigates to a new page before the
// function is done, an error will be thrown
await tab.run(async () => {
console.log('The url is:', document.location.href)
});
// tab.wait():
// Run the given function within the content of the tab and wait until the
// function is done. However if the tab navigates to a new page before the
// function is done, execute the function again on the new page.
// `tab.wait()` (instead of `tab.run()`) is recommended when you are
// waiting for a DOM element to be present because any redirects performed
// by the web page will be handled transparently
await tab.wait(async () => {
await wait.selector('.cookieConsentButton');
});
// tab.waitForNewPage():
// Run the given function within the content of the tab and wait until the
// tab has navigated to a different page.
// This function is useful if you are clicking on a regular link and you
// would like to wait until the next page is starting to load
await tab.wait(async () => { // first find something we would like to click on
cookieButton = await wait.selector('.cookieConsentButton');
});
await tab.waitForNewPage(async () => { // then perform the click
cookieButton.click();
});
// It is also possible to pass data to your function:
await tab.run(async ({foo}) => {
console.log('foo is:', foo);
}, {foo: 'bar baz'});
// Or, to return data from your function:
const bodyClass = await tab.run(async () => {
return document.body.className;
});
console.log('body class is', bodyClass);
// Using the tabs.frame() method, you can run scripts in any iframe. This
// even works cross-origin iframes
await tab.run(async () => {
const iframe = await wait.documentInteractive().selector('iframe#my-frame');
const frame = await tabs.frame(iframe, {timeout: '10s'});
await frame.run(async () => {
console.log('The src of the iframe is:', document.location.href)
});
await frame.wait(async () => {
cookieButton = await wait.selector('.cookieConsentButton');
});
await frame.waitForNewPage(async () => {
cookieButton.click();
});
});
The chaijs library is included to perform assertions with. The expect and assert styles are available. A few useful plugins are also included: chai-subset, chai-as-promised, chai-dom.
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
const assert = await include('assert'); // http://chaijs.com/api/assert/
assert.deepEqual({foo: 123}, {foo: 123});
const tab = await tabs.create();
await tab.navigate('http://computest.nl/', {timeout: '10s'});
await tab.wait(async () => {
await wait.documentComplete();
assert.include(document.body.textContent, 'performance');
});
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
const expect = await include('expect'); // http://chaijs.com/api/bdd/
expect({foo: 123}).to.deep.equal({foo: 123});
const tab = await tabs.create();
await tab.navigate('http://computest.nl/', {timeout: '10s'});
await tab.wait(async () => {
await wait.documentComplete();
expect(document.body.textContent).to.include('performance');
});
During a script run, all sorts of events about the script itself and the page are logged. These events end up in the script result json. All events have a name, begin time, end time and meta data. Examples of events include: performed script commands, http requests, document ready state progression, slow render frames. Many events are not logged by default. A special module has to be activated first
If this module is enabled various events generated by the "content" (the web page) are logged. For now only the following events are logged:
- document readyState progression (navigation start, interactive, complete)
- Slow animation frames, these events usually indicate that the rendering is blocked by slow javascript code on the web page Other events from content will be added in future releases
'Openrunner-Script: v1';
await include('contentEvents');
// ...
If this module is enabled, all http requests generated by the web page are logged. Note that the current implementation is not yet entirely accurate because of a missing feature in firefox, see Issue #3.
'Openrunner-Script: v1';
await include('httpEvents');
// ...
If this module is enabled, a screenshot will be taken if the script run stops because of an error. It also lets you take screenshots manually.
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
const screenshot = await include('screenshot');
const tab = await tabs.create();
await tab.navigate('http://computest.nl/', {timeout: '10s'});
await tab.wait(async () => {
await wait.documentComplete();
});
await screenshot.take('A comment describing my pretty screenshot');
If this module is enabled, element mutations in the DOM on the web page will be logged. This module is intended as a debugging aid, to figure out how the page is modified by its various scripts. It causes a lot of overhead, so it should not be used during benchmarks!
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
await include('contentEvents');
await include('mutationEvents');
const tab = await tabs.create();
await tab.navigate('http://computest.nl/', {timeout: '10s'});
await tab.wait(async () => {
await wait.documentComplete();
// wait a while to figure out if the page is modified further
// after documentComplete
await wait.delay('10s');
});
The "wait" module lets you wait for various conditions on the web page, such as the loading state of the web page, the presence or absence of DOM elements/attributes, the loading of images, etc.
When using this module you construct a wait "expression", which consists of various actions chained together. The script run (and your transaction) is paused until all the actions have been completed.
A complex example:
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
const tab = await tabs.create();
await tab.navigate('http://computest.nl/', {timeout: '10s'});
await transaction('HomePage', async t => {
await tab.wait(async () => {
// 1. wait until document has a readyState of "complete" (All
// synchronous resources of the HTML document have been loaded)
// 2. Wait for the given CSS Selector to return a result
// 3. Wait for the result from step 2 to contain the given text
// 4. Wait for the result from step 2 to be displayed on the screen
await wait.timeout('10s')
.documentComplete()
.selector('#layerslider_1 p.slider-text')
.containsText('Hoe snel zijn je systemen')
.isDisplayed();
});
});
The wait module supports the following actions:
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.documentComplete(); // 30 second timeout
await wait.timeout('1s').documentComplete(); // 1 second timeout
await wait.timeout(1500).documentComplete(); // 1.5 second timeout
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') // if .target() is not used, this is the default
await wait.target(document).documentInteractive()
await wait.target(document.body).check(body => body.classList.contains('foo'))
await wait.target([someElement, anotherElement]).selectorAll('p')
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.selectorAll('img.thumbnail') // 1 or more
await wait.selectorAll('img.thumbnail').amount(1, Infinity) // 1 or more
await wait.selectorAll('img.thumbnail').amount(1) // exactly 1
await wait.selectorAll('img.thumbnail').amount(0) // exactly 0
await wait.selectorAll('img.thumbnail').amount(2, 4) // 2 or 3 or 4
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.selector('a.someLink');
link.click();
const anotherLink = await wait.target([someElement, anotherElement]).selector('a.someLink');
anotherLink.click();
const needsSomeEscaping = '#"b[l]a'
await wait.selector`div[data-foo=${needsSomeEscaping}]`;
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.selectorAll('.todoList a');
links.forEach(link => link.click());
const needsSomeEscaping = '#"b[l]a'
await wait.selectorAll`div[data-foo=${needsSomeEscaping}]`;
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.xpath(`.//h1[./@id = 'introduction']`);
await wait.target(document.body).xpath(`./p`);
Note: XPath expressions often cause more overhead than CSS Selectors.
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.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.
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();
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();
This action causes the execution to remain pending until the given duration
has passed (since the start of the execution).
await wait.delay('2s'); // 2000ms
await wait.delay(2000); // 2000ms
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);
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)
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);
The "eventSimulation" module lets you simulate user input (DOM Events). Such as focusing an input element or clicking on links. Currently, two functions are available, but more will be added in future versions:
Click on a regular link, or any kind of dynamic button. The left mouse button will be held down for a few centiseconds.
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
await include('eventSimulation');
const tab = await tabs.create();
await transaction('HomePage', async t => {
await tab.navigate('http://computest.nl/', {timeout: '10s'});
await tab.waitForNewPage(async () => {
const links = await wait.selectorAll('#menu-main-header-menu > li > a').containsText('Actueel').amount(1);
await eventSimulation.click(links[0]);
});
});
Send keyboard events to the DOM and simulate that a user is entering keyboard keys into a textual form control using the keyboardTextInput()
function. This function can only be used on elements that are able to receive text input, such as <textarea>
, <input type=text>
and <div contenteditable=true></div>
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
await include('eventSimulation');
const tab = await tabs.create();
await transaction('HomePage', async t => {
await tab.navigate('https://www.computest.nl/contact/', {timeout: '10s'});
await tab.run(async () => {
const nameInput = await wait.selector('textarea[name=your-message]');
await eventSimulation.keyboardTextInput(nameInput, [
...'Hallo!',
'Enter',
...'Wereld',
]);
});
});
Send keyboard events to the DOM using the keyboardKeys()
function. This function does not update the value of any input controls! It is only useful if there is any javascript on the page that acts upon input values. However, unlike keyboardTextInput()
, it may be used on ANY element.
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
await include('eventSimulation');
const tab = await tabs.create();
await transaction('HomePage', async t => {
await tab.navigate('http://localhost', {timeout: '10s'});
await tab.run(async () => {
const fancyJSWidget = await wait.selector('div.fancyJSWidget');
await eventSimulation.keyboardKeys(fancyJavascriptWidget, [
...'Hallo!',
'Enter',
...'Wereld',
]);
});
});
Focus an element, even if the browser tab itself is out of focus.
'Openrunner-Script: v1';
const tabs = await include('tabs');
await include('wait');
await include('eventSimulation');
const tab = await tabs.create();
await transaction('HomePage', async t => {
await tab.navigate('https://www.computest.nl/contact/', {timeout: '10s'});
await tab.run(async () => {
const nameInput = await wait.selector('textarea[name=your-message]');
await eventSimulation.focus(nameInput);
document.activeElement.value += 'Hallo!'
});
});
The "requestBlocking" module lets you block HTTP requests using a matching expression (wildcards). It functions very similar as ad-blockers. More information about how to match URLs can be found here: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns
'Openrunner-Script: v1';
const tabs = await include('tabs');
const wait = await include('wait');
const requestBlocking = await include('requestBlocking');
const tab = await tabs.create();
await requestBlocking.block([
'*://*.google-analytics.com/*',
'https://www.computest.nl/wp-content/uploads/2016/05/computest-logo-2016.png',
]);
await transaction('HomePage', async t => {
await tab.navigate('http://computest.nl/');
});
await wait.delay('5s');
The "requestModification" module lets you add/change/remove HTTP request/response headers. More information about how to match URLs can be found here: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Match_patterns
'Openrunner-Script: v1';
const tabs = await include('tabs');
const requestModification = await include('requestModification');
const tab = await tabs.create();
await requestModification.modifyRequestHeaders(['https://*.duckduckgo.com/*'], {
'X-Foo': 'the foo header!', // Add a new header
'User-Agent': 'Mozilla/5.0 Lizard/20100101 Vuurvosje/140.0', // Modify a header
'Accept-Encoding': null, // Remove a header
});
await tab.navigate('https://duckduckgo.com/?q=user+agent&t=ffsb&ia=answer');
'Openrunner-Script: v1';
const tabs = await include('tabs');
const requestModification = await include('requestModification');
const tab = await tabs.create();
await requestModification.modifyResponseHeaders('<all_urls>', {
// disable caching
'Cache-Control': 'no-cache, no-store, must-revalidate',
'X-Foo': 'Bar!',
});
await tab.navigate('https://www.computest.nl/');