<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Phil</title>
    <description>The latest articles on DEV Community by Phil (@philipfong).</description>
    <link>https://dev.to/philipfong</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F974318%2Fc3db4c90-e7cf-41e3-b491-48cc156e73dc.jpg</url>
      <title>DEV Community: Phil</title>
      <link>https://dev.to/philipfong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/philipfong"/>
    <language>en</language>
    <item>
      <title>When Playwright’s Locator Tool Isn’t Enough</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Mon, 19 Jan 2026 21:47:09 +0000</pubDate>
      <link>https://dev.to/philipfong/when-playwrights-locator-tool-isnt-enough-561h</link>
      <guid>https://dev.to/philipfong/when-playwrights-locator-tool-isnt-enough-561h</guid>
      <description>&lt;p&gt;Playwright’s built-in locator tool works fine most of the time, but once you begin dealing with real-world component libraries it can start to miss. It often suggests &lt;code&gt;getByText&lt;/code&gt; or &lt;code&gt;getByRole&lt;/code&gt; against elements that are not truly semantic controls, which makes those locators flaky or unusable in practice.&lt;/p&gt;

&lt;p&gt;Checkboxes are a good example. In most modern apps, vanilla input &lt;code&gt;[type="checkbox"]&lt;/code&gt; elements are rarely seen. What usually exists are custom components built with divs, spans, hidden inputs, aria attributes, and classes that represent state. Because of that, Playwright assertions like &lt;code&gt;toBeChecked&lt;/code&gt; or &lt;code&gt;toBeDisabled&lt;/code&gt; often cannot be trusted. The checkbox looks checked in the UI, but the underlying HTML does not expose the state in the way those helpers expect.&lt;/p&gt;

&lt;p&gt;In these cases you need to own the locator yourself. The most reliable starting point is usually some stable text on the screen. Once you anchor to that text, you can walk up and down the DOM to find the real checkbox element.&lt;/p&gt;

&lt;p&gt;Playwright has a small but very useful API for this: &lt;code&gt;locator('..')&lt;/code&gt;. It moves one level up in the DOM, and you can chain it as many times as you need. It is much cleaner than &lt;code&gt;xpath=../..&lt;/code&gt; and a lot easier to remember. From there, you can navigate back down into the exact node that represents the state you care about.&lt;/p&gt;

&lt;p&gt;For example, a locator chain for a checkbox in our app might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const checkboxLocator = page
  .locator('p.user-select-none', { hasText: 'School-Pupil Activity Bus' })
  .locator('..')
  .locator('..')
  .locator('.special-requirement-checkbox')
  .locator('input[type="checkbox"][aria-checked="true"][disabled="disabled"]')

await expect(checkboxLocator).toBeVisible()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here the anchor is the visible label text. From there, the locator walks up to a shared container, then back down into a wrapper with a class you know is stable enough, and finally onto the actual input element. Because the checkbox is custom, you assert on &lt;code&gt;aria-checked&lt;/code&gt; and &lt;code&gt;disabled&lt;/code&gt; instead of relying on &lt;code&gt;toBeChecked&lt;/code&gt; or &lt;code&gt;toBeDisabled&lt;/code&gt;. A simple &lt;code&gt;toBeVisible&lt;/code&gt; plus the right attributes ends up being more concrete than the higher level assertion API.&lt;/p&gt;

&lt;p&gt;All of this is also to say why I am a little skeptical of AI testing tools that promise automatic locators and assertions. Real applications rarely use simple, semantic HTML controls. There is a lot of custom markup, hidden inputs, and framework specific structure that you have to understand and navigate. For now, a human who can anchor on the right text, walk the DOM, and assert on real attributes is still the most reliable way to write strong Playwright tests.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>automation</category>
      <category>testing</category>
    </item>
    <item>
      <title>Using AI in Playwright Tests</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Mon, 03 Nov 2025 21:20:13 +0000</pubDate>
      <link>https://dev.to/philipfong/using-ai-in-playwright-tests-35od</link>
      <guid>https://dev.to/philipfong/using-ai-in-playwright-tests-35od</guid>
      <description>&lt;p&gt;It's been a long while since I've posted anything. Excuse the clickbaity title, but rest assured that this is content that I think will deliver. And this post isn't written by AI! &lt;/p&gt;

&lt;p&gt;I'll keep this pretty short and sweet though. This idea was first inspired by the mobile automation framework &lt;a href="https://maestro.dev/" rel="noopener noreferrer"&gt;Maestro&lt;/a&gt;. Other than its solid capabilities as a mobile test framework (using YAML of all things), I was impressed to see an API named &lt;code&gt;assertWithAI&lt;/code&gt;. Imagine just prompting an LLM with "assert that a blue-colored Login button appears at the bottom". A real game changer in testing if you ask me!&lt;/p&gt;

&lt;p&gt;That API is gatekept behind a paid subscription so I didn't experiment much from there. But I was curious to see if Playwright was anywhere close to implementing anything like that, since it is maintained by Microsoft, and Microsoft has its own large stake in OpenAI. At the time of this writing, it did not.&lt;/p&gt;

&lt;p&gt;I gained access to an Enterprise version of OpenAI so I decided to experiment with its API. Here is a helpful util that will integrate AI with visual testing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import OpenAI from 'openai'
import fs from 'fs'
import path from 'path'
import { Page } from '@playwright/test'
import { uniqueId } from './stringHelper'

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY as string,
})

export const askAI = async (page: Page, question: string): Promise&amp;lt;string&amp;gt; =&amp;gt; {
  const fileName = `screenshot-ai-${uniqueId()}.png`
  const absPath = path.resolve(fileName)
  await page.screenshot({ path: absPath })
  const imageBase64 = fs.readFileSync(absPath, 'base64')
  const dataUrl = `data:image/png;base64,${imageBase64}`

  const instructions = [
    'You are helping test a web page from a screenshot.',
    'Please answer in this format:',
    'First line: YES or NO only.',
    'Second line: One short sentence explaining your reasoning.',
    'If you are not confident, please answer NO.'
  ].join('\n')

  try {
    const response = await openai.responses.create({
      model: 'gpt-4.1-mini', // Cheapest version
      instructions,
      input: [
        {
          role: 'user',
          content: [
            {
              type: 'input_text',
              text: `Question: ${question}`,
            },
            {
              type: 'input_image',
              image_url: dataUrl,
              detail: 'low', // Really skimping on the dollars
            },
          ],
        },
      ],
      temperature: 0, // Always provide a more deterministic answer each time
      max_output_tokens: 128, // Keep responses short &amp;amp; cheap
    })

    const answer = response.output_text?.trim() || ''

    console.log('\n[AI PAGE CHECK]')
    console.log('Question:', question)
    console.log('Full AI answer:\n', answer)
    console.log('Screenshot file:', absPath, '\n')

    return answer
  } catch (err: any) {
    // Check rate limit
    if (err.status === 429) {
      console.log(err)
      throw new Error('OpenAI rate limit reached.')
    }
    throw err
  }
}

export const checkAIResponse = (aiResponse: string, expected: boolean) =&amp;gt; {
  const firstLine = aiResponse.trim().split('\n')[0].toLowerCase()
  const aiSaidYes = firstLine.includes('yes')

  // Expected an answer of 'Yes' but AI said 'No'
  if (expected &amp;amp;&amp;amp; !aiSaidYes) {
    throw new Error(`Expected YES but got:\n${aiResponse}`)
  }

  // Expected an answer of 'No' but AI said 'Yes'
  if (!expected &amp;amp;&amp;amp; aiSaidYes) {
    throw new Error(`Expected NO but got:\n${aiResponse}`)
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then writing the tests is really straightforward, powerful, and actually pretty fun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test('AI confirms a map is visible on the page', async ({ page }) =&amp;gt; {
  const mapLink = 'https://www.mysite.com/tracking/b3cd29b39b'
  await page.goto(mapLink)
  await expect(page.locator('[aria-label="Map"]')).toBeVisible()

  const aiAnswer = await askAI(page,
    'Is there a map and a visible plotted route that starts in Ann Arbor and ends in Detroit?'
  )

  await checkAIResponse(aiAnswer, true)
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes Playwright snapshots can only take you so far, and they can become flaky if those snapshots are constantly changing. If there are assets in your app that frequently change and are difficult to automate (but easy to visually confirm), then these are the best spots for this kind of AI assist.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Sharding Jest tests. Harder than it should be?</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Thu, 19 Dec 2024 14:58:29 +0000</pubDate>
      <link>https://dev.to/philipfong/sharding-jest-tests-harder-than-it-should-be-4lhc</link>
      <guid>https://dev.to/philipfong/sharding-jest-tests-harder-than-it-should-be-4lhc</guid>
      <description>&lt;p&gt;I haven't posted here in quite a while. It's been a busy year!&lt;/p&gt;

&lt;p&gt;One of the more challenging problems I've run into was getting our API tests in a place where they can run more quickly. I wrote an old post about API testing &lt;a href="https://dev.to/philipfong/api-testing-for-the-win-1o2i"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In CI, our API tests run with one worker. A simple &lt;code&gt;npm test&lt;/code&gt; kicks things off, sends results to a log file and nicely formatted HTML report thanks to &lt;a href="https://github.com/Hazyzh/jest-html-reporters" rel="noopener noreferrer"&gt;jest-html-reporters&lt;/a&gt; (it even has dark mode!). However, as the number of tests increase, so does execution time.&lt;/p&gt;

&lt;p&gt;In Playwright, I describe how we accomplished sharding in our test pipelines in this &lt;a href="https://dev.to/philipfong/playwright-sharding-with-bitbucket-pipelines-14hg"&gt;other post&lt;/a&gt;. Unfortunately, Jest does &lt;em&gt;not&lt;/em&gt; make this easy whatsoever.&lt;/p&gt;

&lt;p&gt;Even though Jest &lt;em&gt;does&lt;/em&gt; support a &lt;code&gt;--shard&lt;/code&gt; option, similar to Playwright, there doesn't appear to be &lt;em&gt;any&lt;/em&gt; out-of-the-box solution to merging reports. That means that test results live in isolation in their own shards and in their own pipelines. Playwright offers this feature in a &lt;code&gt;merge-reports&lt;/code&gt; option that wraps a nice bow on all of the reports generated by each shard.&lt;/p&gt;

&lt;p&gt;I won't post too much code for now. Below is the general gist of how we ended up sharding our Jest API tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving up on the report merge
&lt;/h2&gt;

&lt;p&gt;No feasible solutions exist to merge HTML reports, so we ended up creating an HTML "landing page" with clickable links to reports generated by each shard. At first, I began to experiment with &lt;a href="https://dkelosky.github.io/jest-stare/" rel="noopener noreferrer"&gt;jest-stare&lt;/a&gt;, but my dedication to dark mode in the reports was too much to overcome! At the same time, using this library also meant outputting test results to JSON, then merging those JSON results by hand, then converting those to an HTML report, and &lt;em&gt;then&lt;/em&gt; trying to produce some kind of custom dark mode styling. I felt like I was sinking too much time there, and since we were already producing reports with &lt;a href="https://github.com/Hazyzh/jest-html-reporters" rel="noopener noreferrer"&gt;jest-html-reporters&lt;/a&gt;, the quickest way out was to just serve up the individual reports.&lt;/p&gt;

&lt;p&gt;So this landing page ends being uploaded in a step in our pipeline, kind of looking like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws s3 cp landing_page.html $S3_BUCKET/$BITBUCKET_BUILD_NUMBER/index.html&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The individual shard child pipelines are instructed to upload their own results, which can then be accessed via the index page when all is said and done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aggregating test results: Bonkers use of Bash
&lt;/h2&gt;

&lt;p&gt;When pulling test results from a single Jest run, we were using regex to scrape the test results from a log file. For each individual shard, we would download each log file and &lt;code&gt;cat&lt;/code&gt; them together. There was some seriously awful looking Bash scripts that had been put together to find the lines prefixed with &lt;code&gt;Tests:&lt;/code&gt; in the log file, and then attempts to aggregate the number of tests passed and failed. That was when I learned about &lt;code&gt;BASH_REMATCH&lt;/code&gt;, which sounds like an old NES game or something.&lt;/p&gt;

&lt;p&gt;Here's a little snippet of some of that nastiness:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Match lines that include failed, passed, and total
if [[ "$line" =~ Tests:\ *([0-9]+)\ failed,\ *([0-9]+)\ passed,\ *([0-9]+)\ total ]]; then
  FAILED_TESTS_TOTAL=$((FAILED_TESTS_TOTAL + ${BASH_REMATCH[1]}))
  PASSED_TESTS_TOTAL=$((PASSED_TESTS_TOTAL + ${BASH_REMATCH[2]}))
  TOTAL_TESTS_TOTAL=$((TOTAL_TESTS_TOTAL + ${BASH_REMATCH[3]}))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To my eyes, this looks pretty awful, but thankfully Jest logs are consistent in generating summary of test results. I cannot say with confidence that this will continue to work with any new Jest version updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts, and a small plead for help?
&lt;/h2&gt;

&lt;p&gt;I think it's amazing that Playwright had the foresight to put a lot of work towards reporting, which in my opinion is one of the more vital and underrated pieces of test engineering. But why is this so hard to do in Jest? I know that Jest was originally intended as a quick and lightweight test runner for frontend unit tests, but I feel confident that I'm not the only one using Jest as a test runner for API testing. If anyone out there is also running into scalability challenges in Jest and reporting, give a shout!&lt;/p&gt;

</description>
      <category>jest</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Adding standalone or "one off" scripts to your Playwright suite</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Mon, 08 Apr 2024 13:11:46 +0000</pubDate>
      <link>https://dev.to/philipfong/adding-standalone-or-one-off-scripts-in-your-playwright-suite-3kng</link>
      <guid>https://dev.to/philipfong/adding-standalone-or-one-off-scripts-in-your-playwright-suite-3kng</guid>
      <description>&lt;p&gt;There might be a time where you may be asked to automate some tasks that fall outside of what is considered a traditional test.&lt;/p&gt;

&lt;p&gt;What do I mean by a traditional test? An automated test typically accomplishes some key ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Perform some actions in the UI&lt;/li&gt;
&lt;li&gt;Assert on the behavior resulting from those actions&lt;/li&gt;
&lt;li&gt;Detects change in behavior and reports back on why a change might have occurred&lt;/li&gt;
&lt;li&gt;Runs as often as needed, sometimes multiple times a day&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now there are times where automating the UI is required, but the outcome of that automation isn't needed regularly. I consider these to be standalone or "one off" Playwright scripts. Some examples might be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scraping some data off of pages to be used later (analytics, manual error checking, cataloging)&lt;/li&gt;
&lt;li&gt;Inputting and setting (many) values in a form on a one-time basis&lt;/li&gt;
&lt;li&gt;Reproducing a bug that might require repeated interactions with the same component / api / UI workflow.&lt;/li&gt;
&lt;li&gt;Client requests such as "we have this spreadsheet data of configurations to create 1,000 widgets in your app, but we don't want to go through the UI manually.&lt;/li&gt;
&lt;li&gt;A small set of smoke tests designed for production systems only, that aren't applicable in your lower environments&lt;/li&gt;
&lt;li&gt;Cleanup scripts if tests aren't designed to do cleanup on their own&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can see that these obviously don't fit the bill of a "test suite", with interactions, assumptions, and assertions. So how do you include these type of files &lt;em&gt;without&lt;/em&gt; having them run in CI and causing a disaster?&lt;/p&gt;

&lt;p&gt;And here are some other requirements that were imposed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;We wanted devs to be able to run these scripts locally without any barriers. So a very simple &lt;code&gt;npx playwright test reproduceBug&lt;/code&gt; was the goal&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As just mentioned, we cannot have these files run in CI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They still needed to maintain the &lt;code&gt;.spec.ts&lt;/code&gt; extension, otherwise Playwright will just spit out "No tests found"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The scripts should live in the same code repo we use for our other normal tests. An entirely separate repo just puts up more barriers for engaging with Playwright.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Playwright configs by default look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default defineConfig({
  testDir: './tests',

... rest of config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means you cannot place test files &lt;em&gt;outside&lt;/em&gt; of this directory, which was brought up as a &lt;a href="https://github.com/microsoft/playwright/issues/14039" rel="noopener noreferrer"&gt;question on Github&lt;/a&gt; some time ago. Initially, I thought it would be nice to add another folder in the repo called "scripts", but Playwright &lt;a href="https://github.com/microsoft/playwright/issues/7403" rel="noopener noreferrer"&gt;does not allow multiple &lt;code&gt;testDir&lt;/code&gt; values&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So the easiest solution was simply to add a subfolder called &lt;code&gt;tests-ignored&lt;/code&gt;, so the structure just looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── root
│   ├── tests
│   │   ├── tests-ignored
├── package.json
├── package-lock.json
└── .gitignore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So in CI, you run your full battery of tests like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx playwright test --grep-invert ignored&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And if you're running a test file locally, everything is as normal as can be:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx playwright test widgetCreation.spec.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And if you're running these other "one off" scripts, it's the same exact pattern:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx playwright test reproduceDeadlockBug.spec.ts&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So there you have it. A nice simple solution that avoids creating extra projects in your Playwright config, and avoids having to know extra options to have to pass to your CLI. This setup helps us freely automate anything we need, without it having to fit any rigid test structures purposefully designed for our regression suite.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Playwright sharding with Bitbucket pipelines</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Mon, 20 Nov 2023 14:43:25 +0000</pubDate>
      <link>https://dev.to/philipfong/playwright-sharding-with-bitbucket-pipelines-14hg</link>
      <guid>https://dev.to/philipfong/playwright-sharding-with-bitbucket-pipelines-14hg</guid>
      <description>&lt;p&gt;Everyone seems to have a love/hate relationship with Atlassian products. I've only really worked at "Atlassian shops" my entire career. Jira, Confluence, Bitbucket, StatusPage. It's nice to have everything in "one place" but on occasion, it seems like so many people are always "fighting" with a limitation of their products. Can't get Jira to do the thing? I guess it's Excel again. Can't get Bitbucket to work with Playwright Test sharding? You've come to the right place.&lt;/p&gt;

&lt;p&gt;So what is sharding? The concept is pretty simple and the execution even simpler. The command line pretty much looks like this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx playwright test --shard 1/3&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Then you do the same for shard 2 of 3, and 3 of 3. Ideally, each command runs in its own machine/Docker image, and it's assigned its own little subset of tests.&lt;/p&gt;

&lt;p&gt;And how does reporting work? If you are able to gather up all of the artifacts written (by default) to &lt;code&gt;./blob-report&lt;/code&gt;, then it's just this:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npx playwright merge-reports --reporter html ./blob-report&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Sounds pretty sweet right? Bunch of tests running in parallel, across different pipeline jobs, and you merge a report and serve it up somewhere.&lt;/p&gt;

&lt;p&gt;All of this is made super easy in &lt;a href="https://playwright.dev/docs/test-sharding#github-actions-example" rel="noopener noreferrer"&gt;Github Actions&lt;/a&gt; but unfortunately is absolutely non-existent in Bitbucket pipelines. The idea of a "job triggering other jobs" is just not a thing.&lt;/p&gt;

&lt;p&gt;So how can this be done? Everything is done through shell scripts and some imagination. Firstly, let's take a look at the top level pipelines that we'll need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipelines:
  custom:
    execute-tests:
      - variables:
          - name: Environment
            default: dev
            allowed-values:
              - dev
              - stage
          - name: MaxNumberOfShards
            default: 1
      - step: *run-tests
    run-shard:
      - variables:
          - name: Environment
          - name: ShardNumber
          - name: MaxNumberOfShards
      - step: *run-shard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So what's happening here? The job &lt;code&gt;run-shard&lt;/code&gt; is basically how our individual shards will be run. This is what it looks like from the Bitbucket Pipeline UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8srfm7i5tcnl0aujqput.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8srfm7i5tcnl0aujqput.png" alt=" " width="800" height="718"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you really wanted to, you could go into the Bitbucket pipeline UI, and resubmit this form for all of the shards you want to run. The idea here is to use our &lt;code&gt;execute-tests&lt;/code&gt; pipeline job to automate all of that!&lt;/p&gt;

&lt;p&gt;So what does our run-shard definition actually look like?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;definitions:
  services:
    run-shard: &amp;amp;run-shard
      name: Run shard for playwright tests
      image: mcr.microsoft.com/playwright:v1.37.0-jammy
      size: 2x
      caches:
        - node
      script:
        - echo "TEST_ENV=$Environment" &amp;gt; .env
        - export DEBIAN_FRONTEND=noninteractive # Interactive installation of aws-cli causes issues
        - apt-get update &amp;amp;&amp;amp; apt-get install -y awscli
        - npm install
        - npx playwright test --shard="$ShardNumber"/"$MaxNumberOfShards" || true # Run test shard
        - aws s3 cp blob-report/ s3://my-bucket/blob-report --recursive # Copy blob report to s3
      artifacts:
        - playwright-report/**
        - test-results/**
        - blob-report/**
        - logs/**
        - .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking a little nasty isn't it? We have our Playwright Docker image executing what we want, which is the &lt;code&gt;playwright test --shard&lt;/code&gt; cli command that we needed. From there, we are uploading the blob-report to S3, which means installing &lt;code&gt;aws-cli&lt;/code&gt; during our pipeline. To me, this seemed a lot easier than trying to fetch artifacts from various pipeline jobs that can be fairly difficult to track down.&lt;/p&gt;

&lt;p&gt;We have our individual &lt;code&gt;run-shard&lt;/code&gt; job that can run &lt;code&gt;shardNumber&lt;/code&gt; out of &lt;code&gt;maxNumberOfShards&lt;/code&gt; (i.e. 1/6, 2/6, etc). I refer to these as "child pipelines". Take note that we've added &lt;code&gt;|| true&lt;/code&gt; to the &lt;code&gt;playwright test&lt;/code&gt; step, as honestly we're not interested in seeing the individual test statuses for the child pipelines. Also we want to really focus on examining test results from our "parent pipeline", and not have a bunch of failed child pipelines divert our attention.&lt;/p&gt;

&lt;p&gt;And so what does our parent pipeline look like? Admittedly it's a mess of shell scripts designed to do a few different things.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    run-tests: &amp;amp;run-tests
      name: Run all UI tests
      image: mcr.microsoft.com/playwright:v1.37.0-jammy
      size: 2x
      caches:
        - node
      script:
        - echo "TEST_ENV=$Environment" &amp;gt; .env
        - export DEBIAN_FRONTEND=noninteractive # Interactive installation of aws-cli causes issues
        - apt-get update &amp;amp;&amp;amp; apt-get install -y awscli
        - aws s3 rm s3://my-bucket/blob-report --recursive # Clear out old blob reports from previous test runs
        - npm install
        - /bin/bash ./scripts/start_playwright_shards.sh # Start child pipelines
        - /bin/bash ./scripts/monitor_shards.sh # Monitor child pipelines from parent pipeline
        - /bin/bash ./scripts/merge_reports_from_shards.sh # Download sharded blob reports from S3 and merge
        # Fail the parent pipeline if test failures are found across shards
        - |
          if grep -qE "[0-9]+ failed" ./logs/test-results.log; then
            echo "Failed tests found in log file"
            exit 1
          fi
      artifacts:
        - playwright-report/**
        - test-results/**
        - logs/**
        - .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This parent pipeline, through some shell scripts, will accomplish the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Iterate from &lt;code&gt;1&lt;/code&gt; through &lt;code&gt;$MaxNumberOfShards&lt;/code&gt; and send a &lt;code&gt;POST&lt;/code&gt; to Bitbucket's API to start the &lt;code&gt;run-shard&lt;/code&gt; pipeline job. The &lt;a href="https://bitbucket.org/blog/predefine-values-of-custom-pipeline-variables" rel="noopener noreferrer"&gt;pipeline variables&lt;/a&gt; are sent as part of its payload. &lt;/li&gt;
&lt;li&gt;Poll for any &lt;code&gt;IN_PROGRESS&lt;/code&gt; child pipeline jobs using the Bitbucket API. If the number of &lt;code&gt;run-shard&lt;/code&gt; jobs is &lt;code&gt;0&lt;/code&gt;, that means we're all done and the parent pipeline can finish.&lt;/li&gt;
&lt;li&gt;Download the &lt;code&gt;blob-report&lt;/code&gt; folder from S3 and execute &lt;code&gt;merge-report&lt;/code&gt;. Here, I opt to create an html report as well as a &lt;code&gt;list&lt;/code&gt; report, which is the Playwright default. The former is found as an artifact in &lt;code&gt;playwright-report&lt;/code&gt;, while the latter is found in &lt;code&gt;logs/test-results.log&lt;/code&gt;, which is a file that is normalized and parsed for results.&lt;/li&gt;
&lt;li&gt;If the log file generated contains "X failed", it means at least 1 test failed across all children. And if any of the individual children fail, then the parent is deemed a failure too (hey, just like in real life!)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll spare you the details on the bash scripts, but for the most part the work involved inspecting Bitbucket's network requests and mimicking those via &lt;code&gt;curl&lt;/code&gt;. From there, it's also a good idea to make your test reporting shareable and easily accessible for your team.&lt;/p&gt;

&lt;p&gt;Well that's all there is to it. I wish it were simpler in Bitbucket but... it's not. Github Actions allows for a couple dozen lines of YAML to do the same thing. But here we have another thing to deal with when it comes to Atlassian. Thanks for the the blog idea though.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>Using Playwright fixtures to skip login pages</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Tue, 26 Sep 2023 16:38:03 +0000</pubDate>
      <link>https://dev.to/philipfong/using-playwright-fixtures-to-skip-login-pages-3g01</link>
      <guid>https://dev.to/philipfong/using-playwright-fixtures-to-skip-login-pages-3g01</guid>
      <description>&lt;p&gt;One of the first things that you might do with Playwright when you start automating is writing some code to automate logging into your app. It's so incredibly easy to do with &lt;code&gt;npx playwright codegen&lt;/code&gt; as well, and I've demo'ed this to my team as a rudimentary example while introducing Playwright.&lt;/p&gt;

&lt;p&gt;As you begin to scale up your tests, you'll likely find that interacting with the login page results in a lot of wasted clicks and network requests. In fact, we found that hitting &lt;code&gt;/auth&lt;/code&gt; in every single one of our tests can cause some unintended side effects. Additionally, logging in repeatedly with the same user credentials doesn't coincide with real-world app usage, so why should we do the same in our tests?&lt;/p&gt;

&lt;p&gt;Fixtures are very useful in that they act as a hook to your original tests, similarly to having &lt;code&gt;beforeEach&lt;/code&gt; or &lt;code&gt;afterEach&lt;/code&gt; hooks everywhere. The major upside of fixtures is that it makes for much cleaner and readable code and provides for consistency across all tests.&lt;/p&gt;

&lt;p&gt;Here's what a fixture might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { test as baseTest, type Page } from '@playwright/test'
import { createAuthContext } from './authHelper'

type AuthFixtures = {
  mySiteAuth: Page
}

export const test = baseTest.extend&amp;lt;AuthFixtures&amp;gt;({
  mySiteAuth: async ({ browser }, use) =&amp;gt; {
    const { page: authorizedPage, context: authorizedContext } = await createAuthContext(browser)
    await use(authorizedPage)
    await authorizedContext.close()
  }
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pretty simple but very important stuff going on here: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We're retrieving a page and browser context.&lt;/li&gt;
&lt;li&gt;The call to &lt;code&gt;use&lt;/code&gt; yields back to Playwright &lt;code&gt;test&lt;/code&gt; where all your steps are written.&lt;/li&gt;
&lt;li&gt;Closing context is important so that browser windows and their pages are closed properly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You'll want to spend a decent chunk of time figuring out what your function for &lt;code&gt;createAuthContext&lt;/code&gt; might look like, but in the end, you want it to return a new browser context along with a new page for that context. The browser context itself should take advantage of the &lt;code&gt;storageState&lt;/code&gt; that Playwright offers. This &lt;code&gt;storageState&lt;/code&gt; is basically a JSON blob that is written to mimic the exact local storage state of your app. Finally, you'll want to probably add some actual UI login logic if the storage JSON blobs don't exist.&lt;/p&gt;

&lt;p&gt;I'll provide a little bit of code below, but keep in mind that this is already well documented by the Playwright team:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const MY_SITE_FILE = 'playwright/.auth/my_site.json'

export const createAuthContext = async (browser: Browser) =&amp;gt; {

  const contextOptions: any = { storageState: MY_SITE_FILE }

  const context = await browser.newContext(contextOptions)
  const page = await context.newPage()

  return { page, context }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all you'll have to do is change up your test signatures. Most of them probably looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { test, expect } from '@playwright/test'

test('do some things', async ({ page }) =&amp;gt; {
  page.doSomeThings()
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now they'll just look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { test } from './helpers/testFixtures'
import { expect } from '@playwright/test'

test('do some things', async ({ mySiteAuth: page }) =&amp;gt; {
  page.doTheSameThings()
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's awesome here is that you don't need to go and refactor a bunch of individual tests. The most you'll have to do is perhaps remove any calls to your UI login functions, which hopefully is easy to find and remove if you've followed some pretty consistent patterns in your tests.&lt;/p&gt;

&lt;p&gt;What I find notable here is the use of &lt;code&gt;await use(authorizedPage)&lt;/code&gt; almost acts like &lt;code&gt;yield&lt;/code&gt; in Ruby, which I can definitely appreciate as someone who's loved the language for so long.&lt;/p&gt;

&lt;p&gt;Hopefully this helps explain how to have Playwright fixtures working alongside their authentication patterns. I did feel that Playwright's documentation was a teeny bit lacking in this area. I'll also mention that using this type of approach that decouples from any global setup also allows for extending &lt;code&gt;AuthFixtures&lt;/code&gt; to other sites within the same test suite.&lt;/p&gt;

&lt;p&gt;When compared to Selenium WebDriver and how you typically have to set local storage manually through traditional Web APIs, Playwright really does make it a breeze to have already authorized pages at your disposal. Getting right into auth sessions right off the bat really allows for more focused testing.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
    </item>
    <item>
      <title>Intercepting network requests in Playwright</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Thu, 20 Jul 2023 01:30:25 +0000</pubDate>
      <link>https://dev.to/philipfong/intercepting-network-requests-in-playwright-25op</link>
      <guid>https://dev.to/philipfong/intercepting-network-requests-in-playwright-25op</guid>
      <description>&lt;p&gt;I recall some time ago that the folks behind WebDriver made their intentions very clear: If an action can be taken in the browser viewport with a mouse and keyboard, they would provide an API for it. Anything else beyond that was merely an afterthought. As testing evolved into a more complex, technically challenging discipline in its own right, the tooling had to meet that demand.&lt;/p&gt;

&lt;p&gt;Fast forward to the present day and we now have tools like &lt;a href="https://dev.to/philipfong/using-selenium-webdriver-40-bidirectional-api-fp7"&gt;WebDriver's BiDi API&lt;/a&gt;, as well as Playwright's support for &lt;a href="https://playwright.dev/docs/network#network-events" rel="noopener noreferrer"&gt;network events&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I had described previously that intercepting network requests added to an effective waiting strategy for the DOM, and it still does. Slow network responses combined with very fast interactions in the UI can result in some fairly nasty, difficult-to-troubleshoot race conditions.&lt;/p&gt;

&lt;p&gt;But one thing I had failed to mention was this: Ultimately, is that button that you're clicking actually sending the proper request? Playwright allows us to check on that with ease, and adds a nice wrinkle of defensiveness to our tests.&lt;/p&gt;

&lt;p&gt;Playwright's documentation is pretty straightforward about this. Set up some promises, go about your &lt;a href="https://playwright.dev/docs/input" rel="noopener noreferrer"&gt;Playwright Actions&lt;/a&gt;, and then retrieve the result of those promises. Here, we've written a higher order function that can be used throughout our test suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const expectRequest = async (page: Page, requests: {url: string, method: string}[], action: () =&amp;gt; Promise&amp;lt;void&amp;gt;) =&amp;gt; {
  const promises = requests.map(({url, method}) =&amp;gt; {
    const predicate = (response: Response) =&amp;gt;
      response.url().includes(url) &amp;amp;&amp;amp;
      response.request().method() === method &amp;amp;&amp;amp;
      response.status() === 200
    return page.waitForResponse(predicate)
  })

  await action()

  return await Promise.all(promises)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's an example of how we would use this helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const loginRequest = {url: '/auth', method: 'POST'}
const profileRequest = {url: '/profile', method: 'GET}
await expectRequest(page, [loginRequest, profileRequest], async () =&amp;gt; {
  await page.getByRole('button', { name: 'Log In' }).click()
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So there we have a pretty simple example of a button click to 'Log In', and we are expecting multiple requests to succeed. A POST request to &lt;code&gt;/auth&lt;/code&gt;, and a GET request to &lt;code&gt;/profile&lt;/code&gt;. This is a fairly arbitrary example, but I have come across some areas in our app where this has made a nice positive impact in our tests, and increases our confidence that the frontend and backend are in complete lockstep.&lt;/p&gt;

&lt;p&gt;Happy testing!&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>testing</category>
      <category>automation</category>
      <category>selenium</category>
    </item>
    <item>
      <title>Taking the plunge into Playwright!</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Mon, 15 May 2023 15:30:19 +0000</pubDate>
      <link>https://dev.to/philipfong/taking-the-plunge-into-playwright-2bd9</link>
      <guid>https://dev.to/philipfong/taking-the-plunge-into-playwright-2bd9</guid>
      <description>&lt;p&gt;I've finally got to a point where we decided to experiment with Playwright. There were some very good reasons for this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Our old WebDriver tests couldn't really be trusted anymore. This wasn't really a problem specific with WebDriver, it was just unfortunate that this happens when tests aren't run regularly (daily/weekly), maintenance isn't prioritized, and tests were just written poorly from the beginning. Just to note: We support a product where our WebDriver tests &lt;em&gt;are&lt;/em&gt; maintained &amp;amp; architected properly and executed on a weekly basis, and things are going swimmingly there. It's only when that cadence is abandoned where things can really really go south.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Running them in CI (even as simple as in a Bitbucket pipeline) was a huge pain, with a ton of custom work. This included containerization of our test code, pushing to ECR, starting an ECS instance, developing highly customized HTML reporting, and highly customized retry mechanisms. It was not the friendliest thing to deal with on a long-term basis.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Writing tests in Ruby is great, but with the limited team size, it seemed that moving to a language that aligned with the dev team has &lt;em&gt;some&lt;/em&gt; potential benefits. While expectations are low that the entire dev team would jump into writing e2e tests, I felt that breaking the language barrier was at least the bare minimum.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finding opportunities for upskilling my team is always a priority. Always valuable to learn things that are on trend and desirable in the market.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'll share some more thoughts and quick takes in another post. But so far, Playwright has made things very easy to get started. But as always, automation is easy to start, and difficult to master.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>automation</category>
      <category>playwright</category>
      <category>selenium</category>
    </item>
    <item>
      <title>API testing for the win</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Tue, 04 Apr 2023 19:24:42 +0000</pubDate>
      <link>https://dev.to/philipfong/api-testing-for-the-win-1o2i</link>
      <guid>https://dev.to/philipfong/api-testing-for-the-win-1o2i</guid>
      <description>&lt;p&gt;Once in a while, depending on your product, you may want to steer towards API tests for a few reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you're working with a small test team, you'll get a pretty big win out of API tests. They're easier to write than UI tests, and you're only dealing with the backend.&lt;/li&gt;
&lt;li&gt;Execution times are faster (most of the time, anyway).&lt;/li&gt;
&lt;li&gt;Developers typically don't get involved with UI tests, but there may be a comfort level with API tests that might increase engagement when it comes to adding new tests, troubleshooting failures, and reviewing test results.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's get right to it. Overall, testing Restful services has always followed a pretty simple pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Depending on the type of request, there may be a payload to send.&lt;/li&gt;
&lt;li&gt;Send the request via rest library.&lt;/li&gt;
&lt;li&gt;Assert against the response.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Jest works great as a test runner and assertion library. You can use axios/supertest/request/chai as the rest library (I am personally partial to supertest). Tests are easy to write in Typescript and are (in my opinion) less verbose than using Java. Parsing JSON responses using JS is also a dream. Finally, finding some talent familiar with JS should be fairly easy.&lt;/p&gt;

&lt;p&gt;In breaking down the above 3 steps you might want to build out a set of utils/helpers/whatever to help you do them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You may want to follow a pattern where you can build payloads, massage / transform some of it for your scenarios, and return those payloads so they can be sent off to the backend.&lt;/li&gt;
&lt;li&gt;You'll want to abstract away some of the get/patch/post/delete requests into their own functions so that you don't need to worry about auth and tokens. &lt;/li&gt;
&lt;li&gt;Unfortunately some backend responses can be massive. So asserting against dozens of keys and values can be a counterproductive exercise. Asserting on the status code, and then maybe some important IDs, and other keys that might be important to business logic might be a good place to start.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then all of a sudden tests look very, very simple to write like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { createUserPayload } from '../helpers/userPayloadHelper'
import { post } from '../helpers/requestHelper'

describe('users API', () =&amp;gt; {
  test('create user', async () =&amp;gt; {
    const uri = '/users'
    const user = createUserPayload()
    const response = await post(uri, user)

    expect(response.statusCode).toBe(201)
    expect(response.body.name).toBe(user.name)
    expect(response.body.email).toBe(user.email)
  })
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll find a lot of teams that also go with the whole Postman/Newman route that is pretty popular, but I have pretty strong opinions that maintaining code in the long run will always beat out maintaining a Postman collection. Plus you'll be doing your test team a favor getting them to learn to write code, and API tests follow some pretty simple patterns that are pretty hard to screw up even if you try.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>automation</category>
      <category>jest</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Let's ask ChatGPT about Playwright versus Ruby's Capybara</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Tue, 28 Feb 2023 13:51:50 +0000</pubDate>
      <link>https://dev.to/philipfong/lets-ask-chatgpt-about-playwright-versus-rubys-capybara-3blh</link>
      <guid>https://dev.to/philipfong/lets-ask-chatgpt-about-playwright-versus-rubys-capybara-3blh</guid>
      <description>&lt;p&gt;I wanted to learn more about Playwright, so I simply asked ChatGPT a series of questions to find out more! See some of the below snapshots for my interesting conversation with the AI bot that is all the rage nowadays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffb1dvofdwnpj196yd9p5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffb1dvofdwnpj196yd9p5.png" alt="ChatGPT writes a Playwright test" width="800" height="1075"&gt;&lt;/a&gt;Wow! ChatGPT puts together what looks to be a valid UI test for a Google Search.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ik9zznqg952a9083s5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ik9zznqg952a9083s5o.png" alt="Asking about await" width="800" height="475"&gt;&lt;/a&gt;I have pretty basic knowledge of async and promises, but I wasn't super clear on why it might be so prevalent in JS-based UI testing. Maybe I've always oversimplified my take on the complexity of DOM events. Returning a promise object on a mouse click is a little foreign to me.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo61jqpl821hovi4fc98i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo61jqpl821hovi4fc98i.png" alt="First attempt at refactor" width="800" height="1217"&gt;&lt;/a&gt;I have a tendency for code to simply look cleaner. For example I'll always try to refactor away string literals where I can. I thought maybe the await keyword can be abstracted away somehow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jtdxfngccn5cc6rlmun.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5jtdxfngccn5cc6rlmun.png" alt="Second attempt at refactor" width="800" height="1232"&gt;&lt;/a&gt;I still wasn't too satisfied so I asked ChatGPT to refactor again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyp4alevh648y1428pbg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhyp4alevh648y1428pbg.png" alt="ChatGPT writes a test in Capybara" width="800" height="774"&gt;&lt;/a&gt;I went ahead and asked ChatGPT to write the same test using Capybara. It produces exactly what I expect (and with MUCH fewer lines of code!)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3yc2l27zqr5m1g5ynzaa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3yc2l27zqr5m1g5ynzaa.png" alt="ChatGPT gives a take on the two frameworks" width="800" height="532"&gt;&lt;/a&gt;And finally I ask for ChatGPT's take on what I perceived as a vast difference... verbosity?&lt;/p&gt;

&lt;p&gt;So there you have it! A quick exercise and comparison between Ruby's stack and Playwright, and an interesting assessment on both from ChatGPT.&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>offers</category>
    </item>
    <item>
      <title>Using Selenium WebDriver 4.0 BiDirectional API</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Mon, 16 Jan 2023 15:37:07 +0000</pubDate>
      <link>https://dev.to/philipfong/using-selenium-webdriver-40-bidirectional-api-fp7</link>
      <guid>https://dev.to/philipfong/using-selenium-webdriver-40-bidirectional-api-fp7</guid>
      <description>&lt;p&gt;I had mentioned in an &lt;a href="https://dev.to/philipfong/working-with-throbbers-35km"&gt;earlier post&lt;/a&gt; that checking on throbbers is critical to ensuring that tests are stepping forward the right way, which avoids flaky tests and headaches.&lt;/p&gt;

&lt;p&gt;I ran into a recent case where I was not able to depend on a throbber, newly enabled element, or any other changes in the DOM that would let me know that it was safe to proceed in our test. I did open up a new Jira ticket to address this. After all, if a machine/bot/whatever is able to produce a bug from moving "too fast", then perhaps there are some UX updates that should be made. Ultimately I did expose a race condition that is reproduced under very poor latency conditions.&lt;/p&gt;

&lt;p&gt;I decided to look into &lt;a href="https://www.selenium.dev/documentation/webdriver/bidirectional/bidi_api/" rel="noopener noreferrer"&gt;Selenium's BiDi API&lt;/a&gt;. For one, it looks like testing stacks involving Cypress and Playwright are already trying to incorporate more support at the devtools level. So it's good to see that the dinosaur that is Selenium is keeping pace with those newer Node stacks.&lt;/p&gt;

&lt;p&gt;I'll be speaking specifically about the Network Interception functionality. To me, that is the one with the most utility in the context of "throbbers and waiters".&lt;/p&gt;

&lt;p&gt;From what I understood, it looks as though the BiDi API is designed in such a way that an async function / block is called. From there, any WebDriver actions allow for that function / block to intercept network requests and at a minimum, allow for the developer to output request URLs, responses, response statuses, and even potentially manipulate incoming responses for the browser to act upon. Super cool!&lt;/p&gt;

&lt;p&gt;In the end, I wrote a helper method to ultimately do what I needed it to do: Do a thing in the UI, make sure that the frontend is sending a specific network request, and making sure that the browser is getting a 200 back. So without further ado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  def wait_on_request
    @request_found = false
    page.driver.browser.intercept do |request, &amp;amp;continue|
      if request.method == 'GET' &amp;amp;&amp;amp; request.url.match?(/tables/)
        puts 'Checking for OK response code for %s' % request.url
        continue.call(request) do |response|
          response.code.should == 200
          @request_found = true
          puts 'OK response code found'
        end
      else
        continue.call(request)
      end
    end
    yield
    attempts = 0
    while !@request_found
      puts 'Waiting for request to be sent by frontend'
      sleep 3
      attempts += 1
      fail 'Request was not sent by the frontend within 30 seconds' if attempts == 10
    end
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this block of code, our app under test uses various REST endpoints that include the path &lt;code&gt;/table&lt;/code&gt; and is hard coded here to account for that request. We are using the &lt;code&gt;yield&lt;/code&gt; keyword that allows passing of another block that is expected to trigger the request. The &lt;code&gt;while&lt;/code&gt; loop is triggered to "monitor" whether the request was actually sent. Tests are designed to fail if the request is never sent by the frontend, or if the request returns anything other than a 200.&lt;/p&gt;

&lt;p&gt;So there you have it. It is very, very cool seeing the advancements in WebDriver over just the past year especially since the release of Selenium 4.0. Historically, they were always such a stickler to "we only support things that a user in a browser would do", but are now bending more towards "we support things that a developer in a browser would do". I'm excited to to see what's in store for future releases.&lt;/p&gt;

&lt;p&gt;Something I found last minute: Unfortunately this is not supported by &lt;a href="https://aws.amazon.com/device-farm/" rel="noopener noreferrer"&gt;AWS Device Farm&lt;/a&gt; and it's not clear when it will ever be. This means that this is only supported locally, or for any remote/grid servers that have built-in support for it. I happen to run into the error &lt;code&gt;DevTools is not supported by the Remote Server&lt;/code&gt;. If anyone has any solutions or workarounds for this, I'm all ears!&lt;/p&gt;

</description>
      <category>tooling</category>
    </item>
    <item>
      <title>Improve your testing resume with these tips (with real life examples)</title>
      <dc:creator>Phil</dc:creator>
      <pubDate>Wed, 21 Dec 2022 19:06:18 +0000</pubDate>
      <link>https://dev.to/philipfong/improve-your-testing-resume-with-these-tips-with-real-life-examples-41d1</link>
      <guid>https://dev.to/philipfong/improve-your-testing-resume-with-these-tips-with-real-life-examples-41d1</guid>
      <description>&lt;p&gt;If you survey anyone that spends time screening resumes and ask them how long they spend reviewing each one, nearly every response will be seconds, not minutes. Avoid having your resume stand out for the wrong reasons. I will illustrate a few tips, what to avoid, and why. Trigger warning for those who unknowingly follow some of these blunders (and stand in defense of them).&lt;/p&gt;

&lt;h3&gt;
  
  
  Resume length
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyi5qvycns5y9wx6vxxzi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyi5qvycns5y9wx6vxxzi.png" alt=" " width="430" height="122"&gt;&lt;/a&gt;When your resume is like reading War and Peace&lt;/p&gt;

&lt;p&gt;If you have a multi-page resume with less than 8 years of experience, look to shorten it to 1 page. Portraying your career across multiple pages  demonstrates an inability to succinctly communicate what you have to offer. More often than not, multi-page resumes are far too verbose, and much of the content ends up repetitive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatting and other problems unrelated to content
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34qblnvkfmxsb97jdp0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F34qblnvkfmxsb97jdp0v.png" alt=" " width="800" height="106"&gt;&lt;/a&gt;How bold of you to bold every other word&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Avoid constant bolding or italicizing of random terms throughout your resume.&lt;/li&gt;
&lt;li&gt;Avoid capitalizing words for no reason, like "Test Plan".&lt;/li&gt;
&lt;li&gt;Do capitalize things that make sense, like "Selenium WebDriver" or "Jira".&lt;/li&gt;
&lt;li&gt;Typos will show a true lack of attention to detail (probably the most important trait for a testing role) so inspect for those closely.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bad content
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqaphts0ygf3p6h8hebs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqaphts0ygf3p6h8hebs.png" alt=" " width="800" height="834"&gt;&lt;/a&gt;This is just painful to see&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Avoid describing experience with something like "Good experience with..." or "Good knowledge of...". There are so many other adjectives other than "good".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Get into the job history as soon as possible. A very lengthy list of "skills" bullet points (or humongous table!) will always get skipped over. On the other hand, a quick, eye catching 2-3 liner summary (that avoids sounding too generic) is something I personally like reading if it describes their story in a unique way.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Maybe this is a cultural thing, but it is not necessary to see what the candidate looks like. Avoid inserting a photograph on your resume.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;And finally, if you don't know how to code, please don't lie about it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I promise you that if you follow some of these tips, your resume will be in the top 5% of resumes that get in front of hiring managers. The rest of 'em are just tossed into the virtual trash bin.&lt;/p&gt;

</description>
      <category>ui</category>
      <category>design</category>
    </item>
  </channel>
</rss>
