Saturday 6 May 2017

Selenium and SalesforceDX Scratch Orgs

Selenium and SalesforceDX Scratch Orgs

Screen Shot 2017 05 06 at 13 57 48

Introduction

Like a lot of other Salesforce developers I use Selenium from time to time to automatically test my Visualforce pages and Lightning Components. Now that I’m on the SalesforceDX pilot, I need to be able to use Selenium with scratch orgs. This presents a slight challenge, in that Selenium needs to open the browser and login to the scratch org rather than the sfdx cli. Wade Wegner’s post on using scratch orgs with the Salesforce Workbench detailed how to get set a scratch org password so I started down this route before realising that there’s a simpler way, based on the sfdx force:org:open command. Executing this produces the output:

Access org <org_id> as user <username> with the following URL: https://energy-agility-9184-dev-ed.cs70.my.salesforce.com/secur/frontdoor.jsp?sid=<really long sid>

so I can use the same mechanism once I have the URL and sid for my scratch org which, as Wade’s post pointed out, I can get by executing sfdx force:org:describe. Even better, I can get this information in JSON format, which means I can easily process it in a Node script. Selenium also has a Node web driver so the whole thing comes together nicely.

In the rest of this post I’ll show how to create a Node script that gets the org details programmatically, opens a Chrome browser, opens a page that executes some JavaScript tests and figures out whether the tests succeeded or not. The instructions are for MacOS as that is my platform of choice.

Setting Up

In order to control the chrome browser from Selenium you need to download the Chrome Webdriver and add it to your system PATH - I have a generic tools directory that is already on my path so I downloaded it there. 

Next, clone the github repository by executing:

 git clone https://github.com/keirbowden/dxselenium.git

The Salesforce application is based on my Unit Testing Lightning Components with Jasmine talk from Dreamforce 16. You probably want to update the config/workspace-scratch-def.json file to change the company detail etc to your own information. 

Setting up the Scratch Org

Change to the cloned repo directory:

cd dxselenium

Then login to your dev hub:

sfdx force:auth:web:login --setdefaultdevhubusername --setalias my-hub-org

 and create a scratch org  - to make life easier I set the —setdefaultusername parameter so I don’t have to specify the details on future commands.

sfdx force:org:create --definitionfile config/workspace-scratch-def.json --setalias LCUT —setdefaultusername

Finally for this section, push the source:

sfdx force:source:push

Setting up Node

(Note that I’m assuming here that you have node installed).

Change to the node client directory:

cd node

Get the dependencies:

npm install

Executing the Test

Everything is now good to go, so execute the Node script that carries out the unit tests:

node ltug.js

You should see a chrome browser starting and the Node script producing the following output:

Getting org details
Logging in
Opening the unit test page
Running tests
Checking results
Status = Success

The script exits after 10 seconds to give you a chance to look at the page contents if you are so inclined. 

The Chrome browser output can be viewed on youtube:

Show me the Node

The Node script is shown below:

var child_process=require('child_process');

var webdriver = require('selenium-webdriver'),
By = webdriver.By,
until = webdriver.until;

var driver = new webdriver.Builder()
.forBrowser('chrome')
.build();

var exitStatus=1;

console.log('Getting org details');
var orgDetail=JSON.parse(child_process.execFileSync('sfdx', ['force:org:describe', '--json']));
var instance=orgDetail.instanceUrl;
var token=orgDetail.accessToken;
console.log('Logging in');
driver.get(instance + '/secur/frontdoor.jsp?sid=' + token);
driver.sleep(10000).then(_ => console.log('Opening the unit test page'));
driver.navigate().to(instance + '/c/JobsTestApp.app');
driver.sleep(2000).then(_ => console.log('Running tests'));
driver.findElement(By.id('slds-btn')).click();
driver.sleep(2000).then(_ => console.log('Checking results'));

driver.findElement(By.id("status")).getText().then(function(text) {
    console.log('Status = ' + text);
    if (text==='Success') {
    exitStatus=0;
    }
});
driver.sleep(10000);
driver.quit().then(_ => process.exit(exitStatus));

After the various dependencies are setup, the org details are retrieve via the sfdc force:org:describe command:

var orgDetail=JSON.parse(child_process.execFileSync('sfdx', ['force:org:describe', '--json']));

From the deserialised orgDetail object, the instance URL and access code are extracted:

var instance=orgDetail.instanceUrl;
var token=orgDetail.accessToken;

And then the testing can begin. Note that the Selenium web driver is promise based, but also provides a promise manager which handles everything when using the Selenium API. In the snippet below the driver.sleep won’t execute until the promise returned by the driver.get function has succeeded.

driver.get(instance + '/secur/frontdoor.jsp?sid=' + token);
driver.sleep(10000).then(_ => console.log('Opening the unit test page'));

However, when using non-Selenium functions, such as logging some information to the console, the promise manager isn’t involved so I need to manage this myself by supplying a function to be executed when the promise succeeds, via the then() method.

Note that I’ve added a number of sleeps as I’m testing this on my home internet which is pretty slow over the weekends.

The script then opens my test page and clicks the button to run the tests:

driver.navigate().to(instance + '/c/JobsTestApp.app');
driver.sleep(2000).then(_ => console.log('Running tests'));
driver.findElement(By.id('slds-btn')).click();

Finally, it locates the element with the id of status and checks that the inner text contains the word ‘Success’ - note that again I have to manage the promise result as I’m outside the Selenium API.

driver.findElement(By.id("status")).getText().then(function(text) {
    console.log('Status = ' + text);
    if (text==='Success') {
    exitStatus=0;
    }
});

Related Posts