<?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.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>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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2786%2F1%2AY7Z3XCk4i3Kbym-tsFRy_Q.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%2Fcdn-images-1.medium.com%2Fmax%2F2786%2F1%2AY7Z3XCk4i3Kbym-tsFRy_Q.png" alt="VSCode tab open on a "&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F7360%2F1%2A-Jxv80wsLzSQ8l1zr1lxbA.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%2Fcdn-images-1.medium.com%2Fmax%2F7360%2F1%2A-Jxv80wsLzSQ8l1zr1lxbA.png" alt="Chrome open on Waitrose No.1 Dine In Meal Deal web page. Chrome DevTools is open on the "&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F8128%2F1%2A-CJlRbC_mCG8M07RMi2spA.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%2Fcdn-images-1.medium.com%2Fmax%2F8128%2F1%2A-CJlRbC_mCG8M07RMi2spA.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."&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4668%2F1%2ArlJbjYw6fvWyx3DpukfTLA.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%2Fcdn-images-1.medium.com%2Fmax%2F4668%2F1%2ArlJbjYw6fvWyx3DpukfTLA.png" alt="The John Lewis Women’s Dresses product listing page as viewed by a user with blurred vision."&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F4668%2F1%2AK0NwIRn7RvpCdKJ0KbGbsw.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%2Fcdn-images-1.medium.com%2Fmax%2F4668%2F1%2AK0NwIRn7RvpCdKJ0KbGbsw.png" alt="The John Lewis Women’s Dresses product listing page as viewed by a user with protanopia."&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F3860%2F1%2A1ueqotHdWdvwnlqSNrhUWQ.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%2Fcdn-images-1.medium.com%2Fmax%2F3860%2F1%2A1ueqotHdWdvwnlqSNrhUWQ.png" alt="Waitrose The King’s Coronation Jewel The Jack Russell Cake product detail page with 200% zoom on desktop."&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2532%2F1%2ACOcasHsknmhnKGAUS-QOHQ.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%2Fcdn-images-1.medium.com%2Fmax%2F2532%2F1%2ACOcasHsknmhnKGAUS-QOHQ.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: "&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn-images-1.medium.com%2Fmax%2F2570%2F1%2ARSUwzgqRLdXBi2O-o5LKLg.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%2Fcdn-images-1.medium.com%2Fmax%2F2570%2F1%2ARSUwzgqRLdXBi2O-o5LKLg.png" alt="Waitrose groceries homepage open in a Chrome browser. In the toolbar the Funkify extension has been activated, displaying a popup where the "&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"&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://res.cloudinary.com/practicaldev/image/fetch/s--MLa8yqIC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/10944/0%2AzXsicteUz8C1LnsK" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MLa8yqIC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/10944/0%2AzXsicteUz8C1LnsK" alt="A stack of engineering books, a phone, and a plant. The top book is titled  “Stunning CSS”." width="800" 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"&gt;KOBU Agency&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral"&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"&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"&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"&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"&gt;tabindex&lt;/a&gt; and / or &lt;a href="https://www.w3.org/TR/wai-aria-1.2/#aria-owns"&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"&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://res.cloudinary.com/practicaldev/image/fetch/s--wIXeATpK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/10396/0%2AX2PBBZt9RT5YC6kK" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--wIXeATpK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/10396/0%2AX2PBBZt9RT5YC6kK" alt="Laptop with a CSS file opened in a code editor." width="800" height="533"&gt;&lt;/a&gt; &lt;em&gt;Photo by &lt;a href="https://unsplash.com/@jantined?utm_source=medium&amp;amp;utm_medium=referral"&gt;Jantine Doornbos&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral"&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"&gt;flash of unstyled content (FOUC)&lt;/a&gt; or &lt;a href="https://web.dev/cls/"&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"&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"&gt;“user-agent sniffing”&lt;/a&gt; through packages such as &lt;a href="https://github.com/faisalman/ua-parser-js"&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"&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/"&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"&gt;react-responsive&lt;/a&gt;, a wrapper around the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/matchMedia"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="k"&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&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="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&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"&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"&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/"&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/"&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"&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/"&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/"&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"&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"&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"&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"&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"&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"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&gt;analyze&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="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="nx"&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"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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"&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://res.cloudinary.com/practicaldev/image/fetch/s--cuQK2bcS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5112/1%2AO6Btbgz7gIO9XSm-Yz8Yyw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cuQK2bcS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5112/1%2AO6Btbgz7gIO9XSm-Yz8Yyw.png" alt='Cypress UI with a failed test for "Has no a11y violations after button click". The test lists the error "A11Y ERROR! Heading-order on 2 Nodes" with message "1 accessibility violation was detected: expected 1 to equal 0". This error has been pinned and in the Chrome developer tools the Console tab is open listing further details on the error, including the error id, impact, tags, description, help description and url, and the relevant DOM nodes.' 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"&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/"&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/"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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://res.cloudinary.com/practicaldev/image/fetch/s--VYwaAMVz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4564/1%2AC52eAZ_FiBz6XjNJWwPnMQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VYwaAMVz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/4564/1%2AC52eAZ_FiBz6XjNJWwPnMQ.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 "Images must have alternative text" expanded to show details of the issue. A highlight button is toggled visually highlighting the image on the page that is missing an alt tag.' width="800" 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"&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"&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://res.cloudinary.com/practicaldev/image/fetch/s--kkj0NS0l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5736/1%2AxQxoLBGAhMOPwTno__L6zA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--kkj0NS0l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5736/1%2AxQxoLBGAhMOPwTno__L6zA.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: &amp;quot;Axe Linter (image-alt): Ensures img elements have alternative text or a role of none or presentation (dequeuniversity/image-alt)&amp;quot;." 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"&gt;browser extension&lt;/a&gt;, &lt;a href="https://github.com/GoogleChrome/lighthouse"&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://res.cloudinary.com/practicaldev/image/fetch/s--s0nZU4GJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5640/1%2A9PL9iukKsLT3JDHUMzRuOg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--s0nZU4GJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/5640/1%2A9PL9iukKsLT3JDHUMzRuOg.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"&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"&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"&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"&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://res.cloudinary.com/practicaldev/image/fetch/s--r1vdQsHU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6680/1%2ASDRe51FHo0fcVTtsDKbcUQ.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--r1vdQsHU--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn-images-1.medium.com/max/6680/1%2ASDRe51FHo0fcVTtsDKbcUQ.png" alt="" 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/"&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"&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/"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&gt;all&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;req&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="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="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&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="nx"&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://res.cloudinary.com/practicaldev/image/fetch/s--cvlA5cwZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/12000/0%2Alm55U33s-VhmQDyN" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cvlA5cwZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/12000/0%2Alm55U33s-VhmQDyN" alt="MacBook with VSCode open on an Express JS file." width="880" height="587"&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"&gt;Clément Hélardot&lt;/a&gt; on &lt;a href="https://unsplash.com?utm_source=medium&amp;amp;utm_medium=referral"&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"&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://res.cloudinary.com/practicaldev/image/fetch/s--BphXLSak--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AowDNriAsl4_XKP0ZZ4dB6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BphXLSak--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AowDNriAsl4_XKP0ZZ4dB6w.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="880" height="511"&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://res.cloudinary.com/practicaldev/image/fetch/s--zirMmmaE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AQ4IQ0vsyJ__OEIL-hMmLjA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zirMmmaE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AQ4IQ0vsyJ__OEIL-hMmLjA.png" alt="Chrome DevTools window open on the Memory tab showing options for selecting a profiling type." width="880" height="511"&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://res.cloudinary.com/practicaldev/image/fetch/s--xdQQL0dX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2ArlaZzAoqZbI0ooUEAgznmA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xdQQL0dX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2ArlaZzAoqZbI0ooUEAgznmA.png" alt="Chrome DevTools heap snapshot listing rows of Constructors." width="880" height="511"&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://res.cloudinary.com/practicaldev/image/fetch/s--XIFfOxLZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AqMuSPPMZ5Fog859UAG2zLg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XIFfOxLZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AqMuSPPMZ5Fog859UAG2zLg.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="880" height="511"&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://res.cloudinary.com/practicaldev/image/fetch/s--zRmYMQGA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2880/1%2ADScmAHekb1HVHb1QQOKgOA.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zRmYMQGA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/2880/1%2ADScmAHekb1HVHb1QQOKgOA.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="880" height="483"&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://res.cloudinary.com/practicaldev/image/fetch/s--FRklDjri--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2ATScH5MiYe_PiVC0HGSVGWg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FRklDjri--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2ATScH5MiYe_PiVC0HGSVGWg.png" alt="Chrome DevTools window showing the difference between Snapshot 1 and Snapshot 2" width="880" height="511"&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://res.cloudinary.com/practicaldev/image/fetch/s--CVDpve2u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AVQf-_ViFxHXOpUYB6ywypw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--CVDpve2u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AVQf-_ViFxHXOpUYB6ywypw.png" alt="Chrome DevTools window with the strings memory expanded." width="880" height="511"&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://res.cloudinary.com/practicaldev/image/fetch/s--xv2sTM_u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AJBH2UYY6uzz3_ZwuVoo5xw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--xv2sTM_u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AJBH2UYY6uzz3_ZwuVoo5xw.png" alt="Chrome DevTools window with the arrays memory expanded." width="880" height="511"&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://res.cloudinary.com/practicaldev/image/fetch/s--FliJNCsu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AjfOpBJQ84Kgha91NX9V8qg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--FliJNCsu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn-images-1.medium.com/max/3104/1%2AjfOpBJQ84Kgha91NX9V8qg.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="880" height="511"&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"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-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 driver for Playwright tests.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Guidepup Playwright&lt;/h1&gt;
&lt;/div&gt;
&lt;p&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/402c559da41524ca67eaa284cbabc12339947fa86594e72544e68710e7818f04/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/35b8b59cd683f02454fb3a52ca29e7efc7a4bd2328f73090e3b89f53667bd6ae/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f67756964657075702f67756964657075702d706c6179777269676874"&gt;&lt;/a&gt;&lt;/p&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://apps.apple.com/us/app/macos-monterey/id1576738294" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/46b55115a330fb2f01a9395a6032ddc5934dca37fc962488a2611a1fd9648749/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d4d6f6e65746172792d626c75652e7376673f6c6f676f3d6170706c65" alt="MacOS Monetary Support"&gt;&lt;/a&gt;
&lt;a href="https://apps.apple.com/us/app/macos-ventura/id1638787999" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fffc8b0d6a82ee74aaef1d6babac558a9aeb067b11dcb63de2a7ca1ce3e477d2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d56656e747572612d626c75652e7376673f6c6f676f3d6170706c65" alt="MacOS Ventura Support"&gt;&lt;/a&gt;
&lt;a href="https://apps.apple.com/us/app/macos-sonoma/id6450717509" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/cecc2b8a10cf265d850817f26c1ecafc91b3f57df432479bf00f9463d02e1ccf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d536f6d6f6e612d626c75652e7376673f6c6f676f3d6170706c65" alt="MacOS Sonoma Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-gb/software-download/windows10ISO" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1b557e83a6ff0af4379cf269bfb868eaca5b64c84a7004d801cb11a8da1222d0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77732d31302d626c75652e7376673f6c6f676f3d77696e646f77733130" alt="Windows 10 Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2019" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fcead3ffd22fa584c84b3480fe9bb6d21591da300d469b31a2aba6605604079f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323031392d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2019 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/675a9b297c2d74d058e3684d7eba1550409408d405fca80a34eada5e1ed36e77/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323032322d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2022 Support"&gt;&lt;/a&gt;&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; for writing screen reader tests that automate &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; and &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;/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 environment for screen reader automation with &lt;a href="https://github.com/guidepup/setup" rel="noopener noreferrer"&gt;&lt;code&gt;@guidepup/setup&lt;/code&gt;&lt;/a&gt;:&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&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;If you are using GitHub Actions, check out the dedicated &lt;a href="https://github.com/marketplace/actions/guidepup-setup" rel="noopener noreferrer"&gt;&lt;code&gt;guidepup/setup-action&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-yaml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;- &lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;Setup Environment&lt;/span&gt;
  &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;guidepup/setup-action&lt;/span&gt;&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;p&gt;Note: you require &lt;code&gt;@guidepup/guidepup&lt;/code&gt; and &lt;code&gt;@playwright/test&lt;/code&gt; as they are peer dependencies to this project.&lt;/p&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…&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="k"&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://media.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%2Fx4vnjufu0zxfvz8vdln5.gif" 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%2Fuploads%2Farticles%2Fx4vnjufu0zxfvz8vdln5.gif" alt="Gif over Playwright controlled Safari browser being driven with VoiceOver. Announcements read: "&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://media.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://media.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"&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;
  
  
   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://media.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://media.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 "&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://media.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://media.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 "&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://media.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://media.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 "&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;
  
  
   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://media.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://media.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 "&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/"&gt;Webpack HMR&lt;/a&gt; and &lt;a href="https://www.npmjs.com/package/react-refresh"&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"&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="nx"&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="nx"&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"&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"&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="nx"&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"&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"&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="nx"&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="nx"&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="nx"&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"&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="nb"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="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="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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="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="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;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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="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="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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&gt;fromFileUrl&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="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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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="nx"&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"&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="m"&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="m"&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"&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"&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"&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-677" src="https://platform.twitter.com/embed/Tweet.html?id=852866736848293888"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-852866736848293888-677');
  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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fstackoverflow-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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fstackexchange-arrow-up-eff2e2849e67d156181d258e38802c0b57fa011f74164a7f97675ca3b6ab756b.svg" alt=""&gt;
        &lt;div class="ltag__stackexchange--score-number"&gt;
          34
        &lt;/div&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%2Fstackexchange-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-480" src="https://platform.twitter.com/embed/Tweet.html?id=1407690678591700992"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1407690678591700992-480');
  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-253" src="https://platform.twitter.com/embed/Tweet.html?id=1402711360216223747"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1402711360216223747-253');
  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://media.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://media.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. "&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev.to%2Fassets%2Fgithub-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 driver for test automation.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Guidepup&lt;/h1&gt;
&lt;/div&gt;
&lt;p&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/f4276f81fe7ebc0dff926fed457947610558fad9ff8cd8b2b6c74a6c0c14d0b9/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/4781e4f0544e99cd8dbb7f3ccc3b261be07931234aa4e8aefaf736564518473f/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f67756964657075702f6775696465707570"&gt;&lt;/a&gt;&lt;/p&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://apps.apple.com/us/app/macos-monterey/id1576738294" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/46b55115a330fb2f01a9395a6032ddc5934dca37fc962488a2611a1fd9648749/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d4d6f6e65746172792d626c75652e7376673f6c6f676f3d6170706c65" alt="MacOS Monetary Support"&gt;&lt;/a&gt;
&lt;a href="https://apps.apple.com/us/app/macos-ventura/id1638787999" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fffc8b0d6a82ee74aaef1d6babac558a9aeb067b11dcb63de2a7ca1ce3e477d2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d56656e747572612d626c75652e7376673f6c6f676f3d6170706c65" alt="MacOS Ventura Support"&gt;&lt;/a&gt;
&lt;a href="https://apps.apple.com/us/app/macos-sonoma/id6450717509" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/cecc2b8a10cf265d850817f26c1ecafc91b3f57df432479bf00f9463d02e1ccf/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6d61636f732d536f6d6f6e612d626c75652e7376673f6c6f676f3d6170706c65" alt="MacOS Sonoma Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-gb/software-download/windows10ISO" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/1b557e83a6ff0af4379cf269bfb868eaca5b64c84a7004d801cb11a8da1222d0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77732d31302d626c75652e7376673f6c6f676f3d77696e646f77733130" alt="Windows 10 Support"&gt;&lt;/a&gt;
&lt;a href="https://www.microsoft.com/en-us/evalcenter/evaluate-windows-server-2019" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/fcead3ffd22fa584c84b3480fe9bb6d21591da300d469b31a2aba6605604079f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323031392d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2019 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/675a9b297c2d74d058e3684d7eba1550409408d405fca80a34eada5e1ed36e77/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f77696e646f77735f7365727665722d323032322d626c75652e7376673f6c6f676f3d77696e646f7773" alt="Windows Server 2022 Support"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Guidepup is a screen reader driver for test automation.&lt;/p&gt;
&lt;p&gt;It enables testing for &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; and &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; 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 environment for screen reader automation with &lt;a href="https://github.com/guidepup/setup" rel="noopener noreferrer"&gt;&lt;code&gt;@guidepup/setup&lt;/code&gt;&lt;/a&gt;:&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&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;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;awesome examples&lt;/a&gt; to learn how you could use Guidepup in your projects.&lt;/p&gt;
&lt;p&gt;Alternatively check…&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://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fguidepup%2Fguidepup%2Fc0d8771b68b3c54a571fe03c9447c67612668661%2Fguides%2Fvoiceover-prerequisites%2Fvoiceover_utility_checkbox.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%2Fraw.githubusercontent.com%2Fguidepup%2Fguidepup%2Fc0d8771b68b3c54a571fe03c9447c67612668661%2Fguides%2Fvoiceover-prerequisites%2Fvoiceover_utility_checkbox.png" alt=""&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;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight applescript"&gt;&lt;code&gt;&lt;span class="w"&gt;

&lt;/span&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;/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;/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;/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://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%2F05qclunuzjcloxta0sjq.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%2F05qclunuzjcloxta0sjq.png" alt="Terminal window displaying the output of running the opine-cli --help command"&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://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%2F7hswl97m5oty6cnwr31j.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%2F7hswl97m5oty6cnwr31j.png" alt="Terminal window displaying the output of running the opine-cli command"&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://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%2F8b8f6o67eb7bggzeduec.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%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"&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://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%2Fdueanadq7xaa1hwdtma8.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%2Fdueanadq7xaa1hwdtma8.png" alt="Terminal window displaying the output of running the commnd opine-cli opine-eta-example --view eta"&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://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%2Fwo6r8n7lqaxota8srhu3.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%2Fwo6r8n7lqaxota8srhu3.png" alt="Chrome browser opened on http://localhost:3000/ with a title of "&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;
  
  
   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>
    <item>
      <title>K8s Chaos Dive: Chaos-Mesh Part 2</title>
      <dc:creator>Craig Morten</dc:creator>
      <pubDate>Tue, 25 Aug 2020 14:45:11 +0000</pubDate>
      <link>https://dev.to/craigmorten/k8s-chaos-dive-chaos-mesh-part-2-536m</link>
      <guid>https://dev.to/craigmorten/k8s-chaos-dive-chaos-mesh-part-2-536m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In this series I walk through several different open source offerings for performing chaos testing / engineering within your Kubernetes clusters.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/craigmorten/k8s-chaos-dive-2-chaos-mesh-part-1-2i96"&gt;K8s Chaos Dive: Chaos-Mesh Part 1&lt;/a&gt; I covered an introduction to &lt;a href="https://github.com/chaos-mesh/chaos-mesh"&gt;Chaos-Mesh&lt;/a&gt; in which we installed the toolkit into our cluster and used a &lt;a href="https://chaos-mesh.org/docs/user_guides/podchaos_experiment/"&gt;&lt;code&gt;PodChaos&lt;/code&gt;&lt;/a&gt; experiment to kill off a random percentage of Nginx pods on a schedule.&lt;/p&gt;


&lt;div class="ltag__link"&gt;
  &lt;a href="/craigmorten" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__pic"&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Y1Jo1XbC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--0lf8WrhC--/c_fill%2Cf_auto%2Cfl_progressive%2Ch_150%2Cq_auto%2Cw_150/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/391875/c71634b2-c0ed-4bbb-be5b-91b905ab4a86.jpg" alt="craigmorten"&gt;
    &lt;/div&gt;
  &lt;/a&gt;
  &lt;a href="/craigmorten/k8s-chaos-dive-1-kube-monkey-4431" class="ltag__link__link"&gt;
    &lt;div class="ltag__link__content"&gt;
      &lt;h2&gt;K8s Chaos Dive: Kube-Monkey&lt;/h2&gt;
      &lt;h3&gt;Craig Morten ・ Aug 19 '20 ・ 10 min read&lt;/h3&gt;
      &lt;div class="ltag__link__taglist"&gt;
        &lt;span class="ltag__link__tag"&gt;#kubernetes&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#devops&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#testing&lt;/span&gt;
        &lt;span class="ltag__link__tag"&gt;#tutorial&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/a&gt;
&lt;/div&gt;


&lt;p&gt;Killing pods can be a great exercise for validating resiliency to pod death, something that can happen for a list of reasons in Kubernetes. Solutions tend to revolve around horizontal scaling (dependent on your target SLA):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure you have sufficient replicas for your application to handle &amp;lt; 100% failure - e.g. Node restarts, Node network disruption causes health-checks to fail. For those wanting to achieve a ~99.9% uptime SLA (depending on cloud provider).&lt;/li&gt;
&lt;li&gt;Ensure you have a multi-cluster setup across availability zones to handle 100% failure in a single data-centre - e.g. data-centre power-outage. For those wanting to achieve a ~99.99% uptime SLA (depending on cloud provider).&lt;/li&gt;
&lt;li&gt;Ensure you have a multi-region setup to handle 100% failure in a single region. For those wanting to achieve a ~99.999% uptime SLA (depending on cloud provider).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, short of planned restarts by yourself or your cloud provider, generally it is good to try and avoid your pods dying everywhere! Pod death is generally a secondary effect of some other root cause in the cluster, and it may be that finding and fixing a root issue may save you from having to pay for excessive redundancy in compute.&lt;/p&gt;

&lt;p&gt;In this post we're going to take a look at &lt;a href="https://chaos-mesh.org/docs/user_guides/stresschaos_experiment"&gt;&lt;code&gt;StressChaos&lt;/code&gt;&lt;/a&gt; experiments in the Chaos-Mesh toolkit to see how we can validate our CPU and memory resourcing.&lt;/p&gt;

&lt;h1&gt;
  
  
  StressChaos
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/chaos-mesh/chaos-mesh"&gt;Chaos-Mesh&lt;/a&gt; offers two main supported forms of stress chaos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cpu-burn&lt;/code&gt; - Simulate pod CPU stress.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;memory-burn&lt;/code&gt; - Simulate pod memory stress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example, to generate a &lt;code&gt;StressChaos&lt;/code&gt; which will burn 100% of 1 CPU for 30 seconds, every 5 minutes, for one of your pods in the &lt;code&gt;my-app&lt;/code&gt; namespace, you could write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh.org/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StressChaos&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;burn-cpu&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;one&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
  &lt;span class="na"&gt;stressors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30s"&lt;/span&gt;
  &lt;span class="na"&gt;scheduler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@every&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5m"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Equally, to generate a &lt;code&gt;StressChaos&lt;/code&gt; which will continually grow memory usage for 30s, every 5 minutes, for one of your pods in the &lt;code&gt;my-app&lt;/code&gt; namespace, you could write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh.org/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StressChaos&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;burn-memory&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;one&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-app&lt;/span&gt;
  &lt;span class="na"&gt;stressors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30s"&lt;/span&gt;
  &lt;span class="na"&gt;scheduler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@every&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5m"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chaos-Mesh injects these desired stressors directly into the target pods via the chaos-daemon, which uses some &lt;a href="https://man7.org/linux/man-pages/man1/nsenter.1.html"&gt;&lt;code&gt;nsenter&lt;/code&gt;&lt;/a&gt; magic to pass appropriate &lt;a href="https://manpages.ubuntu.com/manpages/artful/man1/stress-ng.1.html"&gt;&lt;code&gt;stress-ng&lt;/code&gt;&lt;/a&gt; commands.&lt;/p&gt;

&lt;p&gt;You can further customize your stressors as follows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can provide both a &lt;code&gt;cpu&lt;/code&gt; and &lt;code&gt;memory&lt;/code&gt; and stressor in your config.&lt;/li&gt;
&lt;li&gt;Both the &lt;code&gt;cpu&lt;/code&gt; and &lt;code&gt;memory&lt;/code&gt; stressors support an additional &lt;code&gt;options&lt;/code&gt; field where you can pass additional flags / arguments to the &lt;a href="https://manpages.ubuntu.com/manpages/artful/man1/stress-ng.1.html"&gt;&lt;code&gt;stress-ng&lt;/code&gt;&lt;/a&gt; command.&lt;/li&gt;
&lt;li&gt;Alternatively, you can define fully custom stressors by providing a &lt;code&gt;stressngStressors&lt;/code&gt; string instead of the &lt;code&gt;stressors&lt;/code&gt; object. This expects a string of &lt;a href="https://manpages.ubuntu.com/manpages/artful/man1/stress-ng.1.html"&gt;&lt;code&gt;stress-ng&lt;/code&gt;&lt;/a&gt; flags / arguments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that providing custom options (last two points) are not fully tested so should be seen as experimental!&lt;/p&gt;

&lt;p&gt;So without further ado, let's try out some stress chaos!&lt;/p&gt;

&lt;h2&gt;
  
  
  Walk-through
&lt;/h2&gt;

&lt;p&gt;Further details on Chaos-Mesh can be found on it's &lt;a href="https://github.com/chaos-mesh/chaos-mesh"&gt;GitHub repository&lt;/a&gt; and in the &lt;a href="https://chaos-mesh.org/docs/"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here we'll walk through setting up and executing the following two tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A CPU stress test using Kubernetes manifest files.&lt;/li&gt;
&lt;li&gt;A Memory stress test using Kubernetes manifest files.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Setting Up The Cluster
&lt;/h3&gt;

&lt;p&gt;Please refer to &lt;a href="https://dev.to/craigmorten/k8s-chaos-dive-1-kube-monkey-4431#setting-up-a-cluster"&gt;previous tutorials&lt;/a&gt; for detailed instructions! Ultimately we run the following two commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube start &lt;span class="nt"&gt;--driver&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;virtualbox
minikube addons &lt;span class="nb"&gt;enable &lt;/span&gt;metrics-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
   Deploying A Target Application And Chaos-Mesh
&lt;/h3&gt;

&lt;p&gt;If you are following on from &lt;a href="https://dev.to/craigmorten/k8s-chaos-dive-2-chaos-mesh-part-1-2i96"&gt;part 1&lt;/a&gt; of the Chaos-Mesh series then you may not have to do anything! We will re-use our Nginx and Chaos-Mesh deployments from before.&lt;/p&gt;

&lt;p&gt;If you've skipped over the previous tutorial then I recommend following it's &lt;a href="https://dev.to/craigmorten/k8s-chaos-dive-2-chaos-mesh-part-1-2i96#deploying-a-target-application"&gt;target application setup&lt;/a&gt; and &lt;a href="https://dev.to/craigmorten/k8s-chaos-dive-2-chaos-mesh-part-1-2i96#deploying-chaosmesh"&gt;deploying chaos-mesh&lt;/a&gt; instructions. If you followed the clean-up in the previous tutorial, then you can reinstate the setup with the following commands:&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;# Create Nginx Helm charts&lt;/span&gt;
helm create nginx

&lt;span class="c"&gt;# Deploy Nginx to the cluster&lt;/span&gt;
kubectl create ns nginx
helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; nginx ./nginx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; nginx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; &lt;span class="nv"&gt;replicaCount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10

&lt;span class="c"&gt;# Check the Nginx deployment is successful&lt;/span&gt;
helm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; nginx
kubectl get pod &lt;span class="nt"&gt;-n&lt;/span&gt; nginx

&lt;span class="c"&gt;# Pull down the Chaos-Mesh repo&lt;/span&gt;
git clone https://github.com/chaos-mesh/chaos-mesh

&lt;span class="c"&gt;# Apply Chaos-Mesh Custom Resource Definitions&lt;/span&gt;
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; ./chaos-mesh/manifests/crd.yaml

&lt;span class="c"&gt;# Deploy Chaos-Mesh to the cluster&lt;/span&gt;
kubectl create ns chaos-mesh
helm upgrade &lt;span class="nt"&gt;--install&lt;/span&gt; chaos-mesh ./chaos-mesh/helm/chaos-mesh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-n&lt;/span&gt; chaos-mesh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; dashboard.create&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# Check the Chaos-Mesh deployment is successful&lt;/span&gt;
helm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; chaos-mesh
kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; chaos-mesh &lt;span class="nt"&gt;-l&lt;/span&gt; app.kubernetes.io/instance&lt;span class="o"&gt;=&lt;/span&gt;chaos-mesh

&lt;span class="c"&gt;# Load the Chaos-Mesh dashboard&lt;/span&gt;
minikube service chaos-dashboard &lt;span class="nt"&gt;-n&lt;/span&gt; chaos-mesh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Experiment 2: CPU Chaos
&lt;/h3&gt;

&lt;p&gt;Let's start by creating a new &lt;code&gt;stress-cpu.yaml&lt;/code&gt; file with the following contents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh.org/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StressChaos&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stress-cpu&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;labelSelectors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.kubernetes.io/name"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;stressors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
      &lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30s"&lt;/span&gt;
  &lt;span class="na"&gt;scheduler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@every&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;5m"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will add a single CPU stress worker to each of our &lt;code&gt;nginx&lt;/code&gt; pods that will load the pods 100% of the scheduled time. Check out the &lt;a href="https://manpages.ubuntu.com/manpages/artful/man1/stress-ng.1.html"&gt;&lt;code&gt;stress-ng&lt;/code&gt; manpage for the &lt;code&gt;--cpu&lt;/code&gt; and &lt;code&gt;--cpu-load&lt;/code&gt; flags&lt;/a&gt; for further details.&lt;/p&gt;

&lt;p&gt;These workers will be scheduled every 5 minutes and run for 30s in the pod before being stopped, meaning we should expect to see our Nginx pods' CPU spike for 30s and then drop back to near 0 for the remaining 4.5 minutes.&lt;/p&gt;

&lt;p&gt;Let's install our experiment and see what happens!&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;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; stress-cpu.yaml
&lt;span class="go"&gt;
stresschaos.chaos-mesh.org/stress-cpu created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we check the Chaos-Mesh dashboard we can see that the experiment has been deployed and is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube service chaos-dashboard &lt;span class="nt"&gt;-n&lt;/span&gt; chaos-mesh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--L2I7iqiB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/q64y50jf0s20f8jtyabz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--L2I7iqiB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/q64y50jf0s20f8jtyabz.png" alt="Chaos-Mesh Dashboard Overview Page showing single stress test is in operation" width="880" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GHpmisjk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/vxlj7vqzi1xxnv3c76p6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GHpmisjk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/vxlj7vqzi1xxnv3c76p6.png" alt='Chaos-Mesh Dashboard Experiment Details Page for "stress-cpu" experiment' width="880" height="518"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dashboard shows us that the test is successfully running for 30 seconds every 5 minutes 🎉.&lt;/p&gt;

&lt;p&gt;Let's say we don't trust the dashboard? Let's check out what the CPU levels are actually like in the cluster!&lt;/p&gt;

&lt;p&gt;Firstly in a terminal window install the &lt;a href="https://linux.die.net/man/1/watch"&gt;&lt;code&gt;watch&lt;/code&gt; command&lt;/a&gt; if you don't already have it and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;watch kubectl top pods &lt;span class="nt"&gt;-n&lt;/span&gt; nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should output something like:&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;Every 2.0s: kubectl top pods -n nginx              c-machine: Tue Aug 25 14:16:40 2020

NAME                     CPU(cores)   MEMORY(bytes)
nginx-5c96c8f58b-2l85d   0m           25Mi
nginx-5c96c8f58b-btws8   0m           2Mi
nginx-5c96c8f58b-c28ws   0m           2Mi
nginx-5c96c8f58b-gmnkc   0m           2Mi
nginx-5c96c8f58b-hzgx5   0m           2Mi
nginx-5c96c8f58b-jzk2p   0m           2Mi
nginx-5c96c8f58b-vhmqm   0m           2Mi
nginx-5c96c8f58b-vs2cr   0m           21Mi
nginx-5c96c8f58b-zc9qf   0m           2Mi
nginx-5c96c8f58b-zjfc4   0m           2Mi
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which will update every 2 seconds. There can be a bit of a lag getting metrics back from the cluster's metrics-server, but we should see that every 5 minutes these values jump up for ~30 seconds and then return back to 0. Waiting for the next scheduled stress test we can see this happening:&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;Every 2.0s: kubectl top pods -n nginx       c-machine: Tue Aug 25 14:19:11 2020

NAME                     CPU(cores)   MEMORY(bytes)
nginx-5c96c8f58b-2l85d   110m         25Mi
nginx-5c96c8f58b-btws8   80m          2Mi
nginx-5c96c8f58b-c28ws   94m          2Mi
nginx-5c96c8f58b-gmnkc   89m          2Mi
nginx-5c96c8f58b-hzgx5   70m          2Mi
nginx-5c96c8f58b-jzk2p   132m         2Mi
nginx-5c96c8f58b-vhmqm   63m          2Mi
nginx-5c96c8f58b-vs2cr   115m         21Mi
nginx-5c96c8f58b-zc9qf   80m          2Mi
nginx-5c96c8f58b-zjfc4   94m          2Mi
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awesome! The single CPU worker in each pod seems to use up around 100m (100 millicores or 0.1 CPU cores) 💥.&lt;/p&gt;

&lt;p&gt;If you wanted to apply more load to your pods, you could just up the number of workers in the &lt;code&gt;StressChaos&lt;/code&gt; yaml accordingly 🙃.&lt;/p&gt;

&lt;p&gt;If you want to go deeper, we can even check out what is happening in the pod itself! 😲 First we find a target Nginx pod:&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;kubectl get pods &lt;span class="nt"&gt;-n&lt;/span&gt; nginx
&lt;span class="go"&gt;
NAME                     READY   STATUS    RESTARTS   AGE
nginx-5c96c8f58b-2l85d   1/1     Running   0          23m
nginx-5c96c8f58b-btws8   1/1     Running   0          23m
nginx-5c96c8f58b-c28ws   1/1     Running   0          23m
nginx-5c96c8f58b-gmnkc   1/1     Running   0          23m
nginx-5c96c8f58b-hzgx5   1/1     Running   0          23m
nginx-5c96c8f58b-jzk2p   1/1     Running   0          23m
nginx-5c96c8f58b-vhmqm   1/1     Running   0          23m
nginx-5c96c8f58b-vs2cr   1/1     Running   0          23m
nginx-5c96c8f58b-zc9qf   1/1     Running   0          23m
nginx-5c96c8f58b-zjfc4   1/1     Running   0          23m
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll use the first in the list &lt;code&gt;nginx-5c96c8f58b-2l85d&lt;/code&gt;. Next we can actually open a shell inside the pod as follows:&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;kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; nginx-5c96c8f58b-2l85d &lt;span class="nt"&gt;-n&lt;/span&gt; nginx &lt;span class="nt"&gt;--&lt;/span&gt; bash
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;root@nginx-5c96c8f58b-2l85d:/#&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here we can install some utility packages:&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;root@nginx-5c96c8f58b-2l85d:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; procps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which will allow us to use the &lt;code&gt;ps&lt;/code&gt; and &lt;code&gt;top&lt;/code&gt; commands to interrogate the processes running inside the pod. If we wait until load is happening, we can see a &lt;code&gt;stress-ng&lt;/code&gt; processes start running inside the container with:&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;root@nginx-5c96c8f58b-2l85d:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;watch ps aux
&lt;span class="go"&gt;
Every 2.0s: ps aux                                                                                 nginx-5c96c8f58b-9npqq: Tue Aug 25 13:48:56 2020

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
&lt;/span&gt;&lt;span class="gp"&gt;root         1  0.0  0.2  32656  5204 ?        Ss   13:36   0:00 nginx: master process nginx -g daemon off;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="go"&gt;nginx        6  0.0  0.1  33112  3072 ?        S    13:36   0:00 nginx: worker process
root        27  0.0  0.1  18136  3284 pts/0    Ss   13:46   0:00 bash
root       355  0.1  0.1  11104  2520 pts/0    S+   13:48   0:00 watch ps aux
root       383  0.0  0.1  18700  3696 ?        SL   13:48   0:00 stress-ng --cpu 1 --cpu-load 100
root       384 21.0  0.2  19348  4668 ?        R    13:48   0:01 stress-ng --cpu 1 --cpu-load 100
root       397  0.0  0.0  11104   652 pts/0    S+   13:48   0:00 watch ps aux
root       398  0.0  0.0   4280   772 pts/0    S+   13:48   0:00 sh -c ps aux
root       399  0.0  0.1  36636  2804 pts/0    R+   13:48   0:00 ps aux
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or equally we can use &lt;code&gt;top&lt;/code&gt; to see our processes and their resource usage with a slightly nicer UI:&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;root@nginx-5c96c8f58b-2l85d:/#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;top
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Jl9BMgh4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/i3eqlmoofjoch121mr2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Jl9BMgh4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/i3eqlmoofjoch121mr2w.png" alt="Outcome of running  raw `top` endraw  command in terminal inside Nginx pod's container" width="880" height="251"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From this command we can see a process list which shows a &lt;code&gt;stress-ng&lt;/code&gt; parent process and a &lt;code&gt;stress-ng-cpu&lt;/code&gt; worker process which consumes around 18% CPU. After 30s these processes disappear and CPU returns to near 0%.&lt;/p&gt;

&lt;p&gt;Let's stop our experiment by removing the &lt;code&gt;StressChaos&lt;/code&gt; Kubernetes object we added earlier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete &lt;span class="nt"&gt;-f&lt;/span&gt; stress-cpu.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So there we have it, we can successfully stress our pods with increased CPU levels! 🍾&lt;/p&gt;

&lt;h3&gt;
  
  
  Experiment 3: Memory Chaos
&lt;/h3&gt;

&lt;p&gt;Now let's perform a similar experiment, but this time we will load the memory of a single pod.&lt;/p&gt;

&lt;p&gt;Create a new &lt;code&gt;StressChaos&lt;/code&gt; manifest called &lt;code&gt;stress-memory.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh.org/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;StressChaos&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stress-memory&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;chaos-mesh&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;one&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;labelSelectors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app.kubernetes.io/name"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
  &lt;span class="na"&gt;stressors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
      &lt;span class="na"&gt;workers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10s"&lt;/span&gt;
  &lt;span class="na"&gt;scheduler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@every&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2m"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will add a single memory stress worker to one of our &lt;code&gt;nginx&lt;/code&gt; pods that will run for 10s every 2 minutes. The worker will grow it's heap by reallocating memory at a rate of 64K per iteration. Check out the &lt;a href="https://manpages.ubuntu.com/manpages/artful/man1/stress-ng.1.html"&gt;&lt;code&gt;stress-ng&lt;/code&gt; manpage for the &lt;code&gt;--bigheap&lt;/code&gt; flag&lt;/a&gt; for further details.&lt;/p&gt;

&lt;p&gt;Let's install our experiment and see what happens!&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;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; stress-memory.yaml
&lt;span class="go"&gt;
stresschaos.chaos-mesh.org/stress-memory created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watching our Nginx pods, we can see that every 5 minutes one of the pods has an outburst in memory usage:&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;watch kubectl top pods &lt;span class="nt"&gt;-n&lt;/span&gt; nginx
&lt;span class="go"&gt;Every 2.0s...  c-machine: Tue Aug 25 15:12:52 2020

NAME                     CPU(cores)   MEMORY(bytes)
nginx-5c96c8f58b-5tvz5   0m           3Mi
nginx-5c96c8f58b-6fb9h   0m           2Mi
nginx-5c96c8f58b-bjzd2   0m           2Mi
nginx-5c96c8f58b-gfqww   0m           3Mi
nginx-5c96c8f58b-jtn5f   0m           2Mi
nginx-5c96c8f58b-rdlgk   145m         3Mi
nginx-5c96c8f58b-sgx4s   0m           2Mi
nginx-5c96c8f58b-szffx   0m           3Mi
nginx-5c96c8f58b-ttlhr   65m          621Mi
nginx-5c96c8f58b-xgh7j   0m           2Mi
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Similarly, if we're quick and exec on a targetted pod and install &lt;code&gt;top&lt;/code&gt; we can see that the memory worker very quickly starts to consume &lt;em&gt;a lot&lt;/em&gt; of memory!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IaKN47OK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/gze7xhl9btk7zeyjlrjw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IaKN47OK--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/gze7xhl9btk7zeyjlrjw.png" alt="Terminal showing output of  raw `top` endraw  command run inside targeted Nginx pod" width="880" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image above shows our targeted Nginx pod hitting 30.4% of the available memory. Given that we haven't set any resource limits on our Nginx deployment, that is actually 30.4% of the cluster's memory allocated to Minikube!!&lt;/p&gt;

&lt;p&gt;This is why in this experiment I've set the &lt;code&gt;mode&lt;/code&gt; to &lt;code&gt;one&lt;/code&gt; and not &lt;code&gt;all&lt;/code&gt;. Doing so will likely consume all of the memory resources in your cluster and very potentially break it - I tried this initially and had to completely delete the Minikube cluster and start again as it became 100% non-responsive! 😱&lt;/p&gt;

&lt;p&gt;Let's remove our experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete &lt;span class="nt"&gt;-f&lt;/span&gt; stress-memory.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clean-up
&lt;/h3&gt;

&lt;p&gt;Let's clean-up and remove everything we've created today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;helm delete chaos-mesh &lt;span class="nt"&gt;-n&lt;/span&gt; chaos-mesh
kubectl delete ns chaos-mesh
kubectl delete crd iochaos.chaos-mesh.org
kubectl delete crd kernelchaos.chaos-mesh.org 
kubectl delete crd networkchaos.chaos-mesh.org
kubectl delete crd podchaos.chaos-mesh.org 
kubectl delete crd podnetworkchaos.chaos-mesh.org
kubectl delete crd stresschaos.chaos-mesh.org
kubectl delete crd timechaos.chaos-mesh.org
helm delete nginx &lt;span class="nt"&gt;-n&lt;/span&gt; nginx
kubectl delete ns nginx
minikube stop
minikube delete
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Learnings
&lt;/h3&gt;

&lt;p&gt;So what have these two experiments taught us today?&lt;/p&gt;

&lt;p&gt;Our first experiment showed that it is very simple to apply additional CPU strain to our pods, but ultimately 1 worker didn't apply too much pressure. Given we weren't actually trying to use our Nginx pods in any way the experiment is a little moot and there is likely little to be gained.&lt;/p&gt;

&lt;p&gt;However considering CPU is very important in a cluster, and for many applications the CPU profile can directly impact key performance indicators such as latency. By having the tooling to generate artificial strain on your pods (and cluster) you can measure how your applications work when under the kind of pressure you see during a peak load period.&lt;/p&gt;

&lt;p&gt;Given the impact on latency, CPU stress can also be a good way to validate your deployment's healthchecks, where the aim should be to ensure that your application is highly available and that traffic is routed to happily working pods opposed to ones having a CPU meltdown! This could be tested using one of the percentage based modes available.&lt;/p&gt;

&lt;p&gt;Monitoring your application under pod CPU stress can also be a useful way to validate your &lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/"&gt;pod resource and limit values&lt;/a&gt;. Applying too small a resource value for CPU will likely result in CPU throttling which can have devastating impacts on applications, particularly multi-threaded applications.&lt;/p&gt;

&lt;p&gt;Our second experiment showed that memory stressor is certainly quite aggressive! In fact, leaving it running too long on too many pods could result in quite the flurry of out of memory killer jobs, if not entire cluster resource exhaustion and failure!&lt;/p&gt;

&lt;p&gt;Fine-tuning the memory stressor using the &lt;code&gt;options&lt;/code&gt; value is probably advised, but a key take away is that no one pod (or collection of pods) should be allowed to consume an entire cluster's memory allocation! Strict &lt;a href="https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/"&gt;memory resource request and limits&lt;/a&gt; should be set on deployments to ensure that they are killed before they have the chance to disrupt other pods or fundamental cluster processes 🔥.&lt;/p&gt;




&lt;p&gt;Thanks for reading folks!&lt;/p&gt;

&lt;p&gt;Enjoy the tutorial? Have questions or comments? Or do you have an awesome way to run chaos experiments in your Kubernetes clusters? Drop me a message in the section below or tweet me &lt;a href="https://twitter.com/CraigMorten"&gt;@CraigMorten&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Till next time 💥&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>testing</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
