locators Archives - Automated Visual Testing | Applitools https://applitools.com/blog/tag/locators/ Applitools delivers the next generation of test automation powered by AI assisted computer vision technology known as Visual AI. Wed, 11 Oct 2023 21:26:44 +0000 en-US hourly 1 Functional Testing’s New Friend: Applitools Execution Cloud https://applitools.com/blog/functional-testings-new-friend-applitools-execution-cloud/ Mon, 11 Sep 2023 19:59:03 +0000 https://applitools.com/?p=51735 Dmitry Vinnik explores how the Execution Cloud and its self-healing capabilities can be used to run functional test coverage.

The post Functional Testing’s New Friend: Applitools Execution Cloud appeared first on Automated Visual Testing | Applitools.

]]>

In the fast-paced and competitive landscape of software development, ensuring the quality of applications is of utmost importance. Functional testing plays a vital role in verifying the robustness and reliability of software products. With the increasing complexity of applications with a long list of use cases and the need for faster release cycles, organizations are challenged to conduct thorough functional testing across different platforms, devices, and screen resolutions. 

This path to a better quality of software products is where Applitools, a leading provider of functional testing solutions, becomes a must-have tool with its innovative offering, the Execution Cloud.

Applitools’ Execution Cloud is a game-changing platform that revolutionizes functional testing practices. By harnessing the power of cloud computing, the Execution Cloud eliminates the need for resource-heavy local infrastructure, providing organizations with enhanced efficiency, scalability, and reliability in their testing efforts. The cloud-based architecture integrates with existing testing frameworks and tools, empowering development teams to execute tests across various environments effortlessly.

This article explores how the Execution Cloud and its self-healing capabilities can be used to run our functional test coverage. We demonstrate this cloud platform’s features, like auto-fixing selectors caused by a change in the production code. 

Why Execution Cloud

As discussed, the Applitools Execution Cloud is a great tool to enhance any team’s quality pipeline.

One of the main features of this cloud platform is that it can “self-heal” our tests using AI. For example, if, during refactoring or debugging, one of the web elements had its selectors changed and we forgot to update related test coverage, the Execution Cloud would automatically fix our tests. This cloud platform would use one of the previous runs to deduce another relevant selector and let our tests continue running. 

This self-healing capability of the Execution Cloud allows us to focus on actual production issues without getting distracted by outdated tests. 

Functional Testing and Execution Cloud

It’s fair to say that Applitools has been one of the leading innovators and pioneers in visual testing with its Eyes platform. However, with the Execution Cloud in place, Applitools offers its users broader, more scalable test capabilities. This cloud platform lets us focus on all types of functional testing, including non-Visual testing.

One of the best features of the Execution Cloud is that it’s effortless to integrate into any test case with just one line. There is also no requirement to use the Applitools Eyes framework. In other words, we can run any functional test without creating screenshots for visual validation while utilizing the self-healing capability of the Execution Cloud.

Adam Carmi, Applitools CTO, demos the Applitools Execution Cloud and explores how self-healing works under the hood in this on-demand session.

Writing Test Suite

As we mentioned earlier, the Execution Cloud can be integrated with most test cases we already have in place! The only consideration is at the time of writing this post, the current version of the Execution Cloud only supports Selenium WebDriver across all languages (Java, JavaScript, Python, C#, and Ruby), WebdriverIO, and any other WebDriver-based framework. However, more test frameworks will be supported in the near future.

Fortunately, Selenium is a highly used testing framework, giving us plenty of room to demonstrate the power of the Execution Cloud and functional testing.

Setting Up Demo App

Our demo application will be a documentation site built using the Vercel Documentation template. It’s a simple app that uses Next.js, a React framework created by Vercel, a cloud platform that lets us deploy web apps quickly and easily.

To note, all the code for our version of the application is available here.

First, we need to clone the demo app’s repository: 

git clone git@github.com:dmitryvinn/docs-demo-app.git

We will need Node.js of version 10.13 to work with this demo app, which can be installed by following the steps here.

After we set up Node.js, we should open a terminal and run the following command to install the necessary dependencies:

npm install

The next step is to navigate into the project’s directory and start the app locally:

cd docs-demo-app

npm run dev

Now our demo app is accessible at ‘http://localhost:3000/’ and ready to be tested.

Docs Demo App 

Deploying Demo App

While the Execution Cloud allows us to run the tests against a local deployment, we will simulate the production use case by running our demo app on Vercel. The steps for deploying a basic app are very well outlined here, so we won’t spend time reviewing them. 

After we deploy our demo app, it will appear as running on the Vercel Dashboard:

Demo App Deployed on Vercel

Now, we can write our tests for a production URL of our demo application available at `https://docs-demo-app.vercel.app/`.

Setting Up Test Automation

Execution Cloud offers great flexibility when it comes to working with our tests. Rather than re-writing our test suites to run against this self-healing cloud platform, we simply need to update a few lines of code in the setup part of our tests, and we can use the Execution Cloud. 

For our article, our test case will validate navigating to a specific page and pressing a counter button. 

To make our work even more effortless, Applitools offers a great set of quickstart examples that were recently updated to support the Execution Cloud. We will start with one of these samples using JavaScript with Selenium WebDriver and Jest as our baseline.

We can use any Integrated Development Environment (IDE) to write tests like IntelliJ IDEA or Visual Studio Code. Since we use JavaScript as our programming language, we will rely on NPM for the build system and our test runner.

Our tests will use Jest as its primary testing framework, so we must add a particular configuration file called `jest.config.js`. We can copy-paste a basic setup from here, but in its shortest form, the required configurations are the following.

module.exports = {

    clearMocks: true,

    coverageProvider: "v8",

  };

Our tests will require a `package.json` file which should include Jest, Selenium WebDriver, and Applitools packages. Our dependencies’ part of the `package.json` file should eventually look like the one below:

"dependencies": {

      "@applitools/eyes-selenium": "^4.66.0",

      "jest": "^29.5.0",

      "selenium-webdriver": "^4.9.2"

    },

After we install the above dependencies, we are ready to write and execute our tests.

Writing the Tests

Since we are running a purely functional Applitools test with its Eyes disabled (meaning we do not have a visual component), we will need to initialize the test and have a proper wrap-up for it.

In `beforeAll()`, we can set our test batching and naming along with configuring an Applitools API key.

To enable Execution Cloud for our tests, we need to ensure that we activate this cloud platform on the account level. After that’s done, in our tests’ setup, we will need to initialize the WebDriver using the following code:

let url = await Eyes.getExecutionCloudUrl();

driver = new Builder().usingServer(url).withCapabilities(capabilities).build();

For our test case, we will open a demo app, navigate to another page, press a counter button, and validate that the click incremented the value of clicks by one.

describe('Documentation Demo App', () => {

…

    test('should navigate to another page and increment its counter', async () => {

       // Arrange - go to the home page

       await driver.get('https://docs-demo-app.vercel.app/');

       // Act - go to another page and click a counter button

        await driver.findElement(By.xpath("//*[text() = 'Another Page']")).click();

        await driver.findElement(By.className('button-counter')).click();

      // Assert - validate that the counter was clicked

        const finalClickCount = await driver.findElement(By.className('button-counter')).getText();

        await expect(finalClickCount).toContain('Clicked 1 times');

    }

…

Another critical aspect of running our test is that it’s a non-Eyes test. Since we are not taking screenshots, we need to tell the Execution Cloud when a test begins and ends. 

To start the test, we should add the following snippet inside the `beforeEach()` that will name the test and assign it to a proper test batch:

await driver.executeScript(

            'applitools:startTest',

            {

                'testName': expect.getState().currentTestName,

                'appName': APP_NAME,

                'batch': { "id": batch.getId() }

            }

        )

Lastly, we need to tell our automation when the test is done and what were its results. We will add the following code that sets the status of our test in the `afterEach()` hook:

await driver.executeScript('applitools:endTest', 

       { 'status': testStatus })

Now, our test is ready to be run on the Execution Cloud.

Running test

To run our test, we need to set the Applitools API key. We can do it in a terminal or have it set as a global variable:

export APPLITOOLS_API_KEY=[API_KEY]

In the above command, we need to replace [API_KEY] with the API key for our account. The key can be found in the Applitools Dashboard, as shown in this FAQ article.

Now, we need to navigate to the location where our tests are located and run the following npm test command in the terminal:

npm test

It will trigger the test suite that can be seen on the Applitools Dashboard:

Applitools Dashboard with Execution Cloud enabled

Execution Cloud in Action

It’s a well-known fact that apps go through a lifecycle. They get created, get bugs, change, and ultimately shut down. This ever-changing lifecycle of any app is what causes our tests to break. Whether it’s due to a bug or an accidental regression, it’s widespread for a test to fail after a change in an app.

Let’s say a developer working on a counter button component changes its class name to `button-count` from the original `button-counter`. There could be many reasons this change could happen, but nevertheless, these modifications to the production code are extremely common. 

What’s even more common is that the developer who made the change might forget or not find all the tests using the original class name, `button-counter`, to validate this component. As a result, these outdated tests would start failing, distracting us from investigating real production issues, which could significantly impact our users.

Execution Cloud and its self-healing capabilities were built specifically to address this problem. This cloud platform would be able to “self-heal” our tests that were previously running against a class name `button-counter`, and rather than failing these tests, the Execution Cloud would find another selector that hasn’t changed. With this highly scalable solution, our test coverage would remain the same and let us focus on correcting issues that are actually causing a regression in production.

Although we are running non-Eyes tests, the Applitools Dashboard still allows us to see several valuable materials, like a video recording of our test or to export WebDriver commands! 

Want to see more? Request a free trial of Applitools Execution Cloud.

Conclusion

Whether you are a small startup that prioritizes quick iterations, or a large organization that focuses on scale, Applitools Execution Cloud is a perfect choice for any scenario. It offers a reliable way for tests to become what they should be – the first line of defense in ensuring the best customer experience for our users.

With the self-healing capabilities of the Execution Cloud, we get to focus on real production issues that actively affect our customers. With this cloud platform, we are moving towards a space where tests don’t become something we accept as constantly failing or a detriment to our developer velocity. Instead, we treat our test coverage as a trusted companion that raises problems before our users do. 

With these functionalities, Applitools and its Execution Cloud quickly become a must-have for any developer workflow that can supercharge the productivity and efficiency of every engineering team.

The post Functional Testing’s New Friend: Applitools Execution Cloud appeared first on Automated Visual Testing | Applitools.

]]>
Let the Engineers Speak: Selectors in Cypress https://applitools.com/blog/using-web-selectors-in-cypress/ Fri, 07 Apr 2023 23:11:34 +0000 https://applitools.com/?p=48916 Earlier this month, Applitools hosted a webinar, Let the Engineers Speak: Selectors, where testing experts discussed one of the most common pain points that pretty much anyone who’s ever done web...

The post Let the Engineers Speak: Selectors in Cypress appeared first on Automated Visual Testing | Applitools.

]]>
Filip Hric from Cypress

Earlier this month, Applitools hosted a webinar, Let the Engineers Speak: Selectors, where testing experts discussed one of the most common pain points that pretty much anyone who’s ever done web UI testing has felt. The first article in our two-part series defined our terms and challenges of locating web elements using selectors, as well as recapped Christian’s WebDriverIO selectors tutorial and WebdriverIO Q&A. Be sure to read that article first to help set context.

Introducing our experts

I’m Pandy Knight, the Automation Panda. I moderated our two testing experts in our discussion of selectors:

Locating web elements with Cypress

Filip walked us through selectors in Cypress, starting with the basics and using Trello as an example app.

Note: All the following text in this section is based on Filip’s part of the webinar. Filip’s repository is available on GitHub.

When talking about selectors in web applications, we are trying to target an HTML element. Cypress has two basic commands that can help with selecting these elements: cy.get and cy.contains. In Filip’s example demo, he has VS Code on the left side of his screen and Cypress running in graphic user interface mode (open mode) on the right side.

The example test uses different kinds of selector strategies, with each approach using either the get command or the contains command.

Locating elements by class, ID, or attribute

The first first approach calls cy.get(‘h2’) using the H2 tag. In the Cypress window, if you hover over the get h2 command, it will highlight the selector that has been selected.

If you’re struggling to find the right selector in Cypress, there’s this really nice tool that basically works like the inspect element tool, but you don’t have to open the dev tools if you don’t want to.

In our other three approaches with the get command, we are using the class, the ID, and the attribute, respectively:

  cy.get('h2')
  cy.get('.board') // class
  cy.get('#board-1') // id
  cy.get('[data-cy=board-item]') // attribute

The syntax for the get commands is basically just CSS selectors. If you are selecting an element with a class of board, you need to prefix the class with a dot. You can even write more complex selectors, like adding an h2 tag inside the element that has the class board like cy.get(‘.board > h2’). The options are endless. If you have ever worked with CSS, you know that you can pretty much target any element you like.

Locating elements by text

Another strategy that we have in Cypress is selecting elements by text. This approach may not always work, but in cases like a login, sign up, sent, or okay button, these usually need to have specific text. To select an element using text, we use the contains command. The contains command will only select one element, and it’s actually going to look for elements within some context.

The example uses two different texts to search: cy.contains(‘My Shopping’) and cy.contains(‘My’). On the Trello board page, ‘My Shopping’ appears once and ‘My’ appears twice. The contains call using ‘My’ will return with the first element that has the text ‘My’ on the page, which is ‘My Board’ in the example. So that’s something to watch out for. If you want to be more specific with the kind of element you want to select, you can actually combine these two approaches, selecting a CSS element and specifying the text which you want to find. For example, cy.contains(‘.board’, ‘My’) would return the correct element.

  cy.contains('My Shopping') // text
  cy.contains('My') // find the first one
  cy.contains('.board', 'My') // specify element to find

Locating elements using XPath

There are other selector strategies like using XPath. Cypress has an official plugin for XPath. If you install that, you will get the ability to select elements using XPath. You can use XPaths, but they may be harder to read. For example, cy.xpath(‘(//div[contains(@class, “board”)])[1]’) does the same thing as cy.get(‘board’).eq(0).

// Filter an element by index
cy.xpath('(//div(contains(@class, "board") ]) [1]')

// Select an element containing a specific child element
cy.xpath('//div [contains(@class, "list")] [.//div[contains(@class, "card")]]')

// Select an element by text
cy.xpath('//*[text()[contains(., "My Boards")]]')

// Select an element after a specific element
cy.xpath('//div[contains(@class, "card")][preceding::div[contains(., "milk")]]')
//Filter an element by index
cy.get('.board').eq(0)

// Select an element containing a specific child element
cy.get(".card").parents('.list')

// Select an element by text
cy.contains('My Boards')

// Select an element after a specific element
cy.contains('.card', 'milk').next('.card')

Filip’s recommendation is that you don’t really need XPaths. XPaths can be really powerful in traversing the DOM structure and selecting different elements, but there are other options in Cypress.

Traversing elements in Cypress

For our example, we have a Trello app and two lists with item cards. The first list has two cards, and the second has one card. We want to select each of the cards from a list. We can find the last card by doing a pair of commands cy.get(‘[data-cy=card]’).last(). First, we’re using the get command to target the cards, which will return all three cards. When you hover over your get command, you’ll see that all three cards are selected.

When you use the last command, it’s going to filter to the last card, on which you can then do some action like click or make an assertion.

You can also traverse up or down using Cypress. The next example cy.contains(‘[data-cy=card]’, ‘Soap’).parents(‘[data-cy-list]’) tries to target a parent element using text to select the card and then looks for a parent element using a CSS selector. This example is going to select our whole list.

Alternatively, if you want to traverse between the next element or a previous element, that can be done easily with cy.contains(‘[data-cy=card]’, Milk’).next() or cy.contains(‘[data-cy=card]’, Milk’).prev().

it.only('Find an element on page', () => {

  cy.visit('/board/1')

  // find last card
  cy.get('[data-cy=card]')
    .last()

  // find parent element
  cy.contains('[data-cy=card]', 'Soap')
    .parents('[data-cy=list]')

  // find next element
  cy.contains('[data-cy=card]', 'Milk')
    .next()

  // find next element
  cy.contains('[data-cy=card]', 'Bread')
    .prev()

});

You may sometimes deal with tricky situations like elements loading in at different times. The DOM is going to be flaky, because DOM is pretty much always flaky, right? Things get loaded, things get re-rendered, and so on. There was a Cypress 12 update that makes sure that when we are selecting an element and we have an assertion about that element, if the assertion does not pass, we are going to re-query our DOM. So in the background, these should command this assertion and our querying is interconnected.

it('Dealing with flaky situations', () => {

  cardsLoadRandomly(10000)

  cy.visit('/board/1')

  cy.get('[data-cy=card]')
    .last()
    .should('contain.text', 'Soap')

});

Locating elements in a shadow DOM

Dealing with shadows DOM can be tricky. By default, Cypress is not going to look for shadow DOM elements, only DOM elements. If we want to include the shadow DOM elements, we have a few options in Cypress.

it('closes side panel that is in shadow DOM', () => {

  cy.visit('https://lit.dev/playground/#sample=docs%2Fwhat-is-lit&view-mode=preview')

  cy.get('litdev-drawer', { includeShadowDom: true })
    .find('#openCloseButton', { includeShadowDom: true })
    .click()

});

If we don’t have too many of these shadow DOM elements on the page, we can use either of two commands that do essentially the same thing:

  • cy.get(‘litdev-drawer’, { includeShadowDom: true }). find(‘#openCloseButton’, { includeShadowDom: true })
  • cy.get(‘litdev-drawer’).shadow().find(‘#openCloseButton’)

Alternatively, if we have a lot of shadow elements in our application, we can use the approach of changing the option in the config file includeShadowDom from false to true. By default it is set to false, but if you set it to true and save your configuration, Cypress will look for shadow DOM elements automatically.

Locating elements within an iframe

Cypress does not have an iframe command. Whenever we traverse, we interact with this timeline that we have in Cypress to see the state of our application as it was during the execution of that command. In order to support the iframes, Cypress would have to do the snapshot of the iframe as well. Essentially, if you want to access an iframe using Cypress, you write a Cypress promise that will resolve the contents of the iframe.

You can add a custom command to your code base to retry the iframe. So if the iframe takes a little bit of time to appear, it’ll resolve when it appears or when the timeout eventually times out. Another way of dealing with that is installing a plugin.

it('dealing with iframes', () => {

  cy.visit('https://kitchen.applitools.com/ingredients/iframe')

  cy.iframe('#the-kitchen-table')
    .find('section')
    .should('contain.text', 'The Kitchen')

});

Cypress selector recommendations

Cypress works best when you have your test code along with the source code in the same repository. These are the recommendations that Cypress gives in the documentation:

SelectorRecommendation
Generic HTML tags, elements, or classes like:
cy.get(‘button’)
cy.get(‘.btn.btn-large’)
Never recommended.
Lack content or are often paired with styling and therefore highly subject to change.
IDs, HTML name attributes, or title attributes like:
cy.get(‘$main’)
cy.get(‘[name=”submission”]’)
Sparingly recommended.
Still coupled to style or JS event listeners, or coupled to the name attribute, which has HTML semantics.
Text attributes like:
cy.contains(‘Submit’)
Recommendation depends.
This is only suggested for elements where text is not expected to change.
Custom IDs or data test attributes like:
cy.get(‘[data-cy=”submit”]’)
Always recommended.
Isolated from all changes.

The best recommendation is to create custom IDs for elements. As you create your test, you will create those IDs as well. So if you need a test, create an attribute, and that will do a single job, be available for end-to-end test.

Filip recommends two blog posts about selector strategies:

Adding visual testing

We can go beyond accessibility and functional tests and we can add visual tests into our test suite. We can do this with Applitools. Create a free Applitools account to access your API key, and follow our tutorial for testing web apps in JavaScript using Cypress.

Note: Do not share your API key with others. For this demo, Filip rotated his API key, so it’s no longer valid.

The visual testing will have three basic parts:

  1. Open your “eyes”.
  2. Check the window.
  3. Close your “eyes”.

Applitools Eyes will then validate the snapshot it takes against the current baseline. But sometimes we don’t want to test certain areas of the page like dynamic data. Applitools is really intelligent about that and has options in the Test Manager to add ignore regions, but we can help it with our own ignore regions by using selectors.

it('check home screen', () => {

  cy.eyesOpen({
    appName: 'Trello',
  })

  cy.visit('/')

  cy.get('[data-cy=board-item]')
    .should('be.visible')

  cy.eyesCheckWindow({
    ignore: {
      selector: 'hide-in-applitools'
    }
  })

  cy.eyesClose()

});

In the example, we are showing the ignore region and telling which selector it should ignore. You can create a custom hide-in-applitools class to add to all your elements you want to hide, and Applitools will automatically ignore them.

Cypress selectors Q&A

After Filip shared his Cypress demonstration, we turned it over to our Q&A, where Filip responded to questions from the audience.

Using Cypress commands

Question: I was recently working on XPaths and Cypress. I understand cypress-xpath is deprecated and I was suggested to use @cypress/xpath. Is that the case?
Filip’s response: I don’t know. I know this was the situation for the Cypress grab package, which can grab your test so you can just run a subset of your Cypress test. It was a standalone plugin and then they sort of moved it inside a Cypress repository. So now it’s @cypress/grab. I believe the situation with Xpath might be similar.

Using the React testing library

Question: What are your thoughts about using the React testing library plugin for locating elements in Cypress using their accessibility roles and names – findByRole, findByLabel?
Filip’s response: [To clarify the specific library mentioned] there’s this testing library, which is a huge project and has different subprojects. One is the React testing library and their Cypress testing library, and they basically have those commands inside this (findByRole, findByPlaceholder, etc.). So I think the Cypress testing library is just an implementation of the thing you are mentioning. So what’s my opinion on that? I’m a fan. Like I said, I’m not using it right now, but it does two things at once. You can check your functionality as well as accessibility. So if your test fails, it might be annoying, but it also might mean you need to work on the accessibility of the app. So I recommend it.

Using div.board or .board

Question: Do you have a stance/opinion on div.board versus .board for CSS selectors?
Filip’s response: Not really. As I mentioned, I prefer adding my own custom selectors, so I don’t think I would have this dilemma too often.

Tracking API call execution

Question: How can we find out if an API call has executed when we click on an element?
Filip’s response: In Cypress, it’s really easy. There’s this cy.intercept command in which you can define the URL or the method or any kind of details about the API call. And you need to make sure that you put the cy.intercept command before the click happens. So if the click triggers that API call you can then use cy.wait and basically refer through alias to that API call. I suggest you take a look into the intercept command in Cypress docs.

Working with iframes

Question: Is it possible to select elements from an iframe and work with an iframe like normal?

Filip’s response: Well, I don’t see why not. It is a little bit tricky. Iframe is just another location, so it’ll always have a source pointing to a URL. So, alternatively, if you are going to do a lot of testing within that iframe, maybe you just want to open that and test the page that is being iframed. It would be a good thing to consult with developers to see if there’s a communication between the iframe and the parent frame and if there’s anything specific that needs to be covered. But if you do like a lot of heavy testing, I would maybe suggest to open the URL that the iframe opens and test that.

Learn more

So that covers our expert’s takeaways on locating web elements using Cypress. If you want to watch the demos, you can access the on-demand webinar recording.

If you want to learn more about any of these tools, frameworks like Cypress, WebDriverIO, or specifically web element locator strategies, be sure to check out Test Automation University. All the courses and content are free.
Be sure to register for the upcoming Let the Engineers Speak webinar series installment on Test Maintainability coming in May. Engineers Maaret Pyhäjärvi from Selenium and Ed Manlove from Robot Framework will be discussing test maintenance and test maintainability with a live Q&A.

The post Let the Engineers Speak: Selectors in Cypress appeared first on Automated Visual Testing | Applitools.

]]>
Let the Engineers Speak: Selectors in WebdriverIO https://applitools.com/blog/using-web-selectors-in-webdriverio/ Thu, 30 Mar 2023 15:59:26 +0000 https://applitools.com/?p=48883 Earlier this month, Applitools hosted a webinar, Let the Engineers Speak: Selectors, where testing experts discussed one of the most common pain points that pretty much anyone who’s ever done...

The post Let the Engineers Speak: Selectors in WebdriverIO appeared first on Automated Visual Testing | Applitools.

]]>
Christian Bromann

Earlier this month, Applitools hosted a webinar, Let the Engineers Speak: Selectors, where testing experts discussed one of the most common pain points that pretty much anyone who’s ever done web UI testing has felt. In this two-part blog series, I will recap what our experts had to say about locating their web elements using their respective testing frameworks: WebDriverIO and Cypress.

Introducing our experts

I’m Pandy Knight, the Automation Panda. I moderated our two testing experts in our discussion of selectors:

In each article, we’re going to compare and contrast the approaches between these two frameworks. This comparison is meant to be collaborative, not competitive. This series is meant to showcase and highlight the awesomeness that we have with modern frameworks and tools available.

This first article in our two-part series will define our terms and challenges, as well as recap Christian’s WebDriverIO selectors tutorial and WebdriverIO Q&A. The upcoming second article will recap Filip’s Cypress selectors tutorial and Cypress Q&A.

Defining our terms

Let’s define the words that will be used in this series to frame the discussion and so that we have the same understanding as we discuss selectors, locators, and elements:

  • Selectors: A selector is a text-based query used for locating elements. Selectors could be things like CSS selectors, XPaths, or even things like an ID or a class name. In modern frameworks, you can even use things like text selectors to query by the text inside of a button.
  • Locators: A locator is an object in the tool or framework you use that uses the selector for finding the element on the active page.
  • Elements: The element itself is the entity on the page that exists. An element could be a button, it could be a label, an input field, or a text area. Pretty much anything in HTML that you put on a page is an element.

In one sentence, locators use selectors to find elements. We may sometimes use the terms “selector” and “locator” interchangeably, especially in more modern frameworks.

Challenges of locating web elements

We’re going to talk about selectors, locators, and the pain of trying to get elements the right way. In the simple cases, there may be a button on a page with an ID, and you can just use the ID to get it. But what happens if that element is stuck in a shadow DOM? What if it’s in an iframe? We have to do special things to be able to find these elements.

Locating web elements with WebdriverIO

Christian Bromann demonstrated effective web element locator strategies with WebDriverIO. In this series, we used Trello as the example web app to compare the frameworks. For the purposes of the demo, the app is wrapped over a component test, allowing Christian to work with web on the console to easier show you how to find certain elements on the page.

Note: All the following text in this section is based on Christian’s part of the webinar. Christian’s repository is available on GitHub.

Creating a board

First, we want to start creating a new board. We want to write a test to inspect the input element that the end user sees. From the dev tools, the input element can be found.

There are various ways to query for that input element:

  • We could fetch the element with WebdriverIO using $ or $$ commands, depending if you want to fetch one element, the first element on the page, or all elements. Using an input as a selector is not advised, as there could be more inputs and then the wrong input could be fetched.
  • We could also use a CSS class called px-2. Using CSS classes is also not advised, as oftentimes these classes are meant to style an element, not necessarily locate it.
  • We could use a property, such as the name of the input using the data attribute. This is the suggested way to locate a web element in WebdriverIO – using the accessibility properties of an element.

WebdriverIO has a more simplified way to use accessibility properties. This approach is a much better selector, because it’s actually what the user is seeing and interacting with. To make things easier, Chrome helps with finding the accessibility selector for every element under the accessibility tab.

Note: WebdriverIO doesn’t use the accessibility API of the browser and instead uses a complex XPath to compute the accessibility name.

After you’ve located the input element, press enter using the keys API.

  it('can create an initial board', async () => {
    await $('aria/Name of your first board').setValue('Let the Engineers Speak')
    await browser.keys(Key.Enter)
    await expect(browser).toHaveUrlContaining('/board/1')
    
    await browser.eyesCheck('Empty board')
  })

Creating a list

Now that our board is created, we want to start a list. First, we inspect the “Add list” web element for the accessibility name, and then use that to click the element.

Set the title of the list, and then press enter using the keys API.

  it('can add a list on the board', async () => {
    await $('aria/Enter list title...').setValue('Talking Points')
    await $('aria/Add list').click()

    /**
     * Select element by JS function as it is much more perfomant this way
     * (1 roundtrip vs nth roundtrips)
     */
    await expect($(() => (
      [...document.querySelectorAll('input')]
        .find((input) => input.value === 'Talking Points')
    ))).toBePresent()
  })

Adding list items

To add to the list we created, the button is a different element that’s not accessible, as the accessibility name is empty. Another way to approach this is to use a WebdriverIO function that allows me to add a locator search applicator strategy.

The example applicator strategy basically queries all this on the page to find all divs that have no children and that text of a selector that you provide, and now you should be able to query that custom element. After getting the custom elements located, you can assert that three cards have been created as expected. You can inject your JavaScript script to be run in the browser if you don’t have accessibility selectors to work with.

  it('can add a card to the list', async () => {
    await $('aria/Add another card').click()
    await $('aria/Enter a title for this card...').addValue('Selectors')
    await browser.keys(Key.Enter)
    await expect($$('div[data-cy="card"]')).toBeElementsArrayOfSize(1)
    await $('aria/Enter a title for this card...').addValue('Shadow DOM')
    await browser.keys(Key.Enter)
    await $('aria/Enter a title for this card...').addValue('Visual Testing')
    await browser.keys(Key.Enter)
    await expect($$('div[data-cy="card"]')).toBeElementsArrayOfSize(3)

    await browser.eyesCheck('board with items')
  })

Starring a board

Next, we’ll “star” our board by traversing the DOM using WebdriverIO commands. We need to first locate the element that has the name of the board title using the attribute selector and going to the parent element.

From that parent element, to get to the star button, we chain the next command and call the next element. Now we can click the star and see it change to an enabled state. So with WebdriverIO, you can chain all these element queries and then add your action at the end. WebdriverIO uses a proxy in the background to transform everything and execute the promises after each other so that they’re available. One last thing you can also do is query elements by finding links with certain text.

Summarizing Christian’s suggestions for using selectors in WebdriverIO, always try to use the accessibility name of an element. You have the dev tools that give you the name of it. If you don’t have the accessibility name available, improve the accessibility of your application if possible. And if that’s not an option, there are other tricks like finding an element using JavaScript through propertis that the DOM has.

  it('can star the board', async () => {
    const startBtn = $('aria/Let the Engineers Speak').parentElement().nextElement()
    await startBtn.click()
    await expect(startBtn).toHaveStyle({ color: 'rgba(253,224,71,1)' })
  })

Accessing a shadow root

Using a basic web HTML timer component as an example, Christian discussed shadow roots. The example has multiple elements and a nested shadow root. Trying to access a button within the shadow root results in an error that you don’t have access from the main page to the shadow root. WebdriverIO has two things to deal with this challenge. The first method is the deep shadow root selector. This allows you to access all of the shadow root elements or filter by defined attributes of the elements in the shadow root.

A different way to access elements in the shadow is using the browser shadow command, which basically allows you to switch to the search within the shadow root.

    describe('using deep shadow selector (>>>)', () => {
        beforeEach(async () => {
            await browser.url('https://lit.dev/playground/#sample=docs%2Fwhat-is-lit&view-mode=preview')

            const iframe = await $('>>> iframe[title="Project preview"]')
            await browser.waitUntil(async () => (
                (await iframe.getAttribute('src')) !== ''))
            
            await browser.switchToFrame(iframe)
            await browser.waitUntil(async () => (await $('my-timer').getText()) !== '')
        })

        it('should check the timer components to work', async () => {
            for (const customElem of await $$('my-timer')) {
                const originalValue = await customElem.getText()
                await customElem.$('>>> footer').$('span:first-child').click()
                await sleep()
                await customElem.$('>>> footer').$('span:first-child').click()
                await expect(customElem).not.toHaveTextContaining(originalValue)

                await customElem.$('>>> footer').$('span:last-child').click()
                await expect(customElem).toHaveTextContaining(originalValue)
            }
        })
    })

Integrating with Applitools

Lastly, Christian shared using Applitools with WebdriverIO. The Applitools Eyes SDK for WebdriverIO is imported to take snapshots of the app as the test suite runs and upload them to the Applitools Eyes server.

if (process.argv.find((arg) => arg.includes('applitools.e2e.ts'))) {
    config.services?.push([EyesService, {
        viewportSize: {width: 1200, height: 800},
        batch: {name: 'WebdriverIO Test'},
        useVisualGrid: true,
        browsersInfo: [
            {width: 1200, height: 800, name: 'chrome'},
            {width: 1200, height: 800, name: 'firefox'}
        ]
    }])

With this imported, our tests can be simplified to remove some of the functional assertions, because Applitools does this for you. From the Applitools Eyes Test Manager, you can see the tests have been compared against Firefox and Chrome at the same time, even though only one test was run.

WebdriverIO selectors Q&A

After Christian shared his WebdriverIO demonstration, we turned it over to our Q&A, where Christian responded to questions from the audience.

Using Chrome DevTools

Audience question: Is there documentation on how to run WebdriverIO in DevTools like this to be able to use the browser and $ and $$ commands? This would be very helpful for us for day-to-day test implementation.
Christian’s response: [The demo] is actually not using DevTools at all. It uses ChromeDriver in the background, which is automatically spun up by the test runner using the ChromeDriver service. So there’s no DevTools involved. You can also use the DevTools protocol to automate the browser. The functionality is the same. WebdriverIO executes the same XPath. There’s a compliant DevTools implementation to the WebdriverIO protocol so that the WebdriverIO APIs works on both. But you really don’t need DevTools to use all these accessibility selectors to test your application.

Integrating with Chrome Console

Question: How is WebdriverIO integrated with Chrome Console to type await browser.getHtml() etc.?
Christian’s response: With component tests, it’s similar to what other frameworks do. You actually run a website that is generated by WebdriverIO. WebdriverIO injects a couple of JavaScript scripts and it loads web within that page as well. And then it basically sends commands back to the Node.js world where it’s then executed by ChromeDriver and then the responses are being sent back to the browser. So basically WebdriverIO as a framework is being injected into the browser to give you all the access into the APIs. The actual commands are, however, run by Chrome Driver.

Testing in other local languages

Question: If we’re also testing in other languages (like English, Spanish, French, etc.), wouldn’t using the accessibility text fail? Would the ID not be faster in finding the element as well?
Christian’s response: If you have a website that has multiple languages and you have a system to inject those or to maintain the languages, you can use this tool to fetch the accessibility name of that particular language you test the website in. Otherwise, you can say I only tested one language, because I would assume it would not be different in other languages. Or you create a library or a JSON file that contains the same selectors for different languages but the same accessibility name for different languages. And then you import that JSON to your test and just reference the label to have the right language. So there are ways to go around it. It would make it more difficult in different languages, obviously, but still from the way to maintain end-to-end tests and maintain tests in general, I would always recommend accessibility names and accessibility labels.

Using div.board or .board

Question: Do you have a stance/opinion on div.board versus .board for CSS selectors?
Christian’s response: I would always prefer the first one, just more specific. You know, anyone could add another board class name to an input element or what not. And so being very specific is usually the best way to go in my recommendation.

Tracking API call execution

Question: How can we find out if an API call has executed when we click on an element?
Christian’s response: WebdriverIO has mocking capabilities, so you can look into when a certain URL pattern has been called by the browser. Unfortunately, that’s fairly based on DevTools because WebdriverIO, the first protocol, doesn’t support it. We are working with the browser vendors and the new WebdriverIO binary protocol to get mocking of URLs and stubbing and all that stuff to land in the new browsers. And so it’ll be available across not only Chrome, but also Firefox, Safari, and so on.

Working with iframes

Question: Is it possible to select elements from an iframe and work with an iframe like normal?

Christian’s response: An iframe is almost similar to a shadow DOM – just that an iframe has much more browser context than just a shadow DOM. WebdriverIO has an issue currently. To implement the deep shadow selector for iframes as well, you could do three characters and then name any CSS path and it would make finding an iframe very easy. But you can always switch to an iframe and then query it. So it’s a little bit more difficult with iframes, but it’s doable.

Learn more

So that covers our expert’s takeaways on locating web elements using WebdriverIO. In the next article in our two-part series, I’ll recap Filip’s tutorial using Cypress, as well as the Cypress Q&A with the audience.

If you want to learn more about any of these tools, frameworks like Cypress, WebDriverIO, or specifically web element locator strategies, be sure to check out Test Automation University. All the courses and content are free. You can also learn more about visual testing with Applitools in the Visual Testing learning path.
Be sure to register for the upcoming Let the Engineers Speak webinar series installment on Test Maintainability coming in May. Engineers Maaret Pyhäjärvi from Selenium and Ed Manlove from Robot Framework will be discussing test maintenance and test maintainability with a live Q&A.

The post Let the Engineers Speak: Selectors in WebdriverIO appeared first on Automated Visual Testing | Applitools.

]]>