<?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>Forem: Anders Ramsay</title>
    <description>The latest articles on Forem by Anders Ramsay (@andersr).</description>
    <link>https://forem.com/andersr</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%2F278234%2Fc2951448-fcec-408e-8734-1f9930ec6b4d.jpeg</url>
      <title>Forem: Anders Ramsay</title>
      <link>https://forem.com/andersr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/andersr"/>
    <language>en</language>
    <item>
      <title>How to save anonymous content to a database</title>
      <dc:creator>Anders Ramsay</dc:creator>
      <pubDate>Sat, 02 Sep 2023 17:13:45 +0000</pubDate>
      <link>https://forem.com/andersr/how-to-save-anonymous-content-to-a-database-386d</link>
      <guid>https://forem.com/andersr/how-to-save-anonymous-content-to-a-database-386d</guid>
      <description>&lt;p&gt;I recently released a new version of &lt;a href="https://logd.co"&gt;logd&lt;/a&gt;, a side project where part of the vision is to enable just starting to type notes without the ceremony of creating a new document, etc.&lt;/p&gt;

&lt;p&gt;In that spirit, I made it possible for anonymous users to create notes that are saved locally, making it a kind of ad-hoc scratch pad. You can &lt;a href="https://logd.co"&gt;try that out now&lt;/a&gt; if you'd like.&lt;/p&gt;

&lt;p&gt;However, I also wanted to provide the option to save those local notes to the cloud on login.&lt;/p&gt;

&lt;p&gt;I did quite a bit of googling around for a solution that would support this but did not find any patterns I liked. One reason might be that this is a somewhat non-standard user flow. In a conventional model, you sign in first, and then create a note.&lt;/p&gt;

&lt;p&gt;Since others might be interested in supporting saving anonymous content to a database, I decided to write a blog post about how I implemented this feature. &lt;/p&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;p&gt;For this solution, I decided to use a combination of localStorage and query params to manage the process of saving anonymous content to a database. (At the end, I'll talk a bit about some other option I considered.)&lt;/p&gt;

&lt;p&gt;Here is an overview of the solution, which I'll walk through in detail below:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Persist changes locally:&lt;/strong&gt; While a user is anonymous, their note content is persisted via local storage. Nothing unusual there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a param to trigger saving to the database:&lt;/strong&gt; When a user clicks login or signup, we check to see if any local content has been entered, and if so append a query param with the "save anon content" route where we'll handle saving to the database.  We reuse the same query param we'd use if a user tried to access a restricted page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After login, redirect to the "save anon content" page:&lt;/strong&gt; After login completes, our redirect param will send the user to the "save anon content" page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save local content to the database:&lt;/strong&gt; On the "save anon content" page, we retrieve the local content, save it to the database, which generates an id, which we then use to redirect the user to the newly created note page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Redirect to our new note and delete the local content:&lt;/strong&gt; Finally, after redirecting to the new note page, we also include a param that will enable deleting the localStorage content, since it is now safely in the database.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prerequisites and Setup
&lt;/h2&gt;

&lt;p&gt;I implemented this version of Logd notes using the &lt;a href="https://remix.run/"&gt;Remix&lt;/a&gt; framework with Node, and I'll therefore also use the same stack for this walk-through.  I'm also assuming you already have basic knowledge of React with TypeScript.&lt;/p&gt;

&lt;p&gt;You can find a fully working solution in &lt;a href="https://github.com/andersr/save-local-to-db-example"&gt;this repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'll be using the &lt;a href="https://github.com/remix-run/indie-stack"&gt;Remix Indie Stack&lt;/a&gt; as a starting point since it already includes both authentication as well as notes CRUD functionality. If you're following along writing code, you'll want to first create a new app using the Remix Indie Stack, as follows: &lt;code&gt;npx create-remix@latest --template remix-run/indie-stack&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Persist changes locally
&lt;/h2&gt;

&lt;p&gt;This part is mostly basic functionality for persisting changes locally, with an added simple check for if content has been entered, and a function for appending a query param to the login link.  Open up &lt;code&gt;app/routes/_index.tsx&lt;/code&gt; and make the following updates:&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;// app/routes/_index.tsx&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="kd"&gt;function&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="p"&gt;...&lt;/span&gt;
  &lt;span class="c1"&gt;// hook for persisting changes locally&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useLocalStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;ANON_USER_LOCAL_STORAGE_CONTENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// simple check for if content has been entered&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;noteHasContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;trim&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="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// display current save status&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;displaySaveStatus&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;noteHasContent&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;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Saved locally.&lt;span class="si"&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="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/login&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;setParam&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-gray-400 underline"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            Save to my notes
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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="nt"&gt;span&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;return&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;// set the param if content has been entered&lt;/span&gt;
  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;setParam&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;noteHasContent&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;REDIRECT_TO_PARAM&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SAVE_ANON_ROUTE&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;// replace the current view with the following&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="s"&gt;"flex h-full min-h-screen flex-col"&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="nt"&gt;header&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"flex items-center justify-between bg-slate-800 p-4 text-white"&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="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-3xl font-bold"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Anon Note To Db Example&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&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;user&lt;/span&gt; &lt;span class="p"&gt;?&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="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Link&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/notes"&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              View Notes for &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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="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="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="s"&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;Link&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/join&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;setParam&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded bg-slate-600 px-4 py-2 text-blue-100 hover:bg-blue-500 active:bg-blue-600 mr-4"&lt;/span&gt;
            &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              Sign up
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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;Link&lt;/span&gt;
              &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`/login&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;setParam&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"rounded bg-slate-600 px-4 py-2 text-blue-100 hover:bg-blue-500 active:bg-blue-600"&lt;/span&gt;
            &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              Log In
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Link&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="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="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;header&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="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"p-8"&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="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="s"&gt;"w-[400px] mx-auto"&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="nt"&gt;textarea&lt;/span&gt;
            &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"w-full rounded-md border-2 border-blue-500 px-3 py-2 text-lg leading-6"&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&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;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Write something..."&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="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="s"&gt;"py-2 text-sm text-slate-500"&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;displaySaveStatus&lt;/span&gt;&lt;span class="p"&gt;()&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;&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;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the above update, a redirect param will be passed when clicking signup or login, which we'll make use of in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Use a param to trigger saving to the database
&lt;/h2&gt;

&lt;p&gt;Now, when the user clicks on login or signup, if they've typed in any content, a param will be appended to the URL. Note the use of &lt;code&gt;REDIRECT_TO_PARAM&lt;/code&gt; and &lt;code&gt;SAVE_ANON_ROUTE&lt;/code&gt;.  I prefer to store anything pases as a param as a constant since it will be used in at least two places (the originating link and the handler on the target page), so we want to ensure they match.&lt;/p&gt;

&lt;p&gt;Also, note the use of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent"&gt;encodeURIComponent&lt;/a&gt;, which ensures passed values are properly formatted for appearing in a URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. After login, redirect to the "save anon content" page
&lt;/h2&gt;

&lt;p&gt;On the login page, we shouldn't need to make any updates, since we're re-using the existing &lt;a href="https://github.com/andersr/save-local-to-db-example/blob/d60553d276ed52ffc24b35b0432602de49863804/app/routes/login.tsx#L20"&gt;&lt;code&gt;redirectTo&lt;/code&gt; functionality&lt;/a&gt;. After login, the app should redirect to the route we passed in (&lt;code&gt;SAVE_ANON_ROUTE&lt;/code&gt;). Note that the actual route can be whatever you want. In my case it's &lt;code&gt;/save-anon-note&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why a separate page?
&lt;/h3&gt;

&lt;p&gt;Let's talk a bit about why we have a separate view just for saving local content to a database.  One fair question is why we can't just complete this task right on the login page after the user signs in? However, that would not be a good idea, since, once the user is authenticated, they need to be redirected &lt;em&gt;away&lt;/em&gt; from the login page, as that page should only be accessible to anonymous users.&lt;/p&gt;

&lt;p&gt;Ok, so why not just handle everything on the server after the user is signed in?  Unfortunately, that will not work, because localStorage is, by definition, &lt;a href="https://developer.school/snippets/react/localstorage-is-not-defined-nextjs"&gt;only accessible on the client&lt;/a&gt;.  We therefore need an intermediate page that we fully load so that we can retrieve the local content on the client and then save it to the database on the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Save local content to the database
&lt;/h3&gt;

&lt;p&gt;Thanks to the param we passed on the homepage, we are redirected to the "save anon" page. This view contains the core part of the solution.&lt;/p&gt;

&lt;p&gt;Go ahead and create a new page at &lt;code&gt;app/routes/save-anon-note.tsx&lt;/code&gt; and add the code below.&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;// app/routes/save-anon-note.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ActionArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LoaderFunction&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;@remix-run/node&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirect&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;@remix-run/node&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;useNavigate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSubmit&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;@remix-run/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;useEffect&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;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;LiaSpinnerSolid&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;react-icons/lia&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;createNote&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;~/models/note.server&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;getUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireUserId&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;~/session.server&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;ANON_USER_LOCAL_STORAGE_CONTENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;LOCAL_NOTE_SAVED_PARAM&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;~/shared&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// redirect users who are not signed away from this page&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;loader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;LoaderFunction&lt;/span&gt; &lt;span class="o"&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&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;getUserId&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="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;userId&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;redirect&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;return&lt;/span&gt; &lt;span class="nx"&gt;json&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 the programmatic form submit&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;action&lt;/span&gt; &lt;span class="o"&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;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ActionArgs&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;userId&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;requireUserId&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;formData&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;toString&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="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;title&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;redirect&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="mi"&gt;400&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;note&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;createNote&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&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;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/notes/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;note&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;LOCAL_NOTE_SAVED_PARAM&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;=true`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;SaveAnonNote&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;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useNavigate&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;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSubmit&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="c1"&gt;// after the page has mounted, look for content in local storage&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localContent&lt;/span&gt; &lt;span class="o"&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;ANON_USER_LOCAL_STORAGE_CONTENT&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;localContent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;handleAnonNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localContent&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no local content found&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handleAnonNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;localContent&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;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;noteContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localContent&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;noteContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&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="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No note content&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;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;noteContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&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;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&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="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;formData&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;FormData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post&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;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;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;anon note error: &lt;/span&gt;&lt;span class="dl"&gt;"&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;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// on the page itself, we're just displaying a spinner while this brief process completes&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="s"&gt;"flex h-full flex-col items-center justify-center"&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;LiaSpinnerSolid&lt;/span&gt;
        &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;30px&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Loading"&lt;/span&gt;
        &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-primary animate-spin text-6xl"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's quite a bit going on here, so let's break it down step-by-step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the server side (in the &lt;code&gt;loader&lt;/code&gt; method), we confirm the user is authenticated and redirect away if they are not.&lt;/li&gt;
&lt;li&gt;We use the &lt;code&gt;useEffect&lt;/code&gt; method to determine that the page has mounted and that we are able to read from localStorage.&lt;/li&gt;
&lt;li&gt;If no local content is found, we redirect to the homepage, canceling the process.&lt;/li&gt;
&lt;li&gt;If local content is found, we do some basic validation and then parse out the content title vs body. (This part is not really specific to saving anonymous content to a database.) Then, we use Remix's &lt;a href="https://remix.run/docs/en/1.19.3/hooks/use-submit"&gt;useSubmit&lt;/a&gt; hook to programmatically submit our note values to the server, after which point we handle the process very similarly to as if a user had created a new note.&lt;/li&gt;
&lt;li&gt;See the &lt;code&gt;action&lt;/code&gt; method for how we handle the form submit, create the note and redirect to the newly created note. Note also that we are passing in a &lt;code&gt;LOCAL_NOTE_SAVED_PARAM&lt;/code&gt; which we'll use to remove the local content after the redirect.&lt;/li&gt;
&lt;li&gt;Finally, on the page itelf, we just display a spinner while we complete the above process.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5.Redirect to our new note and delete the local content
&lt;/h3&gt;

&lt;p&gt;If all goes well, we redirect to the new note and pass our &lt;code&gt;LOCAL_NOTE_SAVED_PARAM&lt;/code&gt; param.&lt;br&gt;
On the note detail view, we check for this param and use it to safely clear local storage, knowing that it has been saved to the database. And with that we are done 🎉.&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;// Only relevant snippets included&lt;/span&gt;
&lt;span class="c1"&gt;// See app/routes/notes.$noteId.tsx for the complete code&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;ANON_USER_LOCAL_STORAGE_CONTENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;LOCAL_NOTE_SAVED_PARAM&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;~/shared&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;NoteDetailsPage&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;const&lt;/span&gt; &lt;span class="nx"&gt;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useNavigate&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;searchParams&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSearchParams&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;localNoteSaved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LOCAL_NOTE_SAVED_PARAM&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="c1"&gt;// if the param is found, remove the local content and redirect to the current page, and remove the url that had the param from this history stack with replace set to true.&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;localNoteSaved&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;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ANON_USER_LOCAL_STORAGE_CONTENT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;navigate&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;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;replace&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="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;localNoteSaved&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;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;navigate&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;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I considered a few other solutions before going wih the one described above.&lt;/p&gt;

&lt;p&gt;For example, I considered creating a guest session for every anonymous user.  This would have the advantage of allowing for just storing everything in the database.  However, it also comes with some security implications and also felt a bit more complex.  I also considered using a JWT stored as cookie as a temporary note id.&lt;/p&gt;

&lt;p&gt;But in the end, I felt the above solution, while containing many small steps, was the simplest and fastest to implement.  Additionally, there are fewer security concerns, since I do not write to the database until the user has been authenticated.  And last but not least, I am using query params to manage the flow, which is a tried and true pattern.&lt;/p&gt;

&lt;p&gt;This is not a very complex solution, but it does involve a series of small steps, which means there is quite a bit of surface area for failure. For this reason, it's a great candidate for one or more E2E tests, something I might write about in another post.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to set up a local E2E test development environment</title>
      <dc:creator>Anders Ramsay</dc:creator>
      <pubDate>Mon, 05 Sep 2022 18:58:49 +0000</pubDate>
      <link>https://forem.com/andersr/how-to-set-up-a-local-e2e-test-development-environment-28pc</link>
      <guid>https://forem.com/andersr/how-to-set-up-a-local-e2e-test-development-environment-28pc</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of the &lt;a href="https://dev.to/andersr/e2e-testing-series-49lg"&gt;E2E Testing Series&lt;/a&gt;.  If you haven't already, I suggest going back and reading previous posts before continuing.&lt;/em&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Just give me the code...
&lt;/h2&gt;

&lt;p&gt;Here is the "tl;dr" version of the article below, with just the commands to run and the code you'll need to add.  If anything isn't clear, I suggest reading the full post.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install the sample app:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; npx create-next-app e2e-testing &lt;span class="nt"&gt;--ts&lt;/span&gt; &lt;span class="nt"&gt;--use-npm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create a &lt;code&gt;src&lt;/code&gt; directory and move the &lt;code&gt;pages&lt;/code&gt; and &lt;code&gt;styles&lt;/code&gt; directories into it.&lt;/li&gt;
&lt;li&gt;Install these packages:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt; npm i &lt;span class="nt"&gt;-D&lt;/span&gt; cypress cypress-watch-and-reload start-server-and-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Rename the &lt;code&gt;dev&lt;/code&gt; script in your &lt;code&gt;package.json&lt;/code&gt; to &lt;code&gt;dev:next&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add these scripts to your &lt;code&gt;package.json&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cy:e2e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cypress open --e2e --browser chrome --config baseUrl=http://localhost:3000"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start-test dev:next 3000 cy:e2e"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Run &lt;code&gt;npm run dev&lt;/code&gt; to initialize Cypress.&lt;/li&gt;
&lt;li&gt;Update/add these files (click on the file link to view the code):

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/andersr/e2e-testing-tutorial/blob/main/cypress.config.ts"&gt;&lt;code&gt;cypress.config.ts&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andersr/e2e-testing-tutorial/blob/main/cypress/support/e2e.ts"&gt;&lt;code&gt;cypress/support/e2e.js&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andersr/e2e-testing-tutorial/blob/main/cypress/tsconfig.json"&gt;&lt;code&gt;cypress/tsconfig.json&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/andersr/e2e-testing-tutorial/blob/main/tsconfig.json"&gt;&lt;code&gt;tsconfig.json&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Run the &lt;code&gt;dev&lt;/code&gt; script to enable E2E test development with hot reloading of test and source files. &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Introduction: Keeping E2E tests front and center
&lt;/h2&gt;

&lt;p&gt;One of most common anti-patterns I see relating to E2E testing is that, while a test suite has been set up and some tests have been written, it is almost never run locally and new tests are only rarely added for new features. This obviously is not a good thing, since we effectively are wasting all the effort that went into setting up the test suite.&lt;/p&gt;

&lt;p&gt;But why would we let such a valuable resource go to waste?&lt;/p&gt;

&lt;p&gt;One simple answer is that it is out of sight, out of mind.  In other words, while the command to run the E2E test locally does exist, it's something you have to remember to do, and it's kind of a hassle, since you usually have to first fire up the dev environment, and then start the E2E runner in a second terminal.&lt;/p&gt;

&lt;p&gt;One way to address this problem is to automate running of the E2E test suite whenever you start up your development environment.  As we'll see, this can have a significant positive impact on your E2E practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a single command for starting both dev environment and test runner
&lt;/h2&gt;

&lt;p&gt;To help ensure E2E tests get written, we want to make it is easy to have both our app running and the test runner in a hot-reloading state. In other words, as we write tests, we want to be able to get quick feedback on if tests are passing or not.&lt;/p&gt;

&lt;p&gt;When we are done with the steps below, you'll be able to just run &lt;code&gt;npm run dev&lt;/code&gt; to both start you dev environment and also start up your E2E test runner in dev mode, meaning that it will rerun tests automatically if changes are made either to the tests or your code. As an added bonus, it will reduce the amount of terminal windows you have open in your IDE.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up our sample app
&lt;/h2&gt;

&lt;p&gt;We'll be using &lt;a href="https://nextjs.org/"&gt;NextJS&lt;/a&gt; as the basis for our sample app. One reason for this is that our newsletter feature will require an API endpoint, and NextJS makes it super easy to add that.&lt;/p&gt;

&lt;p&gt;To get started, go ahead and run the following to install the sample app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx create-next-app e2e-testing &lt;span class="nt"&gt;--ts&lt;/span&gt; &lt;span class="nt"&gt;--use-npm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can replace &lt;code&gt;e2e-testing&lt;/code&gt; with any app name you prefer. Both the &lt;code&gt;--ts&lt;/code&gt; and &lt;code&gt;--use-npm&lt;/code&gt; are optional.  NPM is just my personal preference, and I am a strong proponent of using TypeScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the E2E test runner and utility packages
&lt;/h2&gt;

&lt;p&gt;For this tutorial, we're going to use &lt;a href="https://www.cypress.io/"&gt;Cypress&lt;/a&gt;, which in my opinion currently is the best end-to-end testing solution. (My one gripe with Cypress is the &lt;a href="https://github.com/cypress-io/cypress/issues/6423"&gt;lack of support&lt;/a&gt; for testing of older browsers, which are the ones often causing regressions.)&lt;/p&gt;

&lt;p&gt;Let's install all the packages we'll need, as development dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-D&lt;/span&gt; cypress cypress-watch-and-reload start-server-and-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add the dev scripts
&lt;/h3&gt;

&lt;p&gt;Next, let's update the &lt;code&gt;scripts&lt;/code&gt; section of our &lt;code&gt;package.json&lt;/code&gt; as follows:&lt;/p&gt;

&lt;p&gt;First, rename the &lt;code&gt;dev&lt;/code&gt; script to &lt;code&gt;dev:next&lt;/code&gt;.  Then, add the following scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cy:e2e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cypress open --e2e --browser chrome --config baseUrl=http://localhost:3000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start-test dev:next 3000 cy:e2e"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring Cypress
&lt;/h2&gt;

&lt;p&gt;We need to configure Cypress to use TypeScript and also to hot reload when our source files change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add Typescript support
&lt;/h3&gt;

&lt;p&gt;Cypress has built-in &lt;a href="https://docs.cypress.io/guides/tooling/typescript-support"&gt;Typescript support&lt;/a&gt;. To enable it, and allow for writing TS-based tests, we need to add a &lt;code&gt;tsconfig.json&lt;/code&gt; to the root of the &lt;code&gt;cypress&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Add the following code to &lt;code&gt;cypress/tsconfig.json&lt;/code&gt; and you should now be able to create test files using Typescript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compilerOptions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"es5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"es5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dom"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"cypress"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"**/*.ts"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, update the main &lt;code&gt;tsconfig.json&lt;/code&gt; in the root directory to exclude the cypress directory, so we do not compile test files as part of compiling the main source code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"node_modules"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cypress"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get Cypress to hot reload if your source files change
&lt;/h3&gt;

&lt;p&gt;By default, the Cypress runner will only re-run if you modify test files.  However, we really also want it to restart when we make changes to our code.  To achieve this, we need to make the following updates:&lt;/p&gt;

&lt;p&gt;First, update &lt;code&gt;cypress.config.ts&lt;/code&gt; as follows:&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="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&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;cypress&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;Config&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Cypress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;UserConfigOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="o"&gt;&amp;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;cypress-watch-and-reload&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;watch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&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;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="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Config&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cypress-watch-and-reload&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;watch&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;src/**/*&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;e2e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;setupNodeEvents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&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;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;cypress-watch-and-reload/plugins&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;(&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;Next, add the following to the end of the file &lt;code&gt;cypress/support/e2e.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="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;cypress-watch-and-reload/support&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;Your local environment and &lt;code&gt;dev&lt;/code&gt; command should now be set up.&lt;/p&gt;

&lt;p&gt;Go ahead and run &lt;code&gt;npm run dev&lt;/code&gt; and you should see the dev server first start up, then the test runner will start. The first time you run Cypress, you may be asked to click through their startup wizard.&lt;/p&gt;

&lt;p&gt;Btw, a huge shout-out to &lt;a href="https://glebbahmutov.com/"&gt;Gleb Bahmutov&lt;/a&gt; who is the author of both the utility packages we are using here! 👏 👏 👏&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.to/andersr/a-suggested-process-for-writing-e2e-tests-20gc"&gt;Next Up: A Suggested Process for writing E2E tests&lt;/a&gt;
&lt;/h2&gt;

</description>
      <category>e2e</category>
      <category>testing</category>
      <category>react</category>
    </item>
    <item>
      <title>A Suggested Process for Writing E2E Tests</title>
      <dc:creator>Anders Ramsay</dc:creator>
      <pubDate>Mon, 05 Sep 2022 18:57:14 +0000</pubDate>
      <link>https://forem.com/andersr/a-suggested-process-for-writing-e2e-tests-20gc</link>
      <guid>https://forem.com/andersr/a-suggested-process-for-writing-e2e-tests-20gc</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of the &lt;a href="https://dev.to/andersr/e2e-testing-series-49lg"&gt;E2E Testing Series&lt;/a&gt;.  If you haven't already, I suggest going back and reading previous posts before continuing.&lt;/em&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Process Overview
&lt;/h2&gt;

&lt;p&gt;We're going to walk through a sample process for how to write E2E tests and touch on some best practices along the way.  Here's an overview of the general process I tend to follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a test file and add pending tests for all the feature requirements.&lt;/li&gt;
&lt;li&gt;Write the minimum code needed to get each test to pass.&lt;/li&gt;
&lt;li&gt;With the test runner in watch mode, go back and refactor and clean up code as needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're familiar with &lt;a href="https://en.wikipedia.org/wiki/Test-driven_development"&gt;Test-Driven Development&lt;/a&gt; (TDD), the above might look familiar.  I've found that writing E2E tests is a great opportunity to put TDD into practice.&lt;/p&gt;

&lt;p&gt;As our sample feature, we'll be walking through writing the tests for the Newsletter signup form I discussed in the series intro.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--o9quyBxe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgzfsxqis7ca7u0fdz8g.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--o9quyBxe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgzfsxqis7ca7u0fdz8g.jpg" alt="newsletter form" width="880" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I picked it because it's a small but still complete feature, so a great basis for conveying the E2E test-writing process.&lt;/p&gt;

&lt;p&gt;To get started, we'll create our main test file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the test file and add pending tests
&lt;/h2&gt;

&lt;p&gt;If possible, I suggest collecting all tests for each feature in the same test file, and then match the name of file to the test title.  Let's do that now.&lt;/p&gt;

&lt;p&gt;(I'm assuming you've already &lt;a href="https://dev.to/blog/e2e-series-setting-up-your-local-environment"&gt;set up your local environment&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;Create a new file at &lt;code&gt;cypress/e2e/newsletterSignup.cy.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Note that in &lt;a href="https://www.cypress.io/blog/2022/06/01/cypress-10-release/"&gt;version 10 of Cypress&lt;/a&gt; E2E test files are placed in the directory &lt;code&gt;cypress/e2e&lt;/code&gt; and use the &lt;code&gt;.cy&lt;/code&gt; extension in the file name, which replaces the previous &lt;code&gt;.spec&lt;/code&gt; extension for E2E tests.&lt;/p&gt;

&lt;p&gt;Add the following code to this file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cypress/e2e/newsletterSignup.cy.ts&lt;/span&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="s2"&gt;Newsletter signup&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="c1"&gt;// ** 1 **&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="c1"&gt;// ** 2 **&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="s2"&gt;/newsletter&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;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;displays a message explaining the form's purpose&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ** 3 **&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="s2"&gt;allows for completing a newsletter signup&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;allows for submitting the form by using the enter key&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;displays an error message if input value is not a valid email address&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;displays an error message if the input field is empty&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;clears the email input if signup is successful&lt;/span&gt;&lt;span class="dl"&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="s2"&gt;persists the email input value if there is an error&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;h3&gt;
  
  
  Code notes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;This is the &lt;code&gt;describe&lt;/code&gt; block, which is the method used to group test specs.  I recommend matching the name of this block (&lt;em&gt;e.g.&lt;/em&gt; here the name is "Newsletter signup") to the name of the test file and, if possible, also to what this feature is named elsewhere, eg in a User Story. This is a convention I like to follow to encourage only having one set of feature tests per file and also to create less confusion when trying to locate a test.  The value of this will become more apparent when you start to have large numbers of tests.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;beforeEach&lt;/code&gt; block will run before every test in the describe block.  This is a great place to tell the test runner where it should navigate to.  Also, when applicable it can be a good place to place the app in desired state, eg using a login command. &lt;/li&gt;
&lt;li&gt;This is our list of &lt;a href="https://glebbahmutov.com/blog/cypress-test-statuses/#pending-tests"&gt;pending tests&lt;/a&gt;. The test descriptions for these tests should together make up the requirements for this feature.  If you are working off a User Story, the list of tests should match your &lt;a href="https://en.wikipedia.org/wiki/User_story#Acceptance_criteria"&gt;acceptance criteria&lt;/a&gt;. &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Using your E2E tests as your "live" todo list
&lt;/h2&gt;

&lt;p&gt;At this point, your Cypress console should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AnSd7gT8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iwi4rz80jyjjdbgsw2q1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AnSd7gT8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iwi4rz80jyjjdbgsw2q1.jpg" alt="E2E Testing console with newsletter test added" width="880" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Go ahead and click on "newsletterSignup" and you should now see the test runner with a list of pending tests.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Fjp2I4ix--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2bd6bqdgh5ess06mgtq7.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Fjp2I4ix--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2bd6bqdgh5ess06mgtq7.jpg" alt="Cypress runner with the pending tests" width="880" height="612"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the test runner now in watch mode, as you update your tests and source code, our tests will re-run automatically and update tests to passing or failing.&lt;/p&gt;

&lt;p&gt;I personally find it to be a great approach to have this type of "live" todo list, helping me remain assured that I am fully aligned and building what actually is being requested, and having a clear sense of my progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding our first test
&lt;/h2&gt;

&lt;p&gt;We're going to start by getting the first pending test to pass, which also happens to be fairly basic. &lt;/p&gt;

&lt;p&gt;In &lt;code&gt;cypress/e2e/newsletterSignup.cy.ts&lt;/code&gt;, go ahead and replace the first pending test with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;it("displays a message explaining the form's purpose", () =&amp;gt; {
  cy.get('[data-test="newsletter"]').contains("Sign up for our newsletter");
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cypress tests allow for &lt;a href="https://www.w3schools.com/jquery/jquery_chaining.asp"&gt;chaining of methods&lt;/a&gt;, which means they often are one-liners like the one above.&lt;/p&gt;

&lt;p&gt;The test divides into two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://docs.cypress.io/api/commands/get"&gt;&lt;code&gt;cy.get&lt;/code&gt;&lt;/a&gt; part, which is where we select the element we want to test.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.cypress.io/api/commands/contains"&gt;&lt;code&gt;.contains&lt;/code&gt;&lt;/a&gt; part which is one of many types of assertions.  In this case, we are asserting that we will find a specific text block in the previously selected element.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(At the risk of causing some confusion, &lt;code&gt;.get&lt;/code&gt; is also an assertion that an element exists, and &lt;code&gt;.contains&lt;/code&gt; can also be used to select an element, for use in other assertions.)&lt;/p&gt;

&lt;p&gt;You might have noticed the use of &lt;code&gt;'[data-test="newsletter"]'&lt;/code&gt; to identify our newsletter DOM element, which might look a bit odd.  We'll talk about that in more detail in a bit, but first let's get the above test to pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting our first test to pass
&lt;/h2&gt;

&lt;p&gt;If we look at our test runner, we see that the test we added above is failing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Vx13e2xv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlb5iraumvu3kaoc2pll.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Vx13e2xv--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlb5iraumvu3kaoc2pll.jpg" alt="E2E Testing console with first failing test" width="880" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is expected, since we haven't yet added the code to make it pass.  Let's do that now.&lt;/p&gt;

&lt;p&gt;Add a new file at &lt;code&gt;src/pages/newsletter.tsx&lt;/code&gt;.  Since we are placing a file in the special &lt;code&gt;pages&lt;/code&gt; directory, NextJS will create the new corresponding route &lt;code&gt;/newsletter&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Add the following code to the new file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/pages/newsletter.tsx&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextPage&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;next&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;Newsletter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextPage&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newsletter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h3&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Sign&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;our&lt;/span&gt; &lt;span class="nx"&gt;newsletter&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h3&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Newsletter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you now go back to the test runner, you should see that there is a green checkmark next to your first test, which means it's passing! :celebrate:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KV7EUT-u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/accqqfws78ebd86ob04w.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KV7EUT-u--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/accqqfws78ebd86ob04w.jpg" alt="First passing test!" width="880" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making tests more resilient with Test IDs
&lt;/h2&gt;

&lt;p&gt;Note the &lt;code&gt;data-test="newsletter"&lt;/code&gt; attribute added to the &lt;code&gt;form&lt;/code&gt; tag above.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newsletter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;"newsletter"&lt;/code&gt; is our "test id" and what we use to select this element in our test with the selector &lt;code&gt;[data-test="newsletter"]&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="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test="newsletter"]&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 is a fairly common pattern in E2E testing, where we make use of a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*"&gt;custom data attribute&lt;/a&gt; to identify the element(s) we want to test.  Note that you can put whatever you want after the &lt;code&gt;data-&lt;/code&gt; part, but I prefer using &lt;code&gt;test&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Why would we want to do this?&lt;/p&gt;

&lt;p&gt;I think the easiest way to understand the benefits of using test ids, is to look at the alternatives to identifying elements on a page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a DOM selector, such as maybe adding an ID to the element, eg &lt;code&gt;&amp;lt;form id="signupForm"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Using a class name, eg &lt;code&gt;&amp;lt;form class="signup-form"&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a selector based on the current DOM structure, such as &lt;code&gt;main form h3&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main issue with these approaches is that they're all fragile.&lt;/p&gt;

&lt;p&gt;Every time a developer touches this code, it is possible they might modify any of these due to some implementation need, and unknowingly cause a false negative test failure.&lt;/p&gt;

&lt;p&gt;Use of test ID allows us to separate test-specific code and implementation code, making tests more resilient to unrelated code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding the main user flow test
&lt;/h2&gt;

&lt;p&gt;Next, let's add a test for the main user flow: completing and submitting the signup form.&lt;/p&gt;

&lt;p&gt;This test will be a bit more advanced.  Go ahead and update the next pending test to be as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// cypress/e2e/newsletterSignup.cy.ts&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="s2"&gt;allows for completing a newsletter signup&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test@email.com&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;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&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;/api/newsletter&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;req&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;// ** 1 **&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;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ** 2 **&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;reply&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;  &lt;span class="c1"&gt;// ** 3 **&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;success&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="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Success 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="p"&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="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test="emailInput"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ** 4 **&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test="formSubmit"]&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="c1"&gt;// ** 5 **&lt;/span&gt;
  &lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test="successMessage"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ** 6 **&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal of this test is to ensure that a "happy path" newsletter signup flow works as expected.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Notes
&lt;/h3&gt;

&lt;p&gt;There's quite a bit going on in this test. Let's walk through it step by step:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;cy.intercept("POST"...&lt;/code&gt; - here, we are telling Cypress to &lt;a href="https://docs.cypress.io/api/commands/intercept"&gt;intercept&lt;/a&gt; a network request that will happen later in the test. With the intercept feature, Cypress basically masquerades as the given endpoint we defined and returns the response we expect.  The intercept feature is truly powerful and I highly recommend reading more about it in their docs. In our case, it allows us to fully test our client side form by actually submitting an email address without having to worry about clogging up some database with fake test email addresses.&lt;/li&gt;
&lt;li&gt;This is where we assert that the client side sent the payload we expected to the back-end.&lt;/li&gt;
&lt;li&gt;If the previous assertion passes, we send the expected success response.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cy.get('[data-test="emailInput...&lt;/code&gt; - this is where we type in the test email address.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cy.get('[data-test="formSubmit"]').click();&lt;/code&gt; - here, we click the form submit button and, which results in an API call, that then is intercepted as per step 1 above.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cy.get('[data-test="successMessage"]')...&lt;/code&gt; - assuming our form submit was successful, we expect a success response from the server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting the main user flow test to pass
&lt;/h2&gt;

&lt;p&gt;Let's add the code needed to get the above test to pass. Update &lt;code&gt;src/pages/newsletter.tsx&lt;/code&gt; with the following:&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextPage&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;next&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;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="s2"&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;ServerResponse&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;../pages/api/newsletter&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;Newsletter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextPage&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setEmailInput&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="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;confirmMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setConfirmMessage&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="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;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;e&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;FormEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLFormElement&lt;/span&gt;&lt;span class="o"&gt;&amp;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;setConfirmMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preventDefault&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;raw&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/newsletter&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&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/json&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;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;emailInput&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;const&lt;/span&gt; &lt;span class="na"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ServerResponse&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;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&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;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;setConfirmMessage&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;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;setEmailInput&lt;/span&gt;&lt;span class="p"&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;return&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="p"&gt;(&lt;/span&gt;
     &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;newsletter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&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;confirmMessage&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;successMessage&lt;/span&gt;&lt;span class="dl"&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;confirmMessage&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h3&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Sign&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;our&lt;/span&gt; &lt;span class="nx"&gt;newsletter&lt;/span&gt;&lt;span class="o"&gt;!&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h3&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
          &lt;span class="kd"&gt;type&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="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;emailInput&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;e&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;setEmailInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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="nx"&gt;test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emailInput&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;emailInput&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
          &lt;span class="nx"&gt;formNoValidate&lt;/span&gt;
        &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="kd"&gt;type&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="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&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="nx"&gt;test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;formSubmit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&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;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Newsletter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to add an API endpoint for submitting our form data.  Create a new file at &lt;code&gt;src/pages/api/newsletter.ts&lt;/code&gt; and add the following:&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="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextApiRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextApiResponse&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;next&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ServerResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;message&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="nl"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handler&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;NextApiRequest&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;NextApiResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ServerResponse&lt;/span&gt;&lt;span class="o"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&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;body&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`\"&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;\{email\}\" was added to the mailing list. Thanks for signing up!&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;,
    success: true,
  });
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(I won't be going through this code in detail, as this is not a post about TS or React.)&lt;/p&gt;

&lt;p&gt;Note that this API endpoint doesn't actually store the submitted email in a data store. However, if this were a real newsletter signup, the place to do it would likely be in the above file.&lt;/p&gt;

&lt;p&gt;If we now check our test runner, we should see that the main user flow test ("allows for completing a newsletter signup") test is passing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--gnBpSJBe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0suz8vg0a97nm8qggfbn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--gnBpSJBe--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0suz8vg0a97nm8qggfbn.jpg" alt="Main user flow test passing." width="880" height="535"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting remaining tests to pass
&lt;/h2&gt;

&lt;p&gt;We've written the minimum code needed to get the current tests to pass. However, there are still several requirements that need to be fulfilled, such as ensuring that emails are validly formed. You'll find the completed tests and the corresponding code to get them to pass in the &lt;a href="https://github.com/andersr/e2e-testing-tutorial"&gt;github repo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor and clean up your code with confidence
&lt;/h2&gt;

&lt;p&gt;Now that we have passing tests for all the feature requirements, we can reap the benefits of our efforts when refactoring.&lt;/p&gt;

&lt;p&gt;For example, we may want to DRY up our code, which, at least in my experience, can often lead to unintentionally breaking things.  Additionally, I haven't added much in the way to styling, and that is also something we now can do with far less concern about breaking something.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.to/andersr/ensuring-your-e2e-tests-run-on-every-code-push-1ppe"&gt;Next Up: Ensuring tests run automatically on every code push&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Now that we've written some tests, let's be sure and put them to work.   In the next post, we'll show how to make sure your full test suite runs every time you push new code, and why that matters.&lt;/p&gt;

</description>
      <category>e2e</category>
      <category>testing</category>
    </item>
    <item>
      <title>Ensuring Your E2E Tests Run On Every Code Push</title>
      <dc:creator>Anders Ramsay</dc:creator>
      <pubDate>Mon, 05 Sep 2022 18:56:11 +0000</pubDate>
      <link>https://forem.com/andersr/ensuring-your-e2e-tests-run-on-every-code-push-1ppe</link>
      <guid>https://forem.com/andersr/ensuring-your-e2e-tests-run-on-every-code-push-1ppe</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of the &lt;a href="https://dev.to/andersr/e2e-testing-series-49lg"&gt;E2E Testing Series&lt;/a&gt;.  If you haven't already, I suggest going back and reading previous posts before continuing.&lt;/em&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Writing tests is only the beginning
&lt;/h2&gt;

&lt;p&gt;We've walked through the process of how to write E2E tests. We showed the benefits of writing tests even before you write any code. And we showed the value of having your E2E tests running in watch mode to get quick feedback.&lt;/p&gt;

&lt;p&gt;While this is all great, it's unfortunately not enough. While we're doing a great job of writing our tests we're still not going to get quick feedback on a general app regression. (Remember, when you are running tests in watch mode, that is only covering the current feature you are working on, and not on the full test suite.)&lt;/p&gt;

&lt;p&gt;To achieve that, we need to ensure that our full test suite runs when we add new code and that we are notified about any failures before we merge the code.&lt;/p&gt;

&lt;p&gt;These are the three main steps we'll complete in this post to achieve that goal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable running the full test suite against a production build.&lt;/li&gt;
&lt;li&gt;Automatically run the full suite when we push new code.&lt;/li&gt;
&lt;li&gt;Prevent merge of code into the main branch unless all tests pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are a number of tools and techniques we can use to achieve the above. In our case, we're going to use a combination of npm scripts, &lt;a href="https://docs.github.com/en/actions"&gt;Github Actions&lt;/a&gt;, and &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches"&gt;Github branch protection&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let's get started!&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable running the full test suite against a production build
&lt;/h2&gt;

&lt;p&gt;Up until now, we've only been running the specific tests associated with the feature we are working on, and we've been running them against the development server. However, to prevent regression, we need to run all the tests, and run them against a production build. This is the version of your code that is served to users on the live site, and that is what we want to run our full test suite against.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add full test suite scripts
&lt;/h3&gt;

&lt;p&gt;Most of what we need to do is to add the necessary scripts to run the full suite. Go ahead and make the following updates to &lt;code&gt;scripts&lt;/code&gt; in your &lt;code&gt;package.json&lt;/code&gt; file:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rename the &lt;code&gt;"build"&lt;/code&gt; script to &lt;code&gt;"prestart"&lt;/code&gt;.&lt;/strong&gt; This leverages a &lt;a href="https://docs.npmjs.com/cli/v8/using-npm/scripts"&gt;scripts feature&lt;/a&gt; in npm, in which adding &lt;code&gt;pre&lt;/code&gt; to another script will run that script first. In this case, we want to run a build before we run the &lt;code&gt;start&lt;/code&gt; script, which is what is run to start the production server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update the &lt;code&gt;start&lt;/code&gt; script to use a different port:&lt;/strong&gt; Add &lt;code&gt;-p 8080&lt;/code&gt; to the end of the &lt;code&gt;"start"&lt;/code&gt; script. This will allow for running the production server locally while also running the dev server, eg for debugging purposes. (Port 8080 is just my personal preference, you can pick a different port if you wish.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a script for running the full Cypress test suite:&lt;/strong&gt; &lt;code&gt;"cy:run": "cypress run --e2e --browser chrome --config baseUrl=http://localhost:8080"&lt;/code&gt;. In contrast to the &lt;code&gt;"cy:open"&lt;/code&gt; script, this will run the full test suite in &lt;a href="https://www.softwaretestinghelp.com/headless-browser-testing/"&gt;headless&lt;/a&gt; mode, and we can use it both for local debugging and to run in a deployed environment. Note that the port used in &lt;code&gt;baseUrl&lt;/code&gt; has to match what we use in the &lt;code&gt;start&lt;/code&gt; script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a single command E2E script&lt;/strong&gt;: &lt;code&gt;"e2e": "start-test start 8080 cy:run"&lt;/code&gt;. This script pulls together all the other scripts above. We are first running the &lt;code&gt;start&lt;/code&gt; script (which in turn will be preceded by the &lt;code&gt;prestart&lt;/code&gt; build script), then waiting for a server to be listening on port 8080, and finally running the full test suite against that port. As before, this port has to match the two other ports you set previously.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your &lt;code&gt;scripts&lt;/code&gt; section should now include the following scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prestart"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"next start -p 8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cy:run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cypress run --e2e --browser chrome --config baseUrl=http://localhost:8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"e2e"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start-test start 8080 cy:run"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make sure everything is working as expected, go ahead and run the &lt;code&gt;e2e&lt;/code&gt; script, and you should see the entire above sequence being run, concluding with an 'All specs passed!' message in your terminal:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--IjB8Cw1o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bmg7li5go9aoiyalgg88.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--IjB8Cw1o--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bmg7li5go9aoiyalgg88.jpg" alt="Run all E2E tests locally with all tests passing" width="880" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll be using this last script in the next section when pushing our code, but you can also use it locally for debugging purposes, such as when you are getting a test failure only when pushing code, but not seeing an error when running in watch mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatically run the full suite when we push new code
&lt;/h2&gt;

&lt;p&gt;As a reminder, this and the next step in this tutorial assumes you are using &lt;a href="https://github.com/"&gt;github&lt;/a&gt; to store your code. There are many other options out there for achieving the same goal.&lt;/p&gt;

&lt;p&gt;To ensure our &lt;code&gt;e2e&lt;/code&gt; script runs on all new code that we push, we're going to add a &lt;a href="https://docs.github.com/en/actions"&gt;github action&lt;/a&gt;. Add the following to &lt;code&gt;.github/workflows/e2e.yml&lt;/code&gt; (You must place the file in this directory, though the name &lt;code&gt;e2e&lt;/code&gt; is my personal preference.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;name: E2E Tests

on: &lt;span class="o"&gt;[&lt;/span&gt;push] &lt;span class="c"&gt;# ** 1 **&lt;/span&gt;

&lt;span class="nb"&gt;jobs&lt;/span&gt;:
cypress-run:
runs-on: ubuntu-20.04 &lt;span class="c"&gt;# ** 2 **&lt;/span&gt;
steps: - name: Checkout
uses: actions/checkout@v2 - name: Cypress run
uses: cypress-io/github-action@v2 &lt;span class="c"&gt;# ** 3 **&lt;/span&gt;
with:
&lt;span class="nb"&gt;command&lt;/span&gt;: npm run e2e &lt;span class="c"&gt;# ** 4 **&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code will be triggered when we push to a github repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Notes
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;This is telling github to run this action every time we push new code.&lt;/li&gt;
&lt;li&gt;This is the OS and version used for the virtual environment in which run our tests. I recommend always using a specific version, such as the latest stable version, rather than &lt;code&gt;latest&lt;/code&gt;, which is risky because you may then suddenly start to see test failures caused by a version update that has nothing to do with your tests. See &lt;a href="https://github.com/actions/virtual-environments"&gt;virtual-enviroments&lt;/a&gt; for the latest stable version.&lt;/li&gt;
&lt;li&gt;We are using a ready-made &lt;a href="https://github.com/marketplace/actions/cypress-io"&gt;action&lt;/a&gt; created by Cypress to run our tests.&lt;/li&gt;
&lt;li&gt;This script name needs to match the &lt;code&gt;e2e&lt;/code&gt; script we created earlier.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Go ahead and commit this code and &lt;a href="https://docs.github.com/en/get-started/importing-your-projects-to-github/importing-source-code-to-github/adding-locally-hosted-code-to-github"&gt;push it to a github repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Open up your repo in your browser and click on the &lt;a href="https://github.com/andersr/e2e-testing-tutorial/actions"&gt;"Actions"&lt;/a&gt; tab, and you should see your action running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2VMBUlsw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/plvon802wa8wqkjw881l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2VMBUlsw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/plvon802wa8wqkjw881l.jpg" alt="Github action running" width="880" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can click into the action and see a detailed log of all the action steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevent merge of code into the main branch unless all tests pass.
&lt;/h2&gt;

&lt;p&gt;Just one more step to go!&lt;/p&gt;

&lt;p&gt;Now that we've got our test running automatically on push, we want ensure that we don't merge code with failing tests. Similar to above, these instructions are specific to Github, but the general principles should be applicable regardless of the service you are using.&lt;/p&gt;

&lt;p&gt;Achieving this requires setting up a branch protection rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Protecting the main branch
&lt;/h3&gt;

&lt;p&gt;The main branch (ie the branch containing code that will be pushed to production) is the one we need to protect from any code that contains regressions.&lt;/p&gt;

&lt;p&gt;In github, we can add this protection by creating a branch rule, which should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Not allow pushing code directly to the main branch, but instead require a pull request.&lt;/li&gt;
&lt;li&gt;Require that all checks, in our case just the E2E tests, pass before merge is allowed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can find instructions for how to do add branch rules in the &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches"&gt;github docs&lt;/a&gt;, but very briefly, in your repo, click on Settings &amp;gt; Branches &amp;gt; Add Rule.&lt;/p&gt;

&lt;p&gt;In "Branch name pattern", enter the name of your main branch, eg "main" and check the box "Require a pull request before merging". (I'll also uncheck "Require reviews" for the purposes of this tutorial, but it's probably a good idea to leave it checked.)&lt;/p&gt;

&lt;p&gt;Next,check off "Require status check to pass before merging", which will prompt you to "search for status checks". Somewhat confusingly, a "status check" corresponds to the name of &lt;em&gt;job&lt;/em&gt; within an action rather than the name of the action itself. In our case, the name is &lt;code&gt;cypress-run&lt;/code&gt; (which you'll see under &lt;code&gt;jobs:&lt;/code&gt; in &lt;code&gt;.github/workflows/e2e.yml&lt;/code&gt;.) Go ahead and type that in and you should see the job name to select.&lt;/p&gt;

&lt;p&gt;Your form should look something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KWAh3bFW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfl86alqfijvn5uc241z.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KWAh3bFW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nfl86alqfijvn5uc241z.jpg" alt="Adding main branch protection rule" width="880" height="1116"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll down and click on "Create".&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing that everything works
&lt;/h2&gt;

&lt;p&gt;Let's make sure that our new branch rule works as expected. Go ahead and make a change to a test that you know will cause a failure, eg by adding an extra character to a test id.&lt;/p&gt;

&lt;p&gt;To confirm that your test will fail, run your test suite locally using the &lt;code&gt;e2e&lt;/code&gt; script.&lt;/p&gt;

&lt;p&gt;Next, go ahead and commit the change and push your branch and &lt;a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request"&gt;create a pull request&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You should now see a message showing your checks have failed with the merge button disabled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GcVBQ5Lu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8i1naj1lwcqjtjc080aw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GcVBQ5Lu--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8i1naj1lwcqjtjc080aw.jpg" alt="Github message showing merges blocked" width="880" height="273"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, undo the change you made and push again and this time you should (eventually) see all green checkmarks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J8N8Zqdi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9vmgn8g9oibteffpqc5r.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J8N8Zqdi--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9vmgn8g9oibteffpqc5r.jpg" alt="Github PR all green checkmarks" width="880" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You've now set up a basic E2E testing system for writing tests and preventing code regressions! :celebrate: :celebrate: :celebrate: &lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This wraps up the E2E Testing Series. I hope you found it useful. If you want to keep learning, I recommend watching &lt;a href="https://www.youtube.com/watch?v=5XQOK0v_YRE"&gt;this video&lt;/a&gt;, in which the founder of Cypress talks about E2E testing best practices.&lt;/p&gt;

</description>
      <category>github</category>
      <category>e2e</category>
    </item>
    <item>
      <title>E2E Testing Series</title>
      <dc:creator>Anders Ramsay</dc:creator>
      <pubDate>Mon, 05 Sep 2022 18:55:49 +0000</pubDate>
      <link>https://forem.com/andersr/e2e-testing-series-49lg</link>
      <guid>https://forem.com/andersr/e2e-testing-series-49lg</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This is a series of posts I've written about the process and benefits of writing and using End to End Tests (E2E).&lt;/p&gt;

&lt;p&gt;We'll walk through how to set up your system to write them, how to use the tests you've created to stay focused, and last but not least, how to use E2E tests to prevent &lt;a href="https://www.google.com/search?q=code+regression"&gt;code regression&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I've broken the series into the following parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Series intro and prerequisites (current post).&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/andersr/how-to-set-up-a-local-e2e-test-development-environment-28pc"&gt;Setting up your local environment for writing E2E tests&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/andersr/a-suggested-process-for-writing-e2e-tests-20gc"&gt;A suggested process for writing tests&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/andersr/ensuring-your-e2e-tests-run-on-every-code-push-1ppe"&gt;Ensuring your test suite runs every time you push code&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this post, I'll offer a general intro to E2E testing and assumed prerequisites. I hope you find it useful.  Please feel free to post feedback in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are E2E tests and why should you care?
&lt;/h2&gt;

&lt;p&gt;One good way to understand E2E testing is to look at what can happen without it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A project without E2E testing
&lt;/h3&gt;

&lt;p&gt;You're working on a project that doesn't have any formal E2E testing in place.&lt;/p&gt;

&lt;p&gt;Let's say you're adding a newsletter signup form to your web app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--k6bSIzoa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z61t1mvcs3r9mjhbg3r1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--k6bSIzoa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z61t1mvcs3r9mjhbg3r1.jpg" alt="Newsletter signup form" width="880" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To keep your code &lt;a href="https://en.wikipedia.org/wiki/Don%27t_repeat_yourself"&gt;DRY&lt;/a&gt;, you re-use existing input and form submit components to implement your form. You test the form locally and everything is working great. You also click around your web app and everything else seems to be working great as well.&lt;/p&gt;

&lt;p&gt;However, after deploying your code, you start getting notices that users no longer can reset their passwords.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://i.giphy.com/media/ZOEoDuFPcMwENGwULP/giphy-downsized-large.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://i.giphy.com/media/ZOEoDuFPcMwENGwULP/giphy-downsized-large.gif" alt="No bueno animated gif" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But wait, you were working on newsletter signup, how could that possibly break password reset?&lt;/p&gt;

&lt;p&gt;Actually, as it turns out, the password reset form uses some of the same components that you used when implementing newsletter signup.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--AFIFyIQc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tz3rdi0ypb3wrfzn0k75.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--AFIFyIQc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tz3rdi0ypb3wrfzn0k75.jpg" alt="Password reset form" width="880" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you made some tweaks to those components (maybe you decided to rename a prop) while adding the newsletter feature, you unintentionally broke the reset feature.  Worse, since that feature is out of sight, out of mind, you didn't think to test it before shipping newsletter signup.&lt;/p&gt;

&lt;p&gt;This is an example of what we call a &lt;a href="https://en.wikipedia.org/wiki/Software_regression"&gt;code regression&lt;/a&gt;, where something that previously worked unexpectedly stops working, and it is one of the main reasons we write E2E tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  E2E testing to the rescue
&lt;/h3&gt;

&lt;p&gt;If you'd had an E2E testing system in place, such as the one we'll be creating, and you had good feature coverage in your tests, after modifying the components being used in password reset, you'd have received a test failure notification, and then be able to quickly address the problem, preventing a reset password failure in production.&lt;/p&gt;

&lt;p&gt;As apps get more complex and more feature-rich, and as we get better at re-using code across our apps, the risk of regression increases, so writing these types of tests and ensuring we run them regularly becomes increasingly important.&lt;/p&gt;

&lt;p&gt;In this series, we're going to walk through the process of setting yourself up for success by enabling quickly and effectively writing these types of tests, running them locally against your development environment to get quick feedback on possible issues, and then automatically running the full suite of tests every time you push code, to prevent merging in code that will lead to regressions.&lt;/p&gt;

&lt;p&gt;We'll also discuss how integrating E2E tests into your development process can you help you write better code overall and help ensure that you remain focused on your goals.&lt;/p&gt;

&lt;p&gt;In my opinion, E2E tests are the most valuable test type, because they simulate actual end users and are focused on outcomes rather than implementation details. However, while valuable, these types of tests can also be costly, in that setting up your system to run them and writing the tests themselves can be complex and time-consuming. And as your test suite grows in size, they can take a long time to run.&lt;/p&gt;

&lt;p&gt;But here's the thing: if done right, you can overcome these challenges, and get the best of both worlds. Achieving that is one of the top goals of this series.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick note about terminology
&lt;/h2&gt;

&lt;p&gt;I am using the term "E2E" or End to End Testing a bit loosely here. Some might call the types of tests we are writing a functional test, or you may have a different name.  There are countless names and nuances for different types of tests, such as smoke test, integration test, regression test, and more, many of them overlapping.&lt;/p&gt;

&lt;p&gt;It's easy to get caught up in the terminology and lose sight of the actual goals, such as writing higher quality code and preventing stuff from breaking.  I prefer just using E2E as an umbrella term for any type of test that simulates a user using the app and which treats the app itself as a black box, just as a real user would. (I.e. they don't know and don't care about the inner workings of the app.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I am assuming you already have some experience with JavaScript generally and specifically with React and Node.&lt;/li&gt;
&lt;li&gt;I'm also assuming you already have the latest stable version of Node and npm installed on your machine.&lt;/li&gt;
&lt;li&gt;I'll be writing the sample code mostly in Typescript so I am assuming you are familiar with that as well.&lt;/li&gt;
&lt;li&gt;Finally, I am assuming you have some experience using git, as we'll be using that in the last part of the series, to create branches, pull requests, and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, to be clear, many of the principles we discuss will be agnostic to whatever language or tool you're using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code Repo
&lt;/h2&gt;

&lt;p&gt;You can find all the source code we'll be walking through below &lt;a href="https://github.com/andersr/e2e-testing-tutorial"&gt;in this repo&lt;/a&gt;.  I recommend writing your own code rather than just copying from the repo, but you might find it to be a good reference in case you get stuck.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;a href="https://dev.to/andersr/how-to-set-up-a-local-e2e-test-development-environment-28pc"&gt;Next up: Setting up your local testing environment&lt;/a&gt;
&lt;/h2&gt;

</description>
      <category>e2e</category>
      <category>testing</category>
      <category>cypress</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
