<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Hugh McCamphill's Blog]]></title><description><![CDATA[I'm Hugh - a software testing person with ESW, working on test automation, CI/CD activities, test leadership and a whole bunch besides]]></description><link>https://hughmccamphill.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 02:21:12 GMT</lastBuildDate><atom:link href="https://hughmccamphill.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Appium, iframes and iOS]]></title><description><![CDATA[When performing end to end journey test automation on an eccommerce style website, it’s common for security reasons for the credit card fields to be loaded in an iframe to allow payment forms from a payment service provider to added to a website in a...]]></description><link>https://hughmccamphill.com/appium-iframes-and-ios</link><guid isPermaLink="true">https://hughmccamphill.com/appium-iframes-and-ios</guid><category><![CDATA[appium]]></category><category><![CDATA[Webdriver.io]]></category><category><![CDATA[iOS]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Wed, 16 Oct 2024 21:25:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/3_Xwxya43hE/upload/845aa31af04e1b57cfce9bfbeba6810c.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When performing end to end journey test automation on an eccommerce style website, it’s common for security reasons for the credit card fields to be loaded in an iframe to allow payment forms from a payment service provider to added to a website in a secure manner.</p>
<p>This presents no issues for a human user when entering their credit card details, but that’s not always the case with test automation.</p>
<p>On desktop browsers, and on Chrome on Android, the steps to enter card details within an iframe is typically fairly straightforward:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> iframe = $(<span class="hljs-string">'#iframe'</span>)
<span class="hljs-keyword">await</span> browser.switchToFrame(<span class="hljs-keyword">await</span> iframe)
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'#name'</span>).setValue(<span class="hljs-string">'Test User'</span>)
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'#card-number'</span>).setValue(<span class="hljs-string">'4111 1111 1111 1111'</span>)
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'#expiry'</span>).setValue(<span class="hljs-string">'11/30'</span>)
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'#cvv'</span>).setValue(<span class="hljs-string">'123'</span>)

<span class="hljs-keyword">await</span> browser.switchToParentFrame()
</code></pre>
<p>However performing this on safari on iOS, will result in an error like this:</p>
<pre><code class="lang-typescript">org.openqa.selenium.WebDriverException: An unknown server-side error occurred <span class="hljs-keyword">while</span> processing the command. Original error: Blocked a frame <span class="hljs-keyword">with</span> origin “https:<span class="hljs-comment">//example.com 1” from accessing a cross-origin frame. Protocols, domains, and ports must match.</span>
</code></pre>
<p>Sure enough, if you type "appium iframes iOS" into a search engine, and you'll find many results about issues with switching to iframes due to cross site security issues (for example - <a target="_blank" href="https://discuss.appium.io/t/not-able-to-select-iframes-in-ios-safari/37944">https://discuss.appium.io/t/not-able-to-select-iframes-in-ios-safari/37944</a>).</p>
<p>Still, I thought there might still be a away to force some data into fields. I had some experience finding workarounds in similar situations. For example, it’s typically possible to send arbitrary low level keys to an element if you have focus on that element. So, how could we brute force focus on to the elements?</p>
<p>First step - what happens if we click the iframe? Can we click an iframe?? Turns out we can:</p>
<p>so I started by asking myself, "Can I at least force a click into the iframe?" It turns out we can when we use the <code>nativeWebTap</code> capability (for much more on this see <a target="_blank" href="https://www.headspin.io/blog/using-the-nativewebtap-capability">https://www.headspin.io/blog/using-the-nativewebtap-capability</a>)</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> $(<span class="hljs-string">'#iframe'</span>).click()
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729112761981/f7a490ac-92fe-4040-ad1d-68d5fb2681bf.png" alt class="image--center mx-auto" /></p>
<p>So by clicking on the iframe, we’ve got focus on one of the fields we need to fill in.</p>
<p>This leaves us two problems / questions</p>
<ol>
<li><p>How do we send keys to the field?</p>
</li>
<li><p>How do we get focus on the <strong>other</strong> fields?</p>
</li>
</ol>
<h3 id="heading-entering-keys-into-a-field">Entering keys into a field</h3>
<p>For these fields we need to leverage low level actions API, as using the normal methods will not work - in this case we use key presses.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> browser.action(<span class="hljs-string">'key'</span>)
        .down(<span class="hljs-string">'1'</span>).up(<span class="hljs-string">'1'</span>)
        .down(<span class="hljs-string">'2'</span>).up(<span class="hljs-string">'2'</span>)
        .down(<span class="hljs-string">'3'</span>).up(<span class="hljs-string">'3'</span>)
        .perform()
</code></pre>
<p>Having successfully entered text into a field, we needed to be able to navigate to other fields. Looking again at the screen, the keyboard was showing, so perhaps we could leverage that to tab between fields?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729112984083/bac1649d-9b22-4194-a929-f3addc4a99b5.png" alt class="image--center mx-auto" /></p>
<p>They keyboard is outside of the context of the browser, but given we're already using WebdriverIO, switching to that <strong>native context</strong> using Appium is trivial - and we can then tap that <strong><em>previous</em></strong> button</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// store web view context for future use</span>
<span class="hljs-keyword">const</span> webViewContext = <span class="hljs-keyword">await</span> browser.getContext()
<span class="hljs-keyword">await</span> browser.switchContext(<span class="hljs-string">'NATIVE_APP'</span>)
<span class="hljs-comment">// navigates to expiry date</span>
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'~Previous'</span>).touchAction(<span class="hljs-string">'tap'</span>) 
<span class="hljs-comment">// note, touchAction is deprecated, use Actions api is recommended</span>
</code></pre>
<p>We can then switch back to the browser context, and enter the next piece of data:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> browser.switchContext(currentContext.toString())
<span class="hljs-keyword">await</span> browser.action(<span class="hljs-string">'key'</span>)
        .down(<span class="hljs-string">'1'</span>)
        .down(<span class="hljs-string">'1'</span>)
        .down(<span class="hljs-string">'3'</span>)
        .down(<span class="hljs-string">'0'</span>)
        .up(<span class="hljs-string">'1'</span>)
        .up(<span class="hljs-string">'1'</span>)
        .up(<span class="hljs-string">'3'</span>)
        .up(<span class="hljs-string">'0'</span>)  
        .perform()
</code></pre>
<p>Bringing it all together, using a convenience method for entering the text by actions (enterDigits)</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> iframe = $(<span class="hljs-string">'#iframe'</span>)

<span class="hljs-keyword">await</span> iframe.scrollIntoView()
<span class="hljs-keyword">await</span> iframe.click()

<span class="hljs-keyword">const</span> currentContext = <span class="hljs-keyword">await</span> browser.getContext()
<span class="hljs-keyword">await</span> browser.switchContext(<span class="hljs-string">'NATIVE_APP'</span>)
<span class="hljs-comment">// make sure we are on cvv</span>
<span class="hljs-keyword">await</span> browser.waitUntil(<span class="hljs-keyword">async</span> () =&gt; {
<span class="hljs-keyword">if</span> (!(<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.next.isEnabled())) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>
    }
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.next.touchAction(<span class="hljs-string">'tap'</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>
})

<span class="hljs-comment">// cvv</span>
<span class="hljs-keyword">await</span> browser.switchContext(currentContext.toString())
<span class="hljs-keyword">await</span> browser.enterDigits(<span class="hljs-string">'123'</span>)
<span class="hljs-keyword">await</span> browser.switchContext(<span class="hljs-string">'NATIVE_APP'</span>)
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'~Previous'</span>).touchAction(<span class="hljs-string">'tap'</span>)

<span class="hljs-comment">// repeat for expiry date</span>
<span class="hljs-keyword">await</span> browser.switchContext(currentContext.toString())
<span class="hljs-keyword">await</span> browser.enterDigits(<span class="hljs-string">'1130'</span>)
<span class="hljs-keyword">await</span> browser.switchContext(<span class="hljs-string">'NATIVE_APP'</span>)
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'~Previous'</span>).touchAction(<span class="hljs-string">'tap'</span>)

<span class="hljs-comment">// and so on for card number and name fields</span>
<span class="hljs-comment">// finally, close the keyboard, and switch back to the browser context</span>
<span class="hljs-keyword">await</span> $(<span class="hljs-string">'~Done'</span>).click()
<span class="hljs-keyword">await</span> browser.switchContext(currentContext.toString())
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729113443364/6e96bfa0-6a8a-40e8-aed5-d7fd1cfd3807.png" alt class="image--center mx-auto" /></p>
<p>So in summary, to make this work we have</p>
<ul>
<li><p>Clicked the iframe which clicks the CVV field</p>
<ul>
<li>This has shown to always be reliable, but we navigate to CVV in case we land on a different field</li>
</ul>
</li>
<li><p>Use actions api to send key presses to the active element</p>
</li>
<li><p>We use appium to interact with the ‘previous’ and Done buttons, switching between the native contect and browser context each time</p>
</li>
</ul>
<p>Hopefully this helps a few people!</p>
]]></content:encoded></item><item><title><![CDATA[What?! How to improve your Test Automation]]></title><description><![CDATA[After 20 years of working with test automation, I like to think I've become quite good at it. Recently, I was advocating for a change in approach at a company I had just joined. I knew it was crucial to clearly explain my method for writing maintaina...]]></description><link>https://hughmccamphill.com/what-how-to-improve-your-test-automation</link><guid isPermaLink="true">https://hughmccamphill.com/what-how-to-improve-your-test-automation</guid><category><![CDATA[test-automation]]></category><category><![CDATA[test]]></category><category><![CDATA[patterns]]></category><category><![CDATA[development]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Fri, 12 Jul 2024 16:05:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/MfBnqUOz_qY/upload/6abfee46167c9a19dc581d8ee09adf86.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After 20 years of working with test automation, I like to think I've become quite good at it. Recently, I was advocating for a change in approach at a company I had just joined. I knew it was crucial to clearly explain my method for writing maintainable and intent-revealing test automation.</p>
<p>Many people know they should use a pattern to avoid repeating themselves in their tests (like using page objects). But what are we really trying to achieve with a page object? Is it just about following the DRY principle?</p>
<p>In Gojko Adzic's <a target="_blank" href="https://vimeo.com/154289460">talk</a> at NDC London in 2016, one of the five key patterns for successful test automation he referenced was this:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Divide expectations, workflows, interactions</div>
</div>

<p>Where we might consider workflows activities such as (as in his examples):</p>
<pre><code class="lang-typescript">Authorise()
AddItems()
Checkout()
</code></pre>
<p>And interactions examples such as clicking a link or setting value in a field.</p>
<pre><code class="lang-typescript">$(<span class="hljs-string">'a[role=...'</span>).click()
$(<span class="hljs-string">'input[name=....]'</span>).setValue(<span class="hljs-string">'...'</span>)
</code></pre>
<p>We can summarize this as taking a <strong>what</strong> vs <strong>how</strong> approach to writing tests. Our tests should mainly refer to <strong>what</strong> we want to happen (e.g., checkout), while the technical steps required to complete it (the <strong>how</strong>) are abstracted away.</p>
<h3 id="heading-benefits">Benefits</h3>
<p>Tests that focus on "how" tend to be fragile because they are closely tied to the specific details of the application. Any change in the UI or workflow can require extensive updates to the tests themselves. By focusing on the "what," your tests become more resilient to changes. They are based on high-level behaviors and outcomes, which are less likely to be affected by minor UI changes or workflow adjustments.</p>
<p><strong>Not all your workflow need be UI driven</strong></p>
<p>Whenever we specify workflows at the "what" level, it becomes easier to identify steps that can be achieved through other mechanisms. For example, our Authorize step could be accomplished using a combination of API calls and cookie settings, bypassing the need to perform the same steps via multiple pages.</p>
<p><strong>Improved Test Development</strong></p>
<p>When tests are designed around the "what," the complexity of test scripts is reduced. This minimizes the risk of falling into the copy/paste trap, where people might copy a series of "how" actions to reach a certain stage in the application without truly understanding the business behavior.</p>
<p>By removing specific interactions from the tests, test developers are encouraged to think about the business flows instead of just making it work through copying and pasting.</p>
<h4 id="heading-the-right-level-of-what"><strong>The right level of "what"</strong></h4>
<p>A "what" focused approach allows for greater flexibility in adapting to changes and scaling the test suite. However, it still requires care, attention, and a focus on true business behaviors, while removing incidental details.</p>
<p>Consider the example below. It might seem like a suitable level of abstraction since the interactions are not in the tests (no "how" level detail), but it doesn't go far enough.</p>
<pre><code class="lang-js">DetailsPage.selectSize(‘XL’);
DetailsPage.selectQuantity(<span class="hljs-number">1</span>);
DetailsPage.selectColor(“white”);
DetailsPage.selectReoccuringPurchase(<span class="hljs-literal">false</span>);
DetailsPage.addToCart();
</code></pre>
<p>In his wonderful <a target="_blank" href="https://medium.com/slalom-build/chekhovs-gun-for-automated-tests-d7f79d0de58d">blog post</a> Blake Norrish discusses the literary idea of Checkov's Gun:</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">“Chekhov’s Gun” is a fancy literary term coined by Russian playwright and author Anton Chekhov, who stated that everything mentioned in a story must, in some way, come into play later in the story, and everything that does not should be removed.</div>
</div>

<p>Taking that idea we might come up with something like the following, where we've removed incidental detail and focused on the core workflow.</p>
<pre><code class="lang-js">DetailsPage.addValidItem();
</code></pre>
<p>And of course we may not stop there, as the same principal can apply across multiple pages and not just one.</p>
<p>Imagine we've done the above and we have removed incidental detail so our interactions look might look like this:</p>
<pre><code class="lang-typescript">DetailsPage.addValidItem()
ShippingPage.enterShippingAddress()
BillingPage.enterBillingAddress()
PaymentPage.enterPaymentInformation()
PaymentPage.reviewOrder()
PaymentPage.placeOrder()
</code></pre>
<p>Note in this example, again we don't have any details of "how" in the tests (the technical interactions), but we haven't gone far enough, as we would be potentially repeating these workflows in many tests.</p>
<p>This could be summarised as the following where we've removed some "what" details about replaced it with a higher level facade, which communicates what we need to do.</p>
<pre><code class="lang-typescript">DetailsPage.addValidItem()
Payment.placeOrder()
</code></pre>
<p>Of course, a lot of these code example are a gross simplification, but should give you the idea of how you can think about achieving the right level of "what".</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1721035774491/8ff5fda6-0ea2-4294-becd-667433f97c05.png" alt class="image--center mx-auto" /></p>
<p>By focusing on the desired outcomes and behaviors of our applications, through this focus on "what vs how" we've been able to create more resilient, maintainable and business focused tests to support the company in it's journey to deliver better software quality and ultimately to deliver greater value to our customers.</p>
]]></content:encoded></item><item><title><![CDATA[Using a context map checklist]]></title><description><![CDATA[At Glofox, we regularly perform whole-team exploratory testing sessions. These are very effective at discovering problems that are not found when testing around acceptance criteria, as we get multiple different perspectives, and it gives developers t...]]></description><link>https://hughmccamphill.com/using-a-context-map-checklist</link><guid isPermaLink="true">https://hughmccamphill.com/using-a-context-map-checklist</guid><category><![CDATA[Testing]]></category><category><![CDATA[Collaboration]]></category><category><![CDATA[domain]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Wed, 20 Jul 2022 14:10:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/unsplash/aGxMCcAh2Pw/upload/v1658099354678/9uc_1-U5Q.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At Glofox, we regularly perform whole-team exploratory testing sessions. These are very effective at discovering problems that are not found when testing around acceptance criteria, as we get multiple different perspectives, and it gives developers the headspace to explicitly perform exploratory testing in a way they might not when focused on writing the code. However, I'd observed that there were occasions when we would discover bugs that arguably should have been discovered earlier in the process, either when user story mapping or during 3 amigo sessions. These would take the form of missed use cases or inconsistency in behaviour between applications, or inconsistency in behaviour within the application, for example with unsuccessful transactions. </p>
<p>I'd recently read the excellent <a target="_blank" href="https://leanpub.com/50quickideas-tests">50 Quick Ideas to Improve your Tests</a>, and one of the items that stuck out was using risk checklists for cross-cutting concerns. This entails using a 'Do-Confirm' checklist as outlined in the <a target="_blank" href="https://www.goodreads.com/book/show/6667514-the-checklist-manifesto">Checklist manifesto</a>, where instead of using a checklist to tick off items as people work, you use the checklist as a review aid - ie, do the work, pause and review to see if anything was missed. </p>
<p>This gave me the idea of creating a checklist that would list out the main domain and sub-domain areas within our apps. One of the other items mentioned in the book is that <em>"good checklists should not aim to be comprehensive how-to guides"</em> - with that in mind, I felt it was important not to create an exhaustive checklist that would list all the functionality, but to merely highlight the main areas - getting the right level of detail is tricky but important! Too high level and it's not valuable enough, too low level and you get lost in the detail - you need to be able to scan it relatively quickly. </p>
<p>So this is what we came up with:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1658326147606/h8YVZCnwI.png" alt="Screenshot 2022-07-20 at 15.08.46.png" /></p>
<p>As noted at the top, we want people to try to recall functionality/domain knowledge first, and then refer to the checklist; we don't want people's focus to narrow to what is in the checklist as it is absolutely not complete (we can't list everything), but it acts as a reminder of parts of the overall Glofox domain that some people may not be as familiar with. </p>
<p>Historically testers (in addition to providing such things as a differing perspective and applying critical thinking) have been valued for being perceived as having deeper domain knowledge than developers - I think we need to change how we work in order to improve the domain knowledge of everyone in the team without making ourselves the single point of failure regarding domain knowledge - what happens if a tester is off on holiday? I think this is one way to help. </p>
]]></content:encoded></item><item><title><![CDATA[Measuring user flow performance with Lighthouse and WebdriverIO]]></title><description><![CDATA[Lighthouse has a new user-flow API that allows lab testing at any point within a page's lifespan. There is support for generating a lighthouse report from a puppeteer script, but I wanted to explore using WebdriverIO to do the same!
Why would you wan...]]></description><link>https://hughmccamphill.com/measuring-user-flow-performance-with-lighthouse-and-webdriverio</link><guid isPermaLink="true">https://hughmccamphill.com/measuring-user-flow-performance-with-lighthouse-and-webdriverio</guid><category><![CDATA[Testing]]></category><category><![CDATA[performance]]></category><category><![CDATA[web performance]]></category><category><![CDATA[webdriver]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sun, 13 Feb 2022 16:36:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644769439805/2ovP1LJQz.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Lighthouse has a new <a target="_blank" href="https://developer.chrome.com/blog/lighthouse-9-0/#lighthouse-user-flows">user-flow API</a> that allows lab testing at any point within a page's lifespan. There is support for generating a lighthouse report from a puppeteer script, but I wanted to explore using WebdriverIO to do the same!</p>
<h3 id="heading-why-would-you-want-to-do-this-though">Why would you want to do this though?</h3>
<p>Some of you will know that WebdriverIO has some inbuilt support for <a target="_blank" href="https://webdriver.io/docs/devtools-service/#performance-testing">performance testing</a>, which leverages chrome devtools as well, but I find one of the large benefits of Lighthouse is the report that gets generated, and not just the scores themselves. </p>
<p>Additionally, we can leverage pre-existing WebdriverIO code to perform <a target="_blank" href="https://web.dev/lighthouse-user-flows/#snapshots">snapshots</a> or <a target="_blank" href="https://web.dev/lighthouse-user-flows/#timespans">timespans</a>, other important aspects of front end performance beyond the initial page load. </p>
<p><em>All the examples here reference https://web.dev/lighthouse-user-flows/#timespans, where you can see the original Puppeteer code and resulting lighthouse reports; this blog focuses on how to use WebdriverIO with lighthouse flow.
</em></p>
<p>Firstly though, what does a basic puppeteer script look like, when combined with the new user-flow API? From the example in https://web.dev/lighthouse-user-flows/ for a basic page load performance analysis we have:</p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-title">fs</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'fs'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">open</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'open'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">puppeteer</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'puppeteer'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">startFlow</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'lighthouse/lighthouse-core/fraggle-rock/api.js'</span>;

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">captureReport</span>(<span class="hljs-params"></span>) </span>{
  const browser <span class="hljs-operator">=</span> await puppeteer.launch({ headless: <span class="hljs-literal">false</span> });
  const page <span class="hljs-operator">=</span> await browser.newPage();

  const flow <span class="hljs-operator">=</span> await startFlow(page, {name: <span class="hljs-string">'Single Navigation'</span>});
  await flow.navigate(<span class="hljs-string">'https://web.dev/performance-scoring/'</span>);

  await browser.close();

  const report <span class="hljs-operator">=</span> flow.generateReport();
  fs.writeFileSync(<span class="hljs-string">'flow.report.html'</span>, report);
  open(<span class="hljs-string">'flow.report.html'</span>, { wait: <span class="hljs-literal">false</span> });
}

captureReport();
</code></pre><p>To do the same in WebdriverIO, we just need to retrieve <a target="_blank" href="https://webdriver.io/docs/api/browser/getPuppeteer/">Puppeteer</a> and access the first page:</p>
<pre><code>const puppeteerBrowser <span class="hljs-operator">=</span> await browser.getPuppeteer();
const page <span class="hljs-operator">=</span> (await puppeteerBrowser.pages())[<span class="hljs-number">0</span>];
</code></pre><p>It's possible to run WebdriverIO in standalone mode, but as most people are familiar with looking at it used in a test:</p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-title">fs</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'fs'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">open</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'open'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">startFlow</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'lighthouse/lighthouse-core/fraggle-rock/api.js'</span>;

describe(<span class="hljs-string">'Lighthouse Performance '</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  it(<span class="hljs-string">'Should be able to generate lighthouse report with Webdriverio'</span>, async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
     const puppeteerBrowser <span class="hljs-operator">=</span> await browser.getPuppeteer();
     const page <span class="hljs-operator">=</span> (await puppeteerBrowser.pages())[<span class="hljs-number">0</span>];

     const flow <span class="hljs-operator">=</span> await startFlow(page, {name: <span class="hljs-string">'Single Navigation'</span>});
     await flow.navigate(<span class="hljs-string">'https://web.dev/performance-scoring/'</span>);

     await browser.close();

     const report <span class="hljs-operator">=</span> flow.generateReport();
     fs.writeFileSync(<span class="hljs-string">'flow.report.html'</span>, report);
     open(<span class="hljs-string">'flow.report.html'</span>, {wait: <span class="hljs-literal">false</span>});
  }
}
</code></pre><p>This though doesn't really offer any benefit over using Puppeteer directly. For that, as mentioned, we want to be performing snapshot or timespan performance assessments, getting the benefit of (arguably) a nicer syntax, or more ideally, leveraging pre-existing WebdriverIO code that will perform the navigation and interactions required. </p>
<p>For example, instead of:</p>
<pre><code>async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">captureReport</span>(<span class="hljs-params"></span>) </span>{
  const browser <span class="hljs-operator">=</span> await puppeteer.launch({headless: <span class="hljs-literal">false</span>});
  const page <span class="hljs-operator">=</span> await browser.newPage();

  const flow <span class="hljs-operator">=</span> await startFlow(page, {name: <span class="hljs-string">'Squoosh snapshots'</span>});

  await page.goto(<span class="hljs-string">'https://squoosh.app/'</span>, { waitUntil: <span class="hljs-string">'networkidle0'</span>} );

  <span class="hljs-comment">// Wait for first demo-image button, then open it.</span>
  const demoImageSelector <span class="hljs-operator">=</span> <span class="hljs-string">'ul[class*="demos"] button'</span>;
  await page.waitForSelector(demoImageSelector);
  await flow.snapshot({ stepName: <span class="hljs-string">'Page loaded'</span> });
  await page.click(demoImageSelector);

  <span class="hljs-comment">// Wait for advanced settings button in UI, then open them.</span>
  const advancedSettingsSelector <span class="hljs-operator">=</span> <span class="hljs-string">'form label[class*="option-reveal"]'</span>;
  await page.waitForSelector(advancedSettingsSelector);
  await flow.snapshot({ stepName: <span class="hljs-string">'Demo loaded'</span> });
  await page.click(advancedSettingsSelector);

  await flow.snapshot({ stepName: <span class="hljs-string">'Advanced settings opened'</span> });

  browser.close();

  const report <span class="hljs-operator">=</span> flow.generateReport();
  fs.writeFileSync(<span class="hljs-string">'flow.report.html'</span>, report);
  open(<span class="hljs-string">'flow.report.html'</span>, {wait: <span class="hljs-literal">false</span>});
}

captureReport();
</code></pre><p>we can instead write the below, using WebdriverIO for navigation and interaction:</p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-title">fs</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'fs'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-title">open</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'open'</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">startFlow</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">'lighthouse/lighthouse-core/fraggle-rock/api.js'</span>;

describe(<span class="hljs-string">'Lighthouse Performance '</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  it(<span class="hljs-string">'Should be able to generate lighthouse report with Webdriverio'</span>, async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
     const puppeteerBrowser <span class="hljs-operator">=</span> await browser.getPuppeteer();
     const page <span class="hljs-operator">=</span> (await puppeteerBrowser.pages())[<span class="hljs-number">0</span>];

     const flow <span class="hljs-operator">=</span> await startFlow(page, {name: <span class="hljs-string">'Single Navigation'</span>});
     await browser.url(<span class="hljs-string">'https://squoosh.app/'</span>);

     <span class="hljs-comment">// Wait for first demo-image button, then open it.</span>
     const demoImageSelector <span class="hljs-operator">=</span> await $(<span class="hljs-string">'ul[class*="demos"] button'</span>);
     await demoImageSelector.waitForDisplayed();
     await flow.snapshot({ stepName: <span class="hljs-string">'Page loaded'</span> });
     await demoImageSelector.click();

     <span class="hljs-comment">// Wait for advanced settings button in UI, then open them.</span>
     const advancedSettingsSelector <span class="hljs-operator">=</span> $(<span class="hljs-string">'label[class*="option-reveal"]'</span>);
     await advancedSettingsSelector.waitForDisplayed();

     await flow.snapshot({ stepName: <span class="hljs-string">'Demo loaded'</span> });
     await advancedSettingsSelector.click();

     await flow.snapshot({ stepName: <span class="hljs-string">'Advanced settings opened'</span> });

     const report <span class="hljs-operator">=</span> flow.generateReport();
     fs.writeFileSync(<span class="hljs-string">'flow.report.html'</span>, report);
     open(<span class="hljs-string">'flow.report.html'</span>, { wait: <span class="hljs-literal">false</span> });
  }
}
</code></pre><p>This is obviously a trivial amount of navigation; the following is an example of performing a timespan performance assessment, taken from the company where I work, <a target="_blank" href="https://www.glofox.com/">Glofox</a>, where we use the applications APIs to setup data as well as some high level flows that then use WebdriverIO behind the scenes:</p>
<pre><code>describe(<span class="hljs-string">'Dashboard Performance'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  it(<span class="hljs-string">'Lighthouse flow for booking class for member'</span>, async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    const { className } <span class="hljs-operator">=</span> fakeData();
    const puppeteerBrowser <span class="hljs-operator">=</span> await browser.getPuppeteer();
    const page <span class="hljs-operator">=</span> (await puppeteerBrowser.pages())[<span class="hljs-number">0</span>];

    const flow <span class="hljs-operator">=</span> await startFlow(page);

    await flow.startTimespan({ stepName: <span class="hljs-string">'Login'</span> });
    <span class="hljs-comment">// WebdriverIO code - performs invitial browser.url navigations, filling in data across two pages</span>
    await Dashboard.login();
    await flow.endTimespan();

    await flow.snapshot({ stepName: <span class="hljs-string">'Logged in'</span> });

    <span class="hljs-comment">// create a 'member' via API</span>
    const member <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Member();
    const { id: memberId } <span class="hljs-operator">=</span> await DashboardAPICalls.Members.create({ member });

    <span class="hljs-comment">// create a class for member to book via API</span>
    await DashboardAPICalls.Classes.createClass({ className });

    await flow.startTimespan({ stepName: <span class="hljs-string">'Book Class'</span> });
    <span class="hljs-comment">// book class via WebdriverIO - multiple pages and clicks involved</span>
    await Dashboard.bookClass({ member, className });
    await flow.endTimespan();
  }
}
</code></pre><p>With this example, hopefully, you can see how we're leveraging our existing WebriverIO code (and API abstractions to set up data!) to perform navigations and interactions, then making the lighthouse flow analysis. </p>
<h2 id="heading-final-notes">Final Notes</h2>
<p>The new Lighthouse user-flow API provides extra possibilities for analyzing user front-end performance - as shown you can leverage WebdriverIO existing code to do any required navigation and form filling. One additional consideration is how to run these types of tests as part of a pipeline - but that's for another day. </p>
<h3 id="heading-appendix">Appendix</h3>
<p>If you want to run the test on desktop, you can pass in a desktop configuration to the <code>startFlow</code> method:</p>
<pre><code>const config <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'lighthouse/lighthouse-core/config/desktop-config.js'</span>);

const puppeteerBrowser <span class="hljs-operator">=</span> await browser.getPuppeteer();
const page <span class="hljs-operator">=</span> (await puppeteerBrowser.pages())[<span class="hljs-number">0</span>];
const flow <span class="hljs-operator">=</span> await startFlow(page, { config });
</code></pre>]]></content:encoded></item><item><title><![CDATA[Quarantine new tests via CircleCI with Allure]]></title><description><![CDATA[Much has been written about flaky tests, the perils of ignoring the signal they may be giving you and how to deal with them from a tools perspective, and here but I've never really read anything about how to stop flaky tests getting added in the firs...]]></description><link>https://hughmccamphill.com/quarantine-new-tests-via-circleci-with-allure</link><guid isPermaLink="true">https://hughmccamphill.com/quarantine-new-tests-via-circleci-with-allure</guid><category><![CDATA[automation]]></category><category><![CDATA[Testing]]></category><category><![CDATA[webdriver]]></category><category><![CDATA[test]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sat, 12 Feb 2022 20:44:43 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644708934819/VKQNRHeht.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Much has been written about <a target="_blank" href="https://martinfowler.com/articles/nonDeterminism.html">flaky tests</a>, the perils of <a target="_blank" href="https://alisterbscott.com/2015/11/11/your-tests-arent-flaky/">ignoring the signal they may be giving you</a> and how to deal with them from a <a target="_blank" href="https://engineering.atspotify.com/2019/11/18/test-flakiness-methods-for-identifying-and-dealing-with-flaky-tests/">tools perspective</a>, and <a target="_blank" href="https://www.slideshare.net/eviltester/your-automated-execution-does-not-have-to-be-flaky">here</a> but I've never really read anything about how to stop flaky tests getting added in the first place (in so far as we can). Of course, flakiness may only emerge after time, but often we can discover problems simply by running the same test a number of times, particularly on the infrastructure that will run the test normally (ie not just locally).</p>
<h2 id="heading-making-use-of-allure">Making use of Allure</h2>
<p>I've written up how we are (<a target="_blank" href="https://www.glofox.com/">Glofox</a>) are performing this discovery and thought it might be useful to share.</p>
<p>We were already using <a target="_blank" href="https://docs.qameta.io/allure/">Allure</a> for our test results reporting, and one of the features of Allure is that it captures retry information for a test.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644708533013/JGF_9E86n.png" alt="retries.png" /></p>
<p>I thought it might be useful to leverage this to give a quick visual of the stability of a test that has just been added, and that's what we have implemented as a lightweight step as part of a pull request. </p>
<p>The steps to achieve this were as follows: </p>
<ul>
<li>Find out which tests have been added</li>
<li>Run each of them (for example) 10 times</li>
<li>Fail the job if the test failed any of the 10 times it was executed. </li>
</ul>
<h3 id="heading-which-tests-have-been-changed-or-added">Which tests have been changed or added</h3>
<p>To find out which of the tests have changed been added, we can use <code>git diff</code>, and grep for test files. </p>
<pre><code>git diff origin<span class="hljs-operator">/</span>master...$CIRCLE_BRANCH <span class="hljs-operator">-</span><span class="hljs-operator">-</span>name<span class="hljs-operator">-</span>only <span class="hljs-operator">|</span> grep test.js
</code></pre><p>This will give us a list of test files that have been added. As this solution could be re-used for <em>changed</em> tests, we could further filter to ignore spaces and blank lines. We then pipe the results to a file. </p>
<pre><code>do
    DIFF<span class="hljs-operator">=</span>`git diff origin<span class="hljs-operator">/</span>master...$CIRCLE_BRANCH <span class="hljs-operator">-</span><span class="hljs-operator">-</span>ignore<span class="hljs-operator">-</span>all<span class="hljs-operator">-</span>space <span class="hljs-operator">-</span><span class="hljs-operator">-</span>ignore<span class="hljs-operator">-</span>blank<span class="hljs-operator">-</span>lines ${f}`
    <span class="hljs-keyword">if</span> [[ <span class="hljs-operator">!</span> ${DIFF} <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">""</span> ]];
    then
        echo ${f} <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> <span class="hljs-operator">/</span>tmp<span class="hljs-operator">/</span>tests<span class="hljs-operator">-</span>to<span class="hljs-operator">-</span>run.txt
    fi
done
</code></pre><h3 id="heading-pipe-the-tests-to-wdio">Pipe the tests to WDIO</h3>
<p>The tests that have been written to this file can then be piped to the <a target="_blank" href="https://webdriver.io/docs/organizingsuites.html#run-multiple-specific-test-specs">wdio command-line utility</a>: </p>
<pre><code>cat <span class="hljs-operator">/</span>tmp<span class="hljs-operator">/</span>tests<span class="hljs-operator">-</span>to<span class="hljs-operator">-</span>run.txt <span class="hljs-operator">|</span> ./node_modules<span class="hljs-operator">/</span>.bin/wdio ./config<span class="hljs-operator">/</span>wdio.conf.js
</code></pre><p>To run the tests a number of times, we can write a simple loop</p>
<pre><code>COUNTER<span class="hljs-operator">=</span><span class="hljs-number">10</span>
until [  $COUNTER <span class="hljs-operator">-</span>lt <span class="hljs-number">1</span> ]; do
    cat <span class="hljs-operator">/</span>tmp<span class="hljs-operator">/</span>tests<span class="hljs-operator">-</span>to<span class="hljs-operator">-</span>run.txt <span class="hljs-operator">|</span> ./node_modules<span class="hljs-operator">/</span>.bin/wdio ./config<span class="hljs-operator">/</span>wdio.conf.js
    let COUNTER<span class="hljs-operator">-</span><span class="hljs-operator">=</span><span class="hljs-number">1</span>
done
</code></pre><h4 id="heading-check-there-are-tests-to-run">Check there are tests to run</h4>
<p>However, we may have made updates that resulted in no tests being changed, so we can add a check for that:</p>
<pre><code><span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>f <span class="hljs-string">"/tmp/tests-to-run.txt"</span> ]; then
    COUNTER<span class="hljs-operator">=</span><span class="hljs-number">10</span>
    until [  $COUNTER <span class="hljs-operator">-</span>lt <span class="hljs-number">1</span> ]; do
        cat <span class="hljs-operator">/</span>tmp<span class="hljs-operator">/</span>tests<span class="hljs-operator">-</span>to<span class="hljs-operator">-</span>run.txt <span class="hljs-operator">|</span> ./node_modules<span class="hljs-operator">/</span>.bin/wdio ./config<span class="hljs-operator">/</span>wdio.conf.js
        let COUNTER<span class="hljs-operator">-</span><span class="hljs-operator">=</span><span class="hljs-number">1</span>
    done
fi
</code></pre><h3 id="heading-allow-the-loop-to-complete">Allow the loop to complete</h3>
<p> Due to the <a target="_blank" href="https://circleci.com/docs/2.0/configuration-reference/#default-shell-options">default shell settings</a> if any iteration of the test(s) were to fail, the step would end without completing the loop. While this would tell us the test had a problem, it wouldn't show if any other additional problems exised in the test(s). To allow the loop to continue even if there is a failure, we add <code>set + e</code> at the start, so it doesn't exit immediately. </p>
<p>Adding this all together (determining the tests that have been added (or indeed changed), run them in a loop, and allowing the loop to continue even with failures), we end up with:</p>
<pre><code> <span class="hljs-operator">-</span> run:
    name: Test changed tests
    command: <span class="hljs-operator">|</span>
        set <span class="hljs-operator">+</span>e
        <span class="hljs-keyword">for</span> f in `git diff origin<span class="hljs-operator">/</span>master...$CIRCLE_BRANCH <span class="hljs-operator">-</span><span class="hljs-operator">-</span>name<span class="hljs-operator">-</span>only <span class="hljs-operator">|</span> grep test.js`;
        do
            DIFF<span class="hljs-operator">=</span>`git diff origin<span class="hljs-operator">/</span>master...$CIRCLE_BRANCH <span class="hljs-operator">-</span><span class="hljs-operator">-</span>ignore<span class="hljs-operator">-</span>all<span class="hljs-operator">-</span>space <span class="hljs-operator">-</span><span class="hljs-operator">-</span>ignore<span class="hljs-operator">-</span>blank<span class="hljs-operator">-</span>lines ${f}`
            <span class="hljs-keyword">if</span> [[ <span class="hljs-operator">!</span> ${DIFF} <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">""</span> ]];
            then
                echo ${f} <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> <span class="hljs-operator">/</span>tmp<span class="hljs-operator">/</span>tests<span class="hljs-operator">-</span>to<span class="hljs-operator">-</span>run.txt
            fi
        done
        <span class="hljs-keyword">if</span> [ <span class="hljs-operator">-</span>f <span class="hljs-string">"/tmp/tests-to-run.txt"</span> ]; then
            COUNTER<span class="hljs-operator">=</span><span class="hljs-number">10</span>
            until [  $COUNTER <span class="hljs-operator">-</span>lt <span class="hljs-number">1</span> ]; do
            cat <span class="hljs-operator">/</span>tmp<span class="hljs-operator">/</span>tests<span class="hljs-operator">-</span>to<span class="hljs-operator">-</span>run.txt <span class="hljs-operator">|</span> ./node_modules<span class="hljs-operator">/</span>.bin/wdio ./config<span class="hljs-operator">/</span>wdio.conf.js
            let COUNTER<span class="hljs-operator">-</span><span class="hljs-operator">=</span><span class="hljs-number">1</span>
            done
        fi
        <span class="hljs-literal">true</span>
</code></pre><p>You will notice that we added true at the end - this has the effect of making the step exit with 0. So how will we know if the test(s) failed at any point? </p>
<p>To determine this, we can inspect the <code>test cases</code> that makes up the retries of the Allure report as mentioned above. These are contained within <code>allure-report/data/test-cases/</code> directory. Using <a target="_blank" href="https://circleci.com/orbs/registry/orb/circleci/jq">jq</a>, we can check for any instances of a failure, and <code>exit 1</code> to force the job to fail. </p>
<pre><code><span class="hljs-operator">-</span> run:
    name: Set job result
    command: <span class="hljs-operator">|</span>
        chmod <span class="hljs-operator">+</span>x <span class="hljs-operator">/</span>home<span class="hljs-operator">/</span>circleci<span class="hljs-operator">/</span>project<span class="hljs-operator">/</span>allure<span class="hljs-operator">-</span>report<span class="hljs-operator">/</span>data<span class="hljs-operator">/</span>test<span class="hljs-operator">-</span>cases<span class="hljs-comment">/*.json
        failed_tests=$(cat /home/circleci/project/allure-report/data/test-cases/*.json | jq '. | select(.status=="failed") | .status')
        [[ ! -z "$failed_tests" ]] &amp;&amp; exit 1 || echo "No flaky tests"
    when: always</span>
</code></pre><p>A failing step would look like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644708682383/bI8CFUUKy.png" alt="job_result.png" /></p>
<p>When inspecting the <a target="_blank" href="https://hughmccamphill.com/blog/webdriverio-circleci/">Allure report</a>, you will then be able to see how often it failed, and the reason for failure(s).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644708728585/ZkT0NpHU2.png" alt="retries_with_fail.png" /></p>
<p>So there you have it! Any mechanism like this is really useful for discovering tests that might be flaky before they are added to the repo. Of course, 10 executions as in this example may not be statistically significant enough to discover all types of flakiness, but we have found it useful for the most obvious instances. </p>
<p>What's next?  </p>
<ul>
<li>Maybe you want to distribute the loop to threads to run in parallel instead of sequentially</li>
<li>A filter on the branch name to ignore PRs where you explicitly don't want this to run</li>
</ul>
<p>What about you? If you have any mechanism for quarantining new tests let me know on Twitter or LinkedIn via the links below!</p>
]]></content:encoded></item><item><title><![CDATA[Four reasons why WebdriverIO is better than Cypress]]></title><description><![CDATA[Having recently seen Alister Scott's reasons for preferring Playwright over Cypress, I thought it would be nice to do a comparison with WebdriverIO. I've never used Cypress in anger, but I certainly identify with the challenges it has for performing ...]]></description><link>https://hughmccamphill.com/four-reasons-why-webdriverio-is-better-than-cypress</link><guid isPermaLink="true">https://hughmccamphill.com/four-reasons-why-webdriverio-is-better-than-cypress</guid><category><![CDATA[Cypress]]></category><category><![CDATA[webdriver]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sat, 12 Feb 2022 20:44:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709100143/PgrPqf9kh.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Having recently seen <a target="_blank" href="https://twitter.com/alisterbscott">Alister Scott's</a> reasons for preferring <a target="_blank" href="https://alisterbscott.com/2021/10/27/five-reasons-why-playwright-is-better-than-cypress/">Playwright over Cypress</a>, I thought it would be nice to do a comparison with WebdriverIO. I've never used Cypress in anger, but I certainly identify with the challenges it has for performing end-to-end test automation. </p>
<p>You may see some similar reasons</p>
<h3 id="heading-reason-1-webdriverio-is-so-much-faster-than-cypress">Reason 1: WebdriverIO is so much faster than Cypress</h3>
<p>Reusing some of Alisters' blog: </p>
<p><a target="_blank" href="https://blog.checklyhq.com/cypress-vs-selenium-vs-playwright-vs-puppeteer-speed-comparison/">Up to 4 times faster</a></p>
<p>This is the simple example taken from Cypress' docs:</p>
<pre><code>describe(<span class="hljs-string">'My First Test'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    it(<span class="hljs-string">'clicking "type" shows the right headings'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
      cy.visit(<span class="hljs-string">'https://example.cypress.io'</span>)
      cy.contains(<span class="hljs-string">'type'</span>).click()
      <span class="hljs-comment">// Should be on a new URL which includes '/commands/actions'</span>
      cy.url().should(<span class="hljs-string">'include'</span>, <span class="hljs-string">'/commands/actions'</span>)
      <span class="hljs-comment">// Get an input, type into it and verify that the value has been updated</span>
      cy.get(<span class="hljs-string">'.action-email'</span>)
        .type(<span class="hljs-string">'fake@email.com'</span>)
        .should(<span class="hljs-string">'have.value'</span>, <span class="hljs-string">'fake@email.com'</span>)
    })
})
</code></pre><p>And as per Alister, this is how long it takes to run locally: </p>
<blockquote>
<blockquote>
<p>Takes 8 seconds running on my M1 Macbook Air</p>
</blockquote>
</blockquote>
<p>This is the same test written with WebdriverIO: </p>
<pre><code>describe(<span class="hljs-string">'My First Test'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  it(<span class="hljs-string">'clicking "type" shows the right headings'</span>, async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    await browser.url(<span class="hljs-string">'https://example.cypress.io'</span>);
    await $(<span class="hljs-string">'=type'</span>).click();
    <span class="hljs-comment">// Should be on a new URL which includes '/commands/actions'</span>
    await expect(browser).toHaveUrlContaining(<span class="hljs-string">'/commands/actions'</span>);
    <span class="hljs-comment">// Get an input, type into it and verify that the value has been updated</span>
    await $(<span class="hljs-string">'.action-email'</span>).setValue(<span class="hljs-string">'fake@email.com'</span>);
    await expect($(<span class="hljs-string">'.action-email'</span>)).toHaveValue(<span class="hljs-string">'fake@email.com'</span>);
  })
})
</code></pre><p>And how long it takes to run:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709141803/csA0R4ppb.png" alt="cypress_compare.png" /></p>
<p>1.8 seconds, so yeah 4 times faster as well</p>
<h3 id="heading-reason-2-webdriverio-has-first-class-support-for-parallel-running-of-tests-on-a-single-machine-both-locally-and-on-ci-without-a-subscription-or-account-required">Reason 2: WebdriverIO has first-class support for parallel running of tests on a single machine both locally and on CI without a subscription or account required</h3>
<p>Currently, we run our WebdriverIO tests in CircleCI using <a target="_blank" href="https://circleci.com/docs/2.0/configuration-reference/#resourceclass">Docker Medium Executor</a>, with a max instance of 4.</p>
<h3 id="heading-reason-3-webdriverio-supports-parallel-tests-within-a-single-test-file-to-run-cypress-tests-in-parallel-you-need-to-split-them-across-files">Reason 3: WebdriverIO supports parallel tests within a single test file – to run Cypress tests in parallel you need to split them across files.</h3>
<p>At the time of writing, there is no first-class support for running parallel tests within a single test file for WebdriverIO. There is a project <a target="_blank" href="https://github.com/webdriverio/webdriverio/projects/6">here</a> in the WebdriverIO backlog and service from <a target="_blank" href="https://github.com/wswebcreation/wdio-parallel-runner-service">Wim Selles</a> with some limited support for it. </p>
<p>That said, for the given example, running the tests in one file is still super quick, so probably need a more representative example</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709175121/DWACTiLXn.png" alt="cypress_compare_parallel.png" /></p>
<blockquote>
<blockquote>
<p>And to run them on the same machine I ran these 8 scenarios in Playwright in 6 seconds takes 45 seconds! 7.5 times slower!</p>
</blockquote>
</blockquote>
<h3 id="heading-reason-4-webdriverio-fully-supports-asyncawait-syntax-for-clean-readable-code">Reason 4: WebdriverIO fully supports async/await syntax for clean, readable code.</h3>
<p>As discussed by Alister, here is the Cypress example: </p>
<pre><code>describe(<span class="hljs-string">'My First Test'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    it(<span class="hljs-string">'clicking "type" shows the right headings'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
      cy.visit(<span class="hljs-string">'https://example.cypress.io'</span>)
      cy.contains(<span class="hljs-string">'type'</span>).click()
      <span class="hljs-comment">// Should be on a new URL which includes '/commands/actions'</span>
      cy.url().should(<span class="hljs-string">'include'</span>, <span class="hljs-string">'/commands/actions'</span>)
      <span class="hljs-comment">// Get an input, type into it and verify that the value has been updated</span>
      cy.get(<span class="hljs-string">'.action-email'</span>)
        .type(<span class="hljs-string">'fake@email.com'</span>)
        .should(<span class="hljs-string">'have.value'</span>, <span class="hljs-string">'fake@email.com'</span>)
    })
})
</code></pre><blockquote>
<blockquote>
<p>Note how the function calls chain together – in this simple example it’s not too bad but it quickly gets out of hand. Also, note there’s no indication that this is synchronous code – no await on any calls.</p>
</blockquote>
</blockquote>
<p>While it is still possible to use WebdriverIO in <a target="_blank" href="https://webdriver.io/docs/sync-vs-async/#sync-mode">Sync Mode</a>, its use is <a target="_blank" href="https://webdriver.io/docs/sync-vs-async/">discouraged</a>, so here is the test in WebdriverIO, using async / await throughout.</p>
<pre><code>describe(<span class="hljs-string">'My First Test'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  it(<span class="hljs-string">'clicking "type" shows the right headings'</span>, async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    await browser.url(<span class="hljs-string">'https://example.cypress.io'</span>);
    await $(<span class="hljs-string">'=type'</span>).click();
    <span class="hljs-comment">// Should be on a new URL which includes '/commands/actions'</span>
    await expect(browser).toHaveUrlContaining(<span class="hljs-string">'/commands/actions'</span>);
    <span class="hljs-comment">// Get an input, type into it and verify that the value has been updated</span>
    await $(<span class="hljs-string">'.action-email'</span>).setValue(<span class="hljs-string">'fake@email.com'</span>);
    await expect($(<span class="hljs-string">'.action-email'</span>)).toHaveValue(<span class="hljs-string">'fake@email.com'</span>);
  })
})
</code></pre><h3 id="heading-reason-5-webdriverio-doesnt-need-plugins-for-basic-interactions">Reason 5: WebdriverIO doesn’t need plugins (for basic interactions)</h3>
<p>WebdriverIO features a huge variety of community plugins allow you to easily integrate and extend your setup to fulfill your requirements, but this is typically for extending capabilities beyond running tests locally (eg running remotely). There is full native support for interactions, as you would expect for a framework implementing the <a target="_blank" href="https://w3c.github.io/webdriver/">W3C Webdriver Protocol</a>. </p>
<blockquote>
<p>There’s so many limitations in Cypress there’s pretty much a plugin for everything. And there’s still so may trade-offs that can’t be be fixed by a plugin.</p>
<p>Need to send the tab key to the browser? There’s a plugin for that. And it doesn’t work well at all according to the comments on the Github issue for lack of tab key support open since 2016 (and still open since I last wrote about Cypress).</p>
<p>Need to upload a file? There’s also a plugin for that.</p>
<p>Need to run tests n-times in a row to measure repeatability? There’s a plugin for that.</p>
</blockquote>
<p>To this I would add: </p>
<ul>
<li>Need to fill in Stripe? There's a <a target="_blank" href="https://www.npmjs.com/package/cypress-plugin-stripe-elements">plugin</a> for that.</li>
<li>Need to interact with an iframe? There's a <a target="_blank" href="https://www.npmjs.com/package/cypress-iframe">module</a> for that. </li>
</ul>
<h2 id="heading-summary">Summary</h2>
<p>I'm definitely biased towards a framework that offers native control of the browser and is not sandboxed, and with the development of protocols like <a target="_blank" href="https://developer.chrome.com/blog/webdriver-bidi/">Webdriver BiDi</a>, we can use frameworks based around WebDriver that will continue to be compatible with how people need to write tests to deal with modern applications, in a modern JS syntax. </p>
]]></content:encoded></item><item><title><![CDATA[Visual regression testing with WebdriverIO, Jasmine and Allure]]></title><description><![CDATA[We have recently started using the Visual Regression Testing Service, and I thought I would share how we incorporate it in our test automation. 
It's quite straightforward to capture an image, then run the test again and perform an assertion, checkin...]]></description><link>https://hughmccamphill.com/visual-regression-testing-with-webdriverio-jasmine-and-allure</link><guid isPermaLink="true">https://hughmccamphill.com/visual-regression-testing-with-webdriverio-jasmine-and-allure</guid><category><![CDATA[Testing]]></category><category><![CDATA[webdriver]]></category><category><![CDATA[jasmine]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sat, 12 Feb 2022 20:44:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644710216750/W5spRRAmU.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>We have recently started using the <a target="_blank" href="https://webdriver.io/docs/wdio-image-comparison-service">Visual Regression Testing Service</a>, and I thought I would share how we incorporate it in our test automation. </p>
<p>It's quite straightforward to capture an image, then run the test again and perform an assertion, checking there are no differences between the captured baseline image and the newly captured image, for example:</p>
<pre><code><span class="hljs-comment">// check screen</span>
expect(browser.checkScreen(<span class="hljs-string">'example page'</span>)).toEqual(<span class="hljs-number">0</span>);

<span class="hljs-comment">// Check an element</span>
expect(browser.checkElement($(<span class="hljs-string">'#element-id'</span>), <span class="hljs-string">'example element'</span>)).toEqual(<span class="hljs-number">0</span>);
</code></pre><p>Having previously had some experience with writing <a target="_blank" href="https://jasmine.github.io/2.0/custom_matcher.html">custom matchers</a> for Jasmine, I thought it would make sense to wrap some of the functionality inside a matcher, for the following reasons: </p>
<ul>
<li><p>Firstly, we could set a consistent screen size before we do the comparison</p>
</li>
<li><p>Secondly, by setting the <a target="_blank" href="https://github.com/wswebcreation/wdio-image-comparison-service/blob/master/docs/OPTIONS.md#plugin-options">plugin option</a> <code>returnAllCompareData: true</code>, it would allow us to return more information that would be useful when a test fails: </p>
</li>
</ul>
<pre><code><span class="hljs-keyword">const</span> checkResult = {
  <span class="hljs-comment">// The formatted filename, this depends on the options `formatImageName`</span>
  fileName: <span class="hljs-string">'examplePage-chrome-headless-latest-1366x768.png'</span>,
  folders: {
      <span class="hljs-comment">// The actual folder and the file name</span>
      actual: <span class="hljs-string">'/Users/wswebcreation/Git/wdio-image-comparison-service/.tmp/actual/desktop_chrome/examplePage-chrome-headless-latest-1366x768.png'</span>,
      <span class="hljs-comment">// The baseline folder and the file name</span>
      baseline: <span class="hljs-string">'/Users/wswebcreation/Git/wdio-image-comparison-service/localBaseline/desktop_chrome/examplePage-chrome-headless-latest-1366x768.png'</span>,
      <span class="hljs-comment">// This following folder is optional and only if there is a mismatch</span>
      <span class="hljs-comment">// The folder that holds the diffs and the file name</span>
      diff: <span class="hljs-string">'/Users/wswebcreation/Git/wdio-image-comparison-service/.tmp/diff/desktop_chrome/examplePage-chrome-headless-latest-1366x768.png'</span>,
    },
    <span class="hljs-comment">// The mismatch percentage</span>
    misMatchPercentage: <span class="hljs-number">2.34</span>
};
</code></pre><ul>
<li>And finally, since we are already using <a target="_blank" href="https://docs.qameta.io/allure/">Allure</a>, we could leverage the <a target="_blank" href="https://github.com/allure-framework/allure2/tree/master/plugins/screen-diff-plugin">Allure Screen Diff Plugin</a>. </li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644710279749/vCxtJUCn9.png" alt="show-diff.png" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644710291343/h4fAYz6dq.gif" alt="show-overlay.gif" /></p>
<p>Bringing this all together we end up with: </p>
<pre><code>jasmine.addMatchers({
    toMatchImageSnapshot: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">util, customEqualityTesters</span>) </span>{
        <span class="hljs-string">"use strict"</span>;
        <span class="hljs-keyword">return</span> {
            compare: <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">actual, expected</span>) </span>{
                browser.setWindowSize(<span class="hljs-number">1200</span>, <span class="hljs-number">900</span>);

                let checkResult;
                <span class="hljs-keyword">if</span>(actual.hasOwnProperty(<span class="hljs-string">'element'</span>)) {
                  actual.element.waitForDisplayed();
                  checkResult <span class="hljs-operator">=</span> browser.checkElement(actual.element, actual.tag);
                }
                <span class="hljs-keyword">else</span> {
                  checkResult <span class="hljs-operator">=</span> browser.checkScreen(actual.tag);
                }

                const pass <span class="hljs-operator">=</span> checkResult.misMatchPercentage <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

                <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>pass) { 
                  const actualImage <span class="hljs-operator">=</span> fs.readFileSync(checkResult.folders.actual);
                  const expectedImage <span class="hljs-operator">=</span> fs.readFileSync(checkResult.folders.baseline);
                  const differenceImage <span class="hljs-operator">=</span> fs.readFileSync(checkResult.folders.diff);
                  allure.addLabel(<span class="hljs-string">'testType'</span>, <span class="hljs-string">'screenshotDiff'</span>);
                  allure.addAttachment(<span class="hljs-string">'diff'</span>, differenceImage, <span class="hljs-string">'image/png'</span>);
                  allure.addAttachment(<span class="hljs-string">'actual'</span>, actualImage, <span class="hljs-string">'image/png'</span>);
                  allure.addAttachment(<span class="hljs-string">'expected'</span>, expectedImage, <span class="hljs-string">'image/png'</span>);
                }

                <span class="hljs-keyword">return</span> {
                    pass,
                    message: `Expected to have matched image at ${checkResult.folders.actual}, but there was a difference of ${checkResult.misMatchPercentage}<span class="hljs-operator">%</span>. 
                    Difference can be viewed at ${checkResult.folders.diff}. Original can be viewed at ${checkResult.folders.baseline}`
                };
            }
        };
    }
});
</code></pre><p>This allows us to re-write the example assertions above as: </p>
<pre><code><span class="hljs-comment">// check screen</span>
expect({ tag: <span class="hljs-string">'example page'</span> }).toMatchImageSnapshot();

<span class="hljs-comment">// Check an element</span>
expect({ element: $(<span class="hljs-string">'#element-id'</span>), tag: <span class="hljs-string">'example element'</span> }).toMatchImageSnapshot();
</code></pre><h2 id="heading-whats-next">What's next</h2>
<p>We plan to add functionality to allow for updating the baseline, rather than having to manually update the baselines. </p>
]]></content:encoded></item><item><title><![CDATA[Waiting for an element to stop moving]]></title><description><![CDATA[Sometimes with Selenium and libraries or frameworks that use it, we need to wait for an element to stop moving before attempting to click on it (to not enconter StaleElementReferenceExceptions, for example). One approach to wait for the element to st...]]></description><link>https://hughmccamphill.com/waiting-for-an-element-to-stop-moving</link><guid isPermaLink="true">https://hughmccamphill.com/waiting-for-an-element-to-stop-moving</guid><category><![CDATA[webdriver]]></category><category><![CDATA[Testing]]></category><category><![CDATA[selenium]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sat, 12 Feb 2022 20:44:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709787912/9YOq-3PJc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sometimes with Selenium and libraries or frameworks that use it, we need to wait for an element to stop moving before attempting to click on it (to not enconter StaleElementReferenceExceptions, for example). One approach to wait for the element to stop moving is to query it's location and check this twice, with a small sleep between the calls. If the location of the element is the same both times, then we can consider the element to have stopped moving. </p>
<p>The most appropriate way to implement this will differ with languages and libraries, but <a target="_blank" href="https://webdriver.io/">WebDriverIO</a> provides a nice way to encapsulate this with <a target="_blank" href="https://webdriver.io/docs/customcommands.html">Custom Commands</a></p>
<pre><code>browser.addCommand(
      <span class="hljs-string">"waitUntilStopMoving"</span>,
      <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        browser.waitUntil(
          () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
            <span class="hljs-built_in">this</span>.waitForDisplayed();
            let initialLocation <span class="hljs-operator">=</span> <span class="hljs-built_in">this</span>.getLocation();
            let initialX <span class="hljs-operator">=</span> initialLocation.x;
            let initialY <span class="hljs-operator">=</span> initialLocation.y;
            browser.pause(<span class="hljs-number">200</span>);
            let finalLocation <span class="hljs-operator">=</span> <span class="hljs-built_in">this</span>.getLocation();
            let finalX <span class="hljs-operator">=</span> finalLocation.x;
            let finalY <span class="hljs-operator">=</span> finalLocation.y;
            <span class="hljs-keyword">return</span> initialX <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> finalX <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> initialY <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> finalY;
          },
          {
            timeout: <span class="hljs-number">5000</span>,
            timeoutMsg: <span class="hljs-string">"expected element to have stopped moving"</span>,
          }
        );
      },
      <span class="hljs-literal">true</span>
    );
</code></pre><p>If we add the custom command to <code>before</code> in the <code>wdio.config.js</code></p>
<pre><code><span class="hljs-comment">/**
   * Gets executed before test execution begins. At this point you can access to all global
   * variables like `browser`. It is the perfect place to define custom commands.
   * <span class="hljs-doctag">@param</span> {Array.&lt;Object&gt;} capabilities list of capabilities details
   * <span class="hljs-doctag">@param</span> {Array.&lt;String&gt;} specs List of spec file paths that are to be run
   */</span>
  before: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">capabilities, specs</span>) </span>{
</code></pre><p>We can then call the custom command quite simply before clicking the element</p>
<pre><code>$(<span class="hljs-string">'div.confirm'</span>).waitUntilStopMoving();
$(<span class="hljs-string">'div.confirm'</span>).click();
</code></pre>]]></content:encoded></item><item><title><![CDATA[Visual regression testing React Native apps with Detox and Jest]]></title><description><![CDATA[Visual regression testing with Detox
Detox supports taking screenshots, which can be used for visual regression testing purposes. 
As discussed in the Detox documentation: 


In both cases, the concept is mainly useful for verifying the proper visual...]]></description><link>https://hughmccamphill.com/visual-regression-testing-react-native-apps-with-detox-and-jest</link><guid isPermaLink="true">https://hughmccamphill.com/visual-regression-testing-react-native-apps-with-detox-and-jest</guid><category><![CDATA[mobile]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sat, 12 Feb 2022 20:44:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709918301/lTsakDeKH.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-visual-regression-testing-with-detox">Visual regression testing with Detox</h2>
<p>Detox supports taking <a target="_blank" href="https://github.com/wix/Detox/blob/master/docs/APIRef.Screenshots.md#taking-screenshots">screenshots</a>, which can be used for visual regression testing purposes. </p>
<p>As discussed in the Detox documentation: </p>
<blockquote>
<blockquote>
<p>In both cases, the concept is mainly useful for verifying the proper visual structure and layout of elements appearing on the device's screen, in the form of a snapshot-test. Namely, by following these conceptual steps:</p>
<ol>
<li>Taking a screenshot, once, and manually verifying it, visually.</li>
<li>Storing it as an e2e-test asset (i.e. the snapshot).</li>
<li>Using it as the point-of-reference for comparison against screenshots taken in consequent tests, from that point on.</li>
</ol>
</blockquote>
</blockquote>
<p>This is the code sample they provide:</p>
<pre><code>const fs <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);

describe(<span class="hljs-string">'Members area'</span>, () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  const snapshottedImagePath <span class="hljs-operator">=</span> <span class="hljs-string">'./e2e/assets/snapshotted-image.png'</span>;

  it(<span class="hljs-string">'should greet the member with an announcement'</span>, async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
    const imagePath <span class="hljs-operator">=</span> (take screenshot <span class="hljs-keyword">from</span> the device); <span class="hljs-comment">// Discussed below</span>
    expectBitmapsToBeEqual(imagePath, snapshottedImagePath);  
  });  
});

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">expectBitmapsToBeEqual</span>(<span class="hljs-params">imagePath, expectedImagePath</span>) </span>{
  const bitmapBuffer <span class="hljs-operator">=</span> fs.readFileSync(imagePath);
  const expectedBitmapBuffer <span class="hljs-operator">=</span> fs.readFileSync(expectedImagePath);
  <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>bitmapBuffer.equals(expectedBitmapBuffer)) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(`Expected image at ${imagePath} to be equal to image at ${expectedImagePath}, but it was different<span class="hljs-operator">!</span>`);
  }
}
</code></pre><p>But, they state: </p>
<blockquote>
<blockquote>
<p>Important: The recommended, more practical way of doing this, is by utilizing more advanced 3rd-party image snapshotting &amp; comparison tools such as Applitools.</p>
</blockquote>
</blockquote>
<p>However, in my experience, if you can control the data, then doing a pixel-by-pixel comparison is reasonably possible on mobile, at least for a small number of comparisons. This blog post will take you through how we set it up for Detox. </p>
<h2 id="heading-taking-screenshots">Taking screenshots</h2>
<p>As discussed in the linked docs, we have to put the device into demo mode, as the battery level, time and network information could change each time we run the tests. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709953776/E6wWwSHZo.png" alt="initial_time.png" /></p>
<blockquote>
<blockquote>
<p>As the image includes, for example, the current time (at the top-left corner), running the test in any different time would unnecessarily result in an utter comparison failure, making the test downright useless. Fortunately, this can be resolved, by putting the device into "demo mode" (i.e. freezing the irrelevant, volatile elements) </p>
</blockquote>
</blockquote>
<pre><code><span class="hljs-selector-tag">async</span> <span class="hljs-selector-tag">function</span> <span class="hljs-selector-tag">setDemoMode</span>() {
  <span class="hljs-selector-tag">if</span> (device.getPlatform() === <span class="hljs-string">'ios'</span>) {
    <span class="hljs-selector-tag">await</span> <span class="hljs-selector-tag">device</span><span class="hljs-selector-class">.setStatusBar</span>({ <span class="hljs-attribute">time</span>: <span class="hljs-string">'12:34'</span>, <span class="hljs-attribute">dataNetwork</span>: <span class="hljs-string">'wifi'</span>,  <span class="hljs-attribute">wifiBars</span>: <span class="hljs-string">'3'</span>,  <span class="hljs-attribute">batteryState</span>: <span class="hljs-string">'charging'</span>, <span class="hljs-attribute">batteryLevel</span>: <span class="hljs-string">'100'</span>});
  } else {
    <span class="hljs-comment">// enter demo mode</span>
    <span class="hljs-selector-tag">execSync</span>(<span class="hljs-string">'adb shell settings put global sysui_demo_allowed 1'</span>);
    <span class="hljs-comment">// display time 12:00</span>
    <span class="hljs-selector-tag">execSync</span>(<span class="hljs-string">'adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1200'</span>);
    <span class="hljs-comment">// Display full mobile data with 4g type and no wifi</span>
    <span class="hljs-selector-tag">execSync</span>(
      <span class="hljs-string">'adb shell am broadcast -a com.android.systemui.demo -e command network -e mobile show -e level 4 -e datatype 4g -e wifi false'</span>
    );
    <span class="hljs-comment">// Hide notifications</span>
    <span class="hljs-selector-tag">execSync</span>(<span class="hljs-string">'adb shell am broadcast -a com.android.systemui.demo -e command notifications -e visible false'</span>);
    <span class="hljs-comment">// Show full battery but not in charging state</span>
    <span class="hljs-selector-tag">execSync</span>(<span class="hljs-string">'adb shell am broadcast -a com.android.systemui.demo -e command battery -e plugged false -e level 100'</span>);
  }
}
</code></pre><h2 id="heading-jest-image-snapshot">Jest Image Snapshot</h2>
<p>Given we were already using <a target="_blank" href="https://medium.com/wix-engineering/detox-in-2020-e34525548123">Jest Circus</a> as our test runner, it made sense to see if we could incorporate<a target="_blank" href="https://github.com/americanexpress/jest-image-snapshot">Jest Image Snapshot</a>, which looked extremely promising. </p>
<p>By default Jest Image Snapshot uses <a target="_blank" href="https://github.com/mapbox/pixelmatch">pixelmatch</a>, but as it turned out using a pixel by pixel comparison was indeed not sufficient, at least when running on a CI server (CircleCI in our case). The following example shows one of the comparison failures we had with Pixelmatch:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709984012/5wUklJJsB.png" alt="detox-difference.png" /></p>
<p>Upon further reading I saw Jest Image Snapshot also provides the option to use <a target="_blank" href="https://github.com/obartra/ssim">SSIM</a>; here is the same comparison using that algorithm - it shows a less of difference:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709996817/gYlN4CX0J.png" alt="ssim_difference.png" /></p>
<p>With a bit of experimentation as per <a target="_blank" href="https://github.com/americanexpress/jest-image-snapshot#recommendations-when-using-ssim-comparison">Recommendations using SSIM Comparison</a>, we are able to set the comparison to SSIM as follows, with an extremely small allowance for mismatches that the human eye would either not see, or care about. </p>
<pre><code><span class="hljs-string">const</span> <span class="hljs-string">toMatchImage</span> <span class="hljs-string">=</span> <span class="hljs-string">configureToMatchImageSnapshot({</span>
  <span class="hljs-attr">comparisonMethod:</span> <span class="hljs-string">'ssim'</span><span class="hljs-string">,</span> <span class="hljs-attr">failureThreshold:</span> <span class="hljs-number">0.002</span><span class="hljs-string">,</span> <span class="hljs-attr">failureThresholdType:</span> <span class="hljs-string">'percent'</span>
<span class="hljs-string">});</span>
</code></pre><p>Using this comparison method, and failure threshold results in a match for the above comparison. </p>
<h2 id="heading-extend-jest-expect">Extend jest expect</h2>
<p>To aid writing tests, I thought it made sense to provide some convenience methods by extending jest expect and automatically taking a screenshot if the method is invoked; in addition, we need to consider the platform, device name, and device type when doing the comparisons. </p>
<pre><code>const { configureToMatchImageSnapshot } <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'jest-image-snapshot'</span>);
const fs <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
const path <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);
const kebabCase <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'lodash/kebabCase'</span>);

const toMatchImage <span class="hljs-operator">=</span> configureToMatchImageSnapshot({
  comparisonMethod: <span class="hljs-string">'ssim'</span>, failureThreshold: <span class="hljs-number">0</span><span class="hljs-number">.002</span>, failureThresholdType: <span class="hljs-string">'percent'</span>
});

jestExpect.extend({ toMatchImage });

jestExpect.extend({
  async toMatchImageSnapshot(screenName) {
    const platform <span class="hljs-operator">=</span> await device.getPlatform();
    const deviceName <span class="hljs-operator">=</span> await device.<span class="hljs-built_in">name</span>.split(<span class="hljs-string">' '</span>).slice(<span class="hljs-number">1</span>).join(<span class="hljs-string">''</span>);
    const deviceType <span class="hljs-operator">=</span> JSON.parse(deviceName).type.replace(<span class="hljs-string">','</span>,<span class="hljs-string">''</span>);

    const SNAPSHOTS_DIR <span class="hljs-operator">=</span> `__image_snapshots__<span class="hljs-operator">/</span>${platform}<span class="hljs-operator">/</span>${deviceType}`;

    const { testPath, currentTestName } <span class="hljs-operator">=</span> <span class="hljs-built_in">this</span>;

    const customSnapshotsDir <span class="hljs-operator">=</span> path.join(path.dirname(testPath), SNAPSHOTS_DIR);
    const customSnapshotIdentifier <span class="hljs-operator">=</span> kebabCase(`${path.basename(testPath)}<span class="hljs-operator">-</span>${currentTestName}<span class="hljs-operator">-</span>${screenName}`)

    const tempPath <span class="hljs-operator">=</span> await device.takeScreenshot(screenName);
    const image <span class="hljs-operator">=</span> fs.readFileSync(tempPath);
    jestExpect(image).toMatchImage({ customSnapshotIdentifier, customSnapshotsDir });

    <span class="hljs-keyword">return</span> { pass: <span class="hljs-literal">true</span> }
  },
});

global.jestExpect <span class="hljs-operator">=</span> jestExpect
</code></pre><p>Writing an expectation for an image comparison then becomes as simple as:</p>
<pre><code>it(<span class="hljs-string">'Terms and conditions should match snapshot'</span>, async () <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> {
  await jestExpect(<span class="hljs-string">'Terms and conditions'</span>).toMatchImageSnapshot();
})
</code></pre><h2 id="heading-attaching-the-failing-diff-to-allure-report">Attaching the failing diff to Allure Report</h2>
<p>We discussed in a previous blog post how to add an <a target="_blank" href="https://hughmccamphill.com/adding-a-custom-reporter-to-detox/">Allure Reporter for Detox</a>; we can make a small update to capture the diff image if that was the reason for failure. </p>
<pre><code> async test_done(<span class="hljs-function"><span class="hljs-keyword">event</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">event</span>.test.errors.<span class="hljs-built_in">length</span> <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
      const { test } <span class="hljs-operator">=</span> <span class="hljs-function"><span class="hljs-keyword">event</span></span>;
      const screenshotPath <span class="hljs-operator">=</span> await <span class="hljs-built_in">this</span>.detox.device.takeScreenshot(`${test.startedAt}<span class="hljs-operator">-</span>failed`);
      const buffer <span class="hljs-operator">=</span> fs.readFileSync(`${screenshotPath}`, {encoding: <span class="hljs-string">'base64'</span>});
      <span class="hljs-built_in">this</span>._allure.addAttachment(<span class="hljs-string">'Test failure screenshot'</span>, Buffer.from(buffer, <span class="hljs-string">'base64'</span>), <span class="hljs-string">'image/png'</span>);

      const err <span class="hljs-operator">=</span> test.errors[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>];
      err.message <span class="hljs-operator">=</span> stripAnsi(err.message);
      err.stack <span class="hljs-operator">=</span> stripAnsi(err.stack);

      <span class="hljs-keyword">if</span> (err.message.includes(<span class="hljs-string">'See diff for details:'</span>)) {
        const file <span class="hljs-operator">=</span> err.message.split(<span class="hljs-string">'See diff for details:'</span>)[<span class="hljs-number">1</span>].trim();
        const buffer <span class="hljs-operator">=</span> fs.readFileSync(`${file}`, {encoding: <span class="hljs-string">'base64'</span>});
        <span class="hljs-built_in">this</span>._allure.addAttachment(<span class="hljs-string">'Image comparison failure'</span>, Buffer.from(buffer, <span class="hljs-string">'base64'</span>), <span class="hljs-string">'image/png'</span>);
      }
      <span class="hljs-built_in">this</span>._allure.endCase(<span class="hljs-string">'failed'</span>, err);
    }
    <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">this</span>._allure.endCase(<span class="hljs-string">'passed'</span>)
    }
  }
</code></pre><h2 id="heading-summary">Summary</h2>
<p>This solution required a little bit of experimentation with the comparison algorithms, but with sensible defaults and some extension of existing functionality, we have a straightforward way of doing automatic visual regression testing, all using open source tooling!</p>
]]></content:encoded></item><item><title><![CDATA[Attaching Allure Report to a CircleCI job]]></title><description><![CDATA[Tests Reports are of course an important part of any automation framework. While most (all?) test runners and languages will provide a Junit output that can be parsed easily by CI tools (including CircleCI), it can be useful to add other types of rep...]]></description><link>https://hughmccamphill.com/attaching-allure-report-to-a-circleci-job</link><guid isPermaLink="true">https://hughmccamphill.com/attaching-allure-report-to-a-circleci-job</guid><category><![CDATA[ci-cd]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sat, 12 Feb 2022 20:44:24 GMT</pubDate><content:encoded><![CDATA[<p>Tests Reports are of course an important part of any automation framework. While most (all?) test runners and languages will provide a Junit output that can be parsed easily by CI tools (including CircleCI), it can be useful to add other types of reports that may be getting generated as part of a test run. </p>
<p>One popular reporting tool that I've personally been using for years is <a target="_blank" href="https://docs.qameta.io/allure/">Allure</a></p>
<blockquote>
<p>Allure Framework is a flexible lightweight multi-language test report tool that not only shows a very concise representation of what has been tested in a neat web report form but allows everyone participating in the development process to extract the maximum of useful information from the everyday execution of tests.</p>
</blockquote>
<p>Allure comes with multiple framework support, so we'll not cover the generation of the report here, but assuming you are able to generate allure results locally, here are the steps you can take to attach a generated allure report as an artifact as part of Continous Integration job.</p>
<h2 id="heading-how-to-attach-the-report">How to attach the report</h2>
<h3 id="heading-installing-allure">Installing allure</h3>
<p>There are multiple ways to install Allure, depending on the machine or image being used. We are using a node image so we just do a global install with npm. </p>
<pre><code>sudo npm install <span class="hljs-operator">-</span>g allure<span class="hljs-operator">-</span>commandline
</code></pre><h3 id="heading-generating-the-report">Generating the report</h3>
<p>To generate the report, you will need Java installed and configured. We are using a prebuilt node image, and then to be able to run our <a target="_blank" href="https://webdriver.io/">WebDriverIO</a> tests, we are appending <a target="_blank" href="https://circleci.com/docs/2.0/browser-testing/#prerequisites">-browsers</a> to that pre-built node image. Our image now has Java 8 installed and configured. </p>
<p>Generating the report is as follows:</p>
<pre><code>allure generate <span class="hljs-operator">-</span><span class="hljs-operator">-</span>clean
</code></pre><p>The step in your CircleCI config will look like this:</p>
<pre><code><span class="hljs-attr">run:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">generate</span> <span class="hljs-string">allure</span> <span class="hljs-string">report</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">|
        sudo npm install -g allure-commandline
        allure generate --clean
</span>    <span class="hljs-attr">when:</span> <span class="hljs-string">always</span>
</code></pre><p>You may also want to have the Allure Report display some branch and commit information. The step would then look this:</p>
<pre><code>run:
  name: generate allure report
    command: <span class="hljs-operator">|</span>
        sudo npm install <span class="hljs-operator">-</span>g allure<span class="hljs-operator">-</span>commandline
        echo BRANCH<span class="hljs-operator">=</span>development <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> allure<span class="hljs-operator">-</span>results<span class="hljs-operator">/</span>environment.properties
        echo COMMIT<span class="hljs-operator">=</span>$CI_PULL_REQUEST<span class="hljs-operator">/</span>commits<span class="hljs-operator">/</span>$CIRCLE_SHA1 <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> allure<span class="hljs-operator">-</span>results<span class="hljs-operator">/</span>environment.properties
        allure generate <span class="hljs-operator">-</span><span class="hljs-operator">-</span>clean
    when: always
</code></pre><p>Now that we have generated the report, the final step is to attach the Allure Report as an artifact.</p>
<pre><code><span class="hljs-operator">-</span> store_artifacts:
     path: <span class="hljs-operator">/</span>home<span class="hljs-operator">/</span>circleci<span class="hljs-operator">/</span>project<span class="hljs-operator">/</span>allure<span class="hljs-operator">-</span>report
</code></pre><p>You can now navigate to the artifacts tab and click on the link ending in index.html</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1644710412831/jDnigjG9f.png" alt="circleci.png" /></p>
]]></content:encoded></item><item><title><![CDATA[Adding a custom reporter to Detox]]></title><description><![CDATA[Detox is a popular automation library for mobile apps, usually those built with React Native. I recently upgraded our implementation of Detox to use Jest Circus following this as the test runner instead of Mocha, as recommended.
One of the things men...]]></description><link>https://hughmccamphill.com/adding-a-custom-reporter-to-detox</link><guid isPermaLink="true">https://hughmccamphill.com/adding-a-custom-reporter-to-detox</guid><category><![CDATA[automation]]></category><category><![CDATA[Testing]]></category><category><![CDATA[mobile]]></category><dc:creator><![CDATA[Hugh McCamphill]]></dc:creator><pubDate>Sat, 12 Feb 2022 20:44:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1644709672266/_vCmDKsFwx.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/wix/Detox">Detox</a> is a popular automation library for mobile apps, usually those built with React Native. I recently upgraded our implementation of Detox to use <a target="_blank" href="https://www.npmjs.com/package/jest-circus">Jest Circus</a> following <a target="_blank" href="https://github.com/wix/Detox/blob/master/docs/Guide.Jest.md">this</a> as the test runner instead of Mocha, as <a target="_blank" href="https://medium.com/wix-engineering/detox-in-2020-e34525548123">recommended</a>.</p>
<p>One of the things mentioned in the guide is the use of a <a target="_blank" href="https://github.com/wix/Detox/blob/master/docs/Guide.Jest.md#e2eenvironmentjs">CustomDetoxEnvironment</a>, is that there is no guide for writing custom detox listeners, and indeed there isn't! So I'll take you through how we added one for <a target="_blank" href="https://github.com/allure-framework">Allure</a></p>
<h2 id="heading-adding-an-allure-reporter-listener-for-detox">Adding an Allure Reporter Listener for Detox</h2>
<p>To understand what was going on I looked at the code inside <a target="_blank" href="https://github.com/wix/Detox/blob/master/detox/runners/jest-circus/environment.js">DetoxCircusEnvironment</a>, and specifically lines <a target="_blank" href="https://github.com/wix/Detox/blob/master/detox/runners/jest-circus/environment.js#L52-L86">L52-L86</a></p>
<p>Within this file we can see the following code:</p>
<pre><code><span class="hljs-keyword">for</span> (const listener of <span class="hljs-built_in">this</span>.testEventListeners) {
  <span class="hljs-keyword">if</span> (typeof listener[name] <span class="hljs-operator">=</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-string">'function'</span>) {
    <span class="hljs-keyword">try</span> {
      await <span class="hljs-built_in">this</span>._timer.run(() <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> listener[name](<span class="hljs-function"><span class="hljs-keyword">event</span>, <span class="hljs-title">state</span>))</span>;
    } <span class="hljs-keyword">catch</span> (listenerError) {
      <span class="hljs-built_in">this</span>._logger.error(`${listenerError}`);
    }
  }
}
</code></pre><p>so if we implemented functions that matched the names of events that we are interested in listed <a target="_blank" href="https://github.com/facebook/jest/blob/master/packages/jest-types/src/Circus.ts">here</a> then it would handle the events as part of the Jest Circus lifecycle for us. </p>
<pre><code>const Allure <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'allure-js-commons'</span>); <span class="hljs-comment">// version "1.3.2",</span>
const fs <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
const stripAnsi <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'strip-ansi'</span>);

class AllureReporterCircus {

  <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">{ detox }</span>) </span>{
    <span class="hljs-built_in">this</span>.allure <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Allure();
    <span class="hljs-built_in">this</span>.detox <span class="hljs-operator">=</span> detox;
  }

  run_describe_start(<span class="hljs-function"><span class="hljs-keyword">event</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">event</span>.describeBlock.parent <span class="hljs-operator">!</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> undefined) {
      <span class="hljs-built_in">this</span>.allure.startSuite(<span class="hljs-keyword">event</span>.describeBlock.<span class="hljs-built_in">name</span>);
    }
  }

  run_describe_finish(<span class="hljs-function"><span class="hljs-keyword">event</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">event</span>.describeBlock.parent <span class="hljs-operator">!</span><span class="hljs-operator">=</span><span class="hljs-operator">=</span> undefined) {
      <span class="hljs-built_in">this</span>.allure.endSuite();
    }
  }

  test_start(<span class="hljs-function"><span class="hljs-keyword">event</span>) </span>{
    const { test } <span class="hljs-operator">=</span> <span class="hljs-function"><span class="hljs-keyword">event</span></span>;
    <span class="hljs-built_in">this</span>.allure.startCase(test.<span class="hljs-built_in">name</span>)
  }

  async test_done(<span class="hljs-function"><span class="hljs-keyword">event</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">event</span>.test.errors.<span class="hljs-built_in">length</span> <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
      const { test } <span class="hljs-operator">=</span> <span class="hljs-function"><span class="hljs-keyword">event</span></span>;
      const screenshotPath <span class="hljs-operator">=</span> await <span class="hljs-built_in">this</span>.detox.device.takeScreenshot(`${test.startedAt}<span class="hljs-operator">-</span>failed`);
      const buffer <span class="hljs-operator">=</span> fs.readFileSync(`${screenshotPath}`);
      <span class="hljs-built_in">this</span>.allure.addAttachment(<span class="hljs-string">'Screenshot test failue'</span>, Buffer.from(buffer, <span class="hljs-string">'base64'</span>), <span class="hljs-string">'image/png'</span>);

      const err <span class="hljs-operator">=</span> test.errors[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>];
      err.message <span class="hljs-operator">=</span> stripAnsi(err.message);
      err.stack <span class="hljs-operator">=</span> stripAnsi(err.stack);

      <span class="hljs-built_in">this</span>.allure.endCase(<span class="hljs-string">'failed'</span>, err);
    }
    <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">this</span>.allure.endCase(<span class="hljs-string">'passed'</span>)
    }
  }

  test_skip(<span class="hljs-function"><span class="hljs-keyword">event</span>) </span>{
    const { test } <span class="hljs-operator">=</span> <span class="hljs-function"><span class="hljs-keyword">event</span></span>;
    <span class="hljs-built_in">this</span>.allure.startCase(test.<span class="hljs-built_in">name</span>);
    <span class="hljs-built_in">this</span>.allure.pendingCase(test.<span class="hljs-built_in">name</span>);
  }
}

module.exports <span class="hljs-operator">=</span> AllureReporterCircus;
</code></pre><p>The final step is to add the reporter as a listener with the CustomDetoxEnvironment</p>
<pre><code>const {    
  DetoxCircusEnvironment,    
  SpecReporter,    
  WorkerAssignReporter,    
} <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'detox/runners/jest-circus'</span>);

const AllureReporter <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">'./reporters/AllureReporterCircus'</span>)

class CustomDetoxEnvironment extends DetoxCircusEnvironment {    
  <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">config, context</span>) </span>{    
    <span class="hljs-built_in">super</span>(config, context);    

    <span class="hljs-built_in">this</span>.initTimeout <span class="hljs-operator">=</span> <span class="hljs-number">300000</span>;    

    <span class="hljs-built_in">this</span>.registerListeners({    
      SpecReporter,
      AllureReporter,
      WorkerAssignReporter,
    });    
  }    
}    

module.exports <span class="hljs-operator">=</span> CustomDetoxEnvironment;
</code></pre>]]></content:encoded></item></channel></rss>