Doug
Doug

Reputation: 1515

Selenium (Node.js) screenshots are too slow

I am using Selenium + Mocha (in Node.js) to do some UI testing on a site. After each test, I want to capture a screen shot so that I can review/share the visible results.

However, after the basic UI changes are made (i.e. insert text and hit submit), there is an AJAX call + JavaScript animation that triggers after that I would like to be part of my screen shot.

The screenshots I'm getting appear to be in taken after the next step has already started. (So, the first test runs > trigger the screenshot > second test starts > screenshot is actually captured)

I've tried .sleep() and timeouts -- however my use could have been wrong.

function takeScreenshot(name='out'){
      driver.takeScreenshot().then(function(data){
        var base64Data = data.replace(/^data:image\/png;base64,/,"")
        fs.writeFile(`screenshots/${name}.png`, base64Data, 'base64', function(err) {
          if(err) console.log(err);
        });
      });
}

describe('UI Tests', async function() {

  before(async function() {
    driver = await new Builder().forBrowser('chrome').build();
    await driver.get('http://localhost:8000').then( async function(){ await getAssets(); });
    /* search | submit | reset are declared in getAssets() */
  });

  after(async function() {
    driver.quit();
  });

  afterEach(async function(){
    step++;
    driver.sleep(3000).then(
      takeScreenshot(step) 
    );
  });

  describe('User Types/Modifies Input and Submits Query', async function() {

    it('type in search + click submit', async function(){
        search.sendKeys("Puppies and Kittens");
        submit.click();
    });

    it('click the X icon to clear search', async function(){
        reset.click();
    });

  });

});

In the above snippet, the screenshot for the first test ('type in search + click submit') would show the input field already cleared from the second test.

Upvotes: 1

Views: 1495

Answers (1)

Matt
Matt

Reputation: 74620

The commands being sent to selenium are asynchronous, meaning a function will schedule the work to run at some point in the future but return immediately so the code moves on.

In selenium-webdrivers case, the functions return a "promise" that will resolve at some point in the future. Mocha can use a promise to wait for the result.

Promises and Mocha

Mocha uses the return value of a test to determine if it should wait for a promise to resolve. You can control promise flow with await or .then.

If Promise A+ code is being used, then you will always have return someFormOfPromise in the test.

it(waits, function(){
  return Promise.resolve(true)
    .then(res => expect(res).to.be.true)
})

If async is being used, that guarantees a promise is returned but you must await anything asynchronous or the tests will continue on

it(waits, async function(){
  let res = await Promise.resolve(true)
  expect(res).to.be.true
})

Callbacks are similar, but they use a done function to signal the tests to move on.

it(waits, function(done){
  someTrueCallback((err, res) => {
    if (err) return done(err)
    expect(res).to.be.true
    done()
  }) 
})

There's a lot of mixed Promise A+ and async in the posted code, there's even a callback thrown in. Pick one way and stick with it where possible, I'll use async/await.

Callback to Promise

Any asynchronous callbacks can be converted to promises, so starting with the fs.writeFile callback function, turn it into a promise:

function writeFile(path, data, format){
  return new Promise((resolve, reject)=>{
    fs.writeFile(path, data, format, function(err){
      if (err) return reject(err)
      resolve(true)
    })
  })
}

Luckily this is pretty standard and Node.js has a helper for this in util.promisify so the helpers can become:

const writeFile = util.promisify(fs.writeFile)

function async takeScreenshot(name='out'){
    let data = await driver.takeScreenshot()
    var base64Data = data.replace(/^data:image\/png;base64,/,"")
    return await writeFile(`screenshots/${name}.png`, base64Data, 'base64')
}

Tests

There's no need to use .then when await is available.

Whenever a function returning a promise is called, you probably need an await or let res = await... if you want to use the resolved value.

describe('UI Tests', function() {

  before(async function() {
    driver = await new Builder().forBrowser('chrome').build();
    await driver.get('http://localhost:8000')
    await getAssets()
    /* search | submit | reset are declared in getAssets() */
  });

  after(async function() {
    await driver.quit();
  });

  afterEach(async function(){
    step++;
    await takeScreenshot(step) 
  });

  describe('User Types/Modifies Input and Submits Query', function() {

    it('type in search + click submit', async function(){
        await search.sendKeys("Puppies and Kittens");
        await submit.click();
    });

    it('click the X icon to clear search', async function(){
        await reset.click();
    });

  });

});

As a side note these tests aren't actually checking anything. Normally an assertion library like chai would be used to ensure things are the way you expect.

Upvotes: 1

Related Questions