<?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: Craig Morten</title>
    <description>The latest articles on DEV Community by Craig Morten (@craigmorten).</description>
    <link>https://dev.to/craigmorten</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F391875%2Fc71634b2-c0ed-4bbb-be5b-91b905ab4a86.jpg</url>
      <title>DEV Community: Craig Morten</title>
      <link>https://dev.to/craigmorten</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/craigmorten"/>
    <language>en</language>
    <item>
      <title>You know you can automate screen reader tests right?</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:36:32 +0000</pubDate>
      <link>https://dev.to/craigmorten/you-know-you-can-automate-screen-reader-tests-right-2dnk</link>
      <guid>https://dev.to/craigmorten/you-know-you-can-automate-screen-reader-tests-right-2dnk</guid>
      <description>&lt;p&gt;There is a common misconception that accessibility testing can only fall into two groups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Some limited amount of static analysis performed by something like Axe&lt;/li&gt;
&lt;li&gt;Manual testing for everything else, either by yourself, your tester, your AI agent(?), your resident accessibility expert, or hopefully real assistive technology users&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've gone a step further, you might even have some coverage in your functional test suite of choice for keyboard interactions (most cool!), but have you ever considered that you could also be testing with some popular screen reader technologies as well?&lt;/p&gt;

&lt;p&gt;Instead of querying the DOM to find a button, or checking its accessible name from the browser's internal accessibility tree, you could be navigating using a real screen reader and assert against what it actually announces.&lt;/p&gt;

&lt;p&gt;In this post we'll walk you through how to set it up with &lt;a href="https://guidepup.dev/" rel="noopener noreferrer"&gt;Guidepup&lt;/a&gt; running against the &lt;a href="https://www.bbc.co.uk/" rel="noopener noreferrer"&gt;BBC Homepage&lt;/a&gt; - spoiler alert, it's two extra CLI commands and then whatever tests you fancy writing!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;For this walkthrough we're going to use &lt;a href="https://playwright.dev" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt;, but you can bring your own testing library of choice!&lt;/p&gt;

&lt;p&gt;The first step to screen reader automation is to configure your machine, which can be done in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Add the --ci flag if running in CI!&lt;/span&gt;
npx @guidepup/setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your project directory install your test dependencies as you would usually, but with a few extra &lt;a href="https://github.com/guidepup/guidepup" rel="noopener noreferrer"&gt;Guidepup&lt;/a&gt; packages that will be handling the screen reader automation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# If you're not using Playwright then drop the last package!&lt;/span&gt;
npm i @playwright/test @guidepup/guidepup @guidepup/playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then as you would normally, install your Playwright browser(s) of choice - for this walkthrough we'll stick to WebKit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;install &lt;/span&gt;webkit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for the last "extra" step vs a standard Playwright project setup, we run the following command to install our screen reader assets - basically the equivalent to the Playwright command we just ran, but for screen readers instead of browsers!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @guidepup/setup &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup done! 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Playwright config
&lt;/h2&gt;

&lt;p&gt;We need this anyway, the only difference is we wrap our config in a &lt;code&gt;withGuidepup()&lt;/code&gt; utility:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;withGuidepup&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@guidepup/playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withGuidepup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webkit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Desktop Safari&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're introducing this to an existing test suite you can also create this as a separate &lt;code&gt;playwright.guidepup-config.ts&lt;/code&gt; or similar.&lt;/p&gt;

&lt;p&gt;This wrapper makes sure that the Playwright test runs are always headed as screen readers cannot operate against headless browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Playwright test
&lt;/h2&gt;

&lt;p&gt;Last step! Let's dive straight in with the pertinent code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// bbc.test.ts&lt;/span&gt;

&lt;span class="c1"&gt;// We import our test instance from Guidepup's Playwright package -&lt;/span&gt;
&lt;span class="c1"&gt;// it's the same as the Playwright test version but with a&lt;/span&gt;
&lt;span class="c1"&gt;// `screenReader` instance available, a bit like the `page`&lt;/span&gt;
&lt;span class="c1"&gt;// instance&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;screenReaderTest&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@guidepup/playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_NAVIGATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toUTCString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BBC screen reader tests&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Skip the pesky cookie banner.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addCookies&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ckns_explicit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;www.bbc.co.uk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;]);&lt;/span&gt;

      &lt;span class="c1"&gt;// Prevent the BBC redirecting us to their international site.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.bbc.co.uk/userinfo**&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Navigate to our target page.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.bbc.co.uk/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;load&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="c1"&gt;// Wait for the page to load.&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// Navigate the screen reader to the web content!!&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigateToWebContent&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

      &lt;span class="c1"&gt;// That's us now at the beginning of the web page&lt;/span&gt;
      &lt;span class="c1"&gt;// with our screen reader :o&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I can navigate to BBC Sounds using a screen reader&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;navigationCounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Let's navigate to their "More menu" button on the&lt;/span&gt;
    &lt;span class="c1"&gt;// site header.&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/More menu/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigationCounter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_NAVIGATION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to find 'More menu' button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Navigate to the next item with the screen reader.&lt;/span&gt;
      &lt;span class="c1"&gt;// For VoiceOver this does "VO+Right Arrow", for NVDA this&lt;/span&gt;
      &lt;span class="c1"&gt;// does "Right Arrow", for Orca this does&lt;/span&gt;
      &lt;span class="c1"&gt;// "Orca+Ctrl+Right Arrow".&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Last spoken phrase: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;navigationCounter&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Activate the control under the screen reader cursor, usually&lt;/span&gt;
    &lt;span class="c1"&gt;// similar to keying "Enter" with a keyboard.&lt;/span&gt;
    &lt;span class="c1"&gt;// This is so we can expand the menu to find BBC Sounds!&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Last spoken phrase: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;navigationCounter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// And finally, continue our search for the desired link.&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="sr"&gt;/Sounds/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;navigationCounter&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;MAX_NAVIGATION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to find 'Sounds' link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Last spoken phrase: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;navigationCounter&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// We're here, visit the link!&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;act&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Last spoken phrase: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lastSpokenPhrase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Let's assert that we actually got to BBC Sounds!&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/bbc&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;co&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;uk&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;sounds/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// And finally log out the entire journey - in your tests this&lt;/span&gt;
    &lt;span class="c1"&gt;// could be assertions or even a snapshot.&lt;/span&gt;
    &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Spoken phrase log: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;screenReader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spokenPhraseLog&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Not working for you? I don't own the website so it could have changed! Please also be considerate when testing against sites you don't own!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Follow along ok? 😅&lt;/p&gt;

&lt;p&gt;The key bits are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Import your &lt;code&gt;test&lt;/code&gt; instance from &lt;code&gt;@guidepup/playwright&lt;/code&gt; not &lt;code&gt;@playwright/test&lt;/code&gt; to get access to the generic &lt;code&gt;screenReader&lt;/code&gt; instance, or &lt;code&gt;voiceOver&lt;/code&gt;, &lt;code&gt;nvda&lt;/code&gt;, or &lt;code&gt;orca&lt;/code&gt; instances if targeting a specific screen reader.&lt;/li&gt;
&lt;li&gt;Once your page is loaded, use the &lt;code&gt;screenReader.navigateToWebContent()&lt;/code&gt; command to land on the web content.&lt;/li&gt;
&lt;li&gt;Make use of &lt;code&gt;screenReader&lt;/code&gt; commands to navigate through the page and assert on what was spoken through the screen reader.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This test is basic - we're mostly using &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;act&lt;/code&gt; and &lt;code&gt;lastSpokenPhrase&lt;/code&gt;, but Guidepup also supports the full command set for VoiceOver, NVDA, and Orca, so you can go wild with more realistic journeys, e.g. navigating by headings, landmarks, and links.&lt;/p&gt;

&lt;p&gt;For your own tests you can probably drop some of the &lt;code&gt;log()&lt;/code&gt; logic, it's there so I can easily dump the output for this post 😄 - talking of which here's the log output from a &lt;code&gt;macos-latest&lt;/code&gt; GitHub actions run (which automatically uses VoiceOver):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; playwright test bbc.test.ts

Running 1 tests using 1 worker
┌─────────────────────┬────────────────────┐
│ (index)             │ Values             │
├─────────────────────┼────────────────────┤
│ osName              │ 'darwin'           │
│ osVersion           │ '25.6.0'           │
│ browserName         │ 'webkit'           │
│ browserVersion      │ '27.2'             │
│ screenReaderName    │ 'VoiceOver'        │
│ screenReaderVersion │ '0.0.1-VoiceOver4' │
│ retry               │ 0                  │
└─────────────────────┴────────────────────┘
[Sat, 24 Oct 2026 16:32:37 GMT] Last spoken phrase: BBC navigation
[Sat, 24 Oct 2026 16:32:38 GMT] Last spoken phrase: link BBC Homepage
[Sat, 24 Oct 2026 16:32:38 GMT] Last spoken phrase: list 2 items
[Sat, 24 Oct 2026 16:32:38 GMT] Last spoken phrase: link Skip to content 1 of 2
[Sat, 24 Oct 2026 16:32:39 GMT] Last spoken phrase: link Accessibility Help 2 of 2
[Sat, 24 Oct 2026 16:32:39 GMT] Last spoken phrase: end of list
[Sat, 24 Oct 2026 16:32:39 GMT] Last spoken phrase: list 1 item
[Sat, 24 Oct 2026 16:32:40 GMT] Last spoken phrase: link Sign in
[Sat, 24 Oct 2026 16:32:40 GMT] Last spoken phrase: end of list
[Sat, 24 Oct 2026 16:32:40 GMT] Last spoken phrase: list 7 items
[Sat, 24 Oct 2026 16:32:40 GMT] Last spoken phrase: link Home 1 of 7
[Sat, 24 Oct 2026 16:32:41 GMT] Last spoken phrase: link News 2 of 7
[Sat, 24 Oct 2026 16:32:41 GMT] Last spoken phrase: link Sport 3 of 7
[Sat, 24 Oct 2026 16:32:41 GMT] Last spoken phrase: link Earth 4 of 7
[Sat, 24 Oct 2026 16:32:42 GMT] Last spoken phrase: link Reel 5 of 7
[Sat, 24 Oct 2026 16:32:42 GMT] Last spoken phrase: link Worklife 6 of 7
[Sat, 24 Oct 2026 16:32:43 GMT] Last spoken phrase: link Travel 7 of 7
[Sat, 24 Oct 2026 16:32:43 GMT] Last spoken phrase: end of list
[Sat, 24 Oct 2026 16:32:43 GMT] Last spoken phrase: More menu menu pop up button
[Sat, 24 Oct 2026 16:32:44 GMT] Last spoken phrase: 
[Sat, 24 Oct 2026 16:32:44 GMT] Last spoken phrase: link Future 2 of 6
[Sat, 24 Oct 2026 16:32:44 GMT] Last spoken phrase: link Music 3 of 6
[Sat, 24 Oct 2026 16:32:45 GMT] Last spoken phrase: link TV 4 of 6
[Sat, 24 Oct 2026 16:32:45 GMT] Last spoken phrase: link Weather 5 of 6
[Sat, 24 Oct 2026 16:32:45 GMT] Last spoken phrase: link Sounds 6 of 6
[Sat, 24 Oct 2026 16:32:47 GMT] Last spoken phrase: 
[Sat, 24 Oct 2026 16:32:47 GMT] Spoken phrase log: ["banner","BBC navigation","link BBC Homepage","list 2 items","link Skip to content 1 of 2","link Accessibility Help 2 of 2","end of list","list 1 item","link Sign in","end of list","list 7 items","link Home 1 of 7","link News 2 of 7","link Sport 3 of 7","link Earth 4 of 7","link Reel 5 of 7","link Worklife 6 of 7","link Travel 7 of 7","end of list","More menu menu pop up button","","link Future 2 of 6","link Music 3 of 6","link TV 4 of 6","link Weather 5 of 6","link Sounds 6 of 6",""]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start with your static analysis tests such as Axe for a base level&lt;/li&gt;
&lt;li&gt;Have a think about what critical user journeys you have&lt;/li&gt;
&lt;li&gt;Test them out manually with some key OS + browser + screen reader combos&lt;/li&gt;
&lt;li&gt;Fix any issues&lt;/li&gt;
&lt;li&gt;Then convert your manual test to &lt;a href="https://guidepup.dev/" rel="noopener noreferrer"&gt;Guidepup&lt;/a&gt; based tests to cover you from future regressions!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same as mouse and keyboard regression tests, you can't test everything, but a small set of golden path tests for your core features can go a long way to ensuring your site can work for screen reader users!&lt;/p&gt;

&lt;p&gt;The real benefit here is catching accessibility regressions before they reach your users. Once you've manually tested a critical user journey and automated it with &lt;a href="https://guidepup.dev/" rel="noopener noreferrer"&gt;Guidepup&lt;/a&gt;, you can run it as part of your existing test suite every time you make a change. If a button's announcement changes, a link becomes harder to find, or a journey stops working as expected, your tests can flag it before it ships.&lt;/p&gt;

&lt;p&gt;Think you want to go further? With the dawn of Agentic AI I am curious what level of exploratory and/or chaos testing you can achieve to battle test a site? Maybe give it a go and report back? 😉&lt;/p&gt;




&lt;p&gt;Want the code? Check out the &lt;a href="https://github.com/cmorten/sr-automation-demos/tree/main/bbc-homepage" rel="noopener noreferrer"&gt;BBC Homepage screen reader demo in GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>playwright</category>
      <category>node</category>
    </item>
    <item>
      <title>Automating a11y testing: Part 2 - Beyond Axe</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Thu, 20 Jul 2023 09:51:40 +0000</pubDate>
      <link>https://dev.to/craigmorten/automating-a11y-testing-part-2-beyond-axe-41hl</link>
      <guid>https://dev.to/craigmorten/automating-a11y-testing-part-2-beyond-axe-41hl</guid>
      <description>&lt;p&gt;Earlier this year I gave a talk on the topic of automating web accessibility (a11y) testing at the John Lewis Partnership Tech Profession Conference 2023 which I’m delighted to be sharing here in an article series format.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://medium.com/john-lewis-software-engineering/automating-a11y-testing-part-1-axe-ed3d215de126" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; of this series I covered a number of tools from the &lt;a href="https://www.deque.com/axe/" rel="noopener noreferrer"&gt;Axe suite&lt;/a&gt; for static analysis of sites to find a11y violations, from framework specific packages to VSCode integrations, and sharing a few learnings with caveats and gotchas from using Axe tools.&lt;/p&gt;

&lt;p&gt;However, this is just a small subset of available test automation tools. In this article we will start to explore some of the non-Axe tooling (though Axe will certainly pop up again!) in the wider a11y test automation space. Let’s get going!&lt;/p&gt;

&lt;h2&gt;
  
  
  Linting for a11y
&lt;/h2&gt;

&lt;p&gt;Although some aspects of web accessibility require manual validation as they are subjective regarding the user experience, many requirements of the &lt;a href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noopener noreferrer"&gt;Web Content Accessibility Guidelines (WCAG)&lt;/a&gt; are for ensuring you use the correct semantic markup and associated attributes to correctly present your content to users. Such requirements are well suited to static analysis checks as they are deterministic and rule based — indeed there is such a set of official rules set out in the &lt;a href="https://www.w3.org/WAI/standards-guidelines/act/" rel="noopener noreferrer"&gt;Accessibility Conformance Tests (ACT) specification&lt;/a&gt; which are used by static analysis tools such as Axe that we covered in &lt;a href="https://medium.com/john-lewis-software-engineering/automating-a11y-testing-part-1-axe-ed3d215de126" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Throughout this series I will be mostly focusing on the automation side of a11y testing, but it is also key to remember the importance of manual validation and exploratory testing" — Key considerations from &lt;a href="https://medium.com/john-lewis-software-engineering/automating-a11y-testing-part-1-axe-ed3d215de126" rel="noopener noreferrer"&gt;Part 1&lt;/a&gt; still apply!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Although we can run static analysis checks in CI, as a form of push left and also early feedback, a really natural and effective place to run these checks is in code linting. What is linting if not static analysis of code / markup! We’ve already seen the &lt;a href="https://medium.com/john-lewis-software-engineering/automating-a11y-testing-part-1-axe-ed3d215de126#2279" rel="noopener noreferrer"&gt;VSCode Axe Linter&lt;/a&gt; in the previous article, let’s look at some others.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Eslint&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;If you are already plugged into the &lt;a href="https://eslint.org/" rel="noopener noreferrer"&gt;Eslint&lt;/a&gt; ecosystem, there are a number of plugins that you can reach for depending on your framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React and other JSX based frameworks — &lt;a href="https://www.npmjs.com/package/eslint-plugin-jsx-a11y" rel="noopener noreferrer"&gt;eslint-plugin-jsx-a11y&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;React Native — &lt;a href="https://github.com/FormidableLabs/eslint-plugin-react-native-a11y" rel="noopener noreferrer"&gt;eslint-plugin-react-native-a11y&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Vue — &lt;a href="https://github.com/maranran/eslint-plugin-vue-a11y" rel="noopener noreferrer"&gt;eslint-plugin-vue-a11y&lt;/a&gt; and / or &lt;a href="https://github.com/vue-a11y/eslint-plugin-vuejs-accessibility" rel="noopener noreferrer"&gt;eslint-plugin-vuejs-accessibility&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Angular — &lt;a href="https://www.npmjs.com/package/@angular-eslint/eslint-plugin" rel="noopener noreferrer"&gt;@angular-eslint/eslint-plugin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lit Web Components — &lt;a href="https://www.npmjs.com/package/eslint-plugin-lit-a11y" rel="noopener noreferrer"&gt;eslint-plugin-lit-a11y&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mileage may vary with coverage from plugin to plugin, but each comes with the same great developer experience of real time feedback &lt;em&gt;as&lt;/em&gt; you write your code as well as all the other awesome stuff you get with using Eslint such as IDE integrations with intellisense, auto-fix, etc.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqg7fo5v3pgx1raskrmr.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsqg7fo5v3pgx1raskrmr.png" alt="VSCode tab open on a " width="800" height="328"&gt;&lt;/a&gt; &lt;em&gt;A warning from &lt;a href="https://www.npmjs.com/package/eslint-plugin-jsx-a11y" rel="noopener noreferrer"&gt;eslint-plugin-jsx-a11y&lt;/a&gt; in VSCode&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Same as your other linting rules, you can also lean into the power of running the linting both locally, whether manually or through git hooks, as well as in CI to ensure you catch issues before you even consider raising that pull request.&lt;/p&gt;

&lt;h3&gt;
  
  
  Svelte
&lt;/h3&gt;

&lt;p&gt;Those reading closely may have noticed that the Eslint plugin list above (which was by no means exhaustive) was missing the popular framework Svelte.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"SvelteKit strives to provide an accessible platform for your app by default. Svelte’s &lt;a href="https://svelte.dev/docs#accessibility-warnings" rel="noopener noreferrer"&gt;compile-time accessibility checks&lt;/a&gt; will also apply to any SvelteKit application you build." — &lt;a href="https://kit.svelte.dev/docs/accessibility" rel="noopener noreferrer"&gt;https://kit.svelte.dev/docs/accessibility&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Svelte (and SvelteKit) have a number of &lt;a href="https://svelte.dev/docs/accessibility-warnings" rel="noopener noreferrer"&gt;accessibility warnings&lt;/a&gt; built into the compiler itself which cover a subset of a11y rules. It’s super cool to have these built into the framework itself, but it is worth flagging that these aren’t a catch-all. Even within the limited scope of static analysis checkers, &lt;a href="https://twitter.com/geoffrich_/status/1383170662651482115?s=20" rel="noopener noreferrer"&gt;as flagged by one of the Svelte maintainers Geoff Rich&lt;/a&gt;, there are a number of gotchas around these compiler checks.&lt;/p&gt;

&lt;p&gt;For example, the Svelte compiler is only able to check hard-coded values in markup. If you provide a dynamic value via a variable the Svelte compiler isn’t able to determine the possible values for that attribute and thus doesn’t yeild the desired warning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;{href}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;More Information&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this example the &lt;code&gt;href="#"&lt;/code&gt; should normally triggers an a11y warning from the compiler, but using the variable results in the warning being surpressed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other
&lt;/h3&gt;

&lt;p&gt;There are a limited number of other linting options out there for a11y, some of which are paid for options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.npmjs.com/package/ember-template-lint" rel="noopener noreferrer"&gt;ember-template-lint&lt;/a&gt; — A linter for Ember projects that includes a11y rules.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/marketplace/accesslint" rel="noopener noreferrer"&gt;AccessLint &lt;/a&gt;— A paid for (with free trial) GitHub App Integration: "AccessLint brings automated web accessibility testing into your development workflow."&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.deque.com/axe/devtools/linter/" rel="noopener noreferrer"&gt;Axe DevTools Linter&lt;/a&gt; — In addition to the VSCode plugin covered in Part 1, Axe also have paid for (with free trial) integrations with GitHub Actions, SonarQube, a REST API, and a CLI for their a11y linter.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Visual testing
&lt;/h2&gt;

&lt;p&gt;Let’s pivot over to some ideas around a11y visual testing!&lt;/p&gt;

&lt;h3&gt;
  
  
  Emulated vision deficiencies
&lt;/h3&gt;

&lt;p&gt;Back in Chrome 83 the Chromium team released a DevTools change that allowed you to &lt;a href="https://developer.chrome.com/blog/new-in-devtools-83/#vision-deficiencies" rel="noopener noreferrer"&gt;emulate different vision deficiencies&lt;/a&gt; in the browser. This is accessible from the &lt;a href="https://developer.chrome.com/docs/devtools/command-menu/" rel="noopener noreferrer"&gt;Chrome DevTools Command Palette&lt;/a&gt; (open DevTools and key CMD+SHIFT+P on Mac or CTRL+SHIFT+P for Windows) by searching for "Rendering", where you can then pick from six different emulated vision deficiencies.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63qq847a275seh1zkzlc.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F63qq847a275seh1zkzlc.png" alt="Chrome open on Waitrose No.1 Dine In Meal Deal web page. Chrome DevTools is open on the " width="799" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Something that I’ve learned recently is that Chrome also boasts a &lt;a href="https://chromedevtools.github.io/devtools-protocol/" rel="noopener noreferrer"&gt;Chrome DevTools Protocol (CDP)&lt;/a&gt; that allows you to set almost every feature you get in DevTools via an API. For example, for those using Playwright this example demonstrates how we can set up a CDP session and request to set an emulated blurred vision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;EmulatedVisionDeficiency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blurredVision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reducedContrast&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;achromatopsia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deuteranopia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;protanopia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tritanopia&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visually acceptable when have blurred vision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;context&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;newCDPSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Emulation.setEmulatedVisionDeficiency&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;blurredVision&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// ... visual test with Playwright / other third party lib&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provided your test framework supports a Chromium based browser then CDP should be available and you can make use of this capability in your based visual tests, e.g. using &lt;a href="https://playwright.dev/docs/test-snapshots" rel="noopener noreferrer"&gt;Playwright screenshots&lt;/a&gt; or &lt;a href="https://docs.cypress.io/guides/tooling/visual-testing" rel="noopener noreferrer"&gt;Cypress image snapshots&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, here is emulated achromatopsia (where you can’t perceive colour) for a snapshot test on John Lewis Women’s Dresses product listing page (PLP):&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fslaquw3080by16kz41vt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fslaquw3080by16kz41vt.png" alt="VSCode window with two tabs open. One containing Playwright tests for setting up different vision deficiencies via CDP and then using the Playwright screenshot API. The other tab shows the resulting screenshot for the achromatopsia test — a greyscale image of the John Lewis Women’s Dresses product listing page." width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s the same PLP from the blurred vision test:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uektrf45vk0376mltq6.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1uektrf45vk0376mltq6.png" alt="The John Lewis Women’s Dresses product listing page as viewed by a user with blurred vision." width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And finally one from the protanopia (where you can’t perceive Red light) test:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqtq7be8b9yc6wn5rmaa.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdqtq7be8b9yc6wn5rmaa.png" alt="The John Lewis Women’s Dresses product listing page as viewed by a user with protanopia." width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I wouldn’t recommend going overboard with running all of these variations for every test — visual tests are typically heavy and slow by nature, and fragile to browser upgrades, anti-aliasing, and other false positive issues (although this is somewhat mitigated through using third party vendors who typically have intelligent diffing algorithms).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mileage may vary with third part integrations — if your provider performs the snapshot on the browser instance you’re running (locally or in CI) then this may well be an option, but if they do HTML + CSS snapshotting to recreate the page in different browsers on their servers then this is likely not a goer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It could be worth considering adding to a couple to your suites for golden path smoke tests. Alternatively, it can be nice to have such tests running in production on a nightly, or weekly, where the snapshots form the basis of design review discussion opposed to a blocking gate in release.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"In the UK, more than 2 million people are living with sight loss. Of these, around 340,000 are registered as blind or partially sighted." — &lt;a href="https://www.nhs.uk/conditions/vision-loss/" rel="noopener noreferrer"&gt;https://www.nhs.uk/conditions/vision-loss/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Globally, at least 2.2 billion people have a near or distance vision impairment." — &lt;a href="https://www.who.int/news-room/fact-sheets/detail/blindness-and-visual-impairment" rel="noopener noreferrer"&gt;https://www.who.int/news-room/fact-sheets/detail/blindness-and-visual-impairment&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;"Colour blindness (colour vision deficiency, or CVD) affects approximately 1 in 12 men (8%) and 1 in 200 women in the world. In Britain this means that there are approximately 3 million colour blind people (about 4.5% of the entire population), most of whom are male. Worldwide, there are approximately 300 million people." — &lt;a href="https://www.colourblindawareness.org/" rel="noopener noreferrer"&gt;https://www.colourblindawareness.org/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The key is to be aware and empathetic in your design and implementation towards folks who have visual impairments, and understanding of their experience of your site. A few extra, simple tests can help bring colour (pun very much intended) to otherwise dry requirements around font-size and colour contrast which are hard to appreciate in isolation — a quick change to a base font-size in your design system could be a game change for some folks experience on your site.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automating zoom
&lt;/h3&gt;

&lt;p&gt;Another important aspect of visual accessibility is supporting higher zoom levels, typically of up to 200%. This is laid out in several success criteria in WCAG for text resize and content reflow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This Success Criterion helps people with low vision by letting them increase text size in content so that they can read it." — &lt;a href="https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html" rel="noopener noreferrer"&gt;https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unfortunately most major frameworks don’t support APIs to instrument browser zoom at the moment. Searching around you might see references to the --force-device-scale-factor=2.0 device scale factor flag for Chrome, but this relates to pixel density and is not for zoom (can be passed to launchOptions.args array within your framework’s browser setup options if you want to try and see).&lt;/p&gt;

&lt;p&gt;This limitation has been flagged for some testing frameworks so we can hope it might be supported in future. For now you can upvote the likes of &lt;a href="https://github.com/microsoft/playwright/issues/2497" rel="noopener noreferrer"&gt;this Playwright issue for adding an option for browser zoom&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While we wait for frameworks to catch up, the best solution that I’ve found for the time being is to simulate zoom by adding a before hook in your tests which sets the zoom style of the body. For example, this is a snippet from a Playwright visual test in which the a 200% zoom is applied through CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document.body.style.zoom=2.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqyvhfn5mvr7d0qjwcggl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqyvhfn5mvr7d0qjwcggl.png" alt="Waitrose The King’s Coronation Jewel The Jack Russell Cake product detail page with 200% zoom on desktop." width="799" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In many cases this isn’t necessarily representative of using the native browser zoom feature, nor the OS level zoom features for enlarging text, which is what the WCAG guidance is really for. Nevertheless, it somewhat provides an idea as to how your page behaves when magnified, and can be a useful addition to your golden path test suite to understand if critical functionality still works.&lt;/p&gt;

&lt;p&gt;If anything this goes to show how important it is to keep up manual and exploratory testing when it comes to a11y, there is no way to get the coverage otherwise! When it comes to vision deficiencies we are really still quite limited on how much we can shift to automation — some smoke tests on Chrome are great, but what about other browsers? What about tablet and mobile?!&lt;/p&gt;

&lt;p&gt;If anyone has any ideas on automation in this space I would love to hear them!&lt;/p&gt;

&lt;h2&gt;
  
  
  Time to get funky
&lt;/h2&gt;

&lt;p&gt;In this last section I wanted to share one last tool called &lt;a href="https://www.funkify.org/" rel="noopener noreferrer"&gt;Funkify&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Funkify is an extension for Chrome that helps you experience the web and interfaces through the eyes of extreme users with different abilities and disabilities." — &lt;a href="https://www.funkify.org/" rel="noopener noreferrer"&gt;https://www.funkify.org/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is questionably accessibility test automation, as it is a browser extension... but I’ve decided it qualifies as something that automates customer personas to supplement your exploratory testing. It’s also just very cool so worth the share on that premise alone.&lt;/p&gt;

&lt;p&gt;Because it is rather good unfortunately (but quite understandably) the maintainers have introduced a premium level around a lot of the functionality. But even if you just have a dabble with the free features I think it’s well worth it for expanding horizons.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F13xtirv5hk4v0ajovi9y.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F13xtirv5hk4v0ajovi9y.png" alt="Waitrose groceries homepage open in a Chrome browser. In the toolbar the Funkify extension has been activated, displaying a popup with a number of personna simulators that can be selected: " width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The extension works well on most sites, and as you can see, provides a series of simple personas or modes that you can adopt while browsing the site. Everything from vision defects to simulated dyslexia with scrambling letters to trembling hands.&lt;/p&gt;

&lt;p&gt;For example, here is the Waitrose groceries landing page with the Dyslexia Dani persona example where we can get a flavour of what the user experience might be like (be wary this is an example simulation, it is not necessarily representative of the experience for all folks with Dyslexia):&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvbzpwlsum2at7qhay78.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhvbzpwlsum2at7qhay78.png" alt="Waitrose groceries homepage open in a Chrome browser. In the toolbar the Funkify extension has been activated, displaying a popup where the " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One option that really struck home for me is the Trembling Trevor persona which simulates how it might be like to use the site if you suffer from motor degenerative disorders such as Parkinson’s. Unfortunately Medium doesn’t support embedding videos directly to show this off, so I really encourage giving it a go with the free trial at least once! Once you’ve had a play with the extension on desktop, have a think about touch devices — ask yourself how confident are you with your site’s buttons, links, and dropdowns being usable on a small touch screen?&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing notes
&lt;/h2&gt;

&lt;p&gt;In this article I’ve covered off a few additional tools and techniques for expanding a11y coverage, exploring extensions to the likes of visual testing and how this can supplement manual and exploratory testing.&lt;/p&gt;

&lt;p&gt;This article hasn’t been extensive by far, but hopefully a flavour of what tooling is out there. If you’re keen to explore further, these are some awesome sites where you can learn more and find other tools in the a11y automation space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.a11yproject.com/resources/" rel="noopener noreferrer"&gt;https://www.a11yproject.com/resources/&lt;/a&gt; — absolute wealth of informationa and resources, from blogs and books to tools and organisations.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://a11y-automation.dev/automated-tools" rel="noopener noreferrer"&gt;https://a11y-automation.dev/automated-tools&lt;/a&gt; —a comprehensive list of automated tools for a11y testing.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.w3.org/WAI/ER/tools/" rel="noopener noreferrer"&gt;https://www.w3.org/WAI/ER/tools/&lt;/a&gt; — a list of evaluation tools for web accessibility as curated by W3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stay tuned for part 3 of this series where we will take a look at the popular "accessibility first" &lt;a href="https://testing-library.com/" rel="noopener noreferrer"&gt;Testing Library&lt;/a&gt; framework and start to explore the emerging field of screen reader automation tooling. See you soon! 👋&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Hi, my name is &lt;a href="https://twitter.com/CraigMorten" rel="noopener noreferrer"&gt;Craig Morten&lt;/a&gt;. I am a senior product engineer at the John Lewis Partnership. When I’m not hunched over my laptop I can be found drinking excessive amounts of tea or running around in circles at my local athletics track.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The John Lewis Partnership are hiring across a range of roles. If you love web development and are excited by accessibility, performance, SEO, and all the other awesome tech in web, we would love to hear from you! See our open positions &lt;a href="https://www.jlpjobs.com/engineering-jobs/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Eliminating CLS when using SSR for viewport specific responsive designs</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Tue, 04 Jul 2023 09:30:06 +0000</pubDate>
      <link>https://dev.to/craigmorten/eliminating-cls-when-using-ssr-for-viewport-specific-responsive-designs-2af1</link>
      <guid>https://dev.to/craigmorten/eliminating-cls-when-using-ssr-for-viewport-specific-responsive-designs-2af1</guid>
      <description>&lt;p&gt;A common gotcha when building some responsive sites is that the desired designs for different viewports are sometimes quite different. In many cases, utilising &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries" rel="noopener noreferrer"&gt;CSS media queries&lt;/a&gt; is sufficient to be able to cater for these differences, but there are occasions where this isn’t necessarily sufficient on it’s own. It is also not always a pragmatic approach to refuse to implement such designs!&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%2Fcdn-images-1.medium.com%2Fmax%2F10944%2F0%2AzXsicteUz8C1LnsK" 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%2Fcdn-images-1.medium.com%2Fmax%2F10944%2F0%2AzXsicteUz8C1LnsK" alt="A stack of engineering books, a phone, and a plant. The top book is titled  “Stunning CSS”." width="799" height="533"&gt;&lt;/a&gt; &lt;em&gt;Photo by &lt;a href="https://unsplash.com/@kobuagency?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;KOBU Agency&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS only gets you so far
&lt;/h2&gt;

&lt;p&gt;For example, take the following markup for a ficticious article component where on desktop information about the author is contained within a slidedown from the top of the component, and on mobile it is inside a static section at the bottom:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleAuthorInformationDesktop&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleImage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleHeading&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticlePreviewText&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleAuthorInformationMobile&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now if the markup used for the “top” desktop and the “bottom” mobile author information components were exactly the same you could instead opt to make use of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout" rel="noopener noreferrer"&gt;CCS grid&lt;/a&gt; or the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Ordering_flex_items" rel="noopener noreferrer"&gt;CSS flexbox order&lt;/a&gt; to simply have the component once in markup, and use CSS media queries to set the appropriate CSS rule to position the component as desired for the different viewports.&lt;/p&gt;

&lt;p&gt;However, this comes with caveats:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Use of the order property has exactly the same implications for accessibility as changing the direction with flex-direction. Using order changes the order in which items are painted, and the order in which they appear visually. It does not change the sequential navigation order of the items. Therefore if a user is tabbing between the items, they could find themselves jumping around your layout in a very confusing way.” — &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Ordering_flex_items#the_order_property_and_accessibility" rel="noopener noreferrer"&gt;https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_flexible_box_layout/Ordering_flex_items#the_order_property_and_accessibility&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If we were to be clever here and try to achieve the desired differences in layout entirely with CSS, we would be introducing potential accessibility (a11y) issues as the experience for visual and non-visual users would differ.&lt;/p&gt;

&lt;p&gt;In theory this can be worked around with either clever use of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex" rel="noopener noreferrer"&gt;tabindex&lt;/a&gt; and / or &lt;a href="https://www.w3.org/TR/wai-aria-1.2/#aria-owns" rel="noopener noreferrer"&gt;aria-owns&lt;/a&gt; where in either case you can identify the order in which elements are expected to be traversed — however both cases are almost always strongly advised against. It is risky to try to hijack the default tab order of a page, and &lt;a href="https://a11ysupport.io/tech/aria/aria-owns_attribute" rel="noopener noreferrer"&gt;aria-owns is not supported by VoiceOver&lt;/a&gt; meaning it’s use would only resolve the issue for non fruit related devices. Both options are incredibly fragile to changes to associated components where there is a real maintenance overhead to ensuring this is never regressed.&lt;/p&gt;

&lt;p&gt;This also all falls apart if the desktop and mobile versions of the component need to be completely different, e.g. a slidedown vs just a static content section — you can’t use CSS to change HTML markup! This means you will almost certainly be needing different components and then either not rendering or hiding the one that doesn’t fit the viewport.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-side rethinkings
&lt;/h2&gt;

&lt;p&gt;This is fine right? Well, it is until you start server-side rendering (SSR) this article component. The server doesn’t have a concept of a viewport out of the box, so there is no way to know which of the two variations will need to be in the HTML response that you’re sending to your users’ browsers.&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%2Fcdn-images-1.medium.com%2Fmax%2F10396%2F0%2AX2PBBZt9RT5YC6kK" 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%2Fcdn-images-1.medium.com%2Fmax%2F10396%2F0%2AX2PBBZt9RT5YC6kK" alt="Laptop with a CSS file opened in a code editor." width="5198" height="3465"&gt;&lt;/a&gt; &lt;em&gt;Photo by &lt;a href="https://unsplash.com/@jantined?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Jantine Doornbos&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the absence of viewport details on the server when rendering, this means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You can guess (ahem, I mean “adopt mobile-first”), but this will inevitably result in some sort of &lt;a href="https://en.wikipedia.org/wiki/Flash_of_unstyled_content" rel="noopener noreferrer"&gt;flash of unstyled content (FOUC)&lt;/a&gt; or &lt;a href="https://web.dev/cls/" rel="noopener noreferrer"&gt;cumulative layout shift (CLS)&lt;/a&gt; whenever you guess wrong, which is a bit naff!&lt;/li&gt;
&lt;li&gt;You can choose to not render either server-side, but then you will almost definitely get a CLS when the component is hydrated client-side, or best case it will pop in late which is annoying.&lt;/li&gt;
&lt;li&gt;You can choose to render both, but then there’s the same issues as above where one of the variations is there on page load and then get’s removed shortly after either looking visually poor and potentially causing a CLS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This isn’t necessarily a new nor unsolved problem. Aside from the options above, one idea is to make use of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Client_hints" rel="noopener noreferrer"&gt;Client Hints&lt;/a&gt; to provide your server with context of where this component will be rendered so an appropriate choice can be made. Alternatively a similar flow can be achieved with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent" rel="noopener noreferrer"&gt;“user-agent sniffing”&lt;/a&gt; through packages such as &lt;a href="https://github.com/faisalman/ua-parser-js" rel="noopener noreferrer"&gt;ua-parse-js&lt;/a&gt; where you detect the user-agent header and from that deduce a likely device type and size.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Be sure to take care that you include the source of the hint in any cache-keys you might have otherwise you might cache poison if you render differently for different viewports!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Lastly, perhaps my preferred solution is (1) to use one of the above hint techniques if possible (and reliable) and then (2) do the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you have a trusted hint then render only what is needed in your HTML and stop here.&lt;/li&gt;
&lt;li&gt;Otherwise server-side render both variations of the component in your HTML response.&lt;/li&gt;
&lt;li&gt;In addition to this double render, add a small snippet of CSS and appropriate styles / classes to the components with CSS media queries ensuring that client-side they are only displayed if the viewport matches.&lt;/li&gt;
&lt;li&gt;Upon hydration you can let JS take over, remove the component from the DOM which isn’t required for the viewport and strip the “SSR smoothing over CSS” attributes as they are no longer required.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to see an example implementation of this idea check out the &lt;a href="https://www.npmjs.com/package/@artsy/fresnel" rel="noopener noreferrer"&gt;@artsy/fresnel&lt;/a&gt; package (and I’m sure there are others).&lt;/p&gt;

&lt;h2&gt;
  
  
  How can I do this myself?
&lt;/h2&gt;

&lt;p&gt;It’s not always appropriate to start pulling in more packages to achieve a goal, certainly if you’re keeping performance in mind (with the likes of &lt;a href="https://bundlephobia.com/" rel="noopener noreferrer"&gt;bundlephobia&lt;/a&gt;), and said package may be doing more than you need.&lt;/p&gt;

&lt;p&gt;I was in this situation recently — the project was already using &lt;a href="https://www.npmjs.com/package/react-responsive" rel="noopener noreferrer"&gt;react-responsive&lt;/a&gt;, a wrapper around the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia" rel="noopener noreferrer"&gt;matchMedia API&lt;/a&gt; with some server-side options for fallbacks, which is only useful if there is a sensible default (uncommon) or if you are using hints to be able to pass through to the package. This doesn’t do enough as I didn’t have hints to work with and pass to the package, so more work was needed to enable CLS-less SSR.&lt;/p&gt;

&lt;p&gt;Here’s the first pass solution I settled on for my use-case (subject to change, and mileage may vary):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useMediaQuery&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-responsive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BreakpointWidth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./breakpoints&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./index.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;SSRMediaQueryProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;BreakpointWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;BreakpointWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DisplayValueProps&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;SSRMediaQueryProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BreakpointWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getDisplayValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;DisplayValueProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;breakpoint&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;breakpoint&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;breakpoint&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;breakpoint&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * This component serves to allow viewport specific components be server-side
 * rendered without causing a CLS.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SSRMediaQuery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SSRMediaQueryProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isClientSide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsClientSide&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMediaQuery&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setIsClientSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setIsClientSide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isClientSide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--xs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getDisplayValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;xs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--sm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getDisplayValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getDisplayValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--lg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getDisplayValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--xl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;getDisplayValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;breakpoint&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;xl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minWidth&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;as &lt;/span&gt;&lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CSSProperties&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaQueryContainer&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the imported CSS file is roughly (I’m using SCSS mixins to avoid writing long-hand!):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="c"&gt;/* index.css */&lt;/span&gt;

&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;543px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.mediaQueryContainer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--xs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;544px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;767px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.mediaQueryContainer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--sm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;768px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;991px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.mediaQueryContainer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--md&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;992px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1199px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.mediaQueryContainer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--lg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1200px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.mediaQueryContainer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="l"&gt;--xl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the usage is something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SSRMediaQuery&lt;/span&gt; &lt;span class="na"&gt;minWidth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleAuthorInformationDesktop&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SSRMediaQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleImage&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleHeading&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticlePreviewText&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SSRMediaQuery&lt;/span&gt; &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;BREAKPOINTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sm&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ArticleAuthorInformationMobile&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SSRMediaQuery&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking through the implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We first assume that we are always in a server-side or pre-hydration world until an effect (which only runs client-side) tells us otherwise.&lt;/li&gt;
&lt;li&gt;In this server-side world we always render the provided children, and furthermore, do so with them wrapped in a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; that has appropriate styles associated with it to ensure it is only displayed to users for on the desired viewports. Here I was feeling fancy so made use of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties" rel="noopener noreferrer"&gt;CSS variables&lt;/a&gt; to determine how the display value is set for the wrapper &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;, defaulting to unset if no value is passed.&lt;/li&gt;
&lt;li&gt;Once we hit the client we can be safe in the knowledge that the CSS media queries on the wrapper &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; will kick in meaning that although we’ve taken a slight performance hit shipping both variations of component, we don’t suffer from and visual quirks or CLS.&lt;/li&gt;
&lt;li&gt;Post hydration we’ll still be in a state where both variations exist — indeed both variations will render on first pass so care is needed to make sure your components don’t have “onload / onmount” like side effects which could “double fire”. (For further reading on why we do this double pass see &lt;a href="https://github.com/facebook/react/issues/23381#issuecomment-1079494600" rel="noopener noreferrer"&gt;this GitHub issue&lt;/a&gt;. There is a better alternative which is more involved that I’m not covering here – this uses suspense boundaries and manual pruning of the DOM to remove the unwanted tree prior to hydration).&lt;/li&gt;
&lt;li&gt;Following the first render the effect will fire, this will update the state to switch to the client-side mode. This triggers a re-render where we either continue to render the children as there is a viewport match, or nothing otherwise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And just like that we’re free of CLS and can gracefully handle this kind of markup difference between different viewports. 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Parting thoughts
&lt;/h2&gt;

&lt;p&gt;The unfortunate state is that there is no perfect answer here, just trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get exactly what you want, but having to trust client hints which might not always be there... so maybe only sometimes getting what you want?&lt;/li&gt;
&lt;li&gt;Have faster &lt;a href="https://web.dev/ttfb/" rel="noopener noreferrer"&gt;time to first byte (TTFB)&lt;/a&gt; and other similar page load metrics at the cost of having an annoying CLS vs almost certainly having CLS at the cost of page weight bloat slower that first impression.&lt;/li&gt;
&lt;li&gt;Potential search engine optimisation (SEO) gotchas as a result of double rendering content (although it appears generally accepted that you shouldn’t get penalised for this kind of behaviour) coupled with the performance issues above for &lt;a href="https://web.dev/vitals/" rel="noopener noreferrer"&gt;core web vitals&lt;/a&gt; having a direct impact on SEO.&lt;/li&gt;
&lt;li&gt;Care still need for side-effects in SSR double rendered components (unless you put in more work).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Always evaluate what works for your use-case!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you’re in an SPA world where you only client-render, this article was not for you!&lt;/li&gt;
&lt;li&gt;If you are in a SSR world but the component that can be rendered differently is &lt;em&gt;only&lt;/em&gt; rendered upon customer interaction or some other lazy or deferred trigger, then you don’t need to worry — just use client-side techniques.&lt;/li&gt;
&lt;li&gt;If you’re in a SSR world, maybe consider some of the techniques discussed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hopefully this can save some research and thoughts for others in future. If you have any ideas, suggestions, or great ways (or packages) you use to solve this problem I’d love to hear it in the comments!&lt;/p&gt;

&lt;p&gt;Like what I have to say? Consider a follow here or on &lt;a href="https://twitter.com/CraigMorten" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Automating a11y testing: Part 1 — Axe</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Fri, 23 Jun 2023 13:38:27 +0000</pubDate>
      <link>https://dev.to/craigmorten/automating-a11y-testing-part-1-axe-1mn1</link>
      <guid>https://dev.to/craigmorten/automating-a11y-testing-part-1-axe-1mn1</guid>
      <description>&lt;p&gt;Earlier this year I gave a talk at the John Lewis Partnership Tech Profession Conference 2023 on the topic of automating web accessibility (a11y) testing.&lt;/p&gt;

&lt;p&gt;In this series I will cover the contents of this talk, kicking off with insights into the Axe suite of tools, followed by articles covering the wider a11y testing landscape: from visual regression testing for magnification and visual deficiencies, to emerging technology in the screen reader automation space.&lt;/p&gt;

&lt;h2&gt;
  
  
  I am not the user
&lt;/h2&gt;

&lt;p&gt;Before going any futher I’m keen to express a disclaimer for this series: "I am not the user"!&lt;/p&gt;

&lt;p&gt;I don’t have any training beyond "on the job" learnings, I am not a user of assistive technology in my day to day life outside of QA, and other than a mild astigmatism I consider myself able-bodied and neurotypical. Somewhat brings forth imposter syndrome when writing on the topic of a11y testing! Nevertheless, it’s something I’m passionate about.&lt;/p&gt;

&lt;p&gt;Throughout this series I will be mostly focusing on the automation side of a11y testing, but it is also key to remember the importance of manual validation and exploratory testing. At the end of the day it is &lt;em&gt;real people&lt;/em&gt; that are visiting and using your site, so it is incredibly important to ensure the quality of their experience, not just to tickbox that automation has passed. In fact, current tooling can &lt;a href="https://karlgroves.com/web-accessibility-testing-what-can-be-tested-and-how/" rel="noopener noreferrer"&gt;only cover around 25% of WCAG requirements&lt;/a&gt; (granted an old article, but still holds true!) and although later in this series we will explore some emerging technology to start pushing that coverage higher, there are still numerous aspects of the customer experience that can only be tested by a real person.&lt;/p&gt;

&lt;p&gt;With that all covered off, let’s have a wander through the world of automated web accessibility testing!&lt;/p&gt;

&lt;h2&gt;
  
  
  Current state of play
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Axe
&lt;/h3&gt;

&lt;p&gt;It wouldn’t be an article about web accessibility testing if the Axe suite by Deque wasn’t mentioned. Putting simply, this tooling has somewhat of a monopoly at the moment on accessibility automation, and for good reason.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The axe family of tools help you check for digital accessibility" — &lt;a href="https://www.deque.com/axe/" rel="noopener noreferrer"&gt;https://www.deque.com/axe/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Outside of very specific, dedicated tools it has the best coverage out there, good APIs and interfaces, and although I think the technical documentation could be better (likely personal taste — it always ends up being there, just not structured how I expect), it is far from bad and is made up for by a wealth of community tutorials, articles, and repo examples.&lt;/p&gt;

&lt;p&gt;Beyond their &lt;a href="https://www.npmjs.com/package/axe-core" rel="noopener noreferrer"&gt;axe-core&lt;/a&gt; package Deque also maintain a number of dedicated packages for the majority of popular frameworks in the web test automation space, namely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@axe-core/playwright" rel="noopener noreferrer"&gt;@axe-core/playwright&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@axe-core/puppeteer" rel="noopener noreferrer"&gt;@axe-core/puppeteer&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@axe-core/webdriverjs" rel="noopener noreferrer"&gt;@axe-core/webdriverjs&lt;/a&gt; (think Selenium)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@axe-core/webdriverio" rel="noopener noreferrer"&gt;@axe-core/webdriverio&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@axe-core/react" rel="noopener noreferrer"&gt;@axe-core/react&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To give you a flavour, here is a snippet of how you might use the Playwright integration to perform static analysis on the live Waitrose Groceries site to access the accessibility of the primary navigation menu, lovingly referred to as the "mega menu":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;AxeBuilder&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@axe-core/playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mega menu should not have a11y violations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.waitrose.com/ecom/shop/browse/groceries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Groceries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#megamenu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;waitFor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessibilityScanResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AxeBuilder&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#megamenu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;accessibilityScanResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hopefully a very standard looking test — we navigate to the desired page, interact with the Groceries dropdown button, and wait for the mega menu to be visible. The Axe part is then remarkably straight-forward to get started with — we construct a new AxeBuilder, tell it to inspect the mega menu element tree, and run the analysis. Across the other framework packages the API usage is very similar.&lt;/p&gt;

&lt;h3&gt;
  
  
  cypress-axe
&lt;/h3&gt;

&lt;p&gt;Now the eagle eyed of you will have noticed there was a popular framework missing from the list of Deque owned packages for Axe — Cypress!&lt;/p&gt;

&lt;p&gt;Don’t worry, there is a really good community package to support Cypress as well in the form of the &lt;a href="https://www.npmjs.com/package/cypress-axe" rel="noopener noreferrer"&gt;cypress-axe&lt;/a&gt; package.&lt;/p&gt;

&lt;p&gt;The interface is a little different to how you use the other Axe packages, but personally I almost prefer the style which works quite naturally with the Cypress API style. Let’s see how we might test the accessibility of a John Lewis Search Product Listing Page (PLP):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PLP Search A11y Violations&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.johnlewis.com/search?search-term=vans&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;injectAxe&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configureAxe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AXE_CONFIG_OPTIONS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;should not have a11y violations on load&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkA11y&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SELECTOR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;AXE_RUN_OPTIONS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As is often the case with Cypress, the feedback loop from &lt;a href="https://www.npmjs.com/package/cypress-axe" rel="noopener noreferrer"&gt;cypress-axe&lt;/a&gt; is also up there with clear logging explaining the issues when you are using the Cypress UI.&lt;/p&gt;

&lt;p&gt;For example, in this failed test we can see the error description, some guidance, a URL for more information, and if we were to drill into the Nodes array it would give us pointers to the exact elements that are in violation of WCAG.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6a43wd3fbeqwyrwzlxqq.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6a43wd3fbeqwyrwzlxqq.png" alt="Cypress UI with a failed test for " width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pa11y
&lt;/h3&gt;

&lt;p&gt;So what if you don’t currently use one of the aforementioned frameworks for your site?&lt;/p&gt;

&lt;p&gt;In cases where you don’t have a setup ready to plug and play with one of the previous packages, and perhaps you’ve also not got the knowledge or resource to spend building up a suite using one of those frameworks (though these days the developer experience for setting up is pretty good!), then I would recommend taking a peek at &lt;a href="https://github.com/pa11y/pa11y" rel="noopener noreferrer"&gt;Pa11y&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Pa11y is your automated accessibility testing pal. It runs accessibility tests on your pages via the command line or Node.js, so you can automate your testing process." — &lt;a href="https://pa11y.org/" rel="noopener noreferrer"&gt;https://pa11y.org/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The nice thing about Pa11y is you can run it straight from the command line making it natural to use for scripting, or for simple smoke checks in CI. It also ships a Node package so you can write a quick test in JavaScript or Typescript. Because it is a "wrapper" you can also benefit from it’s dual coverage utilising both Axe and &lt;a href="https://squizlabs.github.io/HTML_CodeSniffer/" rel="noopener noreferrer"&gt;HTML Code Sniffer (htmlcs)&lt;/a&gt;, another static analysis tool. The documentation for Pa11y is also &lt;em&gt;really&lt;/em&gt; good.&lt;/p&gt;

&lt;p&gt;Let’s take a look at an example setup to test the page accessibility with the mega menu open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;pa11y&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pa11y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_PA11Y_CONFIGURATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;runners&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;htmlcs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WCAG2AAA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runPa11yTest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pa11y&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.waitrose.com/ecom/shop/browse/groceries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click element #drop-down-nav&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wait for element #megamenu to be visible&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;BASE_PA11Y_CONFIGURATION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;runPa11yTest&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pa11y’s actions API for perfoming customer interactions is a little restricted, but it does has you covered for basic behaviours like clicking, typing, and waiting for something to be visible. Because it uses English based instructions, it has a similar feel to using BDD testing libraries (e.g. think Gherkin syntax for Cucumber) which lowers the barrier to entry for developers or even non-developers to easily tweak or extend the scope of tests — caveated of course if English is not your first language!&lt;/p&gt;

&lt;p&gt;Unfortunately the style of this actions API does mean the tool doesn’t sit well within other frameworks — if you’re trying to use Playwright or Cypress to first act on the page for setup it will just get ignored. What Pa11y does under the hood is use Puppeteer to spin up it’s own Chrome tab to instrument and act out the instructions and a11y testing, so anything set up outside of Pa11y’s actions array will effectively be ignored.&lt;/p&gt;

&lt;p&gt;But if you’re performing integration testing through the likes of Jest, Vitest, Mocha, etc., i.e. a testing framework that doesn’t support it’s own browser instrumentation, this can be a nice fit to extend a suite of tests to include a11y coverage.&lt;/p&gt;

&lt;p&gt;In summary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Conveniently wraps both Axe and htmlcs&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Really good documentation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simple to use action, though somewhat restricted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Doesn’t play so well with others&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Axe browser extension
&lt;/h3&gt;

&lt;p&gt;Moving out of what some folks might strictly class as test automation, it’s worth discussing some of the other tooling in the Axe family.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdb7zo3t8wiyrxurad59.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffdb7zo3t8wiyrxurad59.png" alt="John Lewis website open on the   coffee table product listing page in Chrome. The developer tools pane is open on the axe DevTools tab showing the overview of an Axe scan. The report lists 4 accessibility issues in a list of expandable sections, with one for " width="799" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To complement the Axe packages, Deque also have a really nice &lt;a href="https://chrome.google.com/webstore/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd" rel="noopener noreferrer"&gt;Axe browser extension&lt;/a&gt;. Some features are premium, but the "scan all" feature meets most, if not all, needs for supplementing exploratory testing with a degree of automation, taking the heavy lifting away from manually inspecting HTML, colours, etc. to try and work out if components are compliant.&lt;/p&gt;

&lt;h3&gt;
  
  
  Axe Linter
&lt;/h3&gt;

&lt;p&gt;There is also a VSCode plugin for an &lt;a href="https://marketplace.visualstudio.com/items?itemName=deque-systems.vscode-axe-linter" rel="noopener noreferrer"&gt;Axe Linter&lt;/a&gt; which can be a useful tool in proactively writing accessible code write at the early development stage — eliminating the slower feedback loop of only finding violations at your integration test stage in CI say.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9cp2jfh4w24gen9tk3h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv9cp2jfh4w24gen9tk3h.png" alt="A VSCode window with code for a React based Image component. The cursor is hovered over the img element JSX which doesn't have an alt prop. The component has a red error underline and a hover tooltip which reads: " width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lighthouse
&lt;/h3&gt;

&lt;p&gt;Lastly something that hopefully might be familiar to most readers!&lt;/p&gt;

&lt;p&gt;If you’ve ever used the Accessibility reporting feature of Google Lighthouse, it uses Axe under the hood to drive the tests and provide the nicely displayed violation information.&lt;/p&gt;

&lt;p&gt;This is not just the case for the Chrome DevTools instance of Lighthouse — the &lt;a href="https://chrome.google.com/webstore/detail/lighthouse/blipmdconlkpinefehnmjammfjpmpbjk" rel="noopener noreferrer"&gt;browser extension&lt;/a&gt;, &lt;a href="https://github.com/GoogleChrome/lighthouse" rel="noopener noreferrer"&gt;CLI, and Node package&lt;/a&gt; all make use of Axe for the accessibility score and reporting features.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmz53fcf72pbcd5b83k14.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmz53fcf72pbcd5b83k14.png" alt="Waitrose Groceries favourites page open in Chrome using iOS mobile simulation. The DevTools panel is open on the Lighthouse tab displaying an Accessibility score for the page. WCAG violations are listed with descriptions of the issue, and references to the impacted elements with screenshots highlighting the elements." width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are also some nice packages that wrap Lighthouse for easy use in test automation — for example, the &lt;a href="https://www.npmjs.com/package/cypress-audit" rel="noopener noreferrer"&gt;cypress-audit&lt;/a&gt; package for Cypress worthy of a mention for it’s easy to use &lt;a href="https://github.com/mfrachet/cypress-audit/blob/HEAD/docs/lighthouse.md" rel="noopener noreferrer"&gt;cy.lighthouse()&lt;/a&gt; command which can also be used for performance budget tests (it also has a &lt;a href="https://github.com/mfrachet/cypress-audit/blob/HEAD/docs/pa11y.md" rel="noopener noreferrer"&gt;cy.pa11y()&lt;/a&gt; command so you can kick off Pa11y tests if you prefer that interface instead).&lt;/p&gt;

&lt;h2&gt;
  
  
  Axe lessons
&lt;/h2&gt;

&lt;p&gt;Having realed off and recommended a number of Axe tools, it’s worth pointing out some of the gotcha’s that I’ve experienced with them over the years.&lt;/p&gt;

&lt;h3&gt;
  
  
  React Axe issues
&lt;/h3&gt;

&lt;p&gt;A big lesson for me has been around &lt;a href="https://www.npmjs.com/package/@axe-core/react" rel="noopener noreferrer"&gt;@axe-core/react&lt;/a&gt;. Initially I was very much in favour in the idea of having a tool that can instrument React and report issues to the console while running apps locally in development.&lt;/p&gt;

&lt;p&gt;However... for any application of any size this package can massive tank performance which can make local development painful and slow — especially if you are running a hot module reloading (HMR) setup where every small tweak triggers a hot reload and a potential new Axe scan of the page. If you are using it currently and ever wonder why animations are super laggy and page updates look jarring, then React Axe is a real possibility as to why. Obviously this is my experience working on large applications, mileage may vary!&lt;/p&gt;

&lt;p&gt;My personal recommendation is to scrap React Axe and instead reach for the Axe browser extension instead — you don’t get "live" reports to the console, but the flow is fairly natural and is a definite improvement for developer experience. The UI and level of detail you get on the extension is also far superior to the red text you get in the console scattered amongst everything else that is getting logged.&lt;/p&gt;

&lt;h3&gt;
  
  
  Infallible?
&lt;/h3&gt;

&lt;p&gt;It’s also worth calling out that Axe isn’t perfect — it can occasionally have quite annoying false positives.&lt;/p&gt;

&lt;p&gt;The most common case I’ve found is in it’s reporting of colour contrast, specifically when doing anything complex with either nested elements or using pseudo-elements.&lt;/p&gt;

&lt;p&gt;For example, imagine you have a setup (admittedly contrived, but funnily enough I have experienced this in a side menu implementation!) where there is an element with a green background and then a number of nested transparent that cover the parent element. Above these is then say an absolutely positioned element, or CSS "Z translated" element, or perhaps an element from an entirely different part of the DOM positioned also above the green element and it’s covering children. Say this too has some nested children — the top layer of which has some white text.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgar4di78taf3cczvyf8q.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgar4di78taf3cczvyf8q.png" width="800" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The white text is above the green background, so this is WCAG AA compliant but the element containing the text is sufficiently removed from the background element that Axe fails to recognise which elements (and colours) are involved in the contrast comparison and fails with a violation.&lt;/p&gt;

&lt;p&gt;The trick here is to keep it simple — if you need to ensure contrast just apply the appropriate background (or equivalent) to the element with the text. Yes, this is somewhat fixing a non-issue due to tooling limitations, but it is slightly less fragile to changes in your page markup implementation — in future someone might refactor to reduce the complexity of this component, and having the element satisfy the necessarily colour contrast requirements itself rather than relying on some other element will save future rework getting things compliant again in such a refactor. As the principle says, "things that move together should sit together".&lt;/p&gt;

&lt;p&gt;There is also another lesson here — if you’re seeing false positives reported from Axe you’re probably doing something off in your markup. There are few scenarios that warrant such a complex element layout so Axe failing is probably a good hint that you’re in need of simplifying your HTML. Even if you are able to successfully satisfy the Axe requirement, it is very likely with such a layout that you will have potential issues with other accessibility requirements —what about users who make use of 2x zoom, or assistive technologies like magnifiers and screen readers? There is a real risk with something like above behaving poorly for some of these tools, e.g. screen readers are known for not handling non-semantic deeply nested markup very well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Friction in WIP projects
&lt;/h3&gt;

&lt;p&gt;Unless you’re in the rare position of starting from scratch with a greenfield project, the chances are you may well be pulling something like Axe into an existing project for a site that isn’t all that accessible at the moment, as a means to start improving the situation.&lt;/p&gt;

&lt;p&gt;The thing to be aware of here is Axe’s limitations on ignoring violations, a capability that is often needed as a practical measure when you are looking to progressively improve a site’s accessibility which has a number of existing violations.&lt;/p&gt;

&lt;p&gt;Of course ideally we don’t want to be ignoring anything, but a nice flow when working on such a codebase is to always create a ticket, and then add ignore rules named after or commented with the ticket reference and start ticking these off.&lt;/p&gt;

&lt;p&gt;The frustrating thing with Axe ignores is that you can’t ignore a specific rule for a specific element. Unfortunately you can either ignore all rules for an element, or you can ignore all cases of a rule. The consequence is that often you will be over-ignoring elements or rules until you have cleared the decks of most or all violations, meaning until then you’re not really getting the protection from regression that you might hope for — folks adding new code may well introduce further violations of a similar type and Axe starts ignoring these as well!&lt;/p&gt;

&lt;p&gt;What I recommend here is set up your tests from the start with sensible per test configuration and avoid having a single global configuration (though for DRY, it might be some common config is centralised). This way you can narrow the exclusions just for specific tests, e.g. say the contrast is only inaccessible when you toggle a button, you don’t want to ignore that button or the contrast rule for all other tests and scenarios in your suite!&lt;/p&gt;

&lt;p&gt;Another technique that I’ve seen employed to reasonable effect is to introduce attributes to elements that you want to ignore, for example data-axe-ignore="true", and use this as a way to target elements that you need to temporarily ignore (through a selector such as [data-axe-ignore="true"]) in order to get CI up and running with Axe tests. Once you fix the violation, just remember to remove the data attribute! This does have the downside of polluting production markup unless you have steps to purge the attributes, so there are trade-offs here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing notes
&lt;/h2&gt;

&lt;p&gt;In this article I’ve covered off the majority of the Axe suite, if you want to find out more or explore some of Deque’s other offerings check out their site at &lt;a href="https://www.deque.com/axe/" rel="noopener noreferrer"&gt;https://www.deque.com/axe/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In part 2 of this series I will cover a number of non-Axe tools and techniques for a11y test automation. See you there! 👋&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Hi, my name is &lt;a href="https://twitter.com/CraigMorten" rel="noopener noreferrer"&gt;Craig Morten&lt;/a&gt;. I am a senior product engineer at the John Lewis Partnership. When I’m not hunched over my laptop I can be found drinking excessive amounts of tea or running around in circles at my local athletics track.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The John Lewis Partnership are hiring across a range of roles. If you love web development and are excited by accessibility, performance, SEO, and all the other awesome tech in web, we would love to hear from you! See our open positions &lt;a href="https://www.jlpjobs.com/engineering-jobs/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>a11y</category>
      <category>webdev</category>
      <category>testing</category>
      <category>codenewbie</category>
    </item>
    <item>
      <title>"Reducing" a cryptic memory leak in Production</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Sun, 19 Mar 2023 21:58:47 +0000</pubDate>
      <link>https://dev.to/craigmorten/reducing-a-cryptic-memory-leak-237o</link>
      <guid>https://dev.to/craigmorten/reducing-a-cryptic-memory-leak-237o</guid>
      <description>&lt;p&gt;I recently came across an interesting memory leak scenario which caught me off-guard, let’s see if you can spot the issue before the reveal at the end?&lt;/p&gt;

&lt;p&gt;Below is some simplified replica code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getSponsored&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getRecommendations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getProducts&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./fetchers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mergeResponses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/products&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;responses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;getSponsored&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;getRecommendations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;mergeResponses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The functionality is relatively straight-forward — we have a &lt;code&gt;/products&lt;/code&gt; endpoint when is getting some sponsored, recommended, and normal products, merging parts of the results, and returning this aggregation as the response.&lt;/p&gt;

&lt;p&gt;Now the use of &lt;code&gt;Array.prototype.reduce()&lt;/code&gt; for the response merge is curious — we’re concatenating arrays so a simple for loop or &lt;code&gt;.forEach()&lt;/code&gt; feels more appropriate, but hey for small responses at reasonably low RPS this is micro-optimisation, nothing to get too hung up on!&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%2Fcdn-images-1.medium.com%2Fmax%2F12000%2F0%2Alm55U33s-VhmQDyN" 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%2Fcdn-images-1.medium.com%2Fmax%2F12000%2F0%2Alm55U33s-VhmQDyN" alt="MacBook with VSCode open on an Express JS file." width="760" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Photo by &lt;a href="https://unsplash.com/pt-br/@clemhlrdt?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Clément Hélardot&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But yet running something similar in a production like environment it was apparent that something was off... monitoring showed that old heap size was growing linearly with the number of requests — we had a memory leak!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The memory heap is divided into two major spaces: the New space where new objects are allocated, and the Old space where older objects are moved to after surviving for some time having not been garbage collected.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Debugging a memory leak
&lt;/h2&gt;

&lt;p&gt;A straight-forward way to start an investigation into a Node memory leak when you can run the project locally (debugging in production is a different matter!) is to use the &lt;a href="https://nodejs.org/en/docs/guides/debugging-getting-started" rel="noopener noreferrer"&gt;Node inspector&lt;/a&gt;, this can be invoked easily through a flag when starting your Node application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node --inspect index.js

Debugger listening on ws://127.0.0.1:9229/2a7f05cd-96c8-4f6d-8c5f-adea987ab63b
For help, see: https://nodejs.org/en/docs/inspector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will prompt the Node process to listen for a debugging client which, once connected, allows you to instrument your application with breakpoints as well as resource metrics and analysis for CPU and memory.&lt;/p&gt;

&lt;p&gt;There a few options for debuggers — major IDEs like Visual Studio, Visual Studio Code, and JetBrains Webstorm all support the protocol making for easy debugging from within your editor. Here I’ll demonstrate an alternative, using Chrome DevTools.&lt;/p&gt;

&lt;p&gt;Once your Node process is running with the inspector, open up Chrome and navigate to &lt;code&gt;chrome://inspect&lt;/code&gt; in the URL address bar.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe33hua5yba6zg7qdq4py.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe33hua5yba6zg7qdq4py.png" alt="Chrome browser open on the chrome://inspect page, listing a target for Node v14.20.1 for index.js and an option to inspect." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here under the “Remote Target” section we can click on the “inspect” link to starting interrogating our &lt;code&gt;index.js&lt;/code&gt; Node process.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9znlaee98gy70944ptg.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx9znlaee98gy70944ptg.png" alt="Chrome DevTools window open on the Memory tab showing options for selecting a profiling type." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clicking this link pops open a new Chrome DevTools tab which is very similar to what you would get while debugging a page in the browser.&lt;/p&gt;

&lt;p&gt;On the Memory tab we can click the “Take snapshot” button while the “Heap snapshot” option is selected to record a snapshot in time of the memory heap. This will pop up on the left sidebar and generally will automatically open up.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F830lqiamkof5p9mkee21.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F830lqiamkof5p9mkee21.png" alt="Chrome DevTools heap snapshot listing rows of Constructors." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From this snapshot we can see all of the “constructors” (think primitives or objects) with columns for the distance from the &lt;code&gt;window&lt;/code&gt; object, the size of each object in bytes, and the retained size of the object in bytes (the size of the object and the graph it retains).&lt;/p&gt;

&lt;p&gt;This doesn’t yet give us enough information to easily assess where a memory leak might be, but can be interesting to peruse to understand the shape of your application’s memory.&lt;/p&gt;

&lt;p&gt;Now in our case the memory leak appeared to be correlated to the amount of traffic the server was receiving, so to simulate that we can use tools like autocannon to very easily generate some base level load on the server.&lt;/p&gt;

&lt;p&gt;Here was run one of three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx autocannon &lt;span class="nt"&gt;-c&lt;/span&gt; 100 http://localhost:3000/products 
&lt;span class="go"&gt;
Running 10s test @ http://localhost:3000/products
100 connections

    ┌─────────┬────────┬─────────┬─────────┬─────────┬───────────┬───────────┬─────────┐
    │ Stat    │ 2.5%   │ 50%     │ 97.5%   │ 99%     │ Avg       │ Stdev     │ Max     │
    ├─────────┼────────┼─────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
    │ Latency │ 158 ms │ 1232 ms │ 2860 ms │ 3440 ms │ 1296.6 ms │ 786.21 ms │ 5350 ms │
    └─────────┴────────┴─────────┴─────────┴─────────┴───────────┴───────────┴─────────┘
    ┌───────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
    │ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%   │ Avg     │ Stdev   │ Min     │
    ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
    │ Req/Sec   │ 27      │ 27      │ 46      │ 147     │ 61.8    │ 35.14   │ 27      │
    ├───────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┼─────────┤
    │ Bytes/Sec │ 34.4 MB │ 34.4 MB │ 60.6 MB │ 80.7 MB │ 60.7 MB │ 13.3 MB │ 34.4 MB │
    └───────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘

Req/Bytes counts sampled once per second.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;of samples: 10
&lt;span class="go"&gt;
718 requests in 10.09s, 607 MB read
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... and run three of three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npx autocannon &lt;span class="nt"&gt;-c&lt;/span&gt; 100 http://localhost:3000/products
&lt;span class="go"&gt;
Running 10s test @ http://localhost:3000/products
100 connections

    ┌─────────┬─────────┬─────────┬─────────┬─────────┬────────────┬────────────┬─────────┐
    │ Stat    │ 2.5%    │ 50%     │ 97.5%   │ 99%     │ Avg        │ Stdev      │ Max     │
    ├─────────┼─────────┼─────────┼─────────┼─────────┼────────────┼────────────┼─────────┤
    │ Latency │ 1554 ms │ 5400 ms │ 9049 ms │ 9189 ms │ 5389.29 ms │ 2438.84 ms │ 9314 ms │
    └─────────┴─────────┴─────────┴─────────┴─────────┴────────────┴────────────┴─────────┘
    ┌───────────┬─────┬──────┬─────────┬────────┬─────────┬─────────┬─────────┐
    │ Stat      │ 1%  │ 2.5% │ 50%     │ 97.5%  │ Avg     │ Stdev   │ Min     │
    ├───────────┼─────┼──────┼─────────┼────────┼─────────┼─────────┼─────────┤
    │ Req/Sec   │ 0   │ 0    │ 6       │ 36     │ 10.6    │ 11.62   │ 6       │
    ├───────────┼─────┼──────┼─────────┼────────┼─────────┼─────────┼─────────┤
    │ Bytes/Sec │ 0 B │ 0 B  │ 22.4 MB │ 128 MB │ 38.1 MB │ 41.5 MB │ 22.4 MB │
    └───────────┴─────┴──────┴─────────┴────────┴─────────┴─────────┴─────────┘

Req/Bytes counts sampled once per second.
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;of samples: 10
&lt;span class="go"&gt;
232 requests in 10.09s, 381 MB read
26 errors (26 timeouts)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see here that something is very definitely wrong — after only 3 batches of 10 second load periods serving 100 connections we’ve gone from an average latency of 1296.6 ms to 5389.29 ms, a degradation of over 4 seconds!&lt;/p&gt;

&lt;p&gt;Let’s take another heap snapshot using the “record” button at the top left of the Chrome DevTools Memory tab.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbl3p1d9ptna60tpaab69.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbl3p1d9ptna60tpaab69.png" alt="Chrome DevTools window open on the Memory tab with the record button highlighted on the top left of the Memory tab’s control panel. A second heap snapshot is open." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see that there certainly appears to be a lot more memory in play — the total snapshot size (listed under the snapshot name on the left sidebar) has increased from 5.6 MB to 24.7 MB and there are some large numbers listed for shallow and retained size.&lt;/p&gt;

&lt;p&gt;However hunting blind in any one snapshot doesn’t get you very far typically, where the DevTools comes into it’s own is with the comparison options.&lt;/p&gt;

&lt;p&gt;The last option in the Memory Tab’s control panel is a dropdown which will state “All objects” by default. Clicking on this dropdown gives you a number of options to compare between snapshots.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9b4r62qzsmfu7eldzzx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn9b4r62qzsmfu7eldzzx.png" alt="Chrome DevTools window open on a heap snapshot, with the comparison dropdown open in the Memory Tab control panel. Listing options: All objects, Objects allocated before Snapshot 1, Objects allocated between Snapshot 1 and Snapshot 2." width="799" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we select the “Objects allocated between Snapshot 1 and Snapshot 2” option to show us the delta between the two snapshots.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b98t6woruabay10vcg0.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3b98t6woruabay10vcg0.png" alt="Chrome DevTools window showing the difference between Snapshot 1 and Snapshot 2" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This immediately reduces the number of constructors in play when compared with the long lists for the individual snapshots — anything that hasn’t changed between the snapshots hasn’t been listed, removing a lot of the noise from the view.&lt;/p&gt;

&lt;p&gt;Now comes the interesting part — finding the source of the leak! We start by sorting the list by retained size (typically the default) and navigating down the list of constructors one by one. Here we expand the &lt;code&gt;(string)&lt;/code&gt; options first and take a look:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfaotz4bzsgdmj22h56m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flfaotz4bzsgdmj22h56m.png" alt="Chrome DevTools window with the strings memory expanded." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There’s nothing of interest here — we can tell from the small shallow and retained sizes of each of the strings listed that the issue doesn’t lie here. Although the combined size of each string does make for the largest sized collection, the size associated with each string is very small.&lt;/p&gt;

&lt;p&gt;Intuitively we’re also not expecting a string to be the primary cause of our problem — nowhere in our logic are we growing a string (perhaps if we were doing some sort of string concatination, but we’re not). It’s apparent that a lot of these strings are likely long-lived constants, we likely took our first snapshot too early — if we were to take a third snapshot they would like be removed from the comparison view now the server has had some requests.&lt;/p&gt;

&lt;p&gt;Moving onto the second constructor we can see something a little more interesting:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4zinhtx6xyde7aondfm.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv4zinhtx6xyde7aondfm.png" alt="Chrome DevTools window with the arrays memory expanded." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here the shallow and retained sizes for the top three children are particularly large considering this is a delta!&lt;/p&gt;

&lt;p&gt;Clicking on the first &lt;code&gt;(object elements)[]&lt;/code&gt; row we can populate the “Retainers” bottom panel with detailed information on the object in question. This immediately points to a suspect:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyr3egfh6u94em2o7c96.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyr3egfh6u94em2o7c96.png" alt="Chrome DevTools window with the arrays memory expanded and the first object selected. The Retainers bottom panel is open showing tree graph view of object relationships and their memory allocation." width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the bottom panel we can see that the massive retained size has been associated with the elements in a &lt;code&gt;products&lt;/code&gt; array that exists on a &lt;code&gt;baseResponse&lt;/code&gt; object used by the Express middleware stack! If we click on the second and third child we can see that these are for &lt;code&gt;subCategories&lt;/code&gt; and &lt;code&gt;filters&lt;/code&gt; respectively.&lt;/p&gt;

&lt;p&gt;Somehow these arrays on our &lt;code&gt;baseResponse&lt;/code&gt; object are growing in size after requests. This must mean that they are being mutated with more and more values! In fact if we expand the &lt;code&gt;(object elements)[]&lt;/code&gt; children we can see the exact values that are populating our arrays.&lt;/p&gt;

&lt;p&gt;So our leak is something causing the &lt;code&gt;baseResponse&lt;/code&gt; arrays to grow on a per request basis... let’s go have another look.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Want to learn more about debugging memory with Node? Checkout the &lt;a href="https://nodejs.org/en/docs/guides/diagnostics/memory/using-heap-snapshot" rel="noopener noreferrer"&gt;Node Memory Diagnostics tutorial page&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  When does a reduce grow?
&lt;/h2&gt;

&lt;p&gt;Let’s take another look at our &lt;code&gt;mergeResponses()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mergeResponses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’re reducing over the array of responses, accumulating the associated &lt;code&gt;filters&lt;/code&gt;, &lt;code&gt;products&lt;/code&gt;, and &lt;code&gt;subCategories&lt;/code&gt; and return the result. And in doing so somehow the &lt;code&gt;baseResponse&lt;/code&gt; is being mutated?&lt;/p&gt;

&lt;p&gt;Well exactly that — TIL!&lt;/p&gt;

&lt;p&gt;Turns out that although &lt;code&gt;Array.prototype.reduce()&lt;/code&gt; does not mutate the underlying array it &lt;em&gt;does&lt;/em&gt; mutate the &lt;code&gt;initialValue&lt;/code&gt; if it is provided. In many cases where you might provide a value directly like a number or an empty object this is fine, but if you create a bootstrap object like we have here then it will get mutated by each call, and re-used every time!&lt;/p&gt;

&lt;p&gt;This isn’t mentioned anywhere on &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#initialvalue" rel="noopener noreferrer"&gt;MDN&lt;/a&gt; or similar from what I can find, so is a bit of a gotcha — though I suspect the number of times this pattern is used is likely (hopefully) small.&lt;/p&gt;

&lt;p&gt;So how do we fix this issue?&lt;/p&gt;

&lt;p&gt;Well one option is to move the &lt;code&gt;baseResponse&lt;/code&gt; into the &lt;code&gt;mergeResponses()&lt;/code&gt; method so that it is no longer in top level scope and can be garbage collected once the request has been responded to. I.e.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mergeResponses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;finalResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or as we opted for, move away from a reduce when we’re fundamentally not changing object shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;mergeResponses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;responses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subCategories&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;baseResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;So did you spot the memory leak before the big reveal?&lt;/p&gt;

&lt;p&gt;Have you got the same code in your apps? 😱&lt;/p&gt;

&lt;p&gt;Have you faced a memory leak recently?&lt;/p&gt;

&lt;p&gt;Let me know in the comments along with your thoughts, comments, and queries! Till next time folks 🚀&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>node</category>
      <category>performance</category>
    </item>
    <item>
      <title>A11y Unlocked: Screen Reader Automation Tests</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Sun, 16 Oct 2022 17:31:19 +0000</pubDate>
      <link>https://dev.to/craigmorten/a11y-unlocked-screen-reader-automation-tests-3mc8</link>
      <guid>https://dev.to/craigmorten/a11y-unlocked-screen-reader-automation-tests-3mc8</guid>
      <description>&lt;p&gt;In the past year since my last post &lt;a href="https://dev.to/craigmorten/a11y-testing-automating-screenreaders-1a3n"&gt;A11y Testing: Automating ScreenReaders&lt;/a&gt; on the topic of automating screen readers, we're now in a better place with progress made in standards and tooling.&lt;/p&gt;

&lt;p&gt;It is exciting to see the &lt;a href="https://aria-at.w3.org/" rel="noopener noreferrer"&gt;W3C ARIA-AT Community Group&lt;/a&gt; formed with a mission to improve screen reader interoperability by building out a suite of specifications, test standards, and test automation to mature the screen reader ecosystem towards first-class tooling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Enabling AT interoperability is a large, ongoing endeavor that requires industry-wide collaboration and support. The W3C ARIA-AT Community Group is focusing on a stable and mature test suite for five screen readers by the end of 2023. In the future, we plan to test additional screen readers and other kinds of assistive technologies with a broader set of web design patterns and test material."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source: &lt;a href="https://aria-at.w3.org/" rel="noopener noreferrer"&gt;https://aria-at.w3.org/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;While the standards community slowly work towards their long term goal, I am pleased to share some screen reader automation tooling which you can use &lt;strong&gt;today&lt;/strong&gt;! 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  Guidepup for playwright
&lt;/h2&gt;

&lt;p&gt;In this tutorial we're going to make use of the package &lt;a href="https://www.npmjs.com/package/@guidepup/playwright" rel="noopener noreferrer"&gt;&lt;code&gt;@guidepup/playwright&lt;/code&gt;&lt;/a&gt; to write e2e tests for testing the accessibility of pages for people who use the VoiceOver screen reader for MacOS.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/guidepup" rel="noopener noreferrer"&gt;
        guidepup
      &lt;/a&gt; / &lt;a href="https://github.com/guidepup/guidepup-playwright" rel="noopener noreferrer"&gt;
        guidepup-playwright
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Screen reader automation library for Playwright testing.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/guidepup/guidepup/raw/main/img/logo.png"&gt;&lt;img alt="" height="120px" width="120px" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fguidepup%2Fguidepup%2Fraw%2Fmain%2Fimg%2Flogo.png"&gt;&lt;/a&gt;
  &lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Guidepup for Playwright&lt;/h1&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;a href="https://www.npmjs.com/package/@guidepup/playwright" rel="nofollow noopener noreferrer"&gt;&lt;img alt="@guidepup/playwright available on NPM" src="https://camo.githubusercontent.com/786e0f4af95e5fd626696b728d08327cffe60bd5f93d15506b08e2038c98df3e/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f4067756964657075702f706c6179777269676874"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/guidepup/guidepup-playwright/actions/workflows/test.yml" rel="noopener noreferrer"&gt;&lt;img alt="@guidepup/playwright test workflows" src="https://github.com/guidepup/guidepup-playwright/workflows/Test/badge.svg"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/guidepup/guidepup-playwright/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img alt="@guidepup/playwright uses the MIT license" src="https://camo.githubusercontent.com/327be9941a9e8b69b421f4eb0f69a7ccba0b29d8bdb17297a8989193b01af2d9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f67756964657075702f67756964657075702d706c6179777269676874"&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;
&lt;a href="https://guidepup.dev" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://www.guidepup.dev/docs/api/class-guidepup" rel="nofollow noopener noreferrer"&gt;API Reference&lt;/a&gt;
&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://support.apple.com/en-gb/102662" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/89a454b8b3ced48262be10e6becf528c3c0a3a9c452c09baebdc3a1b85ab4e83/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d536f6e6f6d612d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Sonoma Support"&gt;&lt;/a&gt;
&lt;a href="https://support.apple.com/en-gb/102662" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c2a34109af9267cf5a8e6211b0282743dfdc5774a90dd708741ac591f4704f5e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d536571756f69612d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Sequoia Support"&gt;&lt;/a&gt;
&lt;a href="https://support.apple.com/en-gb/102662" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/24b3eba60fc46e69487a5deea090b32b8b127c55e1a2d8e439754b4326defdb5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d5461686f652d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Tahoe Support"&gt;&lt;/a&gt;
&lt;a href="https://support.apple.com/en-gb/127455" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8b7195bd11f71436ee71ef9dd06f4502f841272ad057f837d5ffdc1f5b036319/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d476f6c64656e253230476174652d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Golden Gate Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2022" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/259f2bff2217376c92d8a67673bf15762bdf1b5777c7ef04e0a5d91dacc9375e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323032322d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2022 Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2025" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1f210e6d14358d56864cdeee873e708af03e4a86b2c5532f402cbb75c511bf0a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323032352d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2025 Support"&gt;&lt;/a&gt;
&lt;a href="https://releases.ubuntu.com/resolute/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6c6c3ddf2b861e4190510a8475a704a923c6977900dd651fa726ce987abf6666/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7562756e74752d32362e30342532422d626c75652e7376673f6c6f676f3d7562756e7475" alt="Ubuntu Resolute Support"&gt;&lt;/a&gt;
&lt;a href="https://www.debian.org/releases/trixie/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1a84301d45d45c72899ae174187fd6e1ff8acbf947c1d70781f80801bcf96c19/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64656269616e2d31332532422d626c75652e7376673f6c6f676f3d64656269616e" alt="Debian Trixie Support"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Guidepup is a screen reader automation library for testing.&lt;/p&gt;
&lt;p&gt;This package provides &lt;a href="https://github.com/guidepup/guidepup" rel="noopener noreferrer"&gt;Guidepup&lt;/a&gt; integration with &lt;a href="https://playwright.dev/" rel="nofollow noopener noreferrer"&gt;Playwright&lt;/a&gt; to enable testing with &lt;a href="https://www.guidepup.dev/docs/api/class-nvda" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;NVDA on Windows&lt;/b&gt;&lt;/a&gt;, &lt;a href="https://www.guidepup.dev/docs/api/class-orca" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Orca on Linux&lt;/b&gt;&lt;/a&gt;, and &lt;a href="https://www.guidepup.dev/docs/api/class-voiceover" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;VoiceOver on macOS&lt;/b&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Capabilities&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Control&lt;/strong&gt; - If a screen reader has a keyboard command, then Guidepup supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mirrors Real User Experience&lt;/strong&gt; - Assert on what users really do and hear when using screen readers.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Getting started&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Set up your machine for screen reader automation:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npx @guidepup/setup setup&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Install &lt;code&gt;@guidepup/playwright&lt;/code&gt; to your project:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm install --save-dev @guidepup/playwright @guidepup/guidepup @playwright/test&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-alert markdown-alert-note"&gt;
&lt;p class="markdown-alert-title"&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;code&gt;@guidepup/guidepup&lt;/code&gt; and &lt;code&gt;@playwright/test&lt;/code&gt; are required as peer dependencies of this project.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Install the Guidepup screen reader assets:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npx @guidepup/setup install&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;And get cracking with your first screen reader tests in Playwright!&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Examples&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Head over to the &lt;a href="https://www.guidepup.dev/" rel="nofollow noopener noreferrer"&gt;Guidepup Website&lt;/a&gt; for guides, real world examples, environment setup, and complete API documentation with examples.&lt;/p&gt;
&lt;p&gt;You…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/guidepup/guidepup-playwright" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The package provides a reliable set of APIs to automate your screen reader a11y workflows when using Playwright. It does this by providing both Playwright configuration as well as a custom Playwright &lt;code&gt;test&lt;/code&gt; instance that exposes a &lt;code&gt;voiceOver&lt;/code&gt; object for controlling VoiceOver with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting setup
&lt;/h2&gt;

&lt;p&gt;Let's create a new project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install the &lt;code&gt;@guidepup/playwright&lt;/code&gt; package as a dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @guidepup/playwright
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll also need Playwright as the test runner for our browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @playwright/test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last but not least we'll want to test against Safari using Playwright, so let's use their tool get grab the browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;install &lt;/span&gt;webkit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have all the dependencies we need! 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  A touch of config
&lt;/h2&gt;

&lt;p&gt;Create a new &lt;code&gt;playwright.config.js&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;voConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@guidepup/playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlaywrightTestConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;voConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="c1"&gt;// Your custom config ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we're pulling in the minimum recommended Playwright config for using Guidepup - you can &lt;a href="https://github.com/guidepup/guidepup-playwright/blob/main/src/voConfig.ts" rel="noopener noreferrer"&gt;check it out in the source code&lt;/a&gt;. This sets the number of workers to &lt;code&gt;1&lt;/code&gt; and sets Playwright to use "headed" mode because when we're using VoiceOver we can only control one browser at a time (and ideally it's visible!).&lt;/p&gt;

&lt;p&gt;We should now be completely set to get going with our first test! 🎉&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing your first screen-reader test
&lt;/h2&gt;

&lt;p&gt;Create a new file &lt;code&gt;voiceover.spec.js&lt;/code&gt; and copy in the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// We use a special test instance from the Guidepup package&lt;/span&gt;
&lt;span class="c1"&gt;// that gives us access to VoiceOver!&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;voTest&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@guidepup/playwright&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// The test setup is exactly the same as normal for&lt;/span&gt;
&lt;span class="c1"&gt;// Playwright, expect we now get a `voiceOver` object as well&lt;/span&gt;
&lt;span class="c1"&gt;// as the `page` object!&lt;/span&gt;
&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Playwright VoiceOver&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;I can navigate the Guidepup Github page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;voiceOver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Let's navigate to Guidepup GitHub page and wait for&lt;/span&gt;
    &lt;span class="c1"&gt;// page to be ready, nothing special here!&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/guidepup/guidepup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domcontentloaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;header[role="banner"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// This is where things start to get awesome! Let's tell&lt;/span&gt;
    &lt;span class="c1"&gt;// VoiceOver to interact with the main page content, just &lt;/span&gt;
    &lt;span class="c1"&gt;// the same as you would when use VoiceOver normally.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;voiceOver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;interact&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Let's do something a lil more exciting - move across&lt;/span&gt;
    &lt;span class="c1"&gt;// the page's headings until we reach the main Guidepup&lt;/span&gt;
    &lt;span class="c1"&gt;// repo title in the README using VoiceOver!&lt;/span&gt;
    &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;voiceOver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;itemText&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Guidepup heading level 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;voiceOver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;voiceOver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keyboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findNextHeading&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check out the comments in the code for what will happen in each part - pretty awesome huh? 🙌&lt;/p&gt;

&lt;h2&gt;
  
  
  Last checks before we launch
&lt;/h2&gt;

&lt;p&gt;In order to automate screen-readers a few OS level settings need to be sorted first!&lt;/p&gt;

&lt;p&gt;You can head over to the &lt;a href="https://www.guidepup.dev/docs/guides/environment" rel="noopener noreferrer"&gt;Guidepup environment setup docs&lt;/a&gt; for more details, but I recommend trying out the &lt;a href="https://github.com/guidepup/setup" rel="noopener noreferrer"&gt;&lt;code&gt;@guidepup/setup&lt;/code&gt;&lt;/a&gt; package which is designed to get you setup no faff!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @guidepup/setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're now ready to rock! 🤘&lt;/p&gt;

&lt;h2&gt;
  
  
  Running your first screen-reader test
&lt;/h2&gt;

&lt;p&gt;No special commands needed here, just the same as with any other Playwright test! Fire off the command then hold tight - we don't want to interact with the machine while the test is running as it might move VoiceOver's focus!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx playwright &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4vnjufu0zxfvz8vdln5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx4vnjufu0zxfvz8vdln5.gif" alt="Gif over Playwright controlled Safari browser being driven with VoiceOver. Announcements read: " width="560" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;To find out more there's a whole load of reading, modules, and docs - check some of these out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.guidepup.dev/" rel="noopener noreferrer"&gt;https://www.guidepup.dev/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/guidepup" rel="noopener noreferrer"&gt;https://github.com/guidepup&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Time to try out some e2e screen-reader tests?&lt;/p&gt;

&lt;p&gt;Let me know your thoughts, questions, and opinions in the comments below!&lt;/p&gt;

&lt;p&gt;Till next time folks 👋&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>a11y</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Setting up Stable Diffusion for MacOS</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Fri, 26 Aug 2022 13:48:00 +0000</pubDate>
      <link>https://dev.to/craigmorten/setting-up-stable-diffusion-for-macos-1igl</link>
      <guid>https://dev.to/craigmorten/setting-up-stable-diffusion-for-macos-1igl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;With the landscape quickly changing, this article is fast becoming outdated!&lt;/p&gt;

&lt;p&gt;If you face issues with the tutorial below I recommend you checkout the &lt;a href="https://github.com/invoke-ai/InvokeAI/blob/main/docs/installation/INSTALL_MAC.md" rel="noopener noreferrer"&gt;latest advice here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;a href="https://github.com/CompVis/stable-diffusion" rel="noopener noreferrer"&gt;Stable Diffusion&lt;/a&gt; is a latent text-to-image diffusion model that was &lt;a href="https://stability.ai/blog/stable-diffusion-public-release" rel="noopener noreferrer"&gt;recently made open source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For Linux users with dedicated NVDIA GPUs the instructions for setup and usage are relatively straight forward. However for MacOS users you can't use the project "out of the box". Not to worry! There are some steps to getting it working nevertheless!&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment Setup
&lt;/h2&gt;

&lt;p&gt;To begin you need &lt;a href="https://www.python.org/" rel="noopener noreferrer"&gt;Python&lt;/a&gt;, &lt;a href="https://docs.conda.io/en/latest/" rel="noopener noreferrer"&gt;Conda&lt;/a&gt;, and a few other libraries set up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Install Python, Cmake, Git, and Protobuf
&lt;span class="go"&gt;brew install python \
  cmake \
  git \
  protobuf

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Install Rust
&lt;span class="go"&gt;curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Install Conda:
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="c"&gt;# Either use this for older "pre-M1" Macs:&lt;/span&gt;
&lt;span class="go"&gt;wget https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh
bash Miniconda3-latest-MacOSX-x86_64.sh

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="c"&gt;# Or use this for older M1 Macs:&lt;/span&gt;
&lt;span class="go"&gt;wget https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh
bash Miniconda3-latest-MacOSX-arm64.sh
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may need to restart your terminal at this point for the changes for the new libraries to be picked up.&lt;/p&gt;

&lt;p&gt;Clone this fork of the project and checkout the apple patch branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;git clone https://github.com/magnusviri/stable-diffusion
cd stable-diffusion
git checkout apple-silicon-mps-support
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point you will need to make sure you're using Python 3, &lt;a href="https://opensource.com/article/19/5/python-3-default-mac" rel="noopener noreferrer"&gt;check out this article for different ways to make Python 3 the default version on your Mac&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Set up the Conda environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;conda env create -f environment-mac.yaml
conda activate ldm
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally set the following environment variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;export PYTORCH_ENABLE_MPS_FALLBACK=1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code Changes
&lt;/h2&gt;

&lt;p&gt;Our environment is now set up, but we have a few tweaks that we need to allow the code to gracefully fallback to using the CPU (if required!).&lt;/p&gt;

&lt;p&gt;Append &lt;code&gt;.contiguous()&lt;/code&gt; at &lt;a href="https://github.com/magnusviri/stable-diffusion/blob/6be63b5bbe95f04ad480e172d40994ecbe242b21/ldm/models/diffusion/plms.py#L27" rel="noopener noreferrer"&gt;ldm/models/diffusion/plms.py#L27&lt;/a&gt; resulting in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-        attr = attr.to(torch.float32).to(torch.device(self.device_available))
&lt;/span&gt;&lt;span class="gi"&gt;+        attr = attr.to(torch.float32).to(torch.device(self.device_available)).contiguous()
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly append a new line &lt;code&gt;x = x.contiguous()&lt;/code&gt; after &lt;a href="https://github.com/magnusviri/stable-diffusion/blob/6be63b5bbe95f04ad480e172d40994ecbe242b21/ldm/modules/attention.py#L211" rel="noopener noreferrer"&gt;ldm/modules/attention.py#L211&lt;/a&gt; so it looks something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;def _forward(self, x, context=None):
&lt;/span&gt;&lt;span class="gi"&gt;+       x = x.contiguous()
&lt;/span&gt;        x = self.attn1(self.norm1(x)) + x
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Download Stable Diffusion Weights
&lt;/h2&gt;

&lt;p&gt;Let's install our diffusion weights&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;curl "https://www.googleapis.com/storage/v1/b/aai-blog-files/o/sd-v1-4.ckpt?alt=media" &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sd-v1-4.ckpt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create Images 🚀
&lt;/h2&gt;

&lt;p&gt;You should now be ready to generate images on your MacOS device using Stable Diffusion! 🎉 🎉&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;python scripts/txt2img.py --prompt "a drawing of web developers" --plms --ckpt sd-v1-4.ckpt --skip_grid --n_samples 1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fynrp56ixjay83azo72ke.jpg" 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%2Fynrp56ixjay83azo72ke.jpg" alt="A drawing of web developers" width="512" height="512"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Tricks and hacks gleamed from &lt;a href="https://github.com/CompVis/stable-diffusion/issues/25" rel="noopener noreferrer"&gt;https://github.com/CompVis/stable-diffusion/issues/25&lt;/a&gt; - credit to all the folks in that thread for figuring out how to get things working!&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Testing Web Vitals With Cypress</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Sun, 03 Apr 2022 10:48:34 +0000</pubDate>
      <link>https://dev.to/craigmorten/web-vitals-cypress-testing-3bpj</link>
      <guid>https://dev.to/craigmorten/web-vitals-cypress-testing-3bpj</guid>
      <description>&lt;p&gt;It is well understood that &lt;a href="https://web.dev/why-speed-matters/" rel="noopener noreferrer"&gt;performance is a critical consideration&lt;/a&gt; for any website which can have far reaching impacts on everything from customer satisfaction, SEO, and ultimately your bottom line. You cannot determine the success of performance work without the ability measure the results and compare to &lt;a href="https://web.dev/performance-budgets-101/" rel="noopener noreferrer"&gt;performance budgets&lt;/a&gt; - this calls for testing infrastructure to make sure you have the necessary visibility on metrics... introducing &lt;a href="https://www.npmjs.com/package/cypress-web-vitals" rel="noopener noreferrer"&gt;&lt;code&gt;cypress-web-vitals&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cypress-web-vitals&lt;/code&gt; allows you to test against the Google Web Vital signals within your Cypress workflows through a new &lt;code&gt;cy.vitals()&lt;/code&gt; custom command.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Web Vitals is an initiative by Google to provide unified guidance for quality signals that are essential to delivering a great user experience on the web.&lt;/p&gt;

&lt;p&gt;Reference: &lt;a href="https://web.dev/vitals/" rel="noopener noreferrer"&gt;https://web.dev/vitals/&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &amp;nbsp;Getting started
&lt;/h2&gt;

&lt;p&gt;In your project, install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;npm install --save-dev cypress-web-vitals cypress-real-events
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note: &lt;code&gt;cypress-web-vitals&lt;/code&gt; currently makes use of &lt;code&gt;cypress-real-events&lt;/code&gt; to click the page to calculate first input delay. Hence it is needed as a peer-dependency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Within you support commands file (normally &lt;code&gt;cypress/support/commands.js&lt;/code&gt;), add this one liner to get setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cypress-web-vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now you're set to get going with Web Vital performance budget tests in your Cypress workflow! 🎉&lt;/p&gt;

&lt;p&gt;Add you first test like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Web Vitals&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;should pass the meet Google's 'Good' thresholds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vitals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.google.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are now set up to test against all of the Web Vitals using Google's "Good" threshold values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://web.dev/lcp/" rel="noopener noreferrer"&gt;Largest contentful paint (LCP)&lt;/a&gt; - &lt;code&gt;2500&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/fid/" rel="noopener noreferrer"&gt;First input delay (FID)&lt;/a&gt; - &lt;code&gt;100&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/cls/" rel="noopener noreferrer"&gt;Cumulative layout shift (CLS)&lt;/a&gt; - &lt;code&gt;0.1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/fcp/" rel="noopener noreferrer"&gt;First contentful paint (FCP)&lt;/a&gt; - &lt;code&gt;1800&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://web.dev/time-to-first-byte/" rel="noopener noreferrer"&gt;Time to first byte (TTFB)&lt;/a&gt; - &lt;code&gt;600&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Customise your tests
&lt;/h2&gt;

&lt;p&gt;You can further customise your tests through additional optional configuration which is passed to the &lt;code&gt;cy.vitals(webVitalsConfig)&lt;/code&gt; call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optional &lt;code&gt;url: string&lt;/code&gt; - The url to audit. If not provided you will need to have called &lt;code&gt;cy.visit(url)&lt;/code&gt; prior to the command.&lt;/li&gt;
&lt;li&gt;Optional &lt;code&gt;firstInputSelector: string&lt;/code&gt; - The element to click for capturing FID. The first matching element is used. Default: &lt;code&gt;"body"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Optional &lt;code&gt;thresholds: object&lt;/code&gt; - The thresholds to audit the Web Vitals against. If not provided Google's "Good" thresholds will be used. If provided, any missing Web Vitals signals will not be audited.&lt;/li&gt;
&lt;li&gt;Optional &lt;code&gt;vitalsReportedTimeout: number&lt;/code&gt; - Time in ms to wait for Web Vitals to be reported before failing. Default: &lt;code&gt;10000&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Use the `main` element for clicking to capture the FID.&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vitals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;firstInputSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Test the page against against a CLS threshold of 0.2.&lt;/span&gt;
&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vitals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more details on usage refer to the &lt;a href="https://www.npmjs.com/package/cypress-web-vitals#api" rel="noopener noreferrer"&gt;API docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The url is visited with the HTML response intercepted and modified by Cypress to include the &lt;a href="https://github.com/GoogleChrome/web-vitals" rel="noopener noreferrer"&gt;web-vitals&lt;/a&gt; script and some code to record the Web Vitals values.&lt;/li&gt;
&lt;li&gt;Several elements (if exist) starting with the provided element (based on &lt;code&gt;firstInputSelector&lt;/code&gt;) are then clicked in quick succession to simulate a user clicking on the page. Note: if choosing a custom element, don't pick something that will navigate away from the page otherwise the plugin will fail to capture the Web Vitals metrics.&lt;/li&gt;
&lt;li&gt;The audit then waits for the page load event to allow for the values of LCP and CLS to settle; which are subject to change as different parts of the page load into view.&lt;/li&gt;
&lt;li&gt;Next the audit simulates a page visibility state change &lt;a href="https://www.npmjs.com/package/web-vitals#basic-usage" rel="noopener noreferrer"&gt;which is required for the CLS Web Vital to be reported&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The audit then attempts to wait for any outstanding Web Vitals to be reported for which thresholds have been provided.&lt;/li&gt;
&lt;li&gt;Finally the Web Vitals values are compared against the thresholds, logging successful results and throwing an error for any unsuccessful signals. &lt;em&gt;Note: if the audit was unable to record a Web Vital then it is logged, but the test will not fail.&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Testing sites in the wild
&lt;/h2&gt;

&lt;p&gt;Here are some example test runs against FAANG homepages to see &lt;code&gt;cypress-web-vitals&lt;/code&gt; in action:&lt;/p&gt;

&lt;h3&gt;
  
  
  Facebook
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vitals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.facebook.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fz5lye3uqo9n219pr56li.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%2Fz5lye3uqo9n219pr56li.png" alt="Cypress Web Vitals test against the Facebook landing page. All metrics below Google's " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Amazon
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vitals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.amazon.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fv7h9sihfx9lg5lj4qzvv.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%2Fv7h9sihfx9lg5lj4qzvv.png" alt="Cypress Web Vitals test against the Amazon home page. All metrics below Google's " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Netflix
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vitals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.netflix.com/gb/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;firstInputSelector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.our-story-card-title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fqc1jb8wzy8hemrz5uxun.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%2Fqc1jb8wzy8hemrz5uxun.png" alt="Cypress Web Vitals test against the Netflix landing page. All metrics below Google's " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For Netflix we have had to introduce a custom element choice for the simulated "first click". Even though &lt;a href="https://web.dev/fid/#what-if-an-interaction-doesn't-have-an-event-listener" rel="noopener noreferrer"&gt;first input delay can be measured when in cases where there is no event listener registered to the element&lt;/a&gt;, there are scenarios where clicking the &lt;code&gt;body&lt;/code&gt; doesn't cut it. Some good examples of elements that will reliably trigger an FID metric are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text fields, checkboxes, and radio buttons (&lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Select dropdowns (&lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Links (&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In order to ensure the Web Vitals can be tested against, it is best to try find an element that reliably triggers an FID, but won't navigate away from the page (perhaps avoid &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;!).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  &amp;nbsp;Google
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;vitals&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://www.google.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fjrdg1bd9y6q9umq93avl.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%2Fjrdg1bd9y6q9umq93avl.png" alt="Cypress Web Vitals test against the Google home page. All metrics below Google's " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Using any awesome performance testing tooling lately? Tried out &lt;a href="https://www.npmjs.com/package/cypress-web-vitals" rel="noopener noreferrer"&gt;cypress-web-vitals&lt;/a&gt; on your site and have results to share? Got any comments, queries, or questions? Leave a comment below!&lt;/p&gt;

&lt;p&gt;That's all folks! 🚀&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Create Your Own Live Browser Refresh In Deno</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Sat, 15 Jan 2022 17:39:07 +0000</pubDate>
      <link>https://dev.to/craigmorten/how-to-code-live-browser-refresh-in-deno-309o</link>
      <guid>https://dev.to/craigmorten/how-to-code-live-browser-refresh-in-deno-309o</guid>
      <description>&lt;p&gt;In modern web development we've grown accustomed to rich developer experience features such as hot module replacement (HMR) from the likes of &lt;a href="https://webpack.js.org/concepts/hot-module-replacement/" rel="noopener noreferrer"&gt;Webpack HMR&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/react-refresh" rel="noopener noreferrer"&gt;React Fast Refresh&lt;/a&gt; which allow us to iterate on our apps fast, without the pain of slow server restarts.&lt;/p&gt;

&lt;p&gt;Ever wondered how this tooling might work? In this tutorial we will build a simple live browser refresh in Deno that demonstrates so of the basics!&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started 🤔
&lt;/h2&gt;

&lt;p&gt;To begin you will need to &lt;a href="https://deno.land/#installation" rel="noopener noreferrer"&gt;install Deno&lt;/a&gt; and create a new directory to work in, e.g. &lt;code&gt;./refresh/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For this tutorial I am using Deno &lt;code&gt;v1.17.3&lt;/code&gt;, but the code should work with future versions of Deno as it doesn't require any external dependencies, provided there are no breaking changes to the Deno APIs (e.g. for &lt;code&gt;v2&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In your new directory create a &lt;code&gt;mod.ts&lt;/code&gt; file. This will act as the entrypoint for our Deno module, and contain all of our server-side code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watching files 👀
&lt;/h2&gt;

&lt;p&gt;The first part of our live browser refresh module is a function to watch for file changes - this will later allow us to tell the browser to refresh when we make and save changes to our application.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Watch files from current directory
 * and trigger a refresh on change.
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Create our file watcher.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watchFs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Wait for, and loop over file events.&lt;/span&gt;
  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;watcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// TODO: trigger a browser refresh.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we define our initial &lt;code&gt;watch()&lt;/code&gt; function which makes use of the built-in &lt;a href="https://doc.deno.land/deno/stable/~/Deno.watchFs" rel="noopener noreferrer"&gt;&lt;code&gt;Deno.watchFs(...)&lt;/code&gt;&lt;/a&gt; method to watch for file system events anywhere within our current directory. We then loop over the events picked up by the watcher, where we will add code to trigger a browser refresh.&lt;/p&gt;

&lt;p&gt;Before we move onto code for triggering browser refresh, it's worth taking a look at &lt;a href="https://doc.deno.land/deno/stable/~/Deno.FsEvent" rel="noopener noreferrer"&gt;different file system events that can fire&lt;/a&gt;. Namely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FsEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;FsEventFlag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;any&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;modify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;other&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It would be a bit annoying if our application reloaded every time a file was accessed but not necessarily changed. Let's update our loop to filter out some of these events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;watcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Skip the "any" and "access" events to reduce&lt;/span&gt;
    &lt;span class="c1"&gt;// unnecessary refreshes.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;any&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// TODO: trigger a browser refresh.&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  WebSocket middleware 🏎
&lt;/h2&gt;

&lt;p&gt;In this tutorial we will be using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API" rel="noopener noreferrer"&gt;WebSockets&lt;/a&gt; to communicate the need to trigger a browser refresh. It is worth noting that you could also use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events" rel="noopener noreferrer"&gt;Server Sent Events&lt;/a&gt; to achieve a similar result. If you try it out, do share in the comments below!&lt;/p&gt;

&lt;p&gt;We will start with setting up the server-side WebSocket behaviour. For this we will create a small middleware function that will accept specific requests to the server, and upgrade the connection to a WebSocket.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Upgrade a request connection to a WebSocket if
 * the url ends with "/refresh"
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refreshMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Only upgrade requests ending with "/refresh".&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Upgrade the request to a WebSocket.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeWebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// TODO: handle the newly created socket.&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Leave all other requests alone.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here our function first checks the request URL to see if it ends with &lt;code&gt;"/refresh"&lt;/code&gt;. If not we leave the request alone.&lt;/p&gt;

&lt;p&gt;When we get a match we use the built-in &lt;a href="https://doc.deno.land/deno/stable/~/Deno.upgradeWebSocket" rel="noopener noreferrer"&gt;&lt;code&gt;Deno.upgradeWebSocket(...)&lt;/code&gt;&lt;/a&gt; method to upgrade our connection to a WebSocket. This method returns an object including the &lt;code&gt;response&lt;/code&gt; that must be returned to the client for the upgrade to be successful, and a &lt;code&gt;socket&lt;/code&gt; instance.&lt;/p&gt;

&lt;p&gt;Given we will be using the &lt;code&gt;socket&lt;/code&gt; instance as our means of instructing the client to refresh the browser, let's add some code to store the WebSocket as well as handle when it closes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * In-memory store of open WebSockets for
 * triggering browser refresh.
 */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sockets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WebSocket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Upgrade a request connection to a WebSocket if
 * the url ends with "/refresh"
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refreshMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upgradeWebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add the new socket to our in-memory store&lt;/span&gt;
    &lt;span class="c1"&gt;// of WebSockets.&lt;/span&gt;
    &lt;span class="nx"&gt;sockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Remove the socket from our in-memory store&lt;/span&gt;
    &lt;span class="c1"&gt;// when the socket closes.&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onclose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;sockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've now added an in-memory store for created WebSockets. When we upgrade the connection we add the new &lt;code&gt;socket&lt;/code&gt; to our store as well as a handler for removing the &lt;code&gt;socket&lt;/code&gt; from the store when it closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Triggering browser refresh 🙌
&lt;/h2&gt;

&lt;p&gt;We're now ready to update our file watching code to trigger a browser refresh. We will do this by using the WebSockets created in our middleware to send a refresh event to the client.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Watch files from current directory
 * and trigger a refresh on change.
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;watcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;watchFs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;watcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;any&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;access&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;sockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we loop over the &lt;code&gt;sockets&lt;/code&gt; in-memory store, and for each WebSocket we send our custom refresh event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finishing our server module 🧑‍💻
&lt;/h2&gt;

&lt;p&gt;To finish off our server module we just need to tie our file watching and server middleware together. For this we create our &lt;code&gt;refresh()&lt;/code&gt; function module export which users can consume to their servers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Constructs a refresh middleware for reloading
 * the browser on file changes.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;refreshMiddleware&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This final exported function ties our work together. First it starts the file watcher, and then returns the middleware that can be used to handle the refresh communications between the server and the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling refresh events client-side 💥
&lt;/h2&gt;

&lt;p&gt;Now we're all sorted on the server, let's hop over the some coding for the client. First we need to create a &lt;code&gt;client.js&lt;/code&gt; file to host our code.&lt;/p&gt;

&lt;p&gt;Let's just dive in with the full code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reconnectionTimerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Construct the WebSocket url from the current&lt;/span&gt;
  &lt;span class="c1"&gt;// page origin.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ws&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;/refresh`&lt;/span&gt;

  &lt;span class="c1"&gt;// Kick off the connection code on load.&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Info message logger.
   */&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[refresh] &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Refresh the browser.
   */&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Create WebSocket, connect to the server and
   * listen for refresh events.
   */&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Close any existing sockets.&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Create a new WebSocket pointing to the server.&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// When the connection opens, execute the callback.&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Add a listener for messages from the server.&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Check whether we should refresh the browser.&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refreshing...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Handle when the WebSocket closes. We log&lt;/span&gt;
    &lt;span class="c1"&gt;// the loss of connection and set a timer to&lt;/span&gt;
    &lt;span class="c1"&gt;// start the connection again after a second.&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;close&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connection lost - reconnecting...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reconnectionTimerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="nx"&gt;reconnectionTimerId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Try to connect again, and if successful&lt;/span&gt;
        &lt;span class="c1"&gt;// trigger a browser refresh.&lt;/span&gt;
        &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lot going on here!&lt;/p&gt;

&lt;p&gt;First we create some variables for storing the current WebSocket and a reconnection timer id. We then construct the url that will be used by the WebSocket for requests. Notice how it ends in &lt;code&gt;/refresh&lt;/code&gt;, just as we coded our server middleware function to detect and handle. Then we kick off the connection with a call to the &lt;code&gt;connect(...)&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;connect(...)&lt;/code&gt; function is where the majority of the work takes place. We ensure that any pre-existing sockets are closed - we want to avoid situations where there are more than one connection resulting in "double" refreshes! The WebSocket is then constructed using the request url, and a series of event listeners are setup to handle different WebSocket events:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The main event listener is for &lt;code&gt;"message"&lt;/code&gt; events. This receives messages from the server, and if it receives our custom refresh event, it fires a call to the &lt;code&gt;refresh()&lt;/code&gt; function which refreshes the browser.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;"close"&lt;/code&gt; event listener handles when we lose the connection from the server. This can happen easily with network blips (e.g. when you pass through a tunnel and lose signal!) so always good to handle. Here we setup a timeout to try and restart the connection again by calling &lt;code&gt;connect(...)&lt;/code&gt; after a second delay. This time we pass the &lt;code&gt;refresh&lt;/code&gt; function as a callback to trigger a refresh once our connection is back.&lt;/li&gt;
&lt;li&gt;Finally, the &lt;code&gt;"open"&lt;/code&gt; event listener fires when the connection opens, and here we just execute the provided callback. This is used in the aforementioned reconnection logic to trigger the browser refresh when we get our connection back.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Congratulations!! 🥳 🎉
&lt;/h2&gt;

&lt;p&gt;And we're done! Between the server &lt;code&gt;mod.ts&lt;/code&gt; and the browser &lt;code&gt;client.js&lt;/code&gt; we've now got all we need to successfully implement a live browser refresh on code change.&lt;/p&gt;

&lt;p&gt;Don't believe me? Let's try it out!&lt;/p&gt;

&lt;p&gt;First we will need to write a simple server to consume our new refresh module. Let's create a &lt;code&gt;server.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;serve&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://deno.land/std/http/server.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fromFileUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://deno.land/std/path/mod.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;refresh&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./mod.ts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create useful file path variable for our code.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;__dirname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fromFileUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./client.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;indexFilePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./index.html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Construct the refresh middleware.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshMiddleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Start a server on port `8000`.&lt;/span&gt;
&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Handle custom refresh middleware requests.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;refreshMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle requests for the client-side refresh code.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;client.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readTextFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clientFilePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/javascript&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Handle requests for the page's HTML.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readTextFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;indexFilePath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/html&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Listening on http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This server code uses the &lt;a href="https://deno.land/std" rel="noopener noreferrer"&gt;Deno Standard Library&lt;/a&gt; for some server and path utilities. It constructs some variables storing the path to files the server needs to return, constructs the refresh middleware using the module that we've created in this tutorial, and then uses the standard library &lt;code&gt;serve(...)&lt;/code&gt; method to start a server on port &lt;code&gt;8000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We first call our refresh middleware with the request, and if we get a non-null response we return it - this means the request was for a WebSocket connection! Otherwise we handle requests for our &lt;code&gt;client.js&lt;/code&gt; code, and otherwise fallback to returning an &lt;code&gt;index.html&lt;/code&gt;. Let's create this &lt;code&gt;index.html&lt;/code&gt; file now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Example Refresh App&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;#2c3e50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Verdana&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Geneva&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tahoma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;#ddd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;text-align&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/client.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Hello Deno!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there we have it! Let's run our new server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;deno run &lt;span class="nt"&gt;--allow-read&lt;/span&gt; &lt;span class="nt"&gt;--allow-net&lt;/span&gt; ./server.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we open a browser on &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;&lt;code&gt;http://localhost:8000&lt;/code&gt;&lt;/a&gt; we should see our simple "Hello Deno!" webpage.&lt;/p&gt;

&lt;p&gt;Now for the exciting part - let's see if the live browser refresh works! Head to your &lt;code&gt;index.html&lt;/code&gt; and try changing the text or some of the CSS. Notice anything different about the page in the browser? 💥&lt;/p&gt;




&lt;p&gt;For an example of all this code working (and more!), check out the finished version at &lt;a href="https://deno.land/x/refresh" rel="noopener noreferrer"&gt;https://deno.land/x/refresh&lt;/a&gt;. 🦕&lt;/p&gt;




&lt;p&gt;Written any cool Deno code lately? Perhaps you've built your own live browser refresh, or even HMR module that is worth a share?&lt;/p&gt;

&lt;p&gt;Reach out on my twitter &lt;a href="https://twitter.com/craigmorten" rel="noopener noreferrer"&gt;@CraigMorten&lt;/a&gt;, or leave a comment below! It would be great to hear from you! 🚀🚀&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>deno</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A11y Testing: Automating Screen Readers</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Thu, 30 Dec 2021 17:37:43 +0000</pubDate>
      <link>https://dev.to/craigmorten/a11y-testing-automating-screenreaders-1a3n</link>
      <guid>https://dev.to/craigmorten/a11y-testing-automating-screenreaders-1a3n</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Since this article there have been some developments, check out &lt;a href="https://dev.to/craigmorten/a11y-unlocked-screen-reader-automation-tests-3mc8"&gt;A11y Unlocked: Screen Reader Automation Tests&lt;/a&gt; for the latest!&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;There are lots of toolkits out there for ensuring the inclusivity and accessibility of our sites: with the likes &lt;a href="https://github.com/dequelabs/axe-core" rel="noopener noreferrer"&gt;axe-core&lt;/a&gt; and &lt;a href="https://squizlabs.github.io/HTML_CodeSniffer/" rel="noopener noreferrer"&gt;htmlcs&lt;/a&gt; for static analysis, and &lt;a href="https://github.com/pa11y/pa11y" rel="noopener noreferrer"&gt;pa11y&lt;/a&gt;, &lt;a href="https://open-indy.github.io/Koa11y/" rel="noopener noreferrer"&gt;koa11y&lt;/a&gt;, and &lt;a href="https://github.com/mfrachet/cypress-audit" rel="noopener noreferrer"&gt;cypress-audit&lt;/a&gt; (there are more) for helping us to explore or automate static analysis checks into our CI pipelines. 🚀🚀&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Chosen by Microsoft, Google, development and testing teams everywhere, axe is the World’s leading digital accessibility toolkit."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Alongside our a11y tooling we've seen amazing growth in the landscape of browser testing automation tooling, with the rise of &lt;a href="https://www.cypress.io/" rel="noopener noreferrer"&gt;Cypress&lt;/a&gt;, &lt;a href="https://playwright.dev/" rel="noopener noreferrer"&gt;Playwright&lt;/a&gt; (a new personal fav 🥰), and several other players. These have evolved to provide a rich collection of reliable APIs and tooling for end-to-end or integration testing sites across a solid matrix of browsers - complemented by looking to the likes of &lt;a href="https://saucelabs.com/" rel="noopener noreferrer"&gt;SauceLabs&lt;/a&gt;, &lt;a href="https://www.browserstack.com/" rel="noopener noreferrer"&gt;BrowserStack&lt;/a&gt;, and &lt;a href="https://applitools.com" rel="noopener noreferrer"&gt;Applitools&lt;/a&gt; who can provide an extensive range of browsers and devices so you don't need to worry about purchasing an expensive device cupboard! 💰💰💰&lt;/p&gt;

&lt;h2&gt;
  
  
  The ScreenReader Test Tooling Gap
&lt;/h2&gt;

&lt;p&gt;But... these browser testing tools tend to lack any visibility or emphasis on a11y. Certainly through the likes of the &lt;a href="https://testing-library.com/" rel="noopener noreferrer"&gt;testing library suite&lt;/a&gt; and it's &lt;a href="https://testing-library.com/docs/guiding-principles" rel="noopener noreferrer"&gt;guiding principles&lt;/a&gt; you can write tests in such a way that it mirrors the customer's usage, but only provides so much confidence, and it very much orientated to mouse, keyboard, and touch users. 🧐&lt;/p&gt;

&lt;p&gt;And with regards to the a11y tools that are out there... we don't typically rely on static analysis for test coverage for other functional aspects of sites - indeed we use our favourite aforementioned cross-browser testing framework! It feels a shame that the "World’s leading digital accessibility toolkit" should be lacking in providing the ability to integration test with the actual assistive technologies that folks are using to navigate the web.&lt;/p&gt;

&lt;p&gt;As a finger in the air of the limitations of current tooling, &lt;a href="https://karlgroves.com/web-accessibility-testing-what-can-be-tested-and-how/" rel="noopener noreferrer"&gt;this article by Karl Groves&lt;/a&gt; from 2012 explains how 40-50% of the WCAG is not feasible to be tested by existing (and I'll admit in some cases also future) tooling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"On the other hand, there are things which can be tested for but the results would need to be verified by a skilled human reviewer. For example, while machine testing can verify that the alt attribute exists for an image, it cannot tell you whether the value supplied for the alt attribute is a suitable alternative for the image. That requires a human."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead teams set on treating the accessibility of their sites as first class, rather than an afterthought, are forced to provide this testing coverage manually. This isn't necessarily a bad thing - manual exploration by your QAs adopting a ScreenReader using personna is the closest you can likely get to gaining confidence your site will work for folks out in the wild. 👩‍💻&lt;/p&gt;

&lt;p&gt;But can we truly afford to have to manually QA every change to our sites to ensure they continue to work functionally for assistive technology? It is already resource-draining enough that teams typically automate golden path flows with browser testing tools to reduce the overhead of manual testing - there are simply too many browsers which demands a degree of trade-off. To name a core few:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safari on MacOS, iOS&lt;/li&gt;
&lt;li&gt;Chrome on MacOS, Windows, Android&lt;/li&gt;
&lt;li&gt;Firefox on MacOS, Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We're already at 7 different browser vs device combinations, and this isn't accounting for different viewports, locales, current and previous versions, and many other variations.&lt;/p&gt;

&lt;p&gt;If we now throw VoiceOver, JAWS, Narrator, NVDA, Talkback, and friends into the mix we can easily triple the workload on our QAs. 😰&lt;/p&gt;

&lt;h2&gt;
  
  
  What is out there currently?
&lt;/h2&gt;

&lt;p&gt;This gap in automated tooling for mirroring existing functional tests for mouse, keyboard, and touch, but for ScreenReaders has been bugging me for a couple of years. A few weekends back I decided to finally explore to see if I simply hadn't looked hard enough.&lt;/p&gt;

&lt;p&gt;Initially it didn't look hopeful...&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-852866736848293888-490" src="https://platform.twitter.com/embed/Tweet.html?id=852866736848293888"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-852866736848293888-490');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=852866736848293888&amp;amp;theme=dark"
  }



&lt;/p&gt;


&lt;div class="ltag__stackexchange--container"&gt;
  &lt;div class="ltag__stackexchange--title-container"&gt;
    
      &lt;div class="ltag__stackexchange--title"&gt;
        &lt;div class="ltag__stackexchange--header"&gt;
          &lt;img src="https://assets.dev.to/assets/stackoverflow-logo-b42691ae545e4810b105ee957979a853a696085e67e43ee14c5699cf3e890fb4.svg" alt=""&gt;
          &lt;a href="https://stackoverflow.com/questions/43340690/is-there-an-online-emulating-screen-reader-tool-to-test-against-a-custom-web-pag/43368748#43368748" rel="noopener noreferrer"&gt;
            &lt;span class="title-flare"&gt;answer&lt;/span&gt; re: Is there an online emulating screen reader tool to test against a custom web page?
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="ltag__stackexchange--post-metadata"&gt;
          &lt;span&gt;Apr 12 '17&lt;/span&gt;
        &lt;/div&gt;
      &lt;/div&gt;
      &lt;a class="ltag__stackexchange--score-container" href="https://stackoverflow.com/questions/43340690/is-there-an-online-emulating-screen-reader-tool-to-test-against-a-custom-web-pag/43368748#43368748" rel="noopener noreferrer"&gt;
        &lt;img src="https://assets.dev.to/assets/stackexchange-arrow-up-eff2e2849e67d156181d258e38802c0b57fa011f74164a7f97675ca3b6ab756b.svg" alt=""&gt;
        &lt;div class="ltag__stackexchange--score-number"&gt;
          34
        &lt;/div&gt;
        &lt;img src="https://assets.dev.to/assets/stackexchange-arrow-down-4349fac0dd932d284fab7e4dd9846f19a3710558efde0d2dfd05897f3eeb9aba.svg" alt=""&gt;
      &lt;/a&gt;
    
  &lt;/div&gt;
  &lt;div class="ltag__stackexchange--body"&gt;
    
&lt;p&gt;No. At least not one that is any good nor represents how a screen reader actually reads a page or responds to ARIA.&lt;/p&gt;
&lt;p&gt;The best answer is to test in real screen readers, ideally by getting real users as they know how to use these tools. Consider contacting your local…&lt;/p&gt;
    
  &lt;/div&gt;
  &lt;div class="ltag__stackexchange--btn--container"&gt;
    &lt;a href="https://stackoverflow.com/questions/43340690/is-there-an-online-emulating-screen-reader-tool-to-test-against-a-custom-web-pag/43368748#43368748" class="ltag__stackexchange--btn" rel="noopener noreferrer"&gt;Open Full Answer&lt;/a&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;There doesn't appear to be any (large) players out there providing automated solutions for ScreenReader testing.&lt;/p&gt;

&lt;p&gt;There are at least promising signs that this &lt;em&gt;could&lt;/em&gt; be on the horizon, with companies starting to offer live testing solutions around assistive technologies such as &lt;a href="https://assistivlabs.com/" rel="noopener noreferrer"&gt;Assistiv Labs&lt;/a&gt; and &lt;a href="https://www.browserstack.com/live/features" rel="noopener noreferrer"&gt;BrowserStack Live&lt;/a&gt; offering the ability to use ScreenReaders on remote VMs so you can manually test setups for devices or tooling you don't own or have licenses for. This makes manual a11y testing more accessibile (🥁), but doesn't solve our manual overhead problem.&lt;/p&gt;

&lt;p&gt;Digging further started to yield a little more promise with the discovery of &lt;a href="https://github.com/AccessLint/auto-vo/" rel="noopener noreferrer"&gt;auto-vo&lt;/a&gt;, &lt;a href="https://github.com/phenomnomnominal/screen-reader-reader" rel="noopener noreferrer"&gt;screen-reader-reader&lt;/a&gt;, and &lt;a href="https://github.com/coryrylan/web-test-runner-voiceover" rel="noopener noreferrer"&gt;web-test-runner-voiceover&lt;/a&gt; (&lt;a href="https://coryrylan.com/blog/testing-screen-readers-with-web-test-runner-voiceover" rel="noopener noreferrer"&gt;article&lt;/a&gt; - just found this while writing this post!) following the finding of a tweet from Smashing Magazine and then rabbit-holing on twitter... 🕳🐇&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1407690678591700992-925" src="https://platform.twitter.com/embed/Tweet.html?id=1407690678591700992"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1407690678591700992-925');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1407690678591700992&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;You can check out the &lt;a href="https://www.smashingmagazine.com/2021/06/automating-screen-reader-testing-macos-autovo/" rel="noopener noreferrer"&gt;Smashing Magazine article here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;These libraries offer a promising start to the idea of automating ScreenReader testing. Focus across the 3 appears to be primarily on VoiceOver for MacOS (perhaps, like me, because thats the development machine they use!), but &lt;code&gt;screen-reader-reader&lt;/code&gt; starts to pave the way for automating NVDA on windows machines. 💥&lt;/p&gt;

&lt;p&gt;I haven't managed to have much success with &lt;code&gt;auto-vo&lt;/code&gt; but, you can see it in action with explanations through &lt;a href="https://twitter.com/ckundo" rel="noopener noreferrer"&gt;@ckundo&lt;/a&gt;'s twitter thread:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1402711360216223747-320" src="https://platform.twitter.com/embed/Tweet.html?id=1402711360216223747"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1402711360216223747-320');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1402711360216223747&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;And with it's precedent, you can imagine taking the core of it and making a far more generic and powerful API for control over VoiceOver, and potentially any ScreenReader... which is exactly what &lt;code&gt;screen-reader-reader&lt;/code&gt; appears to have aimed to do 2 years ago! Sadly neither of these packages appear to have maintained velocity to mature into the testing ecosystem... my suspicions being 2-fold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They aren't gracefully plug-n-play with the popular browser testing frameworks of today&lt;/li&gt;
&lt;li&gt;They don't work in (any?) CI environments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The prior point is where I see more promise with &lt;code&gt;web-test-runner-voiceover&lt;/code&gt;, the ability to easily integrate ScreenReader test flows into your existing test suite is an excellent sell - though I suspect success would need to come in the form of a "core" ScreenReader driver module with additional integration modules so you get support for the Cypress, Playwright, WDIO, Nightwatch, Protractor, etc. of the world.&lt;/p&gt;

&lt;p&gt;The latter point, I'll come to... 😅&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding to the soup
&lt;/h2&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%2Ffxtymqcirbyiz3riq652.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%2Ffxtymqcirbyiz3riq652.png" alt="xkcd comic. How Standards Proliferate: Situation: There are 14 competing standards. " width="500" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fueled by the promise that it could actually be feasible to automate testing with ScreenReaders I have added my own flavour into the mixing pot with:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/guidepup" rel="noopener noreferrer"&gt;
        guidepup
      &lt;/a&gt; / &lt;a href="https://github.com/guidepup/guidepup" rel="noopener noreferrer"&gt;
        guidepup
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Screen reader automation library for testing.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/guidepup/guidepup/raw/main/img/logo.png"&gt;&lt;img alt="" height="120px" width="120px" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fguidepup%2Fguidepup%2Fraw%2Fmain%2Fimg%2Flogo.png"&gt;&lt;/a&gt;
  &lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Guidepup&lt;/h1&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
  &lt;a href="https://www.npmjs.com/package/@guidepup/guidepup" rel="nofollow noopener noreferrer"&gt;&lt;img alt="Guidepup available on NPM" src="https://camo.githubusercontent.com/8f96bbe3fd0dc8e090ba7127ecc9fadeea3cc05ed32bf27a375dead63975cb30/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f4067756964657075702f6775696465707570"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/guidepup/guidepup/actions/workflows/test.yml" rel="noopener noreferrer"&gt;&lt;img alt="Guidepup test workflows" src="https://github.com/guidepup/guidepup/workflows/Test/badge.svg"&gt;&lt;/a&gt;
  &lt;a href="https://github.com/guidepup/guidepup/blob/main/LICENSE" rel="noopener noreferrer"&gt;&lt;img alt="Guidepup uses the MIT license" src="https://camo.githubusercontent.com/c41e10cac6505edd903b68373e965994ea6e313041109d2febfd9c0073dc9608/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f67756964657075702f6775696465707570"&gt;&lt;/a&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;
&lt;a href="https://guidepup.dev" rel="nofollow noopener noreferrer"&gt;Documentation&lt;/a&gt; | &lt;a href="https://www.guidepup.dev/docs/api/class-guidepup" rel="nofollow noopener noreferrer"&gt;API Reference&lt;/a&gt;
&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://support.apple.com/en-gb/102662" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/89a454b8b3ced48262be10e6becf528c3c0a3a9c452c09baebdc3a1b85ab4e83/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d536f6e6f6d612d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Sonoma Support"&gt;&lt;/a&gt;
&lt;a href="https://support.apple.com/en-gb/102662" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/c2a34109af9267cf5a8e6211b0282743dfdc5774a90dd708741ac591f4704f5e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d536571756f69612d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Sequoia Support"&gt;&lt;/a&gt;
&lt;a href="https://support.apple.com/en-gb/102662" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/24b3eba60fc46e69487a5deea090b32b8b127c55e1a2d8e439754b4326defdb5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d5461686f652d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Tahoe Support"&gt;&lt;/a&gt;
&lt;a href="https://support.apple.com/en-gb/127455" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/8b7195bd11f71436ee71ef9dd06f4502f841272ad057f837d5ffdc1f5b036319/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d476f6c64656e253230476174652d626c75652e7376673f6c6f676f3d6170706c65" alt="macOS Golden Gate Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2022" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/259f2bff2217376c92d8a67673bf15762bdf1b5777c7ef04e0a5d91dacc9375e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323032322d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2022 Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2025" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1f210e6d14358d56864cdeee873e708af03e4a86b2c5532f402cbb75c511bf0a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323032352d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2025 Support"&gt;&lt;/a&gt;
&lt;a href="https://releases.ubuntu.com/resolute/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6c6c3ddf2b861e4190510a8475a704a923c6977900dd651fa726ce987abf6666/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7562756e74752d32362e30342532422d626c75652e7376673f6c6f676f3d7562756e7475" alt="Ubuntu Resolute Support"&gt;&lt;/a&gt;
&lt;a href="https://www.debian.org/releases/trixie/" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1a84301d45d45c72899ae174187fd6e1ff8acbf947c1d70781f80801bcf96c19/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f64656269616e2d31332532422d626c75652e7376673f6c6f676f3d64656269616e" alt="Debian Trixie Support"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Guidepup is a screen reader automation library for testing.&lt;/p&gt;
&lt;p&gt;It enables testing for &lt;a href="https://www.guidepup.dev/docs/api/class-nvda" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;NVDA on Windows&lt;/b&gt;&lt;/a&gt;, &lt;a href="https://www.guidepup.dev/docs/api/class-orca" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;Orca on Linux&lt;/b&gt;&lt;/a&gt;, and &lt;a href="https://www.guidepup.dev/docs/api/class-voiceover" rel="nofollow noopener noreferrer"&gt;&lt;b&gt;VoiceOver on macOS&lt;/b&gt;&lt;/a&gt; with a single API.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Capabilities&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Full Control&lt;/strong&gt; - If a screen reader has a keyboard command, then Guidepup supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mirrors Real User Experience&lt;/strong&gt; - Assert on what users really do and hear when using screen readers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework Agnostic&lt;/strong&gt; - Run with Jest, with Playwright, as an independent script, no vendor lock-in.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Getting Started&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Set up your machine for screen reader automation:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npx @guidepup/setup setup&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Install Guidepup to your project:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm install @guidepup/guidepup&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Install the Guidepup screen reader assets:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npx @guidepup/setup install&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;And get cracking with your first screen reader automation code!&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Examples&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Head over to the &lt;a href="https://www.guidepup.dev/" rel="nofollow noopener noreferrer"&gt;Guidepup Website&lt;/a&gt; for guides, real world examples, environment setup, and complete API documentation with examples.&lt;/p&gt;
&lt;p&gt;You can also check out these &lt;a href="https://github.com/guidepup/guidepup/tree/main/examples" rel="noopener noreferrer"&gt;examples&lt;/a&gt; to…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/guidepup/guidepup" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;I fear with 3 attempts already out there I am somewhat living one of my favourite xkcd comics... but worst case hopefully the &lt;code&gt;guidepup&lt;/code&gt; code will serve as a platform to help bolster whatever tool emerges on top! 🙃&lt;/p&gt;

&lt;p&gt;Beyond some of the previous attempts I have &lt;a href="https://www.apple.com/voiceover/info/guide/_1131.html" rel="noopener noreferrer"&gt;codified 226 of the keyboard gestures&lt;/a&gt;, 129 keyboard commander commands, as well as exposed most of the built-in AppleScript APIs for VoiceOver. So far I've had &lt;a href="https://github.com/guidepup/guidepup/tree/main/examples/playwright-voiceover" rel="noopener noreferrer"&gt;good success integrating with Playwright&lt;/a&gt;, and suspect it would be similar for other browser testing tools.&lt;/p&gt;

&lt;p&gt;The real gotcha which I sense others have faced is trying to get any such solution into a CI environment - if a tool can't run reliably on a remote VM then it simply isn't scalable for teams to use for automated workflows. Specifically, trying to get VoiceOver to be controllable in CI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting over the VoiceOver issue
&lt;/h2&gt;

&lt;p&gt;The goto for scripting and automating applications on MacOS is through AppleScript. To ensure users have a degree of protection from malicious scripts, applications that want to run AppleScript to control other applications have to first be allowlisted in the "Accessibility" section of the "System Preferences" "Security &amp;amp; Privacy" pane.&lt;/p&gt;

&lt;p&gt;This leads to the first hurdle, if we want to automate VoiceOver from the terminal instance in CI we can't remote in and click links and enter passwords every run! Luckily, this is a well understood problem space, and many CI providers are already setup with the required changes to the TCC.db (transparency, consent, control!) on MacOS. Check out &lt;a href="https://rainforest.engineering/2021-02-09-macos-tcc/" rel="noopener noreferrer"&gt;this article&lt;/a&gt; for details on TCC. Curious what the code for this configuration looks like? Check out the &lt;a href="https://github.com/actions/virtual-environments/blob/main/images/macos/provision/configuration/configure-tccdb-macos11.sh" rel="noopener noreferrer"&gt;GitHub actions virtual-environment repo&lt;/a&gt; for a good example. 🤓&lt;/p&gt;

&lt;p&gt;There is a second hurdle though. In order to control VoiceOver through AppleScript you also need this checkbox ticked:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wmnf63mvbonu8xuxg3f.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5wmnf63mvbonu8xuxg3f.png" alt="" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Locally you can automate the checking of the tickbox with an AppleScript (feels like privilege escalation to use the very script to unlock more control!), e.g. the following works on Monterey:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Startup delay to reduce chance of "Application isn't running (-600)" errors&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.000000&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c1"&gt;-- Open VoiceOver Utility&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VoiceOver Utility"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;activate&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"System Events"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VoiceOver Utility"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Can not find VoiceOver Utility window"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VoiceOver Utility"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="c1"&gt;-- Prevent VoiceOver welcome message on startup&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;checkbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;splitter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Can not find VoiceOver welcome message checkbox"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;

            &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nv"&gt;click&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;checkbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;splitter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="c1"&gt;-- Enable AppleScript control&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;checkbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;splitter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Can not find AppleScript control checkbox"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;

            &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nv"&gt;click&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;checkbox&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;splitter&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c1"&gt;-- Wait for SecurityAgent dialog to open&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;until&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SecurityAgent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Can not find SecurityAgent window"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c1"&gt;-- Enter credentials&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="na"&gt;key code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;using&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;shift&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;down&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;keystroke&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;USERNAME&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- update accordingly, though beware of credentials in plaintext!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="na"&gt;key code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nv"&gt;keystroke&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;PASSWORD&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- update accordingly, though beware of credentials in plaintext!&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="c1"&gt;-- Wait for SecurityAgent dialog to close&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;timeoutSeconds&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;exists&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SecurityAgent"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;current date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;endDate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SecurityAgent window won't close"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt;

        &lt;/span&gt;&lt;span class="nb"&gt;delay&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;repeat&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="k"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="k"&gt;tell&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;application&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VoiceOver Utility"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;quit&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But when you try such a script in the likes of GitHub actions the "SecurityAgent" dialog never appears meaning we can't authenticate this configuration.&lt;/p&gt;

&lt;p&gt;Diving into the depths of VoiceOver Utility configuration, it appears that the checkbox drives two pieces of configuration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The creation of a &lt;code&gt;/private/var/db/Accessibility/.VoiceOverAppleScriptEnabled&lt;/code&gt; file (containing the single character &lt;code&gt;a&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The setting of VoiceOver system preference defaults&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The latter can be easily resolved through running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;defaults write com.apple.VoiceOver4/default SCREnableAppleScript 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the prior is a bit more of an issue... the &lt;code&gt;/private/var/db/&lt;/code&gt; directory and files therein are protected by Apple's SIP (Security Integrity Protection - &lt;a href="https://support.apple.com/en-gb/HT204899" rel="noopener noreferrer"&gt;read more here!&lt;/a&gt;). This can be disabled in recovery mode (be careful, it’s there for a reason) but rebooting a VM isn’t an option in CI typically. 😞&lt;/p&gt;

&lt;p&gt;This lead me to wonder how do "native" Apple apps manage to write to SIP files if even sudo is a no-go?! Transpires they use the Security Foundation Framework to obtain authorisation with a special &lt;code&gt;"system.preferences"&lt;/code&gt; right using the &lt;a href="https://developer.apple.com/documentation/securityfoundation/sfauthorization/1417652-obtainwithright?language=objc" rel="noopener noreferrer"&gt;&lt;code&gt;obtainWithRight&lt;/code&gt;&lt;/a&gt; API.&lt;/p&gt;

&lt;p&gt;So after a several hour download of Xcode I dived into writing my first piece of swift... somewhat wary that the only prior art for this was for &lt;a href="https://stackoverflow.com/questions/28096630/mac-os-x-10-10-reorder-preferred-networks" rel="noopener noreferrer"&gt;preferred networks&lt;/a&gt;, this &lt;a href="https://developer.apple.com/forums/thread/96740" rel="noopener noreferrer"&gt;unanswered thread&lt;/a&gt; and some defcon Apple malware talks...&lt;/p&gt;

&lt;p&gt;This yielded something that appears to ask for permissions... and then doesn't work 😅&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env swift&lt;/span&gt;

&lt;span class="kd"&gt;import&lt;/span&gt; &lt;span class="kt"&gt;SecurityFoundation&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/var/db/Accessibility/.VoiceOverAppleScriptEnabled"&lt;/span&gt;

&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;guard&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;authref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;SFAuthorization&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as?&lt;/span&gt; &lt;span class="kt"&gt;SFAuthorization&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="kt"&gt;CocoaError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fileWriteNoPermission&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;authent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Authenticator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sharedAuthenticator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;authref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;obtain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;withRight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"system.preferences"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extendRights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactionAllowed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;preAuthorize&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;authref&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;invalidateCredentials&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;fileManager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;FileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Check if file exists&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fileExists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;atPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Delete file&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;fileManager&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;atPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"File does not exist"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kt"&gt;NSError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"An error took place: &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error Code=513 "“.VoiceOverAppleScriptEnabled” couldn’t be removed because you don’t have permission to access it."
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It transpires &lt;a href="https://www.quora.com/Is-there-a-way-to-let-a-specific-app-through-System-Integrity-Protection-on-macOS/answer/Kai-Howells?ch=10&amp;amp;oid=37398215&amp;amp;share=e92371a4&amp;amp;target_type=answer" rel="noopener noreferrer"&gt;even with authorization, SIP is a no-go without Apple signing keys for special entitlements&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  So where do we go from here?
&lt;/h2&gt;

&lt;p&gt;Despite some of the complications mentioned above, it's not all doom and gloom! The folks at Microsoft who maintain the GitHub Actions virtual machines &lt;a href="https://github.com/actions/virtual-environments/issues/4770" rel="noopener noreferrer"&gt;have been very receptive to getting this setting enabled&lt;/a&gt;, and there's no indication (yet) that we should face the same issues for NVDA. ☺️&lt;/p&gt;




&lt;p&gt;Seen any cool ScreenReader automation tools out there that are worth a share?&lt;/p&gt;

&lt;p&gt;Got any ideas to solve some of the challenges mentioned?&lt;/p&gt;

&lt;p&gt;Want to help out building out some cool tooling for a11y automated testing?&lt;/p&gt;

&lt;p&gt;Reach out on my twitter &lt;a href="https://twitter.com/CraigMorten" rel="noopener noreferrer"&gt;@CraigMorten&lt;/a&gt;, or leave a comment below! Will be great to hear from you! 🚀🚀&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>a11y</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Kickstart Your Deno Web Project With The Opine CLI</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Sat, 09 Jan 2021 12:35:16 +0000</pubDate>
      <link>https://dev.to/craigmorten/kickstart-your-deno-web-project-with-the-opine-cli-1mkd</link>
      <guid>https://dev.to/craigmorten/kickstart-your-deno-web-project-with-the-opine-cli-1mkd</guid>
      <description>&lt;p&gt;In this third &lt;a href="https://github.com/asos-craigmorten/opine" rel="noopener noreferrer"&gt;Opine&lt;/a&gt; article we will be looking at how you blast through your website project setup by using the &lt;a href="https://github.com/cmorten/opine-cli" rel="noopener noreferrer"&gt;Opine CLI&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; &lt;a href="https://dev.to/craigmorten/opine-tutorial-part-2-creating-a-website-in-deno-37o3"&gt;Opine Tutorial Part 2: Creating A Website In Deno&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Objective:&lt;/strong&gt; Start your own new website projects using the &lt;a href="https://github.com/cmorten/opine-cli" rel="noopener noreferrer"&gt;Opine CLI&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  Overview
&lt;/h1&gt;

&lt;p&gt;This article will cover how you can create a basic template for a website using the &lt;a href="https://github.com/cmorten/opine-cli" rel="noopener noreferrer"&gt;Opine CLI&lt;/a&gt; for Deno.&lt;/p&gt;

&lt;p&gt;In fact, the template created will be remarkably similar to what we covered in the &lt;a href="https://dev.to/craigmorten/opine-tutorial-part-2-creating-a-website-in-deno-37o3"&gt;previous article&lt;/a&gt; in this series! We recommend checking that out if you want experience building a project from scratch.&lt;/p&gt;

&lt;p&gt;Because we have already covered project structure, and explained concepts such as views, we will focus primarily here on just how to bootstrap your project and the different options available.&lt;/p&gt;

&lt;h1&gt;
  
  
  Using the Opine CLI
&lt;/h1&gt;

&lt;p&gt;You can install the Opine CLI using Deno's &lt;a href="https://deno.land/manual/tools/script_installer" rel="noopener noreferrer"&gt;script installer commandlet&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;deno install -f -q --allow-read --allow-write --allow-net --unstable https://deno.land/x/opinecli@1.0.2/opine-cli.ts
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This install command creates a small executable shell script wrapper which runs Deno using the specified CLI flags and main module. The generated script is then placed into the installation root's bin directory. The exact root directory used is determined by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;--root&lt;/code&gt; flag, if provided;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;DENO_INSTALL_ROOT&lt;/code&gt; environment variable, if set;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$HOME/.deno&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Note you will need to add the installation directory to your path in order to invoke the CLI with providing a full directory path.&lt;/p&gt;

&lt;p&gt;For example, add &lt;code&gt;export PATH="$HOME/.deno/bin:$PATH"&lt;/code&gt; to your &lt;code&gt;$HOME/.bashrc&lt;/code&gt;. Remember you will need to source this file or restart your shell / terminal for it to take effect!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You should now be able to execute the Opine CLI from any shell! 🎉 🎉&lt;/p&gt;

&lt;p&gt;The Opine CLI has several options, the easiest way to view them all is using the &lt;code&gt;--help&lt;/code&gt; or &lt;code&gt;-h&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;opine-cli &lt;span class="nt"&gt;--help&lt;/span&gt;
&lt;span class="go"&gt;
  Usage:   opine-cli [option] [dir]
  Version: v1.0.2                  

  Description:

    Opine's application generator.

  Options:

    -h, --help                      - Show this help.                            
    -V, --version                   - Show the version number for this program.  
&lt;/span&gt;&lt;span class="gp"&gt;    -v, --view     &amp;lt;engine:string&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;- add view &amp;lt;engine&amp;gt; support &lt;span class="o"&gt;(&lt;/span&gt;ejs|eta&lt;span class="o"&gt;)&lt;/span&gt;        
&lt;span class="go"&gt;    -g, --git                       - add .gitignore                             
    -f, --force                     - force on non-empty directory   
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fi%2F05qclunuzjcloxta0sjq.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%2Fi%2F05qclunuzjcloxta0sjq.png" alt="Terminal window displaying the output of running the opine-cli --help command" width="703" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can set up an Opine project in the &lt;em&gt;current directory&lt;/em&gt; you are in without any view engine, just plain old CSS, HTML and JS, by simply running the CLI without any options or flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;opine-cli
&lt;span class="go"&gt;
   create : public/
   create : public/js/
   create : public/images/
   create : public/css/
   create : routes/
   create : public/index.html
   create : public/css/style.css
   create : routes/index.ts
   create : mod.ts
   create : routes/users.ts
   create : app.ts
   create : deps.ts

   run the app:
&lt;/span&gt;&lt;span class="gp"&gt;     $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;deno run &lt;span class="nt"&gt;--allow-net&lt;/span&gt; &lt;span class="nt"&gt;--allow-read&lt;/span&gt; &lt;span class="nt"&gt;--allow-env&lt;/span&gt; mod.ts
&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fi%2F7hswl97m5oty6cnwr31j.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%2Fi%2F7hswl97m5oty6cnwr31j.png" alt="Terminal window displaying the output of running the opine-cli command" width="640" height="460"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;The CLI will list all of the files and directories that it creates, and at the end of the output explains how you can run the application.&lt;/p&gt;

&lt;p&gt;You can also specify a directory to use by providing it to the CLI as an argument (it will be created if it does not exist):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;opine-cli hello-opine
&lt;span class="go"&gt;
   create : hello-opine/
   create : hello-opine/public/
   create : hello-opine/public/js/
   create : hello-opine/public/images/
   create : hello-opine/public/css/
   create : hello-opine/routes/
   create : hello-opine/public/index.html
   create : hello-opine/public/css/style.css
   create : hello-opine/routes/index.ts
   create : hello-opine/mod.ts
   create : hello-opine/routes/users.ts
   create : hello-opine/app.ts
   create : hello-opine/deps.ts

   change directory:
&lt;/span&gt;&lt;span class="gp"&gt;     $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;hello-opine
&lt;span class="go"&gt;
   run the app:
&lt;/span&gt;&lt;span class="gp"&gt;     $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;deno run &lt;span class="nt"&gt;--allow-net&lt;/span&gt; &lt;span class="nt"&gt;--allow-read&lt;/span&gt; &lt;span class="nt"&gt;--allow-env&lt;/span&gt; mod.ts
&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fi%2F8b8f6o67eb7bggzeduec.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%2Fi%2F8b8f6o67eb7bggzeduec.png" alt="Terminal window displaying the output of running the opine-cli command with a single argument of hello-opine to specify the directory to use" width="640" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Opine CLI also offers the ability to set up a view / template engine within your project. To add view logic, simply provide a &lt;code&gt;--view&lt;/code&gt; or &lt;code&gt;-v&lt;/code&gt; flag followed by a supported view engine.&lt;/p&gt;

&lt;p&gt;For example, we can opt to use the &lt;a href="https://github.com/eta-dev/eta" rel="noopener noreferrer"&gt;eta&lt;/a&gt; view engine in our Opine web project by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;opine-cli opine-eta-example &lt;span class="nt"&gt;--view&lt;/span&gt; eta
&lt;span class="go"&gt;
   create : opine-eta-example/
   create : opine-eta-example/public/
   create : opine-eta-example/public/js/
   create : opine-eta-example/public/images/
   create : opine-eta-example/public/css/
   create : opine-eta-example/routes/
   create : opine-eta-example/views/
   create : opine-eta-example/routes/users.ts
   create : opine-eta-example/public/css/style.css
   create : opine-eta-example/mod.ts
   create : opine-eta-example/routes/index.ts
   create : opine-eta-example/views/error.eta
   create : opine-eta-example/app.ts
   create : opine-eta-example/views/index.eta
   create : opine-eta-example/deps.ts

   change directory:
&lt;/span&gt;&lt;span class="gp"&gt;     $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;opine-eta-example
&lt;span class="go"&gt;
   run the app:
&lt;/span&gt;&lt;span class="gp"&gt;     $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;deno run &lt;span class="nt"&gt;--allow-net&lt;/span&gt; &lt;span class="nt"&gt;--allow-read&lt;/span&gt; &lt;span class="nt"&gt;--allow-env&lt;/span&gt; &lt;span class="nt"&gt;--unstable&lt;/span&gt; mod.ts
&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fi%2Fdueanadq7xaa1hwdtma8.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%2Fi%2Fdueanadq7xaa1hwdtma8.png" alt="Terminal window displaying the output of running the commnd opine-cli opine-eta-example --view eta" width="689" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice how by adding the &lt;code&gt;--view&lt;/code&gt; flag the CLI has now introduced a &lt;code&gt;views&lt;/code&gt; directory with our templates, and the &lt;code&gt;app.ts&lt;/code&gt; is automatically set up to use the provided engine.&lt;/p&gt;

&lt;h1&gt;
  
  
  Running the application
&lt;/h1&gt;

&lt;p&gt;Let's use the CLI to build an Opine web application that uses &lt;a href="https://ejs.co/" rel="noopener noreferrer"&gt;ejs&lt;/a&gt; view templates via the &lt;a href="https://github.com/syumai/dejs" rel="noopener noreferrer"&gt;dejs&lt;/a&gt; Deno module, and put in a new &lt;code&gt;./opine-ejs-example&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;opine-cli opine-ejs-example &lt;span class="nt"&gt;--view&lt;/span&gt; ejs
&lt;span class="go"&gt;
   create : opine-ejs-example/
   create : opine-ejs-example/public/
   create : opine-ejs-example/public/js/
   create : opine-ejs-example/public/images/
   create : opine-ejs-example/public/css/
   create : opine-ejs-example/public/css/style.css
   create : opine-ejs-example/routes/
   create : opine-ejs-example/routes/index.ts
   create : opine-ejs-example/routes/users.ts
   create : opine-ejs-example/views/
   create : opine-ejs-example/views/error.ejs
   create : opine-ejs-example/views/index.ejs
   create : opine-ejs-example/mod.ts
   create : opine-ejs-example/app.ts
   create : opine-ejs-example/deps.ts

   change directory:
&lt;/span&gt;&lt;span class="gp"&gt;     $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;opine-ejs-example
&lt;span class="go"&gt;
   run the app:
&lt;/span&gt;&lt;span class="gp"&gt;     $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;deno run &lt;span class="nt"&gt;--allow-net&lt;/span&gt; &lt;span class="nt"&gt;--allow-read&lt;/span&gt; &lt;span class="nt"&gt;--allow-env&lt;/span&gt; mod.ts
&lt;span class="go"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we navigate to the new project directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;opine-ejs-example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now we can run our Opine web application using the provided command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;deno run &lt;span class="nt"&gt;--allow-net&lt;/span&gt; &lt;span class="nt"&gt;--allow-read&lt;/span&gt; &lt;span class="nt"&gt;--allow-env&lt;/span&gt; mod.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's load &lt;a href="http://localhost:3000/" rel="noopener noreferrer"&gt;http://localhost:3000/&lt;/a&gt; in your browser to check out the app. You should see something like:&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%2Fi%2Fwo6r8n7lqaxota8srhu3.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%2Fi%2Fwo6r8n7lqaxota8srhu3.png" alt="Chrome browser opened on http://localhost:3000/ with a title of " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Amazing! 🎉 🎉 You now have a working and running Opine application. Great Job!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Inspiration:&lt;/em&gt; This article draws some pointers from the &lt;a href="https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/skeleton_website" rel="noopener noreferrer"&gt;Express web framework&lt;/a&gt; series.&lt;/p&gt;

</description>
      <category>deno</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Top 6 Web Security Take-Aways From Google CTF 2020</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Thu, 03 Sep 2020 17:47:52 +0000</pubDate>
      <link>https://dev.to/craigmorten/top-6-web-security-take-aways-from-google-ctf-2020-2gg3</link>
      <guid>https://dev.to/craigmorten/top-6-web-security-take-aways-from-google-ctf-2020-2gg3</guid>
      <description>&lt;p&gt;A few weekends ago Google hosted it's annual Capture The Flag (CTF) competition: a set of computer security challenges involving reverse-engineering, cryptography, web technologies, and much more.&lt;/p&gt;

&lt;p&gt;The aim of a CTF is to solve challenges by exploiting vulnerabilities in the provided application, server etc. in order to find a "flag", usually an unguessable string, which can be traded for points. Teams try to find flags and receive points during the limited competition time window, so they rise up the leaderboard. Generally top teams can receive prizes, or are invited to a finals round (which happens in the Google CTF).&lt;/p&gt;

&lt;p&gt;In this post I will cover 5 of my top web security take-aways from the Google CTF web challenges. I won't go into full detail for every challenge, but instead focus on the vulnerabilities themselves and what you can do to prevent similar security holes in your applications.&lt;/p&gt;

&lt;p&gt;If you are interested in full challenge write-ups I recommend you check out the &lt;a href="https://ctftime.org/event/1041/tasks/" rel="noopener noreferrer"&gt;CTFTime Writeups&lt;/a&gt;. Google also post past challenges and solutions on the &lt;a href="https://github.com/google/google-ctf" rel="noopener noreferrer"&gt;Google CTF GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So let's check out some vulnerabilities!&lt;/p&gt;

&lt;h1&gt;
  
  
  1. Avoid Writing Custom Sanitizer Code
&lt;/h1&gt;

&lt;p&gt;Google's beginner challenge for the CTF involved creating "pastes" which could then be shared with another user.&lt;/p&gt;

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

&lt;p&gt;Most challenges involving user inputted content which is then reflected back to the user, and potentially other users, is almost certainly a cross-site scripting [&lt;a href="https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A7-Cross-Site_Scripting_(XSS)" rel="noopener noreferrer"&gt;OWASP 7 - XSS&lt;/a&gt;] challenge. Indeed, being a beginner challenge Google gave a fairly big clue in the page source with a comment including a backlog ticket number for fixing an XSS bug:&lt;/p&gt;

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

&lt;p&gt;In this instance, the pasted content gets passed through the &lt;a href="https://github.com/cure53/DOMPurify" rel="noopener noreferrer"&gt;DOMPurify&lt;/a&gt; library's &lt;code&gt;sanitize()&lt;/code&gt; method, which in this case does not have a known vulnerability. The reference to &lt;code&gt;/source&lt;/code&gt; combined with our pasted contents being added to the &lt;code&gt;note&lt;/code&gt; variable hints at attacking the server code, which for this challenge was provided.&lt;/p&gt;

&lt;p&gt;It is in the server source code that we find the Googlers have created their own custom sanitizer method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Who wants a slice? */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escape_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;unsafe&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unsafe&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;x3C&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;x3E&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The intent is clear: our note is to be written into a double quoted string using ejs templates, so first off a quick way to ensure strings are escaped (and therefore can't close off a set of quotes and perform XSS) is to use &lt;code&gt;JSON.stringify()&lt;/code&gt; which will add backslashes to quotes (i.e. &lt;code&gt;\"&lt;/code&gt;) in any passed string. Indeed if we copy this function into a JS REPL (e.g. Node prompt or developer tools console) we can see a payload of &lt;code&gt;- " -&lt;/code&gt; becomes &lt;code&gt;- \" -&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.slice(1, -1)&lt;/code&gt; operation then removes the first and last character from the output of &lt;code&gt;JSON.stringify()&lt;/code&gt;, which for a string are double quotes. The last two replaces then escape all triangle brackets characters so to prevent you closing / adding script tags.&lt;/p&gt;

&lt;p&gt;At first this might seem like a neat trick for escaping inputs - it certainly seems to work for any payload you can paste into the challenge's website, and it's neat and short. Unfortunately it's made a fundamental flaw in a key assumption about the user's input: that it will always be a string.&lt;/p&gt;

&lt;p&gt;Passing an array (e.g. &lt;code&gt;['- " -']&lt;/code&gt;) to the above method you'll instantly notice a difference. Instead of the first and last characters being a double quote, they are now square brackets which leaves a pair of unescaped double quotes as the first and last characters of the remaining string.&lt;/p&gt;

&lt;p&gt;This means passing a payload of &lt;code&gt;["; alert('xss'); //"]&lt;/code&gt; would allow us to bypass this custom sanitizer and execute an XSS attack. Passing an array is possible because the Express server has the extended &lt;code&gt;bodyParser.urlencoded()&lt;/code&gt; middleware enabled, allowing us to pass malicious payload in a POST body by using the extended syntax &lt;code&gt;content[]=; alert('xss'); //&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Ultimately this is a manifestation of &lt;a href="https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A8-Insecure_Deserialization" rel="noopener noreferrer"&gt;OWASP 8 - Insecure Deserialization&lt;/a&gt;. An insecure parser of a payload allowing attackers to perform a secondary XSS attack. 💥&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestions&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Where possible, always use well tested third party sanitizer libraries which cover all possible inputs. Avoid custom sanitizers as it is very easy miss something.&lt;/li&gt;
&lt;li&gt;Reduce the allowed Accept types to a know allowlist for API endpoints to reduce the scope of user payloads. For example, don't use unnecessary or overscoped body parsing middleware.&lt;/li&gt;
&lt;li&gt;Validate user payloads for type and contents and consider returning with &lt;code&gt;400 Bad Request&lt;/code&gt; like responses for invalid payloads. Using libraries such as &lt;a href="https://express-validator.github.io/docs/" rel="noopener noreferrer"&gt;express-validator&lt;/a&gt; can help make this simple.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Funnily enough, we find that the log-me-in web challenge suffers from very similar issues to the beginner challenge. Extended body parser syntax allows the attacker to bypass sanitization and leads to an SQL Injection [&lt;a href="https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A1-Injection" rel="noopener noreferrer"&gt;OWASP 1&lt;/a&gt;]. I add this note to encourage you to still be cautious with well maintained third party libraries, as they may still have gotchas despite seeming to perform sanitization. Specific payload validation will always harden your server's endpoints. 🔥&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;
  
  
  2. Beware Of document.referrer
&lt;/h1&gt;

&lt;p&gt;A gotcha that caught even the Google CTF creators out is the existence of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/referrer" rel="noopener noreferrer"&gt;document.referrer&lt;/a&gt; property.&lt;/p&gt;

&lt;p&gt;This property is set to either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An empty string in the case of direct navigation;&lt;/li&gt;
&lt;li&gt;The URL of the page where you navigated from, similar to the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer" rel="noopener noreferrer"&gt;Referer header&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The same value as the &lt;code&gt;href&lt;/code&gt; of the parent window's &lt;code&gt;document.location&lt;/code&gt; when inside an iframe.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the case of the Tech Support challenge, the last property setting meant that an iframe vulnerable to XSS leaked the credentials of the admin user, as the frame inherited the parent window's &lt;code&gt;href&lt;/code&gt; in this &lt;code&gt;referrer&lt;/code&gt; property [&lt;a href="https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A3-Sensitive_Data_Exposure" rel="noopener noreferrer"&gt;OWASP 3 - Sensitive Data Exposure&lt;/a&gt;]. 😢&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1297882502661640193-685" src="https://platform.twitter.com/embed/Tweet.html?id=1297882502661640193"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1297882502661640193-685');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1297882502661640193&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestions&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Avoid plaintext (or otherwise) credentials in any part of the URL, ideally for all pages, but especially for any public facing pages or pages containing iframes with a public interface.&lt;/li&gt;
&lt;li&gt;Educate your users about credential security and management best practices.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  3. Avoid User Inputted HTML If Can!
&lt;/h1&gt;

&lt;p&gt;The least solved web challenge with only 10 completions was the Safe HTML Paste challenge. This challenge was remarkably similar to the beginner Pasteurized challenge mentioned above, and allowed you to paste arbitrary content, view it and share it with an admin user.&lt;/p&gt;

&lt;p&gt;Unlike the beginner challenge the server code was off-limits and appeared to be rock-solid. What this CTF demonstrated was how difficult it is to correctly sanitize arbitrary HTML, and how even a popular and well maintained library such as the &lt;a href="https://github.com/google/closure-library" rel="noopener noreferrer"&gt;Google Closure Library&lt;/a&gt; can have weaknesses. Further still, it demonstrates how easy it is to use a library that has a well documented vulnerability and patched version and yet be using an outdated and vulnerable version! [&lt;a href="https://owasp.org/www-project-top-ten/OWASP_Top_Ten_2017/Top_10-2017_A9-Using_Components_with_Known_Vulnerabilities" rel="noopener noreferrer"&gt;OWASP 9 - Using Components with Known Vulnerabilities&lt;/a&gt;]&lt;/p&gt;

&lt;p&gt;The attack is well documented in &lt;a href="https://blog.bi0s.in/2020/08/26/Web/GoogleCTF20-SafeHtmlPaste/" rel="noopener noreferrer"&gt;this write-up&lt;/a&gt; and &lt;a href="https://research.securitum.com/the-curious-case-of-copy-paste/" rel="noopener noreferrer"&gt;this research&lt;/a&gt; if you are interested to go through the DOM mutation details. 😄&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestions&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Avoid user inputted HTML content when possible.&lt;/li&gt;
&lt;li&gt;Always use the latest versions and patches of third party libraries.&lt;/li&gt;
&lt;li&gt;Regularly audit your libraries and their dependencies using tools like &lt;a href="https://github.com/retirejs/retire.js/" rel="noopener noreferrer"&gt;retire.js&lt;/a&gt; or &lt;a href="https://www.npmjs.com/package/snyk" rel="noopener noreferrer"&gt;snyk&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  &amp;nbsp;4. Self-XSS Should Not Be Ignored
&lt;/h1&gt;

&lt;p&gt;Coming back to the Tech Support challenge, the intended vulnerability path had a very interesting message - self-XSS when paired with cross-site request forgery (CSRF) can lead to dangerous session-hijacking.&lt;/p&gt;

&lt;p&gt;In the challenge we find that the lack of CSRF controls on the login allows us to force the victim to join our session in a frame which subsequently runs a self-XSS.&lt;/p&gt;

&lt;p&gt;Given the logged in frame is running in the victim's context, the self-XSS is granted privileged access to sibling frames allowing the attacker to manipulate, or in this case scrape, pages already open with the victim's previous session.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1297885195702501376-959" src="https://platform.twitter.com/embed/Tweet.html?id=1297885195702501376"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1297885195702501376-959');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1297885195702501376&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;This kind of vulnerability is sufficiently open that you don't even require a third-party domain to send leaked data to! See the video below of an attack on the challenge that uses the self-XSS to store the flag in the attacker's address field.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1299007923885146113-710" src="https://platform.twitter.com/embed/Tweet.html?id=1299007923885146113"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1299007923885146113-710');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1299007923885146113&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestions&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable CSRF protection on all forms, or ideally at least on any authentication / login flows.&lt;/li&gt;
&lt;li&gt;Close out any self-XSS vulnerabilities to prevent paired / secondary attacks.&lt;/li&gt;
&lt;li&gt;Enable strict content security policies (CSP) to prevent inline script execution without CSRF protection (e.g. nonce tokens).&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  5. Prototype Pollution Is A Real Issue
&lt;/h1&gt;

&lt;p&gt;Similar to the Tech Support, the All The Little Things challenge also had an unintended solution.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1297796633552400384-706" src="https://platform.twitter.com/embed/Tweet.html?id=1297796633552400384"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1297796633552400384-706');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1297796633552400384&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;One of the issues of this challenge was that user inputted content (via the &lt;code&gt;window.name&lt;/code&gt; property) was able to pollute the prototype of a heavily relied upon object by using the &lt;code&gt;__proto__&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;Prototype pollution can be a serious problem, especially in server-side authentication flows where attackers can attack to mutate the prototype to escalate their privileges. Several well known libraries such as &lt;a href="https://www.npmjs.com/package/lodash" rel="noopener noreferrer"&gt;Lodash&lt;/a&gt; have also &lt;a href="https://hackerone.com/reports/712065" rel="noopener noreferrer"&gt;been caught out as recently as this year&lt;/a&gt; making this a very current and real issue.&lt;/p&gt;

&lt;p&gt;In the case of this challenge, it was interesting to see the vulnerability exposed client-side and is yet another clear warning for website maintainers to always sanitize and validate any user input, no matter how inconspicuous!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestions&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Getting similar to previous ones now!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Where possible, always use well tested third party sanitizer libraries which cover all possible inputs. Avoid custom sanitizers as it is very easy miss something.&lt;/li&gt;
&lt;li&gt;Always use the latest versions and patches of third party libraries. Be sure to regularly audit your libraries and their dependencies using tools like &lt;a href="https://github.com/retirejs/retire.js/" rel="noopener noreferrer"&gt;retire.js&lt;/a&gt; or &lt;a href="https://www.npmjs.com/package/snyk" rel="noopener noreferrer"&gt;snyk&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Where performing custom object assignment, merging or otherwise, ensure that you are deny-listing malicious keys such as &lt;code&gt;__proto__&lt;/code&gt;, &lt;code&gt;constructor&lt;/code&gt;, and any variations thereof which might allow an attacker to change the intended property values of an object.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;
  
  
  6. Parantheses-less XSS Attacks In Strict CSP
&lt;/h1&gt;

&lt;p&gt;The final learning point from the Google CTF was the discovery of parentheses-less XSS attacks. I recommend you check out the below Medium article by the challenge creator &lt;a href="https://twitter.com/terjanq" rel="noopener noreferrer"&gt;@terjanq&lt;/a&gt;.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="https://medium.com/@terjanq/arbitrary-parentheses-less-xss-e4a1cf37c13d" class="ltag__link__link" rel="noopener noreferrer"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmiro.medium.com%2Fv2%2Fresize%3Afill%3A88%3A88%2F1%2A5t2zsHKSKPB_AMJbt0V6WA.png" alt="terjanq"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="https://medium.com/@terjanq/arbitrary-parentheses-less-xss-e4a1cf37c13d" class="ltag__link__link" rel="noopener noreferrer"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;Arbitrary Parentheses-less XSS. against strict CSP policies | by terjanq | Medium&lt;/h2&gt;
      &lt;h3&gt;terjanq ・ &lt;time&gt;Aug 17, 2023&lt;/time&gt; ・ 
      &lt;div class="ltag__link__servicename"&gt;
        &lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fmedium-f709f79cf29704f9f4c2a83f950b2964e95007a3e311b77f686915c71574fef2.svg" alt="Medium Logo"&gt;
        Medium
      &lt;/div&gt;
    &lt;/h3&gt;
&lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;



&lt;p&gt;Ultimately what we learn is that even in a setting as restricted as a JSONP callback, where almost all characters have been restricted, it is still possible to execute arbitrary XSS. In fact, there are several different attack payloads depending on the situation that can be used - check out &lt;a href="https://github.com/RenwaX23/XSS-Payloads" rel="noopener noreferrer"&gt;this GitHub repo&lt;/a&gt; of example payloads.&lt;/p&gt;

&lt;p&gt;What this shows is that even in restricted content security policy (CSP) situations, even the smallest XSS vulnerability can be exploited and escalated to an arbitrary attack.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Suggestions&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure no XSS exploits possible on your site (to the best of your ability!). Check out tools like &lt;a href="https://www.zaproxy.org/" rel="noopener noreferrer"&gt;OWASP ZAP&lt;/a&gt; to help with discovering issues - be sure to always have the permission of the target site first before running any penetration tools!&lt;/li&gt;
&lt;li&gt;Perform strict validation on potential user input. In this challenge, restricting the allowed JSONP callback values to a defined enum of strings would have prevented the exploit.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;That's all for now folks! Hope it made for interesting reading. 😄&lt;/p&gt;

&lt;p&gt;Did you take part in the Google CTF this year? If so, what was your favourite challenge? What security points did you learn? I would love to hear your comments, ideas and suggestions - drop a note in the section below.&lt;/p&gt;

&lt;p&gt;Till next time, stay secure! 🎤&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>node</category>
      <category>hacking</category>
    </item>
  </channel>
</rss>
