Dynamic Tests Pt.2 Conditional Steps Title

Test Design: Dynamic Tests Pt. 2 – Conditional Steps

In part 1, we used JavaScript to start our journey to purchase a random product. In this concluding part, we use conditional steps to complete the journey, making sure our script can handle the different circumstances it could encounter. If you haven’t already looked at part 1, you can view the article here.

Conditional Steps

As mentioned in the test execution speed article, one way we could handle multiple paths in the past was using optional steps. However, this is inefficient, as each unused optional step in a test run will use the full step timeout available. Towards the end of 2018, conditional steps were offered in Ghost Inspector (see their documentation here), not only presenting a way to more effectively deal with variation in script runs, but also increasing the creativity we can employ when composing our scripts.

Conditional steps are run on the basis of a true or false decision returned by running some JavaScript. This doesn’t need to be complex – it can purely be based upon whether an element exists on the page. If we want to use more complex decisions by making use of nested conditions, it’s possible to use conditional steps to inherit steps from another script module.

Making the tests robust

By introducing random elements to our tests, we’ve increased the emphasis on understanding and coping with the different permutations of outcomes from each step. Therefore, our focus with the conditional steps is to cover as many paths as possible to increase the chance of repeated test run success. A sensible approach to discovering the different paths is to outline the basic script and run it until we hit a failure. If we see the script succeed many times in sequence, we can feel confident that we have at least the majority of potential paths covered.

Worked Example

We’ll continue to use johnlewis.com, extending what we produced in part 1. Our main aim is to add a product to the basket, which means dealing with the difference in product page layout.

Category navigation completion

Our first port of call is the outlier from part 1. We noticed that selecting a category from the navigation doesn’t always result in us landing on a products list page. In this case, we need to make another selection to get to a products list. This provides us with a good opportunity to introduce some conditional steps – we only want to take these extra steps if we aren’t yet on a product page.

John Lewis Category Navigation Markup Screenshot
what we get if we land on another category page, rather than product listings

We need to add multiple additional steps, but these will all use the same condition to trigger. All we need to do it find something unique in the DOM that tells us we’re not on a product list page. In this case, we use the class .area-categories__content.

TIP: The Ghost Inspector UI groups together consecutive steps with the same conditions. It’s important to note that the condition will be evaluate for each step, rather than just once at the entry to the visual group. Therefore, you shouldn’t change anything in these steps which will change the outcome of your conditional statement.

We have our selector, so let’s return True for our comparison where this selector is present on the page. If we’re on a product listing page already, the condition will return false, therefore the steps will be ignored. We’re using some similar JS to part 1 to accomplish this. As we have several steps we want to perform, we use the same condition for all:

if (document.querySelectorAll('.area-categories__content').length !== 0) { return true; }
Code language: JavaScript (javascript)

The steps we run are the same as in part 1 – identify the number of options, pick a random number and then perform the selection.

Product variant selection

John Lewis Product Configuration Screenshot
an example of the variant selection on a non-clothing product page

Identifying the permutations

We’re now pretty confident that we’ve reached a product page with the steps from part one and the above conditional steps. Without an in-depth knowledge of the variations in appearance of a johnlewis.com product page, we need to browse the catalogue and inspect the markup to investigate. We discover the following main variations:

  • Only Quantity selection and Add to Basket
  • Clothing Swatch/Colour
  • Clothing Size
  • Non-clothing Swatch/Colour
  • Non-clothing Size

The Size and Swatch controls can appear independently or in combination. The great thing about using conditional steps is that we don’t really have to plan for whether one or both are present, as our condition will do the work for us. What’s important is that we deal with the Swatch selection first, as the sizes are likely to be specific to each colour/design variant.

Writing the conditions

To identify what is available on the page to interact with, we need to find the selectors to use in our JavaScript. We’ll use the same logic of just determining whether the element is present in order to execute the conditional step. Every path will require us to add the product to the basket, therefore the case where there is no variant selection does not need to be conditional. For the others, we just look for the presence of the element:

  • Clothing Swatchdiv[data-cy="desktop_colors"]
  • Non-Clothing Swatchul.swatch-list
  • Clothing Sizeul[data-cy="size-selector-list"]
  • Non-Clothing Sizeul.size-list

Dealing with out of stock variants

Something to be careful of when writing the step to execute when the condition is true is that we don’t want to select an out of stock swatch or size. We just need to isolate what designates an option as unavailable in each type of control we interact with.

  • div[data-cy="desktop_colors"] a:not([class*="disabled"])
  • ul.swatch-list a:not(.swatch--unavailable)
  • ul[data-cy="size-selector-list"] button:not([class*="size-button--out-of-stock"])
  • ul.size-list a:not(.size-link--unavailable)

Once we’ve set out our conditional steps, we just need to add the Add to Basket interaction and the journey from site home to a random product in the cart is complete!

Configuring the tests in Ghost Inspector

Suite setup

Ghost Inspector Script Steps Screenshot
we use the same script as part 1

Script steps example

Additional category selection

GI Extra Category Steps Screenshot
note how the condition only appears once in the group, yet is run for every step

Swatch selection

GI Swatch Selection Steps Screenshot

Size Selection

GI Size Selection Steps Screenshot

Script run example

GI Successful Runs Screenshot
when we see many consecutive successful test runs, we can feel confident we have the majority of path permutations covered

Script export

Save this as a .json and you can import it into Ghost Inspector to view/edit/run the script for yourself.

{ "autoRetry": null, "browser": null, "dataSource": null, "disableVisuals": null, "disallowInsecureCertificates": null, "failOnJavaScriptError": null, "filters": [], "finalDelay": null, "globalStepDelay": null, "httpHeaders": [], "importOnly": false, "language": null, "links": [], "maxAjaxDelay": null, "maxConcurrentDataRows": null, "maxWaitDelay": null, "name": "Random Product to Cart (Category Navigation)", "notifications": null, "publicStatusEnabled": false, "region": null, "screenshotCompareEnabled": false, "screenshotCompareThreshold": 0.1, "startUrl": "https://www.johnlewis.com", "steps": [ { "condition": null, "optional": false, "private": false, "sequence": 0, "command": "click", "target": "button[data-test=\"allow-all\"]", "value": "", "variableName": "", "notes": "Dismiss the Cookie warning" }, { "condition": null, "optional": false, "private": false, "sequence": 1, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('a[class*=\"navigation-item-forward\"]').length;\r\nvar min = 1;\r\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "navitem", "notes": "Count the number of nav items and pick a random number between 1 and that number. Store the result in a variable." }, { "condition": null, "optional": false, "private": false, "sequence": 2, "command": "click", "target": "ul[class*=\"navigation-navBar\"] li:nth-child({{navitem}}) a", "value": "", "variableName": "", "notes": "Click a navigation item based on the random number chosen" }, { "condition": null, "optional": false, "private": false, "sequence": 3, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('ul[class^=\"navigation-items\"] > li[class*=\"navigation-item-item--level1\"]:not([class*=\"navigation-item-mobile\"])').length;\nvar min = 2;\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "subcategory", "notes": "Count the number of sub-navigation sections and pick a random number between 1 and that number. Store the result in a variable. For now, we're avoiding the first section due to an inconsistency in the John Lewis navigation." }, { "condition": null, "optional": false, "private": false, "sequence": 4, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('ul[class^=\"navigation-items\"] > li[class*=\"navigation-item-item--level1\"]:not([class*=\"navigation-item-mobile\"]):nth-child({{subcategory}}) div[class^=\"navigation-navigationPane\"] li:not([class*=\"navigation-item-mobile\"]) a').length;\nvar min = 1;\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "subcategorylink", "notes": "Count the number of links in the randomly selected sub-category, and choose one among that number at random." }, { "condition": null, "optional": false, "private": false, "sequence": 5, "command": "click", "target": "ul[class^=\"navigation-items\"] > li[class*=\"navigation-item-item--level1\"]:not([class*=\"navigation-item-mobile\"]):nth-child({{subcategory}}) div[class^=\"navigation-navigationPane\"] li:not([class*=\"navigation-item-mobile\"]):nth-child({{subcategorylink}}) a", "value": "", "variableName": "", "notes": "Select a sub-category based on the random numbers selected" }, { "condition": { "statement": "if (document.querySelectorAll('.area-categories__content').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 6, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('.area-categories-nav__section').length;\r\nvar min = 1;\r\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "plpsection", "notes": "If we've landed on a Category page, rather than a Product List Page, we need to select another category level. First step is to identify a random section" }, { "condition": { "statement": "if (document.querySelectorAll('.area-categories__content').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 7, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('.area-categories-nav__wrapper li:nth-child({{plpsection}}) li').length;\nvar min = 1;\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "plplink", "notes": "Now choose a random category within the section" }, { "condition": { "statement": "if (document.querySelectorAll('.area-categories__content').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 8, "command": "click", "target": ".area-categories-nav__wrapper li:nth-child({{plpsection}}) li:nth-child({{plplink}}) a", "value": "", "variableName": "", "notes": "Select the random category to advance to a Product List Page" }, { "condition": { "statement": "if (document.querySelectorAll('.area-categories__content').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 9, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('.area-categories-nav__section').length;\r\nvar min = 1;\r\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "plpsection", "notes": "Repeating the above steps in case we still land on another content page, rather than product listing page. This and the following 2 steps will only be executed if the conditions are still met." }, { "condition": { "statement": "if (document.querySelectorAll('.area-categories__content').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 10, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('.area-categories-nav__wrapper li:nth-child({{plpsection}}) li').length;\nvar min = 1;\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "plplink", "notes": "Now choose a random category within the section" }, { "condition": { "statement": "if (document.querySelectorAll('.area-categories__content').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 11, "command": "click", "target": ".area-categories-nav__wrapper li:nth-child({{plpsection}}) li:nth-child({{plplink}}) a", "value": "", "variableName": "", "notes": "Select the random category to advance to a Product List Page" }, { "condition": null, "optional": false, "private": false, "sequence": 12, "command": "assertElementVisible", "target": "a[data-test=\"oos-switch\"]", "value": "", "variableName": "", "notes": "Wait for the OOS switch to be visible before we try to interact with it" }, { "condition": { "statement": "if (document.querySelectorAll('.filter-and-sort-bar').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 13, "command": "click", "target": "#more-filters-button", "value": "", "variableName": "", "notes": "If there's a horizontal filters bar, rather than the regular side bar, expand the additional filters" }, { "condition": { "statement": "if (document.querySelectorAll('.filter-and-sort-bar').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 14, "command": "click", "target": "div.out-of-stock input", "value": "", "variableName": "", "notes": "Then filter the out of stock items out" }, { "condition": { "statement": "if (document.querySelectorAll('a[data-test=\"oos-switch\"]').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 15, "command": "click", "target": "a[data-test=\"oos-switch\"]", "value": "", "variableName": "", "notes": "Hide out of stock products by selecting the OOS toggle (regular filter panel)" }, { "condition": null, "optional": false, "private": false, "sequence": 16, "command": "extractEval", "target": "", "value": "var max = document.querySelectorAll('section[data-test=\"product-card\"]').length;\nvar min = 1;\nreturn Math.floor(Math.random()*(max-min+1)+min);", "variableName": "product", "notes": "Count the number of products displayed in the selected category, and choose one from that selection at random." }, { "condition": null, "optional": false, "private": false, "sequence": 17, "command": "click", "target": "div[data-test=\"component-grid-container\"] > div:nth-of-type({{product}}) a.product__image", "value": "", "variableName": "" }, { "condition": { "statement": "if (document.querySelectorAll('div[data-cy=\"desktop_colors\"]').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 18, "command": "click", "target": "div[data-cy=\"desktop_colors\"] a:not([class*=\"disabled\"])", "value": "", "variableName": "", "notes": "If the clothing swatch selection is present, select an in-stock swatch" }, { "condition": { "statement": "if (document.querySelectorAll('ul.swatch-list').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 19, "command": "click", "target": "ul.swatch-list a:not(.swatch--unavailable)", "value": "", "variableName": "", "notes": "If the non-clothing swatch selection is present, select an in-stock swatch" }, { "condition": null, "optional": false, "private": false, "sequence": 20, "command": "pause", "target": "", "value": "5000", "variableName": "", "notes": "Interaction with the variant selection is too quick. For now we'll use a pause step for now, with the intention of replacing with an implicit wait later." }, { "condition": { "statement": "if (document.querySelectorAll('ul[data-cy=\"size-selector-list\"]').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 21, "command": "click", "target": "ul[data-cy=\"size-selector-list\"] button:not([class*=\"size-button--out-of-stock\"])", "value": "", "variableName": "", "notes": "If the clothing size selection is present, select an in-stock size" }, { "condition": { "statement": "if (document.querySelectorAll('ul.size-list').length !== 0) {\n return true;\n}", "result": false }, "optional": false, "private": false, "sequence": 22, "command": "click", "target": "ul.size-list a:not(.size-link--unavailable)", "value": "", "variableName": "", "notes": "If the non-clothing size selection is present, select an in-stock size" }, { "condition": null, "optional": false, "private": false, "sequence": 23, "command": "click", "target": [ { "selector": "#button--add-to-basket" }, { "selector": "#add-to-basket-button" } ], "value": "", "variableName": "", "notes": "Click the Add to Basket button. If there are no other options on the product page, this is the only step that will be executed. Clothing and Non-clothing have different markup for the button, so we're using a backup selector to cover both" }, { "condition": null, "optional": false, "private": false, "sequence": 24, "command": "assertElementVisible", "target": [ { "selector": "#basket-confirmation" }, { "selector": ".add-to-basket-confirmation-message" } ], "value": "", "variableName": "", "notes": "Check that the success message appears. As before, there's a difference between clothing and non-clothing items." }, { "condition": null, "optional": false, "private": false, "sequence": 25, "command": "click", "target": [ { "selector": ".add-to-basket-view-basket-link" }, { "selector": "#basket-confirmation a" } ], "value": "", "variableName": "", "notes": "Navigate to the Basket" } ], "testFrequency": 0, "testFrequencyAdvanced": [], "viewportSize": null }
Code language: JSON / JSON with Comments (json)

Leave a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.