Custom commands and before(), after() hooks - Testing React apps with Nightwatch

Adam WardeckiAdam Wardecki

In this part of Testing React Apps with Nightwatch, I'll talk about expanding your test framework with before() and after() hooks as well as custom commands.

This is the second part of our "End-to-End Testing of React Apps with Nightwatch" series. In the previous post, I talked about Nightwatch:

In this part, I'll focus on a couple of tricks that'll help you write better tests.

I'll cover:

This post builds upon the previous part of the series, which can be found here End to End Testing of React Apps with Nightwatch

You don't have to go through the first part, but we'll be building on the code that was written there. The code can be found in the part-1 folder of the syncano-testing-examples GitHub repo. The finished code for this part of the Nightwatch tutorial series can be found in the part-2 folder.

Now that we've gotten all the technicalities out of the way, we can get to the good part. Lets start with the before() and after() hooks in Nightwatch.

Using before() and after() hooks in your tests

The before() and after() hooks are quite self descriptive. They let you write code, that'll be executed before or after your test suite (tests that are grouped in one file).

Additional useful variations are beforeEach() and afterEach() hooks. Pieces of code encapsulated in these will be executed before or after each test in a file. Okay, enough with the theory! Lets see those bad boys in action.

It's also possible to use before() and after() hooks in a global context. In this case, they would execute code before and after the whole suite is run. These hooks should be defined in the globals.js file.

Remember the login test we wrote in the previous part (it's in the tests/testLogin.js file)? It looked like this:

export default {  
  'User Logs in': (client) => {
    const loginPage = client.page.loginPage();
    const instancesPage = client.page.instancesPage();

    loginPage
      .navigate()
      .login(process.env.NIGHTWATCH_EMAIL, process.env.NIGHTWATCH_PASSWORD);

    instancesPage.expect.element([email protected]').text.to.contain('Your first instance.');

    client.end();
  }
};

That's very nice. But what if I wanted to:

This is where the hooks come in. Thanks to before() and after() I can extract parts of the logic out of the tests and make them more robust. Lets consider a case, where I'd want a user to login and then view his Instance details. This is how I'd structure such a test:

export default {  
  before(client) {
    const loginPage = client.page.loginPage();
    const instancesPage = client.page.instancesPage();

    loginPage
      .navigate()
      .login(process.env.NIGHTWATCH_EMAIL, process.env.NIGHTWATCH_PASSWORD);

    instancesPage.waitForElementPresent([email protected]');
  },
  after(client) {
    client.end();
  },
  'User goes to Instance details  view': (client) => {
      const instancesPage = client.page.instancesPage();
        const socketsPage = client.page.socketsPage();

        instancesPage
      .navigate()
      .click([email protected]')

        socketsPage.waitForElementPresent('instancesDropdown');
  }
};

Now the before() hook will take care of login steps and the after() hook will close the browser when all tests from this file are done. Simple, right? The only thing I need to do now is fill in the missing selectors. I'll add the @instancesTable selector to the instancesPage, so that it looks like this:

export default {  
  elements: {
    instancesListDescription: {
      selector: '//div[@class="description-field col-flex-1"]',
      locateStrategy: 'xpath'
    },
    instancesTable: {
      selector: 'div[id=instances]'
    }
  }
};

Since it's a CSS selector, I don't have to pass the locateStrategy property in the instancesTable object because Nightwatch is using CSS as a default locator strategy.

I'll also need to add a socketsPage.js file in the pages folder and add these lines:

export default {  
  elements: {
    instancesDropdown: {
      selector: '.instances-dropdown'
    }
};

That's it! The only thing you need to do now, is to export your email and password (if you haven't done so) as environment variables. Open your terminal app
and type these lines:

export $NIGHTWATCH_EMAIL=YOUR_SYNCANO_EMAIL  
export $NIGHTWATCH_PASSWORD=YOUR_SYNCANO_PASSWORD  

If you don't want to use environment variables, you can pass your email and password as strings directly to the loginPage.login() method.

Extending Nightwatch with custom commands

Once your test suite gets bigger, you'll notice steps within your tests that could be abstracted away and reused across your project. This is where custom commands come in. Thanks to this feature, you'll be able to define methods that are accessible from anywhere within a test suite.

The first thing we need to do, is to add a folder for the custom commands. You can add it in the root of the project and name it commands. Once that's done, you'll have to tell Nightwatch where the custom commands are. To do this:

"custom_commands_path": "./commands",

Now Nightwatch will know where to look for the commands.

Since there are a lot of dropdowns in the Syncano Dashboard, it makes sense to abstract the logic around them into a custom command. The command will:

In order to create this command:

// 'listItem' is the item name from the list. Corresponding dropdown menu will be clicked  
// 'dropdoownChoice' can be part of the name of the dropdown option like "Edit" or "Delete"

exports.command = function clickListItemDropdown(listItem, dropdownChoice) {  
  const listItemDropdown =
    `//div[text()="${listItem}"]/../../../following-sibling::div//span[@class="synicon-dots-vertical"]`;
  const choice = `//div[contains(text(), "${dropdownChoice}")]`;

  return this
    .useXpath()
    .waitForElementVisible(listItemDropdown)
    .click(listItemDropdown)
    // Waiting for the dropdown click animation to finish
    .waitForElementNotPresent('//span[@class="synicon-dots-vertical"]/preceding-sibling::span/div')
    .click(choice)
    // Waiting for dropdown to be removed from DOM
    .waitForElementNotPresent('//iframe/following-sibling::div[@style]/div');
};

Now, since we have the command ready, we will want to use it in a test. Create a testInstances.js file in the tests folder. We will use the before() and after() hooks from the first part of this post. The draft for this test will look like this:

export default {  
  before(client) {
    const loginPage = client.page.loginPage();
        const instancesPage = client.page.instancesPage();

    loginPage
      .navigate()
            .login(process.env.NIGHTWATCH_EMAIL, process.env.NIGHTWATCH_PASSWORD);

        instancesPage.waitForElementPresent([email protected]');
  },
  after(client) {
    client.end();
  },
  'User clicks Edit Instance dropdown option': (client) => {
        const instancesPage = client.page.instancesPage();
        const socketsPage = client.page.socketsPage();
        const instanceName = client.globals.instanceName;

        instancesPage
      .clickListItemDropdown(instanceName, 'Edit')
            .waitForElementPresent([email protected]')
            .waitForElementPresent([email protected]')
            .click([email protected]')
            .waitForElementPresent([email protected]')
  }
};

The test will:

What we still need to do is to add the missing selectors in the pages/instancesPage.js file. Copy the code and paste it below the existing selectors (remember to add a comma after the last of the already present selectors):

instanceDialogEditTitle: {  
    selector: '//h3[text()="Update an Instance"]',
    locateStrategy: 'xpath'
},
instanceDialogCancelButton: {  
    selector: '//button//span[text()="Cancel"]',
    locateStrategy: 'xpath'
}

We are also using a global variable within a test. Go to the globals.js file and add a new line:

instanceName: INSTANCE_NAME  

where the INSTANCE_NAME would be the name of your Syncano Instance.

Now, since everything is ready, you can run your tests. We want to run only a single test, so we'll run the suite like this:

nightwatch -t tests/testInstances.js  

That's it for the second part of the "End-to-End Testing of React Apps with Nightwatch" series. In the next part Marcin Godlewski talks about Data Driven Testing at Syncano. If you have any questions, or just want to say hi, drop me a line at [email protected], leave a comment, or tweet @Syncano on Twitter.

Build powerful apps in half the time

Use our serverless platform to set up your backend in minutes.

Learn more
Adam Wardecki
Author

Adam Wardecki

Tester, Software Engineer and Product Owner @ Syncano