selenium-webdriver

This post is a tutorial for writing Selenium tests in a pointfree style. This should reduce the size of your user experience tests, because writing them in a pointfree style rids the test-code of most variables and “awaits” (aysnc/await). Here’s an example of what I mean.

test('User can login', () => S.seq(

S.call('get', ROOT_URL),

S.click('[data-go="login"]'),

S.fillForm(forms.login, user.credentials),

S.click('button[type="submit"]'),

)(opts))

“push button print post” by Ashim D’Silva on Unsplash

Selenium-webdriver and Node.js

I use Selenium-Webdriver and Jest to write automated browser tests. I won’t go into detail about that, but if you search for “selenium-webdriver jest” or “selenium-webdriver mocha” you’ll find some good blog posts that describe how to use the two together in more detail. In general, Jest or Mocha is the test runner, and selenium-webdriver is used within the tests to interact with the browser. The code below is taken from one of those blog posts.

test('01 Drums Access Await', async function () {

await driver.get("https://andreidbr.github.io/JS30/");

const drumsLink = await

driver.findElement(By.xpath('/html/body/div[2]/div[1]'));

await drumsLink.click();

const pageTitle = await driver.getTitle();

await assert.equal(pageTitle, "JS30: 01 Drums");

}

Pointfree-style and function composition

Function composition works by combining a sequence of function calls into one function. There are utility libraries like Ramda that contain helpers to combine functions. These helpers are generally called compose or pipe/sequence.

const log => txt => () => console.log(txt)

const name = { first: 'Bob', last: 'Cobb' } // Composed functions run last to first

let logName = compose(log(name.first), log(', '), log(name.last))

logName() // prints "Cobb, Bob" // Piped or sequenced functions run first to last

logName = pipe(log(name.first), log(' '), log(name.last))

logName() // prints "Bob Cobb"

Pointfree is a style of coding that is possible through function composition. The example above doesn’t show it, but composed functions receive the result of the previous function as the sole argument. Only the first function to run can receive multiple arguments.

const add = x => y => x + y

const add7 = compose(add(2), add(5)) const value = add7(10)

console.log(value) // prints "17"

Above, the first function to run is “add(5)”. It receives 10 as it’s argument, and the result is 15. Then, “add(2)” receives 15 as it’s argument, and the final result is 17. It’s called pointfree, because we never have to store 15 in a variable inside our own code.

Async sequence

Now, how can we compose async functions? There are libraries for doing it, but the code to accomplish it is simple, if you’re using node version 8 or above.

const seq = (...fns) => async function seq (...args) {

if (!fns.length) return

let result = await fns[0](...args)

for (let i = 1; i < fns.length; i++) {

result = await fns[i](result)

}

return result

}

Instead of just passing the result, we can tweak this “seq” function so that a context object gets passed from function to function. This would make it possible for each function to access configuration settings and state, along with the results of previous functions.

const seq = (...fns) => async function seq (ctx) {

for (let i = 0; i < fns.length; i++) {

ctx = Object.assign(ctx, await fns[i](ctx))

}

return ctx

}

Using with Selenium

The last step is to start wrapping the Selenium-Webdriver API into functions that can be inserted into the async pipeline. Let’s group these helper functions into a utility library called “selenium-webdriver-fp”. Here is what it looks like to rewrite the code from above using our fp helper library.

const S = require('selenium-webdriver-fp') test('01 Drums Access Await', () => S.seq(

S.call('get', 'https://andreidbr.github.io/JS30/'),

S.click(By.xpath('/html/body/div[2]/div[1]')),

S.call('getTitle'),

ctx => { assert.equal(ctx.callResult, 'JS30: 01 Drums') }

)({ driver }))

A few things to note about the helper functions:

The composed function, the result of S.seq, must be passed an object with a “driver” property, which is the selenium-webdriver instance.

When run, all functions are called with context as the only argument

When configuring the helpers, all arguments can be a value or a function. If a function, it receives context and returns the value.

If the selector is a string, it is assumed it’s a CSS selector.

Context has some keywords. ‘callResult’ is the value from the last “S.call” invocation, “element” and “selector” are the values from the last calls to “S.getElement”.

Selenium-webdriver-fp