<![CDATA[przemuh.dev [EN]]]>https://przemuh.devGatsbyJSTue, 04 Mar 2025 17:52:19 GMT<![CDATA[Short story about optimisation]]>https://przemuh.dev/en/blog/tree-performance-improvement-case-studyhttps://przemuh.dev/en/blog/tree-performance-improvement-case-studySun, 08 Nov 2020 00:00:00 GMT<p>Some time ago, I posted a screenshot on <a href="https://twitter.com/przemuh/status/1319595759935852544" target="_blank" rel="nofollow noopener noreferrer">Twitter</a> showing a flame-chart from the Profiler tool. At that time, I was working on improving the performance of an application we were developing at Egnyte. A certain functionality, for a large amount of data, took an incredibly long time - 3.5 minutes! During this time, the application displayed a &quot;spinner,&quot; and the user didn&#x27;t know if something was happening or if it had frozen. After a few days of working with the Profiler, I managed to implement improvements that reduced the calculation time from 3.5 minutes to 35 seconds. In this post, I would like to describe how I achieved this.</p><h2>Description of the functionality</h2><p>Let&#x27;s start with a description of the functionality that, to put it briefly, was lacking in terms of performance. One of the main views shows a list of folders with files containing sensitive data. These can be credit card numbers, medical data, personal data, and more. This data can fall under one of several built-in policies, such as HIPAA, GDPR, but we also allow users to define their own policy, for example, based on a previously created dictionary. Initially, the &quot;Sensitive Content&quot; view showed only a flat list of folders. Last year, our Product Owner, along with the UX team, concluded that working with a flat list of folders might be inefficient. Instead, a much better, and essentially more natural way of representing data would be a folder tree.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:944px"> <a class="gatsby-resp-image-link" href="/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:37.883959044368595%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWElEQVQoz01R127DMBDz/39gn4oUKYJsa1nLGmZ5clJEwOE0KIqkpsesITUrgydrrJ+K3eC+JFx8go4ramlIucD5COM8tPUwUpzbJfyvJ2MdvA9YXuUWj5QyaiVBrcjsfi24GwvDCjEi5xWORDEmpJCQ1xWWPHJ3mpWCNoZgAyVzbcZFHwJ665BhSPhN5Yu1A9v6vt9aA9kGVu4K8XS8KJxuBqe7we9V4fxwUEuG8QU2NoTccVlWfF01zvcdZwKtpwbtSeYSbsrjlzyzTZges4KiKqlZ6VE+RBRaraJwA+xacWC+lsqVtuOsc7+Uiq0mRhXxJE+i2t2yFlKNQOmRGUl3zjHHOqy5UvCjPiyL1WGZhJ0qvR+WV7GsCJRA36DPsW17t8zwIMr5yI7t40w+rlGqEM7vDHeFeigav0i7G9FS/cXoaO3ISJzdf1oel3NxIB80FJJDFP4BCJ5pdpZ5/CkAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Sensitive Content List View" title="Sensitive Content List View" src="/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png" srcSet="/static/f622f9b398ed8964cce4a32ffc9df3fb/3cf3e/sc-view.png 293w,/static/f622f9b398ed8964cce4a32ffc9df3fb/78a22/sc-view.png 585w,/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png 944w" sizes="(max-width: 944px) 100vw, 944px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Sensitive Content List View</figcaption> </figure></p><h2>Tree building algorithm</h2><p>There have been several approaches to folder trees in our project. They mainly relied on the unique <code>folderId</code> property. Unfortunately, in the case of &quot;Sensitive Content,&quot; we couldn&#x27;t use this because not all folders could contain sensitive data, and only for such folders did we receive a <code>folderId</code>.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">/Shared/A/B/ In this case, we have 3 folders (Shared, A, B), of which only B has a folderId</code></pre></div><p>There can be a multitude of such SC (Sensitive Content) locations. The performance issue appeared already at 250K locations. And it was not an exception, as confirmed by a client where we found almost <strong>a million</strong> folders. For 1M list elements, the tree-building time was 3.5 minutes. Therefore, my task was to ensure that the tree for 1M elements builds in less than 60 seconds.</p><p>Returning to the algorithm. Very simple or so it seemed :)</p><ol><li>Take the entire path and split it into fragments according to the separator, e.g., <code>/</code></li><li>Insert each folder into two structures: &quot;tree&quot; and &quot;flat&quot;</li><li>If a folder does not have a <code>folderId</code>, treat it as a meta-folder</li></ol><p>For simplicity, I omit the fact that we support different data sources, and these separators can vary greatly :) Moreover, as it later turned out, some data sources can have two folders with the same name at the same nesting level 😱. And how to distinguish them? I will also skip the fact that the resulting tree was to be presented in the form of a &quot;sparse-tree.&quot; In short - it means that if a folder contains only one sub-folder, the parent path should be collapsed/merged.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">List: /Shared/A/B/C /Shared/A/B/D --&gt; Tree: /Shared/A/B /C /D</code></pre></div><h2>First, or rather second implementation</h2><p>As I mentioned earlier, this was not the first tree we had to display in the application. In a completely different view, we also had to create a sparse-tree and didn&#x27;t want to have several different implementations. Therefore, we wrote a simple module for building and managing the tree. It was based on two small components:</p><ul><li>a <code>buildTree</code> function that took a flat array of nodes and the place (path in the tree) from which it was to insert these nodes</li><li>a &quot;slice&quot; from <code>redux-toolkit</code> that managed the tree structure (expanding, collapsing nodes, etc.)</li></ul><p>The entire tree, or rather these two structures &quot;tree&quot; and &quot;flat,&quot; were kept in redux as follows:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> tree<span class="token operator">:</span> <span class="token punctuation">{</span> path<span class="token operator">:</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">,</span> <span class="token comment">// root</span> children<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;Shared&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">// path-part or folder name as a key</span> path<span class="token operator">:</span> <span class="token string">&quot;/Shared&quot;</span><span class="token punctuation">,</span> children<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;A&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> path<span class="token operator">:</span> <span class="token string">&quot;/Shared/A&quot;</span><span class="token punctuation">,</span> children<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> paths<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;/Shared&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> meta<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> ...nodeProps <span class="token punctuation">}</span> <span class="token property">&quot;/Shared/A&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> meta<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> folderId<span class="token operator">:</span> <span class="token string">&quot;some-unique-id&quot;</span><span class="token punctuation">,</span> ...nodeProps <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre></div><p>This structure is obtained from the helper function <code>buildTree</code>.</p><p>Thanks to the use of <a href="https://redux-toolkit.js.org/" target="_blank" rel="nofollow noopener noreferrer">redux-toolkit</a>, and consequently the <a href="https://github.com/immerjs/immer" target="_blank" rel="nofollow noopener noreferrer">immer</a> library, we could perform operations on the tree very easily:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> initialTreeState <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">initialized</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token literal-property property">tree</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">,</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token literal-property property">paths</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">createTreeSlice</span> <span class="token operator">=</span> <span class="token parameter">treeName</span> <span class="token operator">=&gt;</span> <span class="token function">createSlice</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">name</span><span class="token operator">:</span> treeName<span class="token punctuation">,</span> <span class="token literal-property property">initialState</span><span class="token operator">:</span> initialTreeState<span class="token punctuation">,</span> <span class="token literal-property property">reducers</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token function-variable function">insertTree</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> tree<span class="token punctuation">,</span> paths <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> payload <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> paths <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>paths<span class="token punctuation">,</span> <span class="token operator">...</span>payload<span class="token punctuation">.</span>paths<span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">const</span> node <span class="token operator">=</span> <span class="token function">getNodeByPath</span><span class="token punctuation">(</span>payload<span class="token punctuation">.</span>parentPath <span class="token operator">||</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">,</span> tree<span class="token punctuation">)</span> node<span class="token punctuation">.</span>children <span class="token operator">=</span> payload<span class="token punctuation">.</span>tree<span class="token punctuation">.</span>children <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token function-variable function">toggleNode</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> tree <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">payload</span><span class="token operator">:</span> path <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> node <span class="token operator">=</span> <span class="token function">getNodeByPath</span><span class="token punctuation">(</span>path<span class="token punctuation">,</span> tree<span class="token punctuation">)</span> node<span class="token punctuation">.</span>expanded <span class="token operator">=</span> <span class="token operator">!</span>node<span class="token punctuation">.</span>expanded <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>The helper function <code>getNodeByPath</code> is used to search for a node by path. It can also search for a node in a sparse-tree.</p><h2>First attempts and first mistakes</h2><p>And everything was going smoothly, but then a client with 1 million folders came, and boom... The Product Owner sets up an Epic in Jira titled &quot;Support 1M folders on SC tree view.&quot; A quick brainstorming session and a list full of ideas right away:</p><ul><li>maybe build the tree on the fly, as we parse JSON?</li><li>maybe build the tree in a web-worker, at least we won&#x27;t block the main thread for 3.5 minutes?</li><li>or maybe just drop everything and becoma a farmer? ⛰ 🐑</li></ul><p><img src="https://media.giphy.com/media/kPtv3UIPrv36cjxqLs/giphy.gif" alt="Or maybe..."/></p><p>The first mistake - no one even started the Profiler to see what was taking so long. Everyone assumed that the current tree implementation was top-notch and couldn&#x27;t be better. The Profiler itself, at first glance, is not a simple tool, and maybe that was the reason we jumped to ideas like building the tree &quot;on the fly&quot; or moving it to a web-worker.</p><p>You&#x27;re probably wondering - but how on the fly? After all, when a request is made, only when the response comes does the browser parse the JSON and provide the response. Yes, but... in this case, our backend developers also had to work a bit on optimization, and instead of returning full data, they started returning our SC list as a stream. Thanks to this, we could, for example, use the <code>oboe.js</code> library to parse JSON on the fly.</p><p>Of course, I tried this approach because, after all, someone wrote it into the Jira task, so it had to be checked, right? 😜 Cool, the JSON was parsing &quot;on the fly,&quot; but the stream lasted 30s instead of 10s, and I hadn&#x27;t even started building the tree yet. So I gave up and decided to look elsewhere.</p><h2>Web-worker</h2><p>I also tested the approach with a web-worker. But I encountered a completely different problem. Okay - I can download 1M elements and build a tree based on them, but I have to send it later from the web-worker to the main thread. The tree structure is quite extensive, along with the data we saved in this flat <code>paths</code> structure. If we want to send such large data from one thread to another, the browser has to serialize the data, send it, and then parse it again. This also caused the browser to &quot;freeze&quot; during the transfer from one memory location to another. Of course, there are ways to send &quot;directly&quot; (without copying) through so-called Transferable Objects, e.g., ArrayBuffer, but I decided that for now, it might not be worth the effort and decided to check if our tree implementation was as great as we thought 😜</p><h2>Profiler</h2><p>I sat in front of the computer screen, launched the dev-tools, and pressed the &quot;record&quot; button in the Profiler. After a while, I got a colorful graph that reminded me of the defragmenter times of Windows 98 🤣</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/08d444b93bebf165ed87c320556ad7b0/d9ed5/flame-graph-violet.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:62.45733788395904%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB5ElEQVQoz32S63KiQBCFef8X2t97qYq1sWIUXW8gJgICQWBuwIDxbA8Y12xMpuqrmb7MmaZpy/NcbJ89uJ6HjbeCH/sIkgj7FyJNEEUbpHsbeby8kEWLnv2cWCCncxIs4LoLWFo3aJoGdV13VFXV2W/rdAPcsM0qKw0ryzOoqoQs36ggpADPI0iCxx5YuH5HEaxQhCs6r3rb7HsHjPKtQ1qQYAtZHS8IqTGPhrCzn5j9/o6nH0OEgzGCuxF8wuzh3bhn8NjF/MEDQnsKS2RHlPUr1BVlfQLLGhRJjdWji8l0CsfdwNl6WCZrzF+WGLsTrGcuFmMHc3uJJe35H2kE23di16IG8VAhHdHPcQIc/AxCa3BdY+dv8DSy4dkzeKMZ4vsQbCNgcdZ8qPBC9QrmVJBDDTUhti3K9gTqCgq3RPqtQD4owH4JyHvq/c5UyL8QJEQowTwF7tOFlEQ1+RvyJxpsUoIHdQdzJVRUk6BoPxUzl0VqXhYQMVWaGcFT55c53ds1kFSQzKkNvoA60NhIRf2ipGtU3dOdJQkbobyBYm3vM4hjjyQ4TcmhpngDi3ENoRpwpc/Qi90YnRHnKgziX8y0Sqq2h75SMpNDFe52AeI4QcEYuKCB5tRgqf5D3vB9zBFC4i+gT4+kJsCwBQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Flame graph" title="Flame graph" src="/static/08d444b93bebf165ed87c320556ad7b0/105d8/flame-graph-violet.png" srcSet="/static/08d444b93bebf165ed87c320556ad7b0/3cf3e/flame-graph-violet.png 293w,/static/08d444b93bebf165ed87c320556ad7b0/78a22/flame-graph-violet.png 585w,/static/08d444b93bebf165ed87c320556ad7b0/105d8/flame-graph-violet.png 1170w,/static/08d444b93bebf165ed87c320556ad7b0/28884/flame-graph-violet.png 1755w,/static/08d444b93bebf165ed87c320556ad7b0/92bee/flame-graph-violet.png 2340w,/static/08d444b93bebf165ed87c320556ad7b0/d9ed5/flame-graph-violet.png 2880w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Flame graph</figcaption> </figure></p><p>The first thing that caught my eye was this purple color, which dived very, very deep. Upon closer inspection, it turned out that a lot of these purple elements were the work of <code>immer.js</code>. A quick glance at the documentation and boom! A bullseye. It turns out that when &quot;inserting&quot; a large amount of data through <code>immer</code>, we can speed up this process through <code>Object.freeze</code> <a href="https://immerjs.github.io/immer/docs/performance#performance-tips" target="_blank" rel="nofollow noopener noreferrer">more info here</a>. This procedure allowed me to go from 12.54s to 11.24s for 54K elements. For 1M, the jump was, of course, proportionally larger. But it still wasn&#x27;t it...</p><h2>From profiler to sources</h2><p>Did you know that if you click on a block in the Profiler and then move to the file, you get times for individual code blocks? No!? 😎 Now you know ;)</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/1ba2423b674f0cab158baa993f4c7cfd/0d0e4/source-before.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:90.7849829351536%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACTklEQVQ4y4VUa5PaMAzk//+29kOnTO8KB4RHICTYie3Ez5Ct7IO0PMopeBwie0falTSRqkXFGGoh0LYe2+0eH/M1lssa63WDLDthnx+hlIF3Dn3f43w+36zrtxACJl3XQcsCtmOIFh2y1gQksVhU2O0O4ExA64DgA4bhjP9ZvDtp2xaKbwn0SIcvDhPQMAfOLUXVw1mHrvOUgUUMoO/PGOIz/F0joNYaHV/BqhwXPHi6/Gu6x2y2x6li8N6n7/cA93sCdMTL1ZLj4pRKYTaf4+19j80mcihvAK7vD4DGGMjiBzr2ezygSahDecKBNZCdgJQCQsp04VlkD4Bs+w0s//lPlEDTcJQlx3JFHCv3FOBphJFDUUyhTvOrmxwDdrlAtqpwLDiEMNDGJ98nKy9Sjqq1ooRp6xEw/jhT2GQM5VHDmv7m8oAXKVtrEZyhGrOjwzqLuuVoDaWrLQz5bDBwvX2p9phyW72n0rmaMxbZusL0bYPVJkNeMCzof0kixW645+8GUFF5GAKIB2+4IR6LJcfig2FFe76tqS4FlRApLy0cFfzTCCNgoMK9SSH1Z6BIA5T0SWXdORLH0btPbej9C8CWLWHkfkwj0GEXgURPAJYikrQiUP/Qvw8pR5Xr/Hvi8Won3iHLacqUBRV4Q/UYI9SIfGttXosSVTaqomnDx+FgiYJTXYPVPIFbS5MmOMQ2ve/rp53ijYS33UWLgYaDBSs18p1CwxmOJUNVEdeh/7pTUh16gz7YNCgNdYTVJBKpHMKnOHH3/nzp5eFlyn8A2CB77hemAIcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Times before optimization for buildTree" title="Times before optimization for buildTree" src="/static/1ba2423b674f0cab158baa993f4c7cfd/105d8/source-before.png" srcSet="/static/1ba2423b674f0cab158baa993f4c7cfd/3cf3e/source-before.png 293w,/static/1ba2423b674f0cab158baa993f4c7cfd/78a22/source-before.png 585w,/static/1ba2423b674f0cab158baa993f4c7cfd/105d8/source-before.png 1170w,/static/1ba2423b674f0cab158baa993f4c7cfd/0d0e4/source-before.png 1230w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Times before optimization for buildTree</figcaption> </figure></p><p>What stands out is 229ms for building a simple string 🤯 which is the current path. It turned out that this simple oversight could be replaced with a shorter piece of code, which ultimately takes 1.7ms.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/7be33/source-after.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:100.34129692832765%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAACr0lEQVQ4y41UiZKbMBTb//+7trOdZtPN5iKBcBp8c6iy0yXXTloYD8R2hCzpvRelFLQ4wHYplPZIkgyL32ssVyV2iUSWdlivCz4LOGvR955jwDB8PV6MsdD1Bm3xgWkCvPeoig4fqxrfvudYLrfY7zJUlYI1DtM44NlFQA2jGg4BTH9nhxFOedSNg1IWRlt466H1xN8a3rm4bSKD+/EiOsU/FzCyiAwD5uQGNCeL7baF4nrbdmgagaKoECSqa55K9zPoDcNWGvRO8qtqXgwMDukJq+SEtMyprYYQBmUlIaWjLCN1HL8GDEduixWqdHneEO5xgiaTPQEXbwLHY8cTeMhMw1KKz+se7C+ggaIpTb662SSlxm6bYL9X2O06zpPRGDS51exaywjoeLzp7otD36OhTkI8d/TLI2vqo6oVo7O+bCCZvKzwyjyumcvkVOKQH7E+JjgUOaqO5niaZdtHhsE1Vb7z2B+XRd5SSbrZMX8WFdkGp8tSxjyG7F6zewA0XY7BNvOk8xZZWWKf58ibE2rZ4NQ0aExN8v4hg9fgPLKByF5RHn5cFjiUcHhf0OVfW2w2jNCqYC4bZJmMYb9Uwa2OkaG3ikbYGexzXRYKGR3e7PhMa7otCNzy2TLo53APw20eX6SUcM7GWFwfoafTzg/Uq4dl2Xnf833gO+fdSLDz+zjeAcZu0+xh2sNVKbMSFAG6Hq3w6GiIECrWtnP90+jE2LTZTzq9mCe1djjmNeNS0ZwqOl3Xjgx9ZBTKLoxrdjPDWCnlG7ribQZ07C5FlmO9o26JoL4DusC2NXy2ZOuihqE3PgBaNk1nTRyfkyMbZV2ZWHZdG5gZtjAaN/T/VykBbODX5tJz7H1kkaaaLcuwrm00KXTk6V+1HBgG68+JmaKbsrOxEVy0mp52mOvrD45MFooaxFLvAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Times after optimization for buildTree" title="Times after optimization for buildTree" src="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/105d8/source-after.png" srcSet="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/3cf3e/source-after.png 293w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/78a22/source-after.png 585w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/105d8/source-after.png 1170w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/7be33/source-after.png 1558w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Times after optimization for buildTree</figcaption> </figure></p><p>You might think - (ironically) wow... 227ms... &quot;bravo&quot; 👏. What is 227ms? If we look at it as a single value - indeed... micro-optimization. But remember, the goal was to handle 1M elements, and the path concatenation operation concerned each sub-folder.</p><h2>Spread operator AKA Object.assign</h2><p>How to make a shallow copy of an object or extend another object - nothing simpler - spread operator <code>...</code>. If you have to support browsers like IE11, you probably use babel.js - just like us... and such a spread operator, in the end, is translated to <code>Object.assign</code> (*big simplification).</p><p><code>Object.assign</code> is <a href="https://twitter.com/dan_abramov/status/980436488860196864" target="_blank" rel="nofollow noopener noreferrer">relatively slow</a> and can cause problems at a larger scale. In this case, I opted for simple key-by-key copying. Thanks to this simple procedure, I reduced 154ms to 44ms. And again, for individual elements, it doesn&#x27;t matter at all, but when iterating over a large data set, such optimizations can work wonders.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/7bed4f76daac0b292144d25c8a68996a/35252/dan.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:93.85665529010238%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAACkklEQVQ4y51U227aQBD1/39NX9qqUtWnSn2okjTENgZsCGBifL97fQGfzgwQkUppkyIddjy7e+bM7Oxqxs0PzCc/sd15WNg21psthuGA43EkHN+G8bRWtR20758+4ObbF+RNhyhOEKcZ/u83oichWpYXcJ88PD6usSF1W9fFbvck9nqzwXq9hk3KfT/AdusStlitHml+I+A1aXYSMRyIcLlcYjKZwDRNGIYBy7KwIt98PhefTj5zOsXDg06+BaY0z7ZhmLi5vZMAByISQlZoL1e4vZ9ANy1YCwfGzIY5t/FLN2GSPZ3NsVjYUl8ebceR0TSncJwldN2AQwI4E9W20KyoxZ1XwwoV9EBhRt+L+AQn7ZCrAeNV4RnjlT0MgygUm1Mu2wPyHij7EQXZPPk+HAUcpCdy7UjO0HORJTFU00jE96LvezS0V9pmpGJ+/fgZ89kCCbVMWZao6xpVVcvCt/xYaVGUUB0RssyqbpBlOTkLRFFMdoY0TWW8IKVgGW2KixZ5Wcl8TH3LRJwu/Z0UciHDMILn7eEHgZwUK+soGqPvh5c2tca1j1OWtr4Q8iSf0EW653nI8/w5HU7f930opbDf79HQdxCE8v3injwrpIglpbCnTZwuq+U68gJGVVWSLgfmeVYVRpGMF6KXhFLQCt7eJ9JADka1vaAl8NVkf90oPHk+ipLXBijp0F4lrCStAAGp4wciSVK5n0yeE2GcJKLQp1Rrao+QlLbtKwq5brxxs90RXBldesp4rGqFnE4xilM0ilINY1HKgRvV/vHYnAm7jk/0BI7aPn8PZ9/ZL7i2O1zvZXBQjQk4Kke8HhmsUJ2D/AsSnERoFW3kRmWClOolTU6HxEiyQu7npUZ/w+U9/A1xprNXkDEExgAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Dan Abramov on Object.assign" title="Dan Abramov on Object.assign" src="/static/7bed4f76daac0b292144d25c8a68996a/105d8/dan.png" srcSet="/static/7bed4f76daac0b292144d25c8a68996a/3cf3e/dan.png 293w,/static/7bed4f76daac0b292144d25c8a68996a/78a22/dan.png 585w,/static/7bed4f76daac0b292144d25c8a68996a/105d8/dan.png 1170w,/static/7bed4f76daac0b292144d25c8a68996a/35252/dan.png 1204w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Dan Abramov on Object.assign</figcaption> </figure></p><h2>Value aggregation</h2><p>After &quot;tuning&quot; immer and removing a few <code>Object.assign</code> calls, or rewriting them into a simple loop, I ran out of ideas for &quot;simple&quot; optimizations. It was necessary to tweak the way the tree was built.</p><p>The previous implementation divided SC locations by source and built a sub-tree for each of them. For each sub-tree, aggregated values were calculated (e.g., if a folder itself contained 10 SC, but also had 50 sub-folders, we wanted to show the summed values). For each such sub-tree, summations were performed, and then the source node was updated according to the summed values for all folders.</p><p>Each such operation put something into the redux state. I thought - who needs it? Who needs it? After all, we don&#x27;t show the tree until everything is calculated and updated. Therefore, I changed the code so that the entire tree, along with calculated aggregated values, is first built in memory, and then with one operation, the built tree is inserted into redux.</p><p>Moreover - in the tree requirements, it was written that certain nodes were to be expanded by default, e.g., the first level + potentially previously selected item on the list (you can go from the list to the tree with a simple button). Previously, expansion operations were triggered by the <code>toggleNode</code> action. I changed that too - instead of triggering a redux action, I simply change the <code>expanded</code> value to <code>true</code> directly in the node object.</p><p>You might say - numbers please! :)</p><p>For 54K elements, I went from 12.25s to 2.4s 🚀</p><p>Product Owner is over the moon.</p><p><img src="https://media.giphy.com/media/ciwIz38tlvDFH08Yuu/giphy.gif" alt="Wow"/></p><h2>Tests for 1M</h2><p>I asked the backend developers to prepare an environment for testing 1M elements. I wanted to see if my optimizations for 54K would be justified. And the smile didn&#x27;t leave my face :)</p><p>Before optimization, the tree-building time was ~3.5 minutes. After applying the above-mentioned changes, it was reduced to 59s.</p><p>Approximately ~70% savings. In total, one could say - job done - it was supposed to build in less than 60s... 59 is less than 60 😅 It&#x27;s all good...</p><p>I was a bit tired of digging but a teammate rightly pointed out:</p><blockquote><p>Well, nice, nice, but for me, it&#x27;s still slow.</p></blockquote><p>He also added later that it doesn&#x27;t take anything away from me, and in his opinion, I did a great job... but it was hard not to agree with him. From the moment of clicking on the navigation element to the time the view was displayed, the user had to wait a total of 90s:</p><ul><li>25s data retrieval (streaming)</li><li>6s browser parsing JSON</li><li>59s tree building</li></ul><p>As a user, if I saw only a spinner (&quot;spinner&quot;) for 90s, I would be furious :) I don&#x27;t want to think about what our users felt when they had to wait 3.5 minutes... probably none of them lasted 😅</p><h2>ID generation</h2><p>I dived into the Profiler again. For meta-folders, a <code>folderId</code> was generated. This was because another place in the code needed this <code>id</code> (never mind). In the end, the generated id meant nothing (it was never sent to the backend). However, someone came up with the idea that this meta-folder-id should be a hash of the path...</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">createUniqueIdForLocation</span> <span class="token operator">=</span> <span class="token parameter">path</span> <span class="token operator">=&gt;</span> <span class="token function">btoa</span><span class="token punctuation">(</span><span class="token function">encodeURIComponent</span><span class="token punctuation">(</span>path<span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>The <code>btoa</code> function encodes a string as base64. It takes an average of 0.25ms... which is a fraction of a millisecond. But when you think about it more - who needs this hash? who needs it?</p><p><img src="https://media.giphy.com/media/s239QJIh56sRW/giphy.gif" alt="But why?"/></p><p>Exactly! If the meta-folder-id is just base64 of the path, which in fact also contained the source <code>id</code>, so it was unique concerning the entire list, then why even bother with this whole hash?</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">- id: createUniqueIdForLocation(path), + id: path, name: getLocationName(path),</code></pre></div><p>This one diff made me go from 59s to 35s for 1M elements, which gave ~40% gain 🤯</p><p>So now the client no longer waited 90s but 66s - including data retrieval and parsing! Considering that the requirements stated that the tree should be built in less than 60s, the Product Owner and clients should be satisfied 😅</p><h2>Next steps</h2><p>Of course, we don&#x27;t rest on our laurels. Blocking the user for 60s is still a bad idea, so we continue to think about improving the implementation. Maybe we&#x27;ll finally throw it into a web-worker. Who knows? Maybe I&#x27;ll manage to gather material for the next post 😉.</p><h2>Summary</h2><p>Lesson one - instead of guessing it&#x27;s better to measure.</p><p>Lesson two - if you operate on a large scale, iterate over a large data set, optimizations at the <code>ms</code> level for one iteration can work wonders 🚀</p><p>Lesson three - put into redux only when you&#x27;re ready 💪</p><p>Lesson four - if there&#x27;s no need, don&#x27;t complicate the situation 😉 (see ID &amp; btoa).</p><p>I hope that thanks to this story, you&#x27;ll reach for the Profiler earlier and manage to improve the performance of more than one application.</p><![CDATA[Watch out for fixtures in cypress.io]]>https://przemuh.dev/en/blog/watch-out-for-fixtures-in-cypresshttps://przemuh.dev/en/blog/watch-out-for-fixtures-in-cypressFri, 26 Jun 2020 00:00:00 GMT<p>Today I would like to tell you a story about a bug that cost me two days of searching and debugging sessions. It turned out a trivial thing, and with a better error message, it could have taken seconds instead of days. Let&#x27;s go!</p><h2>Hey Przemek! Could you help me?</h2><p>A few days ago, I noticed that our VRT (Visual Regression Tests) suite started to fail for one case. I&#x27;ve asked my colleague, Monica, to check it. She accepted the challenge. After a long day of searching the root cause, she told me that she doesn&#x27;t have any idea why the test is failing. On the local machine, it has been passing all the time, but on our GitlabCI, we got an error. Weird thing, isn&#x27;t it? Monica was resigned and asked me for help. After two days of trying, committing, pushing, waiting, we&#x27;ve finally found it.</p><h2>Fake server</h2><p>We use a lot of tools in our tests. For unit testing, we use <a href="https://jestjs.io/" target="_blank" rel="nofollow noopener noreferrer">jest</a>. In E2E, we use <a href="https://docs.pytest.org/en/stable/" target="_blank" rel="nofollow noopener noreferrer">py.test</a> with webDriver bindings. We also have UI tests that check our app on a higher level (interactions between components, pages, or views). Recently we introduced another test suite - VRT (Visual Regression Tests). The last two (UI and VRT) are based on <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. It is an excellent tool for writing tests - from unit to full E2E.</p><p>Backend in our app is very complicated, and it is tough to setup a local environment. Because of that, for UI and VRT tests, we use a killer feature from cypress.io - network stubbing. Cypress can plug in between our app and network request giving us a possibility to decide about the response from API endpoint.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&quot;test with network stubbing&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token comment">// First, we need to start fake server</span> cy<span class="token punctuation">.</span><span class="token function">server</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// Next, declare the route that we want to stub</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>More info about stub responses can be found in <a href="https://docs.cypress.io/guides/guides/network-requests.html#Stub-Responses" target="_blank" rel="nofollow noopener noreferrer">official Cypress documentation</a>.</p><h2>Fixtures</h2><p>Fixtures are another feature from <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a> that we use a lot, especially in our VRT suite. A fixture is a simple file that holds the data. We can reuse this file in many places. It helps us in organizing tests and managing the common responses from stubbed network requests. To load a fixture, we use a <code>cy.fixture</code> command. It expects a path to the file that we want to load. The path should be relative to a folder specified to hold fixtures (<code>cypress/fixtures</code> by default). Let&#x27;s assume that we have the following file structure:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">- fixtures - myFixture.json - someSubFolder - mySecondFixture.json</code></pre></div><p>And now let&#x27;s look at code which loads fixtures:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&quot;test with fixtures&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token comment">// We don&#x27;t need to specify the file extension</span> <span class="token comment">// Cypress will try to figure it out</span> cy<span class="token punctuation">.</span><span class="token function">fixture</span><span class="token punctuation">(</span><span class="token string">&quot;myFixture&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token comment">// Here we can read the data</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token comment">// We can save the fixture as an alias ...</span> cy<span class="token punctuation">.</span><span class="token function">fixture</span><span class="token punctuation">(</span><span class="token string">&quot;someSubFolder/mySecondFixture&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">as</span><span class="token punctuation">(</span><span class="token string">&quot;myAlias&quot;</span><span class="token punctuation">)</span> <span class="token comment">// ...and then use the alias in stub of response</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;@myAlias&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>Authors of Cypress took care of reducing a boilerplate needed to use a fixture in stubbing network requests 🔥🔥🔥. The <code>cy.route</code> command can take a shortcut to fixture as a response argument:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/path&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fixture:myFixture&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fx:someSubFolder/mySecondFixture&quot;</span><span class="token punctuation">)</span></code></pre></div><p>In this way, we stubbed a network request with data kept in reusable fixture files. Great job!</p><h2>Where is the hero of the story?</h2><p>Ok, but where did our bug go?</p><p>I&#x27;ve created a simple app to visualize the issue. In the beginning, the app displays the <code>Loading…</code> message, then makes a request and replaces the text with a downloaded response.</p><p>Fetching the data in old, good XHR way 😎</p><div class="gatsby-highlight" data-language="html"><pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">&gt;</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>main<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span>Loading...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript"> <span class="token keyword">const</span> mainEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">&quot;#main&quot;</span><span class="token punctuation">)</span> <span class="token keyword">const</span> req <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">XMLHttpRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span> req<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token string">&quot;GET&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span> req<span class="token punctuation">.</span><span class="token function-variable function">onreadystatechange</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>req<span class="token punctuation">.</span>readyState <span class="token operator">==</span> <span class="token number">4</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> msg <span class="token operator">=</span> req<span class="token punctuation">.</span>status <span class="token operator">==</span> <span class="token number">200</span> <span class="token operator">?</span> req<span class="token punctuation">.</span>responseText <span class="token operator">:</span> <span class="token string">&quot;Error&quot;</span> mainEl<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> msg <span class="token punctuation">}</span> <span class="token punctuation">}</span> req<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">&gt;</span></span></code></pre></div><p>I&#x27;ve also written a test:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">&quot;Simple fixture test&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&quot;displays response&quot;</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> cy<span class="token punctuation">.</span><span class="token function">server</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fixture:examplefixture&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;#main&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;Hello&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>And created a fixture file <code>fixtures/exampleFixture.json</code>:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Hello</code></pre></div><p>Have you noticed a bug yet?</p><p>In my case, the screenshot from the failed test was very helpful. Cypress takes them by default for failing tests, which is neat 🔥!</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/405343e368f5fd3d144f7527bee68ba2/21b4d/screenshot.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:56.31399317406143%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAACWklEQVQoz22SS08TYRSG+6s0Ei4tlk5LYqQKVX+BEkUopRQVcMmCBSy4LHoBFhD+h4koiRsTI4FyKW1taWnnPtMhNI/fDFGKcZIn3/kmM0/ek3N8l4rBSUWmcKFQrOsCjdJFjfKvEtvb2+zs7DA3P8+jaJRo7BlDIzGigieiHn7+gsHHQ7yfnWN3d5dcLofP0XQaB0eoh9/RDvbQj39gFU9pCtKZDBnBZDxOfyBAKBRGEoSlMJHIIJFwhL5eP+PjE55sZWUF35WqY/zMYx6eYp+VaBWrOOU6aqVGNpf1Pkwmk0ImEZIi9AiBS3dPHwNC7u8PEp9MsLm5ydraGj5b0akfFWjkz5ELFdSajHYh03CF2Rvh9PQ0wWCQp8MxppIpEoKZdx8YiT335HeEbVmhfZSnfSjIH3MtTvInmJUqGVcoSKVS4sdeIZ6h85mdm+fe/QckppK3wmtNwzgvY4hWb6hgVxtoDVkIM15CT9jTK85/hR//I7RsdN1CNh1ko4Vi2GiWgyKSZzO3Qr/fz+joa/a+7PN57yuf9r/x8s1burq6mUxMdbRsWbRNA920UEVaRVNRdRWl2fwrvBlKyBtC51CCAxL+wEPi8cmOhIaJcdlAVVVaQuo4La6uHDRF8VbGnfQfYVisiiRWRQoPerVLnz/AxETcE66urt7s4eVZAblUxmk0sWp1rIs6VfEunU6zsbHhTdkVurIBb33CHu7dFboJt7a2WF9fx2eKlu1WC9u2sURtmiaOSFgslVhcXGRpaYmxsTEkSbojdNt37/5AP69GR1leXmZhYYHfyel1N016CykAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Screenshot from failed test" title="Screenshot from failed test" src="/static/405343e368f5fd3d144f7527bee68ba2/105d8/screenshot.png" srcSet="/static/405343e368f5fd3d144f7527bee68ba2/3cf3e/screenshot.png 293w,/static/405343e368f5fd3d144f7527bee68ba2/78a22/screenshot.png 585w,/static/405343e368f5fd3d144f7527bee68ba2/105d8/screenshot.png 1170w,/static/405343e368f5fd3d144f7527bee68ba2/21b4d/screenshot.png 1280w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Screenshot from failed test</figcaption> </figure></p><p>And now...Have you noticed a bug yet?</p><p>A message about the status from the stubbed request caught my attention. It was <code>400</code> instead of <code>200</code>. That was a clue.</p><h2>The typo and file systems</h2><p>Our bug, which we&#x27;ve been trying to solve with Monica, was a simple typo. The name of the fixture file was in camelCase, and we tried to load it via shortcut without the same naming convention.</p><p><code>exampleFixture.json</code> vs <code>cy.route(&quot;/api&quot;, &quot;fixture:examplefixture&quot;)</code></p><p>Ok, but why does it work on the local machine and doesn&#x27;t on CI?</p><p>99% of our frontend team works on MacBooks. Our CI runs the tests in the docker container (Linux). You can think - &quot;so what?&quot;. The default file system on Linux is case sensitive. On the other hand, the default file systems on Mac or Windows are not. What does it mean in practice?</p><p>On Linux you can create two files with the &quot;same&quot; name (different letter case):</p><ul><li>myAwesomeFile.js</li><li>myawesomefile.js</li></ul><p>Linux treats them as separate files. Try to do the same on Mac or Windows - you can&#x27;t do it. It has also impact on the way how you load the files, for example in nodejs. On Mac, there is no difference in load file by &quot;myFixture&quot; or &quot;mYFiXtURe&quot; names - the file will be loaded. On Linux, we will get an error - file not found.</p><h2>Let&#x27;s check it</h2><p>If we modify the code of our test in this way:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fixture:ExAmPlEFiXTuRe&quot;</span><span class="token punctuation">)</span></code></pre></div><p>The test is always green on Mac. On Linux we get a <code>400</code> status for stubbed network request and an error message in console.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/9541d7f7ab7da3ac37cb220e4e54e35a/c211c/stub.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:55.972696245733786%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABBklEQVQoz52Sy26DMBBF/f87JEBijQDxQd2kqpQ2CTV1ggPFYGPg1ga6SdOHa+noLjy+8/CQorzgSCuUrAE9N+DXBuK9Buccbdui73t0XYdhGL5FKbXEWci54igog+jNxTBh0CP0uDLPM1wP0SbDWoEy6AW9YQ1dIe2lRvnwBLbb4233bHgBezygKSss9bkaqrqBPJwgT6+Qx2LR3qjiV0xbyy6tE2HaFWaoQkqjclVDZwY8TZO7YScEbhFWTaLF0LXlL4a24k3/9cv24S2fhnbHRrtCWv8JG0vumUkzQ8YY8jxHkiRI0/RXsixDHMf3De3mU0oRRRF830cYhgiC4EdsjOd5+AAGi0INgDaR/gAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Screenshot with 400 for stubbed request" title="Screenshot with 400 for stubbed request" src="/static/9541d7f7ab7da3ac37cb220e4e54e35a/105d8/stub.png" srcSet="/static/9541d7f7ab7da3ac37cb220e4e54e35a/3cf3e/stub.png 293w,/static/9541d7f7ab7da3ac37cb220e4e54e35a/78a22/stub.png 585w,/static/9541d7f7ab7da3ac37cb220e4e54e35a/105d8/stub.png 1170w,/static/9541d7f7ab7da3ac37cb220e4e54e35a/c211c/stub.png 1502w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Screenshot with 400 for stubbed request</figcaption> </figure></p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">CypressError: The following error originated from your application code, not from Cypress. When Cypress detects uncaught errors originating from your application it will automatically fail the current test. This behavior is configurable, and you can choose to turn this off by listening to the `uncaught:exception` event. https://on.cypress.io/uncaught-exception-from-application </code></pre></div><p>Wait, wait, wait...WAT? The following error originated from your application code, not from Cypress. Are you sure Cypress? 🤔</p><p>Let&#x27;s try to load the fixture without a shortcut:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token comment">// We made a mistake in fixture name</span> cy<span class="token punctuation">.</span><span class="token function">fixture</span><span class="token punctuation">(</span><span class="token string">&quot;examplEFixture&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">as</span><span class="token punctuation">(</span><span class="token string">&quot;response&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;@response&quot;</span><span class="token punctuation">)</span> <span class="token comment">// With storing fixture in an alias we can use it in our assertions</span> <span class="token comment">// We don&#x27;t need to hardcode the &quot;Hello&quot; string</span> cy<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;@response&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> cy<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;#main&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>The error message for this code is quite different:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Error: A fixture file could not be found at any of the following paths: &gt; cypress/fixtures/examplEFixture &gt; cypress/fixtures/examplEFixture{{extension}} Cypress looked for these file extensions at the provided path: .json, .js, .coffee, .html, .txt, .csv, .png, .jpg, .jpeg, .gif, .tif, .tiff, .zip Provide a path to an existing fixture file.</code></pre></div><p>And this is the error message that I&#x27;ve been counting on 👏 . We know right the way where we should start looking 😎.</p><h2>Summary</h2><p>There are two takeaways from this story:</p><ul><li>small typo could make you cry for two days of debugging session</li><li>you are as good as the error message from your test runner ;)</li></ul><p>I think that Cypress could return the better message about missing fixtures than <code>CypressError</code>. That&#x27;s why I&#x27;ve created an issue in cypress GitHub repository - <a href="https://github.com/cypress-io/cypress/issues/7818" target="_blank" rel="nofollow noopener noreferrer">here you can check the status</a>.</p><p>Thank you for your attention. I am going to try to solve the issue that I&#x27;ve created 😉. Maybe I will be able to add something to the OpenSource community to make cypress.io even better 😁</p><![CDATA[Stop the time with cy.clock]]>https://przemuh.dev/en/blog/stop-the-time-with-cyclockhttps://przemuh.dev/en/blog/stop-the-time-with-cyclockWed, 22 Apr 2020 17:00:00 GMT<p>Today I’m going to show you how to stop the time with one command. Unfortunately only in <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a> tests. If you know how to do it in real life please DM me. It would be a very helpful tip 🙂. Ok, let’s stop joking and get our hands dirty!</p><h2>App description</h2><p>First, we need to have something to test. Our app will be deadly-simple. We want to display the enter time and a counter shows how many seconds we spent in the app.</p><div style="text-align:center;margin:2em 0;border:1px solid;padding:2em"><p>Enter time: <span data-testid="enter-time"></span></p><p>Time on page: <span data-testid="counter">0</span></p></div><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">&quot;react&quot;</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">[</span>enterDate<span class="token punctuation">,</span> setEnterDate<span class="token punctuation">]</span> <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">const</span> <span class="token punctuation">[</span>counter<span class="token punctuation">,</span> setCounter<span class="token punctuation">]</span> <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> React<span class="token punctuation">.</span><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">setEnterDate</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">const</span> intervalId <span class="token operator">=</span> <span class="token function">setInterval</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">setCounter</span><span class="token punctuation">(</span><span class="token parameter">prev</span> <span class="token operator">=&gt;</span> prev <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token function">clearInterval</span><span class="token punctuation">(</span>intervalId<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token punctuation">(</span> <span class="token operator">&lt;</span>div<span class="token operator">&gt;</span> <span class="token operator">&lt;</span>p<span class="token operator">&gt;</span> Enter time<span class="token operator">:</span> <span class="token operator">&lt;</span>span data<span class="token operator">-</span>testid<span class="token operator">=</span><span class="token string">&quot;enter-time&quot;</span><span class="token operator">&gt;</span><span class="token punctuation">{</span>enterDate<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>span<span class="token operator">&gt;</span> <span class="token operator">&lt;</span><span class="token operator">/</span>p<span class="token operator">&gt;</span> <span class="token operator">&lt;</span>p<span class="token operator">&gt;</span> Time on page<span class="token operator">:</span> <span class="token operator">&lt;</span>span data<span class="token operator">-</span>testid<span class="token operator">=</span><span class="token string">&quot;counter&quot;</span><span class="token operator">&gt;</span><span class="token punctuation">{</span>counter<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>span<span class="token operator">&gt;</span> <span class="token operator">&lt;</span><span class="token operator">/</span>p<span class="token operator">&gt;</span> <span class="token operator">&lt;</span><span class="token operator">/</span>div<span class="token operator">&gt;</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span></code></pre></div><p>Ok, we have our app. Now it is time to write some cypress tests.</p><h2>We are testing!</h2><p>In our test scenario we would like to check:</p><ul><li>if the enter time is displayed properly,</li><li>if the counter increases its value after a one-second tick.</li></ul><p>Let&#x27;s try this way:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>The test looks decent but it doesn’t pass 😢</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:569px"> <a class="gatsby-resp-image-link" href="/static/8ea52e51054d035a2d33cf2a73475a17/854dc/datenow-assert-error.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:50.170648464163826%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABt0lEQVQoz42S23LTMBCG8/7PAm1puYELBhjahrYuCZOUNGkSO87BsWMnPltaWT9rOS29YcAz3+xBq98raTvdN6fY7HYoiwJ1XUMp9cJzTET/pKlN0xQdiwVLUSFLElRlaYQb++xrLvyfr6mPwhCdu7dnaLZEcYIkz1FKyRBKUsYKFpRat9QaQnH8CvHKxtxUxzo9hxIEfzTBmimiGIm7YdZIlx6S5aaFc+l6izyIkAV7ZLsjTeyHKDgXh3vu8N176CCE+jGA+PAZ8tMlZG+A8vIG8q6P6uMXyK6F6uoW4rYHNXwEde9BHNO3G6jeEDQYQT9MkHk+C55dQB8S0HgGGj0Z5K8JxMMY9Dg1YpKFxPd7s1Y7K9RTB+rJNtRzF8p2TS7zg7bD2t9BWD8h+kNUVh/EYtgEjA/wceAFBr3yoPn4xhq2L3719ZqvaH3sMM0gxtyZvQBxUs5syLljcg3kLMya2vogd2VqaLkyNQauR7RHfoiPgvzCRtAI2cbSYgk5nTFz9l3o6MBXE0PvD39gkYaaHwPcVCt4cg40w+ltIR23hf+u84LzCuDRgZBAWf0V3dhKIOex+Q3oz/IYxFT/cwAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Assertion error" title="Assertion error" src="/static/8ea52e51054d035a2d33cf2a73475a17/854dc/datenow-assert-error.png" srcSet="/static/8ea52e51054d035a2d33cf2a73475a17/3cf3e/datenow-assert-error.png 293w,/static/8ea52e51054d035a2d33cf2a73475a17/854dc/datenow-assert-error.png 569w" sizes="(max-width: 569px) 100vw, 569px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Assertion error</figcaption> </figure></p><p>If we would like to show some formatted date (eg. 22-04-2020) instead of a number of milliseconds, then it would not be a problem. But our client wants to display milliseconds and we need to live with this requirement 😉</p><p>The <code>cy.clock</code> command comes with a rescue. It overrides native global functions related to time allowing them to be controlled synchronously via <code>cy.tick()</code> or the yielded <code>clock</code> object. This includes controlling:</p><ul><li><code>setTimeout</code></li><li><code>clearTimeout</code></li><li><code>setInterval</code></li><li><code>clearInterval</code></li><li><code>Date</code></li></ul><p>You can find more info about cy.clock in the cypress.io <a href="https://docs.cypress.io/api/commands/clock.html" target="_blank" rel="nofollow noopener noreferrer">official documentation</a>.</p><p>Now, let’s try to add <code>cy.clock</code> to our test:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>We still get an error. But this time the error message is different.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">expected &lt;span&gt; to have text &#x27;1587547901669&#x27;, but the text was &#x27;0&#x27;</code></pre></div><p>What is going on with that <code>0</code>? Because the time represented in timestamp value is a number of seconds passed from the start of Unix epoch (1st January 1970). We could ask what will happen after the 19th of January 2038 but this is a topic for another blog post 🙂.</p><p>Calling <code>cy.clock</code> without any arguments sets the date in our app to 1st January 1970. We could change it by passing an argument to the <code>cy.clock</code>:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> now <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>now<span class="token punctuation">)</span></code></pre></div><p>Right now, with a little luck, our test will pass. It depends on how fast our computer is 😄. To fix this issue we need to remember that <code>cy.clock</code> overrides the time in our app, not in our tests (command chain). That’s why we need to change <code>Date.now()</code> in our assertion to <code>now</code> value that we’ve created at the beginning of the test.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> now <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>now<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> now<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>The test is green - always! - success! But there is one little difference in how our app works now. Before using <code>cy.clock</code> our timer has been running. Right now it stops on <code>0</code>. Fortunately, it is expected behavior in our test-case scenario. We&#x27;ve set and stopped the time.</p><p>In order to move the time with some value, we need to call <code>cy.tick</code> command:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> now <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>now<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> now<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=counter]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;0&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">tick</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=counter]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;1&quot;</span><span class="token punctuation">)</span></code></pre></div><p>Tada 🎉! We&#x27;ve just wrote the test checking the enter date and the value of the counter.</p><h2>What if we would like to set the date only - without stopping the time? 🤔</h2><p>That’s a great question. Sometimes we would like to override the <code>Date</code> object only, leaving the rest untouched (<code>setTimeout</code>, etc.). In this case, we need to pass a second argument to the <code>cy.clock</code> - an array of timing functions that we want to override.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token constant">UTC</span><span class="token punctuation">(</span><span class="token number">2020</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">22</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">&quot;Date&quot;</span><span class="token punctuation">]</span><span class="token punctuation">)</span></code></pre></div><p>In this example we set the date/time to 22th April 2020 00:00 UTC (yeap - months in <code>Date</code> starts from <code>0</code> that&#x27;s why April = <code>3</code> 🙂). In the same time we don&#x27;t override the <code>setTimeout</code> and the rest time functions.</p><hr/><p>That&#x27;s all for today. I hope that with this knowledge you can go now and stop the time in your tests 😉</p><p>Good luck!</p><![CDATA[3 Steps to Awesome Test Reports with Cypress]]>https://przemuh.dev/en/blog/3-steps-to-awesome-test-reports-with-cypresshttps://przemuh.dev/en/blog/3-steps-to-awesome-test-reports-with-cypressWed, 18 Dec 2019 00:00:00 GMT<p>In this article, you will learn how to generate informative test reports with Cypress and how to enrich them with some screenshot context. This will help you to fix your potential bugs way faster 😄 All you need is three simple steps.</p><h2>At Egnyte we ❤️ to test</h2><p>Keeping the highest possible quality of the product is one of our top priorities at Egnyte. That&#x27;s why we love to test. But our applications are rather large and relying on manual testing would be exhausting for us. That&#x27;s why test automation and Continuous Integration techniques are our best friends. We write a lot of tests: unit, integration, end-to-end, module, etc. The most important part is that, at the end of the day, if our Jenkins pipeline is green, we are sure that we didn&#x27;t break any parts of the system.</p><p>So where is the problem? Didn&#x27;t you know that tests not always pass? And that&#x27;s fine :) We don&#x27;t need to panic right away. First, let&#x27;s calm down, get into the test report on Jenkins, check what is broken, and fix it. That&#x27;s it. <strong>The problem is that the test report is very often just a plain error message plus a stack trace.</strong> And it&#x27;s enough for unit tests or integration tests for our React components, redux connections, and so on. On the other hand, this is not always helpful for tests that are run in the browser. Let&#x27;s imagine the following result from a test:</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/888b4da9d98b5f76f424ce227440d090/29007/jenkins.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:44.36860068259386%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABCklEQVQoz2WQgY6DIAyGef9n3KaCCi0MvYA4hrKrkCx696chX4G/LTBjjJQyxrjv+1b0hbPq5n4Vs9Y6595FKSWqUtea0nrmL9SKTGs9z/NKCuFVVA3kuKQxVj73YN57RFQ0ugSJ06imQVmCgQAsscJ5hAn0TLdzzufhGbUEgHHULcdbA49O3ylafTsCKSUwkw/rO23756rDTJ0BUPSKC+gEdoLMWKoUcwOt0Kh/npNbQtxOJVgIATWdaaUkIPaj5kLxAQdpeI9iQBrYWIfmMJMh51NnOusETSu7Hh6tvLeSuOkUbTZcPe28pfrK/PknltJWfvr4bL+8nD/CL7HA6pYXjUpBb875r/8X6CUBl6eBj18AAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Jenkins console output" title="Jenkins console output" src="/static/888b4da9d98b5f76f424ce227440d090/105d8/jenkins.png" srcSet="/static/888b4da9d98b5f76f424ce227440d090/3cf3e/jenkins.png 293w,/static/888b4da9d98b5f76f424ce227440d090/78a22/jenkins.png 585w,/static/888b4da9d98b5f76f424ce227440d090/105d8/jenkins.png 1170w,/static/888b4da9d98b5f76f424ce227440d090/29007/jenkins.png 1600w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Jenkins console output</figcaption> </figure></p><p>I took this failed test report from our data governance product (<a href="https://www.egnyte.com/protect/content-governance-solution.html" target="_blank" rel="nofollow noopener noreferrer">Egnyte Protect</a>), which is one of our core products. For writing integration-UI tests, we use an awesome tool called <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. I must admit that Cypress and <a href="https://github.com/testing-library/cypress-testing-library" target="_blank" rel="nofollow noopener noreferrer">cypress-testing-library</a> are doing an excellent job in terms of error messages. Judging by the test report shown above, it is clear that we cannot find an element with matching text. Of course. But what is the visual state of the app? As a developer of Egnyte Protect, I know that this message should appear in a dialog. Has this dialog been opened? Or maybe it is only a typo? So many questions and no answers. If we wanted to check it, we would need to run the test locally once again and see what the visual state of the app is. Only then we would know (spoiler alert) that we have a typo :).</p><p>What if we displayed the visual state of the app right in the Jenkins report?</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/6b320f1a3bdfadd990a910edb663855e/f793b/app.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:54.60750853242321%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABaElEQVQoz42QW0+CYBjH+TKds7oQVkFrE19O0+oGdLIp4Omr9Vm6qK22Llq1CeEhzBMoBwV6keWczmr7vc/Fu/9vzwE5IS6OCSJxjh/heAInDnF8D8N2UTRmB0W30eQ+hm0l0e3Ts4MoRsBYXBGG4dgfOJbLZjKyJFXKlbICX6VWrdVr9Xq1JkrKJctRDMst5ZE0xcQAmiXTgBeE1/dGb2B1jL7xNRyatmnZjuePzEmpVErBBM0uFARAbQ4Ff9NUPp9XW23bm/m+H4ZhEIZ+AEtoO44ky9CFsYWyIgOe5w3DgOlInmtBLNv237IgCN1ud+H8V46WAVShUGjqmjW2+4PRBC7rzVxvOvUD0xpLkvRrZ0CJovjZ1uGRegNzPHFdD44fdXZcV1YUktzUmWIBoHO53OPTc7PV0TRd1fSGqmvqx/3L2+3dQ7FYhBcFG+QImuEy2etszNXNMjTDroRXZUiKBOvAgdeT39lzKYXgDuLwAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Screenshot from the app" title="Screenshot from the app" src="/static/6b320f1a3bdfadd990a910edb663855e/105d8/app.png" srcSet="/static/6b320f1a3bdfadd990a910edb663855e/3cf3e/app.png 293w,/static/6b320f1a3bdfadd990a910edb663855e/78a22/app.png 585w,/static/6b320f1a3bdfadd990a910edb663855e/105d8/app.png 1170w,/static/6b320f1a3bdfadd990a910edb663855e/f793b/app.png 1404w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Screenshot from the app</figcaption> </figure></p><p>Wow! Now we know that the dialog is opened, and that the subheader text is incorrect! We have some valuable context just from reading the test report enriched with a single screenshot.</p><p>So how we could add screenshots to our test reports? Let&#x27;s find out!</p><h2>HTML reports to the rescue!</h2><p>Cypress is based on <a href="https://mochajs.org/" target="_blank" rel="nofollow noopener noreferrer">mocha.js</a>. And this is great because mocha.js is a very mature project with many custom extensions. Test results can be generated within elements called reporters. We can write our custom reporter or use an existing one, for example, <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a>. As the name suggests, it generates <strong>AWESOME</strong> reports! Badum tsss.</p><p>And now, I would like to show you how to integrate <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a> with <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress</a> to generate HTML reports with a screenshot context for failed tests. For the sake of this blog post, I&#x27;ve used an example repo <a href="https://github.com/cypress-io/cypress-example-kitchensink" target="_blank" rel="nofollow noopener noreferrer">cypress-example-kitchensink</a>. We will do it in 3 simple steps. Let&#x27;s get our hands dirty!</p><h2>Step 1 - set up the reporter</h2><p>First, we need to install proper reporters. Yes, that&#x27;s right - plural - reporters. We still want to see test results in the console. Maybe you also want to have a JUnit XML report. We need to have one reporter per expected outcome (console, HTML, XML). In order to set up many reporters, we will use the <a href="https://www.npmjs.com/package/cypress-multi-reporters" target="_blank" rel="nofollow noopener noreferrer">cypress-multi-reporters</a> package. On top of that, we also need <code>mocha</code> and, of course, <code>mochawesome</code>.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> --save-dev mocha cypress-multi-reporters mochawesome</code></pre></div><p>Or if you use <code>yarn</code>:</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">-D</span> mocha cypress-multi-reporters mochawesome</code></pre></div><p>Then, in the <code>cypress.config</code> file, we need to specify which reporter we want to use:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">&quot;reporter&quot;</span><span class="token operator">:</span> <span class="token string">&quot;cypress-multi-reporters&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;reporterOptions&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;configFile&quot;</span><span class="token operator">:</span> <span class="token string">&quot;reporter-config.json&quot;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre></div><p>The <code>configFile</code> field points to the reporters configuration file. We need to add this file to our repository. For each of the reporters we can specify some options. Let&#x27;s do that for the mochawesome reporter:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">&quot;reporterEnabled&quot;</span><span class="token operator">:</span> <span class="token string">&quot;mochawesome&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;mochawesomeReporterOptions&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;reportDir&quot;</span><span class="token operator">:</span> <span class="token string">&quot;cypress/results/json&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;overwrite&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;html&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;json&quot;</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre></div><p>In this fragment of config, we specify an output directory for the results file. We want to collect only the JSON files for each spec file. That&#x27;s why the <code>html</code> flag has been set to false. Because cypress is able to run tests in parallel, we need to set the <code>overwrite</code> flag to <code>false</code>. It means that for each spec file, we will generate a separate file. In our case, these will be JSON files.</p><p>Let&#x27;s try to run our tests via <code>npm run local:run</code> command.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash">Running: examples/location.spec.js <span class="token punctuation">(</span><span class="token number">9</span> of <span class="token number">19</span><span class="token punctuation">)</span> Location ✓ cy.hash<span class="token punctuation">(</span><span class="token punctuation">)</span> - get the current URL <span class="token builtin class-name">hash</span> <span class="token punctuation">(</span>169ms<span class="token punctuation">)</span> ✓ cy.location<span class="token punctuation">(</span><span class="token punctuation">)</span> - get window.location <span class="token punctuation">(</span>101ms<span class="token punctuation">)</span> ✓ cy.url<span class="token punctuation">(</span><span class="token punctuation">)</span> - get the current URL <span class="token punctuation">(</span>78ms<span class="token punctuation">)</span> <span class="token number">3</span> passing <span class="token punctuation">(</span>1s<span class="token punctuation">)</span> <span class="token punctuation">[</span>mochawesome<span class="token punctuation">]</span> Report JSON saved to /Users/przemuh/dev/cypress-example-kitchensink/cypress/results/json/mochawesome_008.json</code></pre></div><p>As you can see, after the spec reporter results, we received information that the <code>mochawesome_008.json</code> file has been created. Each of the spec files generated a JSON with results.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:652px"> <a class="gatsby-resp-image-link" href="/static/581d079e46b5dd429410b52138039577/dba9a/list.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:141.97952218430032%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAcCAIAAADuuAg3AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB60lEQVQ4y5WUTY/TMBCGe0OAEJw5LJukWcf59PgjzgHtnnYFHLjzCzhBWXHmByDxo3mTab2mjeoyckd2mqdv3plpNq8+/Hj98bH48uvu25/p6+83n34+v//+4mF3Zr182D273739/LgpWrPtXTn4zt/eKH9VDe+kQr6qOM8bXLmuKaysJlzvxveb6qaUQqhejHZQfd/ICqutZSMFctfUbV3hnrLI41Vk11oNGyEqIaQiQbrTxrlx9H5CNtbioI3BhaZpi2K73ZZh5XlBpAEjJJHQuiUyxuj5o7VSChtrLTZtC3imQ+R5TkR7WGthTAexccSaA6RzznuP3HXdOfigrIMyvrNLYJ9QVmpW1jMIkyMzLI7c930Ml2WZZRlueIJZmZnY86oyjk3THCvjbjYcK596Zj4uWMswByyBnKYJ+1Nljn+U8ZjOYbngGRn7uq4TMNHeM5O0NB0WVj2vKC+giZXZ/6rnU3hfsACz/4QyCnYYEhNaxRZwlFKegzFh1nLB5vHkDuklsE/Dp+NpDpGA44KxZzC0VB/KGKYEfDSetAQfE32OHtvEhrngCZjHk1vDfWbPfEwrhz6HvzFXC/tLPR89Nv/QpcosFWrG/EWtYs+hVf9RsNXxvBQex2F5W3q8APiliQwe77pVEvEXj+QlMy4V7GcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="List of generated results" title="List of generated results" src="/static/581d079e46b5dd429410b52138039577/dba9a/list.png" srcSet="/static/581d079e46b5dd429410b52138039577/3cf3e/list.png 293w,/static/581d079e46b5dd429410b52138039577/78a22/list.png 585w,/static/581d079e46b5dd429410b52138039577/dba9a/list.png 652w" sizes="(max-width: 652px) 100vw, 652px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">List of generated results</figcaption> </figure></p><p>We are ready to go to the next step.</p><h2>Step 2 - generate the report</h2><p>We&#x27;ve collected the test results. Now, we need to merge them into one file and generate an HTML report based on it. We will use the <a href="https://www.npmjs.com/package/mochawesome-merge" target="_blank" rel="nofollow noopener noreferrer">mochawesome-merge</a> tool to merge result files. Let&#x27;s install it.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> i --save-dev mochawesome-merge <span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">-D</span> mochawesome-merge</code></pre></div><p>Now, let&#x27;s add an npm script which will be responsible for running the merge tool.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report:merge&quot;: &quot;mochawesome-merge --reportDir cypress/results/json &gt; cypress/results/mochawesome-bundle.json&quot;</code></pre></div><p>The <code>reportDir</code> flag specifies where we keep results files. The output of the command is passed from stdout to the <code>mochawesome-bundle.json</code>. One caveat here: the result of the merge needs to be put in a different folder than where the single results file is.</p><p>After merging we are ready to generate a final HTML report. In this case, we will use <a href="https://www.npmjs.com/package/mochawesome-report-generator" target="_blank" rel="nofollow noopener noreferrer">mochawesome-report-generator</a>.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> i --save-dev mochawesome-report-generator <span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">-D</span> mochawesome-report-generator</code></pre></div><p>Let&#x27;s create an npm script for that action:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report:generate&quot;: &quot;marge cypress/results/mochawesome-bundle.json -o cypress/reports/html&quot;</code></pre></div><p>Marge is a short form of <strong>M</strong>och<strong>a</strong>wesome<strong>R</strong>eport<strong>GE</strong>nerator in case you&#x27;ve been wondering :)</p><p>Once the script has run, our awesome HTML report should appear in the <code>cypress/results/html</code> folder.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/c44c00ff45ade76717090ba27ef04155/29007/reporter.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:55.631399317406135%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA80lEQVQoz5WR227CMAyG+/IDaRcUuoNE+niM0m6Q2M45abd5RWJotEL7ZOUi8qf8dorHl9e1qFdbsdhUD+vNonpaVs/Ln3OiuLkUotyKsq4rIYqcU0ohpxhz0J6+7pFS5H6umGIBgGcUwOHUdvL9qI6oUTtDlqyzbiSOBO/xAmBBFzQ13f7tY8eyQvDehxjiFZyQT7riV0YiCScyZJzxwecppmVNWqJssbOGc9rPGTj2rNzIhiex1g7DcGvylbdWz8l72RAQ76Lv+0k5ODcfG1qOzTKPx7/yj9gKVTfOjIS82Ek5hjD78kG1hjQAnMe+5U/sb8tlYyeUKfRVAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="HTML report view" title="HTML report view" src="/static/c44c00ff45ade76717090ba27ef04155/105d8/reporter.png" srcSet="/static/c44c00ff45ade76717090ba27ef04155/3cf3e/reporter.png 293w,/static/c44c00ff45ade76717090ba27ef04155/78a22/reporter.png 585w,/static/c44c00ff45ade76717090ba27ef04155/105d8/reporter.png 1170w,/static/c44c00ff45ade76717090ba27ef04155/29007/reporter.png 1600w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">HTML report view</figcaption> </figure></p><p>There is one more thing to do. Add a screenshot to tests that have failed.</p><h2>Step 3 - add screenshot context</h2><p>Cypress automatically generates screenshots for failed tests in the <code>cypress/screenshots</code> folder. You can disable this behavior if you want. Screenshots are collected within the following folder structure:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">path-to-the-specfile/spec.file.js/context - describe - describe - testTitle (failed).png</code></pre></div><p>For example, the following test placed in examples/actions.spec.js:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">context</span><span class="token punctuation">(</span><span class="token string">&#x27;Actions&#x27;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">context</span><span class="token punctuation">(</span><span class="token string">&quot;nested context&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&#x27;.type() - type into a DOM element&#x27;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>will generate something like this on fail:</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1056px"> <a class="gatsby-resp-image-link" href="/static/08b26a7260e306a2e5536e34e45fad84/fd84e/folder-structure.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:17.4061433447099%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAIAAAAcOLh5AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAWklEQVQI15WOSw7AIAhEPYs2EUGkivc/W6eaNt32LQi/gQkiMsaYczJzjPH4QyDKZubuqlpKeQdp8V3dZXq4xTlTN3z2fnZYaK3hFlzAEc4hooPEFnsKaq0QX98cHHXWQ3fFAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Folder with screenshot from failed test" title="Folder with screenshot from failed test" src="/static/08b26a7260e306a2e5536e34e45fad84/fd84e/folder-structure.png" srcSet="/static/08b26a7260e306a2e5536e34e45fad84/3cf3e/folder-structure.png 293w,/static/08b26a7260e306a2e5536e34e45fad84/78a22/folder-structure.png 585w,/static/08b26a7260e306a2e5536e34e45fad84/fd84e/folder-structure.png 1056w" sizes="(max-width: 1056px) 100vw, 1056px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Folder with screenshot from failed test</figcaption> </figure></p><p>Ok, so how we can connect these two elements: a screenshot generated by Cypress and a test result generated by a mochawesome reporter?</p><p>First, let&#x27;s copy our generated screenshots to the folder where we keep the HTML reports. In order to do this, we will use an npm script:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report:copyScreenshots&quot;: &quot;cp -r cypress/screenshots cypress/results/html/screenshots&quot;</code></pre></div><p>Next, we will use the cypress/support/index.js file and write some code that will be listening on the <code>test:after:run</code> event.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">Cypress<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">&quot;test:after:run&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">test<span class="token punctuation">,</span> runnable</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>test<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">&quot;failed&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// do something</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>For adding the screenshot to the test result, we need to use the addContext method from the <code>mochawesome/addContext</code> package. This method takes two arguments: an object with the test, and the context. If the context is a valid URL (could be a local path) to the image, then that image will be displayed. To see more details, visit the <a href="https://www.npmjs.com/package/mochawesome#adding-test-context" target="_blank" rel="nofollow noopener noreferrer">documentation page</a>.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> addContext <span class="token keyword">from</span> <span class="token string">&#x27;mochawesome/addContext&#x27;</span> Cypress<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">&quot;test:after:run&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">test<span class="token punctuation">,</span> runnable</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>test<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">&quot;failed&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> imageUrl <span class="token operator">=</span> <span class="token string">&quot;?&quot;</span><span class="token punctuation">;</span> <span class="token function">addContext</span><span class="token punctuation">(</span><span class="token punctuation">{</span> test <span class="token punctuation">}</span><span class="token punctuation">,</span> imageUrl<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Ok - but how to define the <code>imageUrl</code>? This is a time for magic to happen.</p><p><img src="https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif" style="width:50%;margin:auto;display:block"/></p><p>Just kidding :) we will use the runnable object. As we saw earlier, Cypress generates the name of the screenshot based on the test suite structure. We need to re-create that.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">Cypress<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">&#x27;test:after:run&#x27;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">test<span class="token punctuation">,</span> runnable</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>test<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">&#x27;failed&#x27;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> item <span class="token operator">=</span> runnable <span class="token keyword">const</span> nameParts <span class="token operator">=</span> <span class="token punctuation">[</span>runnable<span class="token punctuation">.</span>title<span class="token punctuation">]</span> <span class="token comment">// Iterate through all parents and grab the titles</span> <span class="token keyword">while</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>parent<span class="token punctuation">)</span> <span class="token punctuation">{</span> nameParts<span class="token punctuation">.</span><span class="token function">unshift</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>parent<span class="token punctuation">.</span>title<span class="token punctuation">)</span> item <span class="token operator">=</span> item<span class="token punctuation">.</span>parent <span class="token punctuation">}</span> <span class="token keyword">const</span> fullTestName <span class="token operator">=</span> nameParts <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>Boolean<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">&#x27; -- &#x27;</span><span class="token punctuation">)</span> <span class="token comment">// this is how cypress joins the test title fragments</span> <span class="token keyword">const</span> imageUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">screenshots/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span> Cypress<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>name <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>fullTestName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> (failed).png</span><span class="token template-punctuation string">`</span></span> <span class="token function">addContext</span><span class="token punctuation">(</span><span class="token punctuation">{</span> test <span class="token punctuation">}</span><span class="token punctuation">,</span> imageUrl<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>From now on, if our test fails, a context field with a local URL to the image will appear in the JSON results file:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">&quot;title&quot;</span><span class="token operator">:</span> <span class="token string">&quot;.type() - type into a DOM element&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;fullTitle&quot;</span><span class="token operator">:</span> <span class="token string">&quot;Actions .type() - type into a DOM element&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;timedOut&quot;</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span> <span class="token property">&quot;duration&quot;</span><span class="token operator">:</span> <span class="token number">10395</span><span class="token punctuation">,</span> <span class="token property">&quot;state&quot;</span><span class="token operator">:</span> <span class="token string">&quot;failed&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;speed&quot;</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span> <span class="token property">&quot;pass&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;fail&quot;</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token property">&quot;pending&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;context&quot;</span><span class="token operator">:</span> <span class="token string">&quot;screenshots/examples/actions.spec.js/Actions -- .type() - type into a DOM element (failed).png&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">}</span></code></pre></div><p>What is more, the image itself will be attached to the HTML report.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/8fd9e50f05c93dd52922645fba1b999e/29007/reporter-error.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:55.631399317406135%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABuklEQVQoz42SW0/CMBiG9/+v1QgoqIx4TUjgX6g3CEbB8zhstFu7tbC5vn7t0KByYZM3b7d8ffod6tXbbTQvL3F8fo6D0ybpFIe1Gg6bTdQv2jjxO2j4PhoUV6Pvett3OmqdufijVguNTse0KM6/8LUHY4CyhHO7rK/XyLPMqVAahdZVzO9lY51KY0xJW6M9xjk4yTpjds8QLZeYTJ/wMJl+6/XtHTyOEScJ0jR1YoxV5zg3vOJoL6GAb4kEMhFgUYhZNEfEIgIQRMYQUiCjjBVlu9lsoMntGSEEuTBCWIZYe/aH0xYqaR8uZniPArCEE0AhL/JKeaWiKH4BE2M9ilbmJ3DrgkpJs5Raqd1BRX2sMsuhqb9WmVJfsCoRKU0YhtgLVFKCCwmZKdez17c5luGKSla2LCpf0oXZLtAQEATcn2EmU0xmc0wDAq0YgtmSgMz1kCbptKYsd0rGFrg/Q81jJPePEHcTCMpMbwqUHz+fje3jv4GKXNIzMDSAcLHA1fUNbm9HGI/HGI1GGA6HCILAPZ2dHv4FutvorWkaiB2CXc8vL+j1ehgMBuj3+8673a6D2oHFFL8L/AQ++DLW9mYuQQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="HTML report with screenshot context" title="HTML report with screenshot context" src="/static/8fd9e50f05c93dd52922645fba1b999e/105d8/reporter-error.png" srcSet="/static/8fd9e50f05c93dd52922645fba1b999e/3cf3e/reporter-error.png 293w,/static/8fd9e50f05c93dd52922645fba1b999e/78a22/reporter-error.png 585w,/static/8fd9e50f05c93dd52922645fba1b999e/105d8/reporter-error.png 1170w,/static/8fd9e50f05c93dd52922645fba1b999e/29007/reporter-error.png 1600w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">HTML report with screenshot context</figcaption> </figure></p><p>TADA 🎉 We got it!</p><p>You can check all necessary code changes that we have done in the following pull request: <a href="https://github.com/przemuh/cypress-example-kitchensink/pull/1/files" target="_blank" rel="nofollow noopener noreferrer">https://github.com/przemuh/cypress-example-kitchensink/pull/1/files</a></p><h2>Optional steps</h2><p>You might want to add <code>cypress/results</code> and <code>cypress/reports</code> folders to your <code>.gitignore</code>.</p><p>It would be good to remove screenshots, results, and reports before the next test run. It can be done by a simple npm script. In our example repo, I&#x27;ve added:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;precy:run&quot;: &quot;rm -rf cypress/screenshots cypress/results cypress/reports&quot;</code></pre></div><p>&quot;Pre&quot; means that this script will be run before every <code>cy:run</code>. See <a href="https://docs.npmjs.com/misc/scripts" target="_blank" rel="nofollow noopener noreferrer">npm docs</a> for more details.</p><p>In the example repo, there is a <a href="https://www.npmjs.com/package/npm-run-all" target="_blank" rel="nofollow noopener noreferrer">npm-run-all</a> package installed. We could use it to run in sequence: merge, generate report and copy screenshots scripts in one command:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report&quot;: &quot;run-s report:*&quot;, &quot;report:merge&quot;: &quot;mochawesome-merge --reportDir cypress/results/json &gt; cypress/results/mochawesome-bundle.json&quot;, &quot;report:generate&quot;: &quot;marge cypress/results/mochawesome-bundle.json -o cypress/reports/html&quot;, &quot;report:copyScreenshots&quot;: &quot;cp -r cypress/screenshots cypress/reports/html/screenshots&quot;</code></pre></div><p>There is also one caveat. The file name in most systems is limited to 255 characters. So what will happen when we have a very nested structure of a test suite with long descriptions? It&#x27;s simple - our file name will be truncated. Cypress truncates the full test name to 220 characters. So we could also do the same in our code:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token constant">MAX_SPEC_NAME_LENGTH</span> <span class="token operator">=</span> <span class="token number">220</span><span class="token punctuation">;</span> <span class="token keyword">const</span> fullTestName <span class="token operator">=</span> nameParts <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>Boolean<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">&quot; -- &quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token constant">MAX_SPEC_NAME_LENGTH</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>But this is an implementation detail. We don&#x27;t know whether Cypress devs are about to change that number. So, a better option would be to read an article from <a href="https://kentcdodds.com/" target="_blank" rel="nofollow noopener noreferrer">Kent C Dodds</a> about <a href="https://kentcdodds.com/blog/avoid-nesting-when-youre-testing" target="_blank" rel="nofollow noopener noreferrer">avoiding nesting when you are testing</a>.</p><h2>Wrap-up time</h2><p>I hope that this article will help you to set up awesome HTML reports in your project. It helps us a lot when it comes to quick investigations of why a given test is failing. Let&#x27;s recap what we did here:</p><ol><li>Install and set up the mochawesome reporter.</li><li>Collect test results and generate an HTML report based on the merged JSON file</li><li>Add screenshot context with an <code>addContext</code> function.</li></ol><p>You can check all code changes here in this <a href="https://github.com/przemuh/cypress-example-kitchensink/pull/1/files" target="_blank" rel="nofollow noopener noreferrer">pull request</a>. And of course, after you generate the HTML report you need to connect it somehow to your Continuous Integration tool. But this is a story for a separate post. :)</p><p>Now…are you ready to create your own Cypress HTML reports?</p>
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>
<![CDATA[ przemuh.dev [EN] ]]>
</title>
<description>
<![CDATA[ Learn, Code, Teach, Repeat. JavaScript dev's blog. Tutorials/courses about frontend related stuff, but not only. ]]>
</description>
<link>https://przemuh.dev</link>
<generator>GatsbyJS</generator>
<lastBuildDate>Tue, 04 Mar 2025 17:52:19 GMT</lastBuildDate>
<item>
<title>
<![CDATA[ Short story about optimisation ]]>
</title>
<description>
<![CDATA[ Some time ago, I posted a screenshot on Twitter showing a flame-chart from the Profiler tool. At that time, I was working on improving the performance of an application we were developing at Egnyte… ]]>
</description>
<link>https://przemuh.dev/en/blog/tree-performance-improvement-case-study</link>
<guid isPermaLink="false">https://przemuh.dev/en/blog/tree-performance-improvement-case-study</guid>
<pubDate>Sun, 08 Nov 2020 00:00:00 GMT</pubDate>
<content:encoded><p>Some time ago, I posted a screenshot on <a href="https://twitter.com/przemuh/status/1319595759935852544" target="_blank" rel="nofollow noopener noreferrer">Twitter</a> showing a flame-chart from the Profiler tool. At that time, I was working on improving the performance of an application we were developing at Egnyte. A certain functionality, for a large amount of data, took an incredibly long time - 3.5 minutes! During this time, the application displayed a &quot;spinner,&quot; and the user didn&#x27;t know if something was happening or if it had frozen. After a few days of working with the Profiler, I managed to implement improvements that reduced the calculation time from 3.5 minutes to 35 seconds. In this post, I would like to describe how I achieved this.</p><h2>Description of the functionality</h2><p>Let&#x27;s start with a description of the functionality that, to put it briefly, was lacking in terms of performance. One of the main views shows a list of folders with files containing sensitive data. These can be credit card numbers, medical data, personal data, and more. This data can fall under one of several built-in policies, such as HIPAA, GDPR, but we also allow users to define their own policy, for example, based on a previously created dictionary. Initially, the &quot;Sensitive Content&quot; view showed only a flat list of folders. Last year, our Product Owner, along with the UX team, concluded that working with a flat list of folders might be inefficient. Instead, a much better, and essentially more natural way of representing data would be a folder tree.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:944px"> <a class="gatsby-resp-image-link" href="/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:37.883959044368595%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsTAAALEwEAmpwYAAABWElEQVQoz01R127DMBDz/39gn4oUKYJsa1nLGmZ5clJEwOE0KIqkpsesITUrgydrrJ+K3eC+JFx8go4ramlIucD5COM8tPUwUpzbJfyvJ2MdvA9YXuUWj5QyaiVBrcjsfi24GwvDCjEi5xWORDEmpJCQ1xWWPHJ3mpWCNoZgAyVzbcZFHwJ665BhSPhN5Yu1A9v6vt9aA9kGVu4K8XS8KJxuBqe7we9V4fxwUEuG8QU2NoTccVlWfF01zvcdZwKtpwbtSeYSbsrjlzyzTZges4KiKqlZ6VE+RBRaraJwA+xacWC+lsqVtuOsc7+Uiq0mRhXxJE+i2t2yFlKNQOmRGUl3zjHHOqy5UvCjPiyL1WGZhJ0qvR+WV7GsCJRA36DPsW17t8zwIMr5yI7t40w+rlGqEM7vDHeFeigav0i7G9FS/cXoaO3ISJzdf1oel3NxIB80FJJDFP4BCJ5pdpZ5/CkAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Sensitive Content List View" title="Sensitive Content List View" src="/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png" srcSet="/static/f622f9b398ed8964cce4a32ffc9df3fb/3cf3e/sc-view.png 293w,/static/f622f9b398ed8964cce4a32ffc9df3fb/78a22/sc-view.png 585w,/static/f622f9b398ed8964cce4a32ffc9df3fb/966a0/sc-view.png 944w" sizes="(max-width: 944px) 100vw, 944px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Sensitive Content List View</figcaption> </figure></p><h2>Tree building algorithm</h2><p>There have been several approaches to folder trees in our project. They mainly relied on the unique <code>folderId</code> property. Unfortunately, in the case of &quot;Sensitive Content,&quot; we couldn&#x27;t use this because not all folders could contain sensitive data, and only for such folders did we receive a <code>folderId</code>.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">/Shared/A/B/ In this case, we have 3 folders (Shared, A, B), of which only B has a folderId</code></pre></div><p>There can be a multitude of such SC (Sensitive Content) locations. The performance issue appeared already at 250K locations. And it was not an exception, as confirmed by a client where we found almost <strong>a million</strong> folders. For 1M list elements, the tree-building time was 3.5 minutes. Therefore, my task was to ensure that the tree for 1M elements builds in less than 60 seconds.</p><p>Returning to the algorithm. Very simple or so it seemed :)</p><ol><li>Take the entire path and split it into fragments according to the separator, e.g., <code>/</code></li><li>Insert each folder into two structures: &quot;tree&quot; and &quot;flat&quot;</li><li>If a folder does not have a <code>folderId</code>, treat it as a meta-folder</li></ol><p>For simplicity, I omit the fact that we support different data sources, and these separators can vary greatly :) Moreover, as it later turned out, some data sources can have two folders with the same name at the same nesting level 😱. And how to distinguish them? I will also skip the fact that the resulting tree was to be presented in the form of a &quot;sparse-tree.&quot; In short - it means that if a folder contains only one sub-folder, the parent path should be collapsed/merged.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">List: /Shared/A/B/C /Shared/A/B/D --&gt; Tree: /Shared/A/B /C /D</code></pre></div><h2>First, or rather second implementation</h2><p>As I mentioned earlier, this was not the first tree we had to display in the application. In a completely different view, we also had to create a sparse-tree and didn&#x27;t want to have several different implementations. Therefore, we wrote a simple module for building and managing the tree. It was based on two small components:</p><ul><li>a <code>buildTree</code> function that took a flat array of nodes and the place (path in the tree) from which it was to insert these nodes</li><li>a &quot;slice&quot; from <code>redux-toolkit</code> that managed the tree structure (expanding, collapsing nodes, etc.)</li></ul><p>The entire tree, or rather these two structures &quot;tree&quot; and &quot;flat,&quot; were kept in redux as follows:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> tree<span class="token operator">:</span> <span class="token punctuation">{</span> path<span class="token operator">:</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">,</span> <span class="token comment">// root</span> children<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;Shared&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token comment">// path-part or folder name as a key</span> path<span class="token operator">:</span> <span class="token string">&quot;/Shared&quot;</span><span class="token punctuation">,</span> children<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;A&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> path<span class="token operator">:</span> <span class="token string">&quot;/Shared/A&quot;</span><span class="token punctuation">,</span> children<span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> paths<span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;/Shared&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> meta<span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> ...nodeProps <span class="token punctuation">}</span> <span class="token property">&quot;/Shared/A&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> meta<span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> folderId<span class="token operator">:</span> <span class="token string">&quot;some-unique-id&quot;</span><span class="token punctuation">,</span> ...nodeProps <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre></div><p>This structure is obtained from the helper function <code>buildTree</code>.</p><p>Thanks to the use of <a href="https://redux-toolkit.js.org/" target="_blank" rel="nofollow noopener noreferrer">redux-toolkit</a>, and consequently the <a href="https://github.com/immerjs/immer" target="_blank" rel="nofollow noopener noreferrer">immer</a> library, we could perform operations on the tree very easily:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> initialTreeState <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token literal-property property">initialized</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token literal-property property">tree</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token literal-property property">path</span><span class="token operator">:</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">,</span> <span class="token literal-property property">children</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token literal-property property">paths</span><span class="token operator">:</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">createTreeSlice</span> <span class="token operator">=</span> <span class="token parameter">treeName</span> <span class="token operator">=&gt;</span> <span class="token function">createSlice</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token literal-property property">name</span><span class="token operator">:</span> treeName<span class="token punctuation">,</span> <span class="token literal-property property">initialState</span><span class="token operator">:</span> initialTreeState<span class="token punctuation">,</span> <span class="token literal-property property">reducers</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token function-variable function">insertTree</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> tree<span class="token punctuation">,</span> paths <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> payload <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> paths <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token operator">...</span>paths<span class="token punctuation">,</span> <span class="token operator">...</span>payload<span class="token punctuation">.</span>paths<span class="token punctuation">,</span> <span class="token punctuation">}</span> <span class="token keyword">const</span> node <span class="token operator">=</span> <span class="token function">getNodeByPath</span><span class="token punctuation">(</span>payload<span class="token punctuation">.</span>parentPath <span class="token operator">||</span> <span class="token string">&quot;&quot;</span><span class="token punctuation">,</span> tree<span class="token punctuation">)</span> node<span class="token punctuation">.</span>children <span class="token operator">=</span> payload<span class="token punctuation">.</span>tree<span class="token punctuation">.</span>children <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token function-variable function">toggleNode</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> tree <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">payload</span><span class="token operator">:</span> path <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> node <span class="token operator">=</span> <span class="token function">getNodeByPath</span><span class="token punctuation">(</span>path<span class="token punctuation">,</span> tree<span class="token punctuation">)</span> node<span class="token punctuation">.</span>expanded <span class="token operator">=</span> <span class="token operator">!</span>node<span class="token punctuation">.</span>expanded <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>The helper function <code>getNodeByPath</code> is used to search for a node by path. It can also search for a node in a sparse-tree.</p><h2>First attempts and first mistakes</h2><p>And everything was going smoothly, but then a client with 1 million folders came, and boom... The Product Owner sets up an Epic in Jira titled &quot;Support 1M folders on SC tree view.&quot; A quick brainstorming session and a list full of ideas right away:</p><ul><li>maybe build the tree on the fly, as we parse JSON?</li><li>maybe build the tree in a web-worker, at least we won&#x27;t block the main thread for 3.5 minutes?</li><li>or maybe just drop everything and becoma a farmer? ⛰ 🐑</li></ul><p><img src="https://media.giphy.com/media/kPtv3UIPrv36cjxqLs/giphy.gif" alt="Or maybe..."/></p><p>The first mistake - no one even started the Profiler to see what was taking so long. Everyone assumed that the current tree implementation was top-notch and couldn&#x27;t be better. The Profiler itself, at first glance, is not a simple tool, and maybe that was the reason we jumped to ideas like building the tree &quot;on the fly&quot; or moving it to a web-worker.</p><p>You&#x27;re probably wondering - but how on the fly? After all, when a request is made, only when the response comes does the browser parse the JSON and provide the response. Yes, but... in this case, our backend developers also had to work a bit on optimization, and instead of returning full data, they started returning our SC list as a stream. Thanks to this, we could, for example, use the <code>oboe.js</code> library to parse JSON on the fly.</p><p>Of course, I tried this approach because, after all, someone wrote it into the Jira task, so it had to be checked, right? 😜 Cool, the JSON was parsing &quot;on the fly,&quot; but the stream lasted 30s instead of 10s, and I hadn&#x27;t even started building the tree yet. So I gave up and decided to look elsewhere.</p><h2>Web-worker</h2><p>I also tested the approach with a web-worker. But I encountered a completely different problem. Okay - I can download 1M elements and build a tree based on them, but I have to send it later from the web-worker to the main thread. The tree structure is quite extensive, along with the data we saved in this flat <code>paths</code> structure. If we want to send such large data from one thread to another, the browser has to serialize the data, send it, and then parse it again. This also caused the browser to &quot;freeze&quot; during the transfer from one memory location to another. Of course, there are ways to send &quot;directly&quot; (without copying) through so-called Transferable Objects, e.g., ArrayBuffer, but I decided that for now, it might not be worth the effort and decided to check if our tree implementation was as great as we thought 😜</p><h2>Profiler</h2><p>I sat in front of the computer screen, launched the dev-tools, and pressed the &quot;record&quot; button in the Profiler. After a while, I got a colorful graph that reminded me of the defragmenter times of Windows 98 🤣</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/08d444b93bebf165ed87c320556ad7b0/d9ed5/flame-graph-violet.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:62.45733788395904%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAMCAYAAABiDJ37AAAACXBIWXMAABYlAAAWJQFJUiTwAAAB5ElEQVQoz32S63KiQBCFef8X2t97qYq1sWIUXW8gJgICQWBuwIDxbA8Y12xMpuqrmb7MmaZpy/NcbJ89uJ6HjbeCH/sIkgj7FyJNEEUbpHsbeby8kEWLnv2cWCCncxIs4LoLWFo3aJoGdV13VFXV2W/rdAPcsM0qKw0ryzOoqoQs36ggpADPI0iCxx5YuH5HEaxQhCs6r3rb7HsHjPKtQ1qQYAtZHS8IqTGPhrCzn5j9/o6nH0OEgzGCuxF8wuzh3bhn8NjF/MEDQnsKS2RHlPUr1BVlfQLLGhRJjdWji8l0CsfdwNl6WCZrzF+WGLsTrGcuFmMHc3uJJe35H2kE23di16IG8VAhHdHPcQIc/AxCa3BdY+dv8DSy4dkzeKMZ4vsQbCNgcdZ8qPBC9QrmVJBDDTUhti3K9gTqCgq3RPqtQD4owH4JyHvq/c5UyL8QJEQowTwF7tOFlEQ1+RvyJxpsUoIHdQdzJVRUk6BoPxUzl0VqXhYQMVWaGcFT55c53ds1kFSQzKkNvoA60NhIRf2ipGtU3dOdJQkbobyBYm3vM4hjjyQ4TcmhpngDi3ENoRpwpc/Qi90YnRHnKgziX8y0Sqq2h75SMpNDFe52AeI4QcEYuKCB5tRgqf5D3vB9zBFC4i+gT4+kJsCwBQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Flame graph" title="Flame graph" src="/static/08d444b93bebf165ed87c320556ad7b0/105d8/flame-graph-violet.png" srcSet="/static/08d444b93bebf165ed87c320556ad7b0/3cf3e/flame-graph-violet.png 293w,/static/08d444b93bebf165ed87c320556ad7b0/78a22/flame-graph-violet.png 585w,/static/08d444b93bebf165ed87c320556ad7b0/105d8/flame-graph-violet.png 1170w,/static/08d444b93bebf165ed87c320556ad7b0/28884/flame-graph-violet.png 1755w,/static/08d444b93bebf165ed87c320556ad7b0/92bee/flame-graph-violet.png 2340w,/static/08d444b93bebf165ed87c320556ad7b0/d9ed5/flame-graph-violet.png 2880w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Flame graph</figcaption> </figure></p><p>The first thing that caught my eye was this purple color, which dived very, very deep. Upon closer inspection, it turned out that a lot of these purple elements were the work of <code>immer.js</code>. A quick glance at the documentation and boom! A bullseye. It turns out that when &quot;inserting&quot; a large amount of data through <code>immer</code>, we can speed up this process through <code>Object.freeze</code> <a href="https://immerjs.github.io/immer/docs/performance#performance-tips" target="_blank" rel="nofollow noopener noreferrer">more info here</a>. This procedure allowed me to go from 12.54s to 11.24s for 54K elements. For 1M, the jump was, of course, proportionally larger. But it still wasn&#x27;t it...</p><h2>From profiler to sources</h2><p>Did you know that if you click on a block in the Profiler and then move to the file, you get times for individual code blocks? No!? 😎 Now you know ;)</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/1ba2423b674f0cab158baa993f4c7cfd/0d0e4/source-before.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:90.7849829351536%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACTklEQVQ4y4VUa5PaMAzk//+29kOnTO8KB4RHICTYie3Ez5Ct7IO0PMopeBwie0falTSRqkXFGGoh0LYe2+0eH/M1lssa63WDLDthnx+hlIF3Dn3f43w+36zrtxACJl3XQcsCtmOIFh2y1gQksVhU2O0O4ExA64DgA4bhjP9ZvDtp2xaKbwn0SIcvDhPQMAfOLUXVw1mHrvOUgUUMoO/PGOIz/F0joNYaHV/BqhwXPHi6/Gu6x2y2x6li8N6n7/cA93sCdMTL1ZLj4pRKYTaf4+19j80mcihvAK7vD4DGGMjiBzr2ezygSahDecKBNZCdgJQCQsp04VlkD4Bs+w0s//lPlEDTcJQlx3JFHCv3FOBphJFDUUyhTvOrmxwDdrlAtqpwLDiEMNDGJ98nKy9Sjqq1ooRp6xEw/jhT2GQM5VHDmv7m8oAXKVtrEZyhGrOjwzqLuuVoDaWrLQz5bDBwvX2p9phyW72n0rmaMxbZusL0bYPVJkNeMCzof0kixW645+8GUFF5GAKIB2+4IR6LJcfig2FFe76tqS4FlRApLy0cFfzTCCNgoMK9SSH1Z6BIA5T0SWXdORLH0btPbej9C8CWLWHkfkwj0GEXgURPAJYikrQiUP/Qvw8pR5Xr/Hvi8Won3iHLacqUBRV4Q/UYI9SIfGttXosSVTaqomnDx+FgiYJTXYPVPIFbS5MmOMQ2ve/rp53ijYS33UWLgYaDBSs18p1CwxmOJUNVEdeh/7pTUh16gz7YNCgNdYTVJBKpHMKnOHH3/nzp5eFlyn8A2CB77hemAIcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Times before optimization for buildTree" title="Times before optimization for buildTree" src="/static/1ba2423b674f0cab158baa993f4c7cfd/105d8/source-before.png" srcSet="/static/1ba2423b674f0cab158baa993f4c7cfd/3cf3e/source-before.png 293w,/static/1ba2423b674f0cab158baa993f4c7cfd/78a22/source-before.png 585w,/static/1ba2423b674f0cab158baa993f4c7cfd/105d8/source-before.png 1170w,/static/1ba2423b674f0cab158baa993f4c7cfd/0d0e4/source-before.png 1230w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Times before optimization for buildTree</figcaption> </figure></p><p>What stands out is 229ms for building a simple string 🤯 which is the current path. It turned out that this simple oversight could be replaced with a shorter piece of code, which ultimately takes 1.7ms.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/7be33/source-after.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:100.34129692832765%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAACr0lEQVQ4y41UiZKbMBTb//+7trOdZtPN5iKBcBp8c6iy0yXXTloYD8R2hCzpvRelFLQ4wHYplPZIkgyL32ssVyV2iUSWdlivCz4LOGvR955jwDB8PV6MsdD1Bm3xgWkCvPeoig4fqxrfvudYLrfY7zJUlYI1DtM44NlFQA2jGg4BTH9nhxFOedSNg1IWRlt466H1xN8a3rm4bSKD+/EiOsU/FzCyiAwD5uQGNCeL7baF4nrbdmgagaKoECSqa55K9zPoDcNWGvRO8qtqXgwMDukJq+SEtMyprYYQBmUlIaWjLCN1HL8GDEduixWqdHneEO5xgiaTPQEXbwLHY8cTeMhMw1KKz+se7C+ggaIpTb662SSlxm6bYL9X2O06zpPRGDS51exaywjoeLzp7otD36OhTkI8d/TLI2vqo6oVo7O+bCCZvKzwyjyumcvkVOKQH7E+JjgUOaqO5niaZdtHhsE1Vb7z2B+XRd5SSbrZMX8WFdkGp8tSxjyG7F6zewA0XY7BNvOk8xZZWWKf58ibE2rZ4NQ0aExN8v4hg9fgPLKByF5RHn5cFjiUcHhf0OVfW2w2jNCqYC4bZJmMYb9Uwa2OkaG3ikbYGexzXRYKGR3e7PhMa7otCNzy2TLo53APw20eX6SUcM7GWFwfoafTzg/Uq4dl2Xnf833gO+fdSLDz+zjeAcZu0+xh2sNVKbMSFAG6Hq3w6GiIECrWtnP90+jE2LTZTzq9mCe1djjmNeNS0ZwqOl3Xjgx9ZBTKLoxrdjPDWCnlG7ribQZ07C5FlmO9o26JoL4DusC2NXy2ZOuihqE3PgBaNk1nTRyfkyMbZV2ZWHZdG5gZtjAaN/T/VykBbODX5tJz7H1kkaaaLcuwrm00KXTk6V+1HBgG68+JmaKbsrOxEVy0mp52mOvrD45MFooaxFLvAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Times after optimization for buildTree" title="Times after optimization for buildTree" src="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/105d8/source-after.png" srcSet="/static/831c9d5d2fd63e39d71b3e58d9daf5e6/3cf3e/source-after.png 293w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/78a22/source-after.png 585w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/105d8/source-after.png 1170w,/static/831c9d5d2fd63e39d71b3e58d9daf5e6/7be33/source-after.png 1558w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Times after optimization for buildTree</figcaption> </figure></p><p>You might think - (ironically) wow... 227ms... &quot;bravo&quot; 👏. What is 227ms? If we look at it as a single value - indeed... micro-optimization. But remember, the goal was to handle 1M elements, and the path concatenation operation concerned each sub-folder.</p><h2>Spread operator AKA Object.assign</h2><p>How to make a shallow copy of an object or extend another object - nothing simpler - spread operator <code>...</code>. If you have to support browsers like IE11, you probably use babel.js - just like us... and such a spread operator, in the end, is translated to <code>Object.assign</code> (*big simplification).</p><p><code>Object.assign</code> is <a href="https://twitter.com/dan_abramov/status/980436488860196864" target="_blank" rel="nofollow noopener noreferrer">relatively slow</a> and can cause problems at a larger scale. In this case, I opted for simple key-by-key copying. Thanks to this simple procedure, I reduced 154ms to 44ms. And again, for individual elements, it doesn&#x27;t matter at all, but when iterating over a large data set, such optimizations can work wonders.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/7bed4f76daac0b292144d25c8a68996a/35252/dan.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:93.85665529010238%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAACkklEQVQ4y51U227aQBD1/39NX9qqUtWnSn2okjTENgZsCGBifL97fQGfzgwQkUppkyIddjy7e+bM7Oxqxs0PzCc/sd15WNg21psthuGA43EkHN+G8bRWtR20758+4ObbF+RNhyhOEKcZ/u83oichWpYXcJ88PD6usSF1W9fFbvck9nqzwXq9hk3KfT/AdusStlitHml+I+A1aXYSMRyIcLlcYjKZwDRNGIYBy7KwIt98PhefTj5zOsXDg06+BaY0z7ZhmLi5vZMAByISQlZoL1e4vZ9ANy1YCwfGzIY5t/FLN2GSPZ3NsVjYUl8ebceR0TSncJwldN2AQwI4E9W20KyoxZ1XwwoV9EBhRt+L+AQn7ZCrAeNV4RnjlT0MgygUm1Mu2wPyHij7EQXZPPk+HAUcpCdy7UjO0HORJTFU00jE96LvezS0V9pmpGJ+/fgZ89kCCbVMWZao6xpVVcvCt/xYaVGUUB0RssyqbpBlOTkLRFFMdoY0TWW8IKVgGW2KixZ5Wcl8TH3LRJwu/Z0UciHDMILn7eEHgZwUK+soGqPvh5c2tca1j1OWtr4Q8iSf0EW653nI8/w5HU7f930opbDf79HQdxCE8v3injwrpIglpbCnTZwuq+U68gJGVVWSLgfmeVYVRpGMF6KXhFLQCt7eJ9JADka1vaAl8NVkf90oPHk+ipLXBijp0F4lrCStAAGp4wciSVK5n0yeE2GcJKLQp1Rrao+QlLbtKwq5brxxs90RXBldesp4rGqFnE4xilM0ilINY1HKgRvV/vHYnAm7jk/0BI7aPn8PZ9/ZL7i2O1zvZXBQjQk4Kke8HhmsUJ2D/AsSnERoFW3kRmWClOolTU6HxEiyQu7npUZ/w+U9/A1xprNXkDEExgAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Dan Abramov on Object.assign" title="Dan Abramov on Object.assign" src="/static/7bed4f76daac0b292144d25c8a68996a/105d8/dan.png" srcSet="/static/7bed4f76daac0b292144d25c8a68996a/3cf3e/dan.png 293w,/static/7bed4f76daac0b292144d25c8a68996a/78a22/dan.png 585w,/static/7bed4f76daac0b292144d25c8a68996a/105d8/dan.png 1170w,/static/7bed4f76daac0b292144d25c8a68996a/35252/dan.png 1204w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Dan Abramov on Object.assign</figcaption> </figure></p><h2>Value aggregation</h2><p>After &quot;tuning&quot; immer and removing a few <code>Object.assign</code> calls, or rewriting them into a simple loop, I ran out of ideas for &quot;simple&quot; optimizations. It was necessary to tweak the way the tree was built.</p><p>The previous implementation divided SC locations by source and built a sub-tree for each of them. For each sub-tree, aggregated values were calculated (e.g., if a folder itself contained 10 SC, but also had 50 sub-folders, we wanted to show the summed values). For each such sub-tree, summations were performed, and then the source node was updated according to the summed values for all folders.</p><p>Each such operation put something into the redux state. I thought - who needs it? Who needs it? After all, we don&#x27;t show the tree until everything is calculated and updated. Therefore, I changed the code so that the entire tree, along with calculated aggregated values, is first built in memory, and then with one operation, the built tree is inserted into redux.</p><p>Moreover - in the tree requirements, it was written that certain nodes were to be expanded by default, e.g., the first level + potentially previously selected item on the list (you can go from the list to the tree with a simple button). Previously, expansion operations were triggered by the <code>toggleNode</code> action. I changed that too - instead of triggering a redux action, I simply change the <code>expanded</code> value to <code>true</code> directly in the node object.</p><p>You might say - numbers please! :)</p><p>For 54K elements, I went from 12.25s to 2.4s 🚀</p><p>Product Owner is over the moon.</p><p><img src="https://media.giphy.com/media/ciwIz38tlvDFH08Yuu/giphy.gif" alt="Wow"/></p><h2>Tests for 1M</h2><p>I asked the backend developers to prepare an environment for testing 1M elements. I wanted to see if my optimizations for 54K would be justified. And the smile didn&#x27;t leave my face :)</p><p>Before optimization, the tree-building time was ~3.5 minutes. After applying the above-mentioned changes, it was reduced to 59s.</p><p>Approximately ~70% savings. In total, one could say - job done - it was supposed to build in less than 60s... 59 is less than 60 😅 It&#x27;s all good...</p><p>I was a bit tired of digging but a teammate rightly pointed out:</p><blockquote><p>Well, nice, nice, but for me, it&#x27;s still slow.</p></blockquote><p>He also added later that it doesn&#x27;t take anything away from me, and in his opinion, I did a great job... but it was hard not to agree with him. From the moment of clicking on the navigation element to the time the view was displayed, the user had to wait a total of 90s:</p><ul><li>25s data retrieval (streaming)</li><li>6s browser parsing JSON</li><li>59s tree building</li></ul><p>As a user, if I saw only a spinner (&quot;spinner&quot;) for 90s, I would be furious :) I don&#x27;t want to think about what our users felt when they had to wait 3.5 minutes... probably none of them lasted 😅</p><h2>ID generation</h2><p>I dived into the Profiler again. For meta-folders, a <code>folderId</code> was generated. This was because another place in the code needed this <code>id</code> (never mind). In the end, the generated id meant nothing (it was never sent to the backend). However, someone came up with the idea that this meta-folder-id should be a hash of the path...</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">export</span> <span class="token keyword">const</span> <span class="token function-variable function">createUniqueIdForLocation</span> <span class="token operator">=</span> <span class="token parameter">path</span> <span class="token operator">=&gt;</span> <span class="token function">btoa</span><span class="token punctuation">(</span><span class="token function">encodeURIComponent</span><span class="token punctuation">(</span>path<span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>The <code>btoa</code> function encodes a string as base64. It takes an average of 0.25ms... which is a fraction of a millisecond. But when you think about it more - who needs this hash? who needs it?</p><p><img src="https://media.giphy.com/media/s239QJIh56sRW/giphy.gif" alt="But why?"/></p><p>Exactly! If the meta-folder-id is just base64 of the path, which in fact also contained the source <code>id</code>, so it was unique concerning the entire list, then why even bother with this whole hash?</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">- id: createUniqueIdForLocation(path), + id: path, name: getLocationName(path),</code></pre></div><p>This one diff made me go from 59s to 35s for 1M elements, which gave ~40% gain 🤯</p><p>So now the client no longer waited 90s but 66s - including data retrieval and parsing! Considering that the requirements stated that the tree should be built in less than 60s, the Product Owner and clients should be satisfied 😅</p><h2>Next steps</h2><p>Of course, we don&#x27;t rest on our laurels. Blocking the user for 60s is still a bad idea, so we continue to think about improving the implementation. Maybe we&#x27;ll finally throw it into a web-worker. Who knows? Maybe I&#x27;ll manage to gather material for the next post 😉.</p><h2>Summary</h2><p>Lesson one - instead of guessing it&#x27;s better to measure.</p><p>Lesson two - if you operate on a large scale, iterate over a large data set, optimizations at the <code>ms</code> level for one iteration can work wonders 🚀</p><p>Lesson three - put into redux only when you&#x27;re ready 💪</p><p>Lesson four - if there&#x27;s no need, don&#x27;t complicate the situation 😉 (see ID &amp; btoa).</p><p>I hope that thanks to this story, you&#x27;ll reach for the Profiler earlier and manage to improve the performance of more than one application.</p></content:encoded>
</item>
<item>
<title>
<![CDATA[ Watch out for fixtures in cypress.io ]]>
</title>
<description>
<![CDATA[ Today I would like to tell you a story about a bug that cost me two days of searching and debugging sessions. It turned out a trivial thing, and with a better error message, it could have taken… ]]>
</description>
<link>https://przemuh.dev/en/blog/watch-out-for-fixtures-in-cypress</link>
<guid isPermaLink="false">https://przemuh.dev/en/blog/watch-out-for-fixtures-in-cypress</guid>
<pubDate>Fri, 26 Jun 2020 00:00:00 GMT</pubDate>
<content:encoded><p>Today I would like to tell you a story about a bug that cost me two days of searching and debugging sessions. It turned out a trivial thing, and with a better error message, it could have taken seconds instead of days. Let&#x27;s go!</p><h2>Hey Przemek! Could you help me?</h2><p>A few days ago, I noticed that our VRT (Visual Regression Tests) suite started to fail for one case. I&#x27;ve asked my colleague, Monica, to check it. She accepted the challenge. After a long day of searching the root cause, she told me that she doesn&#x27;t have any idea why the test is failing. On the local machine, it has been passing all the time, but on our GitlabCI, we got an error. Weird thing, isn&#x27;t it? Monica was resigned and asked me for help. After two days of trying, committing, pushing, waiting, we&#x27;ve finally found it.</p><h2>Fake server</h2><p>We use a lot of tools in our tests. For unit testing, we use <a href="https://jestjs.io/" target="_blank" rel="nofollow noopener noreferrer">jest</a>. In E2E, we use <a href="https://docs.pytest.org/en/stable/" target="_blank" rel="nofollow noopener noreferrer">py.test</a> with webDriver bindings. We also have UI tests that check our app on a higher level (interactions between components, pages, or views). Recently we introduced another test suite - VRT (Visual Regression Tests). The last two (UI and VRT) are based on <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. It is an excellent tool for writing tests - from unit to full E2E.</p><p>Backend in our app is very complicated, and it is tough to setup a local environment. Because of that, for UI and VRT tests, we use a killer feature from cypress.io - network stubbing. Cypress can plug in between our app and network request giving us a possibility to decide about the response from API endpoint.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&quot;test with network stubbing&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token comment">// First, we need to start fake server</span> cy<span class="token punctuation">.</span><span class="token function">server</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment">// Next, declare the route that we want to stub</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">value</span><span class="token operator">:</span> <span class="token number">1</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>More info about stub responses can be found in <a href="https://docs.cypress.io/guides/guides/network-requests.html#Stub-Responses" target="_blank" rel="nofollow noopener noreferrer">official Cypress documentation</a>.</p><h2>Fixtures</h2><p>Fixtures are another feature from <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a> that we use a lot, especially in our VRT suite. A fixture is a simple file that holds the data. We can reuse this file in many places. It helps us in organizing tests and managing the common responses from stubbed network requests. To load a fixture, we use a <code>cy.fixture</code> command. It expects a path to the file that we want to load. The path should be relative to a folder specified to hold fixtures (<code>cypress/fixtures</code> by default). Let&#x27;s assume that we have the following file structure:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">- fixtures - myFixture.json - someSubFolder - mySecondFixture.json</code></pre></div><p>And now let&#x27;s look at code which loads fixtures:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&quot;test with fixtures&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token comment">// We don&#x27;t need to specify the file extension</span> <span class="token comment">// Cypress will try to figure it out</span> cy<span class="token punctuation">.</span><span class="token function">fixture</span><span class="token punctuation">(</span><span class="token string">&quot;myFixture&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token comment">// Here we can read the data</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token comment">// We can save the fixture as an alias ...</span> cy<span class="token punctuation">.</span><span class="token function">fixture</span><span class="token punctuation">(</span><span class="token string">&quot;someSubFolder/mySecondFixture&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">as</span><span class="token punctuation">(</span><span class="token string">&quot;myAlias&quot;</span><span class="token punctuation">)</span> <span class="token comment">// ...and then use the alias in stub of response</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;@myAlias&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>Authors of Cypress took care of reducing a boilerplate needed to use a fixture in stubbing network requests 🔥🔥🔥. The <code>cy.route</code> command can take a shortcut to fixture as a response argument:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/path&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fixture:myFixture&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fx:someSubFolder/mySecondFixture&quot;</span><span class="token punctuation">)</span></code></pre></div><p>In this way, we stubbed a network request with data kept in reusable fixture files. Great job!</p><h2>Where is the hero of the story?</h2><p>Ok, but where did our bug go?</p><p>I&#x27;ve created a simple app to visualize the issue. In the beginning, the app displays the <code>Loading…</code> message, then makes a request and replaces the text with a downloaded response.</p><p>Fetching the data in old, good XHR way 😎</p><div class="gatsby-highlight" data-language="html"><pre class="language-html"><code class="language-html"><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">&gt;</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>div</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">&quot;</span>main<span class="token punctuation">&quot;</span></span><span class="token punctuation">&gt;</span></span>Loading...<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>div</span><span class="token punctuation">&gt;</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span><span class="token punctuation">&gt;</span></span><span class="token script"><span class="token language-javascript"> <span class="token keyword">const</span> mainEl <span class="token operator">=</span> document<span class="token punctuation">.</span><span class="token function">querySelector</span><span class="token punctuation">(</span><span class="token string">&quot;#main&quot;</span><span class="token punctuation">)</span> <span class="token keyword">const</span> req <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">XMLHttpRequest</span><span class="token punctuation">(</span><span class="token punctuation">)</span> req<span class="token punctuation">.</span><span class="token function">open</span><span class="token punctuation">(</span><span class="token string">&quot;GET&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span> req<span class="token punctuation">.</span><span class="token function-variable function">onreadystatechange</span> <span class="token operator">=</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>req<span class="token punctuation">.</span>readyState <span class="token operator">==</span> <span class="token number">4</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> msg <span class="token operator">=</span> req<span class="token punctuation">.</span>status <span class="token operator">==</span> <span class="token number">200</span> <span class="token operator">?</span> req<span class="token punctuation">.</span>responseText <span class="token operator">:</span> <span class="token string">&quot;Error&quot;</span> mainEl<span class="token punctuation">.</span>innerHTML <span class="token operator">=</span> msg <span class="token punctuation">}</span> <span class="token punctuation">}</span> req<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span><span class="token keyword">null</span><span class="token punctuation">)</span> </span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span> <span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">&gt;</span></span></code></pre></div><p>I&#x27;ve also written a test:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">describe</span><span class="token punctuation">(</span><span class="token string">&quot;Simple fixture test&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&quot;displays response&quot;</span><span class="token punctuation">,</span> <span class="token keyword">function</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> cy<span class="token punctuation">.</span><span class="token function">server</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fixture:examplefixture&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;#main&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;Hello&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>And created a fixture file <code>fixtures/exampleFixture.json</code>:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Hello</code></pre></div><p>Have you noticed a bug yet?</p><p>In my case, the screenshot from the failed test was very helpful. Cypress takes them by default for failing tests, which is neat 🔥!</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/405343e368f5fd3d144f7527bee68ba2/21b4d/screenshot.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:56.31399317406143%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAACWklEQVQoz22SS08TYRSG+6s0Ei4tlk5LYqQKVX+BEkUopRQVcMmCBSy4LHoBFhD+h4koiRsTI4FyKW1taWnnPtMhNI/fDFGKcZIn3/kmM0/ek3N8l4rBSUWmcKFQrOsCjdJFjfKvEtvb2+zs7DA3P8+jaJRo7BlDIzGigieiHn7+gsHHQ7yfnWN3d5dcLofP0XQaB0eoh9/RDvbQj39gFU9pCtKZDBnBZDxOfyBAKBRGEoSlMJHIIJFwhL5eP+PjE55sZWUF35WqY/zMYx6eYp+VaBWrOOU6aqVGNpf1Pkwmk0ImEZIi9AiBS3dPHwNC7u8PEp9MsLm5ydraGj5b0akfFWjkz5ELFdSajHYh03CF2Rvh9PQ0wWCQp8MxppIpEoKZdx8YiT335HeEbVmhfZSnfSjIH3MtTvInmJUqGVcoSKVS4sdeIZ6h85mdm+fe/QckppK3wmtNwzgvY4hWb6hgVxtoDVkIM15CT9jTK85/hR//I7RsdN1CNh1ko4Vi2GiWgyKSZzO3Qr/fz+joa/a+7PN57yuf9r/x8s1burq6mUxMdbRsWbRNA920UEVaRVNRdRWl2fwrvBlKyBtC51CCAxL+wEPi8cmOhIaJcdlAVVVaQuo4La6uHDRF8VbGnfQfYVisiiRWRQoPerVLnz/AxETcE66urt7s4eVZAblUxmk0sWp1rIs6VfEunU6zsbHhTdkVurIBb33CHu7dFboJt7a2WF9fx2eKlu1WC9u2sURtmiaOSFgslVhcXGRpaYmxsTEkSbojdNt37/5AP69GR1leXmZhYYHfyel1N016CykAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Screenshot from failed test" title="Screenshot from failed test" src="/static/405343e368f5fd3d144f7527bee68ba2/105d8/screenshot.png" srcSet="/static/405343e368f5fd3d144f7527bee68ba2/3cf3e/screenshot.png 293w,/static/405343e368f5fd3d144f7527bee68ba2/78a22/screenshot.png 585w,/static/405343e368f5fd3d144f7527bee68ba2/105d8/screenshot.png 1170w,/static/405343e368f5fd3d144f7527bee68ba2/21b4d/screenshot.png 1280w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Screenshot from failed test</figcaption> </figure></p><p>And now...Have you noticed a bug yet?</p><p>A message about the status from the stubbed request caught my attention. It was <code>400</code> instead of <code>200</code>. That was a clue.</p><h2>The typo and file systems</h2><p>Our bug, which we&#x27;ve been trying to solve with Monica, was a simple typo. The name of the fixture file was in camelCase, and we tried to load it via shortcut without the same naming convention.</p><p><code>exampleFixture.json</code> vs <code>cy.route(&quot;/api&quot;, &quot;fixture:examplefixture&quot;)</code></p><p>Ok, but why does it work on the local machine and doesn&#x27;t on CI?</p><p>99% of our frontend team works on MacBooks. Our CI runs the tests in the docker container (Linux). You can think - &quot;so what?&quot;. The default file system on Linux is case sensitive. On the other hand, the default file systems on Mac or Windows are not. What does it mean in practice?</p><p>On Linux you can create two files with the &quot;same&quot; name (different letter case):</p><ul><li>myAwesomeFile.js</li><li>myawesomefile.js</li></ul><p>Linux treats them as separate files. Try to do the same on Mac or Windows - you can&#x27;t do it. It has also impact on the way how you load the files, for example in nodejs. On Mac, there is no difference in load file by &quot;myFixture&quot; or &quot;mYFiXtURe&quot; names - the file will be loaded. On Linux, we will get an error - file not found.</p><h2>Let&#x27;s check it</h2><p>If we modify the code of our test in this way:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;fixture:ExAmPlEFiXTuRe&quot;</span><span class="token punctuation">)</span></code></pre></div><p>The test is always green on Mac. On Linux we get a <code>400</code> status for stubbed network request and an error message in console.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/9541d7f7ab7da3ac37cb220e4e54e35a/c211c/stub.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:55.972696245733786%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABBklEQVQoz52Sy26DMBBF/f87JEBijQDxQd2kqpQ2CTV1ggPFYGPg1ga6SdOHa+noLjy+8/CQorzgSCuUrAE9N+DXBuK9Buccbdui73t0XYdhGL5FKbXEWci54igog+jNxTBh0CP0uDLPM1wP0SbDWoEy6AW9YQ1dIe2lRvnwBLbb4233bHgBezygKSss9bkaqrqBPJwgT6+Qx2LR3qjiV0xbyy6tE2HaFWaoQkqjclVDZwY8TZO7YScEbhFWTaLF0LXlL4a24k3/9cv24S2fhnbHRrtCWv8JG0vumUkzQ8YY8jxHkiRI0/RXsixDHMf3De3mU0oRRRF830cYhgiC4EdsjOd5+AAGi0INgDaR/gAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Screenshot with 400 for stubbed request" title="Screenshot with 400 for stubbed request" src="/static/9541d7f7ab7da3ac37cb220e4e54e35a/105d8/stub.png" srcSet="/static/9541d7f7ab7da3ac37cb220e4e54e35a/3cf3e/stub.png 293w,/static/9541d7f7ab7da3ac37cb220e4e54e35a/78a22/stub.png 585w,/static/9541d7f7ab7da3ac37cb220e4e54e35a/105d8/stub.png 1170w,/static/9541d7f7ab7da3ac37cb220e4e54e35a/c211c/stub.png 1502w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Screenshot with 400 for stubbed request</figcaption> </figure></p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">CypressError: The following error originated from your application code, not from Cypress. When Cypress detects uncaught errors originating from your application it will automatically fail the current test. This behavior is configurable, and you can choose to turn this off by listening to the `uncaught:exception` event. https://on.cypress.io/uncaught-exception-from-application </code></pre></div><p>Wait, wait, wait...WAT? The following error originated from your application code, not from Cypress. Are you sure Cypress? 🤔</p><p>Let&#x27;s try to load the fixture without a shortcut:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token comment">// We made a mistake in fixture name</span> cy<span class="token punctuation">.</span><span class="token function">fixture</span><span class="token punctuation">(</span><span class="token string">&quot;examplEFixture&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">as</span><span class="token punctuation">(</span><span class="token string">&quot;response&quot;</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">route</span><span class="token punctuation">(</span><span class="token string">&quot;/api/endpoint&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;@response&quot;</span><span class="token punctuation">)</span> <span class="token comment">// With storing fixture in an alias we can use it in our assertions</span> <span class="token comment">// We don&#x27;t need to hardcode the &quot;Hello&quot; string</span> cy<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;@response&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">then</span><span class="token punctuation">(</span><span class="token parameter">data</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> cy<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;#main&quot;</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> data<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>The error message for this code is quite different:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Error: A fixture file could not be found at any of the following paths: &gt; cypress/fixtures/examplEFixture &gt; cypress/fixtures/examplEFixture{{extension}} Cypress looked for these file extensions at the provided path: .json, .js, .coffee, .html, .txt, .csv, .png, .jpg, .jpeg, .gif, .tif, .tiff, .zip Provide a path to an existing fixture file.</code></pre></div><p>And this is the error message that I&#x27;ve been counting on 👏 . We know right the way where we should start looking 😎.</p><h2>Summary</h2><p>There are two takeaways from this story:</p><ul><li>small typo could make you cry for two days of debugging session</li><li>you are as good as the error message from your test runner ;)</li></ul><p>I think that Cypress could return the better message about missing fixtures than <code>CypressError</code>. That&#x27;s why I&#x27;ve created an issue in cypress GitHub repository - <a href="https://github.com/cypress-io/cypress/issues/7818" target="_blank" rel="nofollow noopener noreferrer">here you can check the status</a>.</p><p>Thank you for your attention. I am going to try to solve the issue that I&#x27;ve created 😉. Maybe I will be able to add something to the OpenSource community to make cypress.io even better 😁</p></content:encoded>
</item>
<item>
<title>
<![CDATA[ Stop the time with cy.clock ]]>
</title>
<description>
<![CDATA[ Today I’m going to show you how to stop the time with one command. Unfortunately only in cypress.io tests. If you know how to do it in real life please DM me. It would be a very helpful tip 🙂. Ok… ]]>
</description>
<link>https://przemuh.dev/en/blog/stop-the-time-with-cyclock</link>
<guid isPermaLink="false">https://przemuh.dev/en/blog/stop-the-time-with-cyclock</guid>
<pubDate>Wed, 22 Apr 2020 17:00:00 GMT</pubDate>
<content:encoded><p>Today I’m going to show you how to stop the time with one command. Unfortunately only in <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a> tests. If you know how to do it in real life please DM me. It would be a very helpful tip 🙂. Ok, let’s stop joking and get our hands dirty!</p><h2>App description</h2><p>First, we need to have something to test. Our app will be deadly-simple. We want to display the enter time and a counter shows how many seconds we spent in the app.</p><div style="text-align:center;margin:2em 0;border:1px solid;padding:2em"><p>Enter time: <span data-testid="enter-time"></span></p><p>Time on page: <span data-testid="counter">0</span></p></div><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> React <span class="token keyword">from</span> <span class="token string">&quot;react&quot;</span> <span class="token keyword">export</span> <span class="token keyword">default</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> <span class="token punctuation">[</span>enterDate<span class="token punctuation">,</span> setEnterDate<span class="token punctuation">]</span> <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">useState</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">const</span> <span class="token punctuation">[</span>counter<span class="token punctuation">,</span> setCounter<span class="token punctuation">]</span> <span class="token operator">=</span> React<span class="token punctuation">.</span><span class="token function">useState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span> React<span class="token punctuation">.</span><span class="token function">useEffect</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">setEnterDate</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">const</span> intervalId <span class="token operator">=</span> <span class="token function">setInterval</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">setCounter</span><span class="token punctuation">(</span><span class="token parameter">prev</span> <span class="token operator">=&gt;</span> prev <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">1000</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token function">clearInterval</span><span class="token punctuation">(</span>intervalId<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token punctuation">(</span> <span class="token operator">&lt;</span>div<span class="token operator">&gt;</span> <span class="token operator">&lt;</span>p<span class="token operator">&gt;</span> Enter time<span class="token operator">:</span> <span class="token operator">&lt;</span>span data<span class="token operator">-</span>testid<span class="token operator">=</span><span class="token string">&quot;enter-time&quot;</span><span class="token operator">&gt;</span><span class="token punctuation">{</span>enterDate<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>span<span class="token operator">&gt;</span> <span class="token operator">&lt;</span><span class="token operator">/</span>p<span class="token operator">&gt;</span> <span class="token operator">&lt;</span>p<span class="token operator">&gt;</span> Time on page<span class="token operator">:</span> <span class="token operator">&lt;</span>span data<span class="token operator">-</span>testid<span class="token operator">=</span><span class="token string">&quot;counter&quot;</span><span class="token operator">&gt;</span><span class="token punctuation">{</span>counter<span class="token punctuation">}</span><span class="token operator">&lt;</span><span class="token operator">/</span>span<span class="token operator">&gt;</span> <span class="token operator">&lt;</span><span class="token operator">/</span>p<span class="token operator">&gt;</span> <span class="token operator">&lt;</span><span class="token operator">/</span>div<span class="token operator">&gt;</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span></code></pre></div><p>Ok, we have our app. Now it is time to write some cypress tests.</p><h2>We are testing!</h2><p>In our test scenario we would like to check:</p><ul><li>if the enter time is displayed properly,</li><li>if the counter increases its value after a one-second tick.</li></ul><p>Let&#x27;s try this way:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>The test looks decent but it doesn’t pass 😢</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:569px"> <a class="gatsby-resp-image-link" href="/static/8ea52e51054d035a2d33cf2a73475a17/854dc/datenow-assert-error.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:50.170648464163826%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsTAAALEwEAmpwYAAABt0lEQVQoz42S23LTMBCG8/7PAm1puYELBhjahrYuCZOUNGkSO87BsWMnPltaWT9rOS29YcAz3+xBq98raTvdN6fY7HYoiwJ1XUMp9cJzTET/pKlN0xQdiwVLUSFLElRlaYQb++xrLvyfr6mPwhCdu7dnaLZEcYIkz1FKyRBKUsYKFpRat9QaQnH8CvHKxtxUxzo9hxIEfzTBmimiGIm7YdZIlx6S5aaFc+l6izyIkAV7ZLsjTeyHKDgXh3vu8N176CCE+jGA+PAZ8tMlZG+A8vIG8q6P6uMXyK6F6uoW4rYHNXwEde9BHNO3G6jeEDQYQT9MkHk+C55dQB8S0HgGGj0Z5K8JxMMY9Dg1YpKFxPd7s1Y7K9RTB+rJNtRzF8p2TS7zg7bD2t9BWD8h+kNUVh/EYtgEjA/wceAFBr3yoPn4xhq2L3719ZqvaH3sMM0gxtyZvQBxUs5syLljcg3kLMya2vogd2VqaLkyNQauR7RHfoiPgvzCRtAI2cbSYgk5nTFz9l3o6MBXE0PvD39gkYaaHwPcVCt4cg40w+ltIR23hf+u84LzCuDRgZBAWf0V3dhKIOex+Q3oz/IYxFT/cwAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Assertion error" title="Assertion error" src="/static/8ea52e51054d035a2d33cf2a73475a17/854dc/datenow-assert-error.png" srcSet="/static/8ea52e51054d035a2d33cf2a73475a17/3cf3e/datenow-assert-error.png 293w,/static/8ea52e51054d035a2d33cf2a73475a17/854dc/datenow-assert-error.png 569w" sizes="(max-width: 569px) 100vw, 569px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Assertion error</figcaption> </figure></p><p>If we would like to show some formatted date (eg. 22-04-2020) instead of a number of milliseconds, then it would not be a problem. But our client wants to display milliseconds and we need to live with this requirement 😉</p><p>The <code>cy.clock</code> command comes with a rescue. It overrides native global functions related to time allowing them to be controlled synchronously via <code>cy.tick()</code> or the yielded <code>clock</code> object. This includes controlling:</p><ul><li><code>setTimeout</code></li><li><code>clearTimeout</code></li><li><code>setInterval</code></li><li><code>clearInterval</code></li><li><code>Date</code></li></ul><p>You can find more info about cy.clock in the cypress.io <a href="https://docs.cypress.io/api/commands/clock.html" target="_blank" rel="nofollow noopener noreferrer">official documentation</a>.</p><p>Now, let’s try to add <code>cy.clock</code> to our test:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>We still get an error. But this time the error message is different.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">expected &lt;span&gt; to have text &#x27;1587547901669&#x27;, but the text was &#x27;0&#x27;</code></pre></div><p>What is going on with that <code>0</code>? Because the time represented in timestamp value is a number of seconds passed from the start of Unix epoch (1st January 1970). We could ask what will happen after the 19th of January 2038 but this is a topic for another blog post 🙂.</p><p>Calling <code>cy.clock</code> without any arguments sets the date in our app to 1st January 1970. We could change it by passing an argument to the <code>cy.clock</code>:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> now <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>now<span class="token punctuation">)</span></code></pre></div><p>Right now, with a little luck, our test will pass. It depends on how fast our computer is 😄. To fix this issue we need to remember that <code>cy.clock</code> overrides the time in our app, not in our tests (command chain). That’s why we need to change <code>Date.now()</code> in our assertion to <code>now</code> value that we’ve created at the beginning of the test.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> now <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>now<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> now<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span></code></pre></div><p>The test is green - always! - success! But there is one little difference in how our app works now. Before using <code>cy.clock</code> our timer has been running. Right now it stops on <code>0</code>. Fortunately, it is expected behavior in our test-case scenario. We&#x27;ve set and stopped the time.</p><p>In order to move the time with some value, we need to call <code>cy.tick</code> command:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> now <span class="token operator">=</span> Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span> cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>now<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">visit</span><span class="token punctuation">(</span><span class="token string">&quot;/&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=enter-time]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> now<span class="token punctuation">.</span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=counter]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;0&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">tick</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string">&quot;[data-testid=counter]&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">should</span><span class="token punctuation">(</span><span class="token string">&quot;have.text&quot;</span><span class="token punctuation">,</span> <span class="token string">&quot;1&quot;</span><span class="token punctuation">)</span></code></pre></div><p>Tada 🎉! We&#x27;ve just wrote the test checking the enter date and the value of the counter.</p><h2>What if we would like to set the date only - without stopping the time? 🤔</h2><p>That’s a great question. Sometimes we would like to override the <code>Date</code> object only, leaving the rest untouched (<code>setTimeout</code>, etc.). In this case, we need to pass a second argument to the <code>cy.clock</code> - an array of timing functions that we want to override.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">cy<span class="token punctuation">.</span><span class="token function">clock</span><span class="token punctuation">(</span>Date<span class="token punctuation">.</span><span class="token constant">UTC</span><span class="token punctuation">(</span><span class="token number">2020</span><span class="token punctuation">,</span> <span class="token number">3</span><span class="token punctuation">,</span> <span class="token number">22</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token string">&quot;Date&quot;</span><span class="token punctuation">]</span><span class="token punctuation">)</span></code></pre></div><p>In this example we set the date/time to 22th April 2020 00:00 UTC (yeap - months in <code>Date</code> starts from <code>0</code> that&#x27;s why April = <code>3</code> 🙂). In the same time we don&#x27;t override the <code>setTimeout</code> and the rest time functions.</p><hr/><p>That&#x27;s all for today. I hope that with this knowledge you can go now and stop the time in your tests 😉</p><p>Good luck!</p></content:encoded>
</item>
<item>
<title>
<![CDATA[ 3 Steps to Awesome Test Reports with Cypress ]]>
</title>
<description>
<![CDATA[ In this article, you will learn how to generate informative test reports with Cypress and how to enrich them with some screenshot context. This will help you to fix your potential bugs way faster… ]]>
</description>
<link>https://przemuh.dev/en/blog/3-steps-to-awesome-test-reports-with-cypress</link>
<guid isPermaLink="false">https://przemuh.dev/en/blog/3-steps-to-awesome-test-reports-with-cypress</guid>
<pubDate>Wed, 18 Dec 2019 00:00:00 GMT</pubDate>
<content:encoded><p>In this article, you will learn how to generate informative test reports with Cypress and how to enrich them with some screenshot context. This will help you to fix your potential bugs way faster 😄 All you need is three simple steps.</p><h2>At Egnyte we ❤️ to test</h2><p>Keeping the highest possible quality of the product is one of our top priorities at Egnyte. That&#x27;s why we love to test. But our applications are rather large and relying on manual testing would be exhausting for us. That&#x27;s why test automation and Continuous Integration techniques are our best friends. We write a lot of tests: unit, integration, end-to-end, module, etc. The most important part is that, at the end of the day, if our Jenkins pipeline is green, we are sure that we didn&#x27;t break any parts of the system.</p><p>So where is the problem? Didn&#x27;t you know that tests not always pass? And that&#x27;s fine :) We don&#x27;t need to panic right away. First, let&#x27;s calm down, get into the test report on Jenkins, check what is broken, and fix it. That&#x27;s it. <strong>The problem is that the test report is very often just a plain error message plus a stack trace.</strong> And it&#x27;s enough for unit tests or integration tests for our React components, redux connections, and so on. On the other hand, this is not always helpful for tests that are run in the browser. Let&#x27;s imagine the following result from a test:</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/888b4da9d98b5f76f424ce227440d090/29007/jenkins.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:44.36860068259386%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsTAAALEwEAmpwYAAABCklEQVQoz2WQgY6DIAyGef9n3KaCCi0MvYA4hrKrkCx696chX4G/LTBjjJQyxrjv+1b0hbPq5n4Vs9Y6595FKSWqUtea0nrmL9SKTGs9z/NKCuFVVA3kuKQxVj73YN57RFQ0ugSJ06imQVmCgQAsscJ5hAn0TLdzzufhGbUEgHHULcdbA49O3ylafTsCKSUwkw/rO23756rDTJ0BUPSKC+gEdoLMWKoUcwOt0Kh/npNbQtxOJVgIATWdaaUkIPaj5kLxAQdpeI9iQBrYWIfmMJMh51NnOusETSu7Hh6tvLeSuOkUbTZcPe28pfrK/PknltJWfvr4bL+8nD/CL7HA6pYXjUpBb875r/8X6CUBl6eBj18AAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Jenkins console output" title="Jenkins console output" src="/static/888b4da9d98b5f76f424ce227440d090/105d8/jenkins.png" srcSet="/static/888b4da9d98b5f76f424ce227440d090/3cf3e/jenkins.png 293w,/static/888b4da9d98b5f76f424ce227440d090/78a22/jenkins.png 585w,/static/888b4da9d98b5f76f424ce227440d090/105d8/jenkins.png 1170w,/static/888b4da9d98b5f76f424ce227440d090/29007/jenkins.png 1600w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Jenkins console output</figcaption> </figure></p><p>I took this failed test report from our data governance product (<a href="https://www.egnyte.com/protect/content-governance-solution.html" target="_blank" rel="nofollow noopener noreferrer">Egnyte Protect</a>), which is one of our core products. For writing integration-UI tests, we use an awesome tool called <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. I must admit that Cypress and <a href="https://github.com/testing-library/cypress-testing-library" target="_blank" rel="nofollow noopener noreferrer">cypress-testing-library</a> are doing an excellent job in terms of error messages. Judging by the test report shown above, it is clear that we cannot find an element with matching text. Of course. But what is the visual state of the app? As a developer of Egnyte Protect, I know that this message should appear in a dialog. Has this dialog been opened? Or maybe it is only a typo? So many questions and no answers. If we wanted to check it, we would need to run the test locally once again and see what the visual state of the app is. Only then we would know (spoiler alert) that we have a typo :).</p><p>What if we displayed the visual state of the app right in the Jenkins report?</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/6b320f1a3bdfadd990a910edb663855e/f793b/app.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:54.60750853242321%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAABaElEQVQoz42QW0+CYBjH+TKds7oQVkFrE19O0+oGdLIp4Omr9Vm6qK22Llq1CeEhzBMoBwV6keWczmr7vc/Fu/9vzwE5IS6OCSJxjh/heAInDnF8D8N2UTRmB0W30eQ+hm0l0e3Ts4MoRsBYXBGG4dgfOJbLZjKyJFXKlbICX6VWrdVr9Xq1JkrKJctRDMst5ZE0xcQAmiXTgBeE1/dGb2B1jL7xNRyatmnZjuePzEmpVErBBM0uFARAbQ4Ff9NUPp9XW23bm/m+H4ZhEIZ+AEtoO44ky9CFsYWyIgOe5w3DgOlInmtBLNv237IgCN1ud+H8V46WAVShUGjqmjW2+4PRBC7rzVxvOvUD0xpLkvRrZ0CJovjZ1uGRegNzPHFdD44fdXZcV1YUktzUmWIBoHO53OPTc7PV0TRd1fSGqmvqx/3L2+3dQ7FYhBcFG+QImuEy2etszNXNMjTDroRXZUiKBOvAgdeT39lzKYXgDuLwAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Screenshot from the app" title="Screenshot from the app" src="/static/6b320f1a3bdfadd990a910edb663855e/105d8/app.png" srcSet="/static/6b320f1a3bdfadd990a910edb663855e/3cf3e/app.png 293w,/static/6b320f1a3bdfadd990a910edb663855e/78a22/app.png 585w,/static/6b320f1a3bdfadd990a910edb663855e/105d8/app.png 1170w,/static/6b320f1a3bdfadd990a910edb663855e/f793b/app.png 1404w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Screenshot from the app</figcaption> </figure></p><p>Wow! Now we know that the dialog is opened, and that the subheader text is incorrect! We have some valuable context just from reading the test report enriched with a single screenshot.</p><p>So how we could add screenshots to our test reports? Let&#x27;s find out!</p><h2>HTML reports to the rescue!</h2><p>Cypress is based on <a href="https://mochajs.org/" target="_blank" rel="nofollow noopener noreferrer">mocha.js</a>. And this is great because mocha.js is a very mature project with many custom extensions. Test results can be generated within elements called reporters. We can write our custom reporter or use an existing one, for example, <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a>. As the name suggests, it generates <strong>AWESOME</strong> reports! Badum tsss.</p><p>And now, I would like to show you how to integrate <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a> with <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress</a> to generate HTML reports with a screenshot context for failed tests. For the sake of this blog post, I&#x27;ve used an example repo <a href="https://github.com/cypress-io/cypress-example-kitchensink" target="_blank" rel="nofollow noopener noreferrer">cypress-example-kitchensink</a>. We will do it in 3 simple steps. Let&#x27;s get our hands dirty!</p><h2>Step 1 - set up the reporter</h2><p>First, we need to install proper reporters. Yes, that&#x27;s right - plural - reporters. We still want to see test results in the console. Maybe you also want to have a JUnit XML report. We need to have one reporter per expected outcome (console, HTML, XML). In order to set up many reporters, we will use the <a href="https://www.npmjs.com/package/cypress-multi-reporters" target="_blank" rel="nofollow noopener noreferrer">cypress-multi-reporters</a> package. On top of that, we also need <code>mocha</code> and, of course, <code>mochawesome</code>.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> <span class="token function">install</span> --save-dev mocha cypress-multi-reporters mochawesome</code></pre></div><p>Or if you use <code>yarn</code>:</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">-D</span> mocha cypress-multi-reporters mochawesome</code></pre></div><p>Then, in the <code>cypress.config</code> file, we need to specify which reporter we want to use:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">&quot;reporter&quot;</span><span class="token operator">:</span> <span class="token string">&quot;cypress-multi-reporters&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;reporterOptions&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;configFile&quot;</span><span class="token operator">:</span> <span class="token string">&quot;reporter-config.json&quot;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre></div><p>The <code>configFile</code> field points to the reporters configuration file. We need to add this file to our repository. For each of the reporters we can specify some options. Let&#x27;s do that for the mochawesome reporter:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">&quot;reporterEnabled&quot;</span><span class="token operator">:</span> <span class="token string">&quot;mochawesome&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;mochawesomeReporterOptions&quot;</span><span class="token operator">:</span> <span class="token punctuation">{</span> <span class="token property">&quot;reportDir&quot;</span><span class="token operator">:</span> <span class="token string">&quot;cypress/results/json&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;overwrite&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;html&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;json&quot;</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span></code></pre></div><p>In this fragment of config, we specify an output directory for the results file. We want to collect only the JSON files for each spec file. That&#x27;s why the <code>html</code> flag has been set to false. Because cypress is able to run tests in parallel, we need to set the <code>overwrite</code> flag to <code>false</code>. It means that for each spec file, we will generate a separate file. In our case, these will be JSON files.</p><p>Let&#x27;s try to run our tests via <code>npm run local:run</code> command.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash">Running: examples/location.spec.js <span class="token punctuation">(</span><span class="token number">9</span> of <span class="token number">19</span><span class="token punctuation">)</span> Location ✓ cy.hash<span class="token punctuation">(</span><span class="token punctuation">)</span> - get the current URL <span class="token builtin class-name">hash</span> <span class="token punctuation">(</span>169ms<span class="token punctuation">)</span> ✓ cy.location<span class="token punctuation">(</span><span class="token punctuation">)</span> - get window.location <span class="token punctuation">(</span>101ms<span class="token punctuation">)</span> ✓ cy.url<span class="token punctuation">(</span><span class="token punctuation">)</span> - get the current URL <span class="token punctuation">(</span>78ms<span class="token punctuation">)</span> <span class="token number">3</span> passing <span class="token punctuation">(</span>1s<span class="token punctuation">)</span> <span class="token punctuation">[</span>mochawesome<span class="token punctuation">]</span> Report JSON saved to /Users/przemuh/dev/cypress-example-kitchensink/cypress/results/json/mochawesome_008.json</code></pre></div><p>As you can see, after the spec reporter results, we received information that the <code>mochawesome_008.json</code> file has been created. Each of the spec files generated a JSON with results.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:652px"> <a class="gatsby-resp-image-link" href="/static/581d079e46b5dd429410b52138039577/dba9a/list.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:141.97952218430032%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAcCAIAAADuuAg3AAAACXBIWXMAAAsTAAALEwEAmpwYAAAB60lEQVQ4y5WUTY/TMBCGe0OAEJw5LJukWcf59PgjzgHtnnYFHLjzCzhBWXHmByDxo3mTab2mjeoyckd2mqdv3plpNq8+/Hj98bH48uvu25/p6+83n34+v//+4mF3Zr182D273739/LgpWrPtXTn4zt/eKH9VDe+kQr6qOM8bXLmuKaysJlzvxveb6qaUQqhejHZQfd/ICqutZSMFctfUbV3hnrLI41Vk11oNGyEqIaQiQbrTxrlx9H5CNtbioI3BhaZpi2K73ZZh5XlBpAEjJJHQuiUyxuj5o7VSChtrLTZtC3imQ+R5TkR7WGthTAexccSaA6RzznuP3HXdOfigrIMyvrNLYJ9QVmpW1jMIkyMzLI7c930Ml2WZZRlueIJZmZnY86oyjk3THCvjbjYcK596Zj4uWMswByyBnKYJ+1Nljn+U8ZjOYbngGRn7uq4TMNHeM5O0NB0WVj2vKC+giZXZ/6rnU3hfsACz/4QyCnYYEhNaxRZwlFKegzFh1nLB5vHkDuklsE/Dp+NpDpGA44KxZzC0VB/KGKYEfDSetAQfE32OHtvEhrngCZjHk1vDfWbPfEwrhz6HvzFXC/tLPR89Nv/QpcosFWrG/EWtYs+hVf9RsNXxvBQex2F5W3q8APiliQwe77pVEvEXj+QlMy4V7GcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="List of generated results" title="List of generated results" src="/static/581d079e46b5dd429410b52138039577/dba9a/list.png" srcSet="/static/581d079e46b5dd429410b52138039577/3cf3e/list.png 293w,/static/581d079e46b5dd429410b52138039577/78a22/list.png 585w,/static/581d079e46b5dd429410b52138039577/dba9a/list.png 652w" sizes="(max-width: 652px) 100vw, 652px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">List of generated results</figcaption> </figure></p><p>We are ready to go to the next step.</p><h2>Step 2 - generate the report</h2><p>We&#x27;ve collected the test results. Now, we need to merge them into one file and generate an HTML report based on it. We will use the <a href="https://www.npmjs.com/package/mochawesome-merge" target="_blank" rel="nofollow noopener noreferrer">mochawesome-merge</a> tool to merge result files. Let&#x27;s install it.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> i --save-dev mochawesome-merge <span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">-D</span> mochawesome-merge</code></pre></div><p>Now, let&#x27;s add an npm script which will be responsible for running the merge tool.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report:merge&quot;: &quot;mochawesome-merge --reportDir cypress/results/json &gt; cypress/results/mochawesome-bundle.json&quot;</code></pre></div><p>The <code>reportDir</code> flag specifies where we keep results files. The output of the command is passed from stdout to the <code>mochawesome-bundle.json</code>. One caveat here: the result of the merge needs to be put in a different folder than where the single results file is.</p><p>After merging we are ready to generate a final HTML report. In this case, we will use <a href="https://www.npmjs.com/package/mochawesome-report-generator" target="_blank" rel="nofollow noopener noreferrer">mochawesome-report-generator</a>.</p><div class="gatsby-highlight" data-language="bash"><pre class="language-bash"><code class="language-bash"><span class="token function">npm</span> i --save-dev mochawesome-report-generator <span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">-D</span> mochawesome-report-generator</code></pre></div><p>Let&#x27;s create an npm script for that action:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report:generate&quot;: &quot;marge cypress/results/mochawesome-bundle.json -o cypress/reports/html&quot;</code></pre></div><p>Marge is a short form of <strong>M</strong>och<strong>a</strong>wesome<strong>R</strong>eport<strong>GE</strong>nerator in case you&#x27;ve been wondering :)</p><p>Once the script has run, our awesome HTML report should appear in the <code>cypress/results/html</code> folder.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/c44c00ff45ade76717090ba27ef04155/29007/reporter.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:55.631399317406135%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAA80lEQVQoz5WR227CMAyG+/IDaRcUuoNE+niM0m6Q2M45abd5RWJotEL7ZOUi8qf8dorHl9e1qFdbsdhUD+vNonpaVs/Ln3OiuLkUotyKsq4rIYqcU0ohpxhz0J6+7pFS5H6umGIBgGcUwOHUdvL9qI6oUTtDlqyzbiSOBO/xAmBBFzQ13f7tY8eyQvDehxjiFZyQT7riV0YiCScyZJzxwecppmVNWqJssbOGc9rPGTj2rNzIhiex1g7DcGvylbdWz8l72RAQ76Lv+0k5ODcfG1qOzTKPx7/yj9gKVTfOjIS82Ek5hjD78kG1hjQAnMe+5U/sb8tlYyeUKfRVAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="HTML report view" title="HTML report view" src="/static/c44c00ff45ade76717090ba27ef04155/105d8/reporter.png" srcSet="/static/c44c00ff45ade76717090ba27ef04155/3cf3e/reporter.png 293w,/static/c44c00ff45ade76717090ba27ef04155/78a22/reporter.png 585w,/static/c44c00ff45ade76717090ba27ef04155/105d8/reporter.png 1170w,/static/c44c00ff45ade76717090ba27ef04155/29007/reporter.png 1600w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">HTML report view</figcaption> </figure></p><p>There is one more thing to do. Add a screenshot to tests that have failed.</p><h2>Step 3 - add screenshot context</h2><p>Cypress automatically generates screenshots for failed tests in the <code>cypress/screenshots</code> folder. You can disable this behavior if you want. Screenshots are collected within the following folder structure:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">path-to-the-specfile/spec.file.js/context - describe - describe - testTitle (failed).png</code></pre></div><p>For example, the following test placed in examples/actions.spec.js:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token function">context</span><span class="token punctuation">(</span><span class="token string">&#x27;Actions&#x27;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">context</span><span class="token punctuation">(</span><span class="token string">&quot;nested context&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token function">it</span><span class="token punctuation">(</span><span class="token string">&#x27;.type() - type into a DOM element&#x27;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span><span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>will generate something like this on fail:</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1056px"> <a class="gatsby-resp-image-link" href="/static/08b26a7260e306a2e5536e34e45fad84/fd84e/folder-structure.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:17.4061433447099%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAIAAAAcOLh5AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAWklEQVQI15WOSw7AIAhEPYs2EUGkivc/W6eaNt32LQi/gQkiMsaYczJzjPH4QyDKZubuqlpKeQdp8V3dZXq4xTlTN3z2fnZYaK3hFlzAEc4hooPEFnsKaq0QX98cHHXWQ3fFAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Folder with screenshot from failed test" title="Folder with screenshot from failed test" src="/static/08b26a7260e306a2e5536e34e45fad84/fd84e/folder-structure.png" srcSet="/static/08b26a7260e306a2e5536e34e45fad84/3cf3e/folder-structure.png 293w,/static/08b26a7260e306a2e5536e34e45fad84/78a22/folder-structure.png 585w,/static/08b26a7260e306a2e5536e34e45fad84/fd84e/folder-structure.png 1056w" sizes="(max-width: 1056px) 100vw, 1056px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">Folder with screenshot from failed test</figcaption> </figure></p><p>Ok, so how we can connect these two elements: a screenshot generated by Cypress and a test result generated by a mochawesome reporter?</p><p>First, let&#x27;s copy our generated screenshots to the folder where we keep the HTML reports. In order to do this, we will use an npm script:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report:copyScreenshots&quot;: &quot;cp -r cypress/screenshots cypress/results/html/screenshots&quot;</code></pre></div><p>Next, we will use the cypress/support/index.js file and write some code that will be listening on the <code>test:after:run</code> event.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">Cypress<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">&quot;test:after:run&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">test<span class="token punctuation">,</span> runnable</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>test<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">&quot;failed&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment">// do something</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>For adding the screenshot to the test result, we need to use the addContext method from the <code>mochawesome/addContext</code> package. This method takes two arguments: an object with the test, and the context. If the context is a valid URL (could be a local path) to the image, then that image will be displayed. To see more details, visit the <a href="https://www.npmjs.com/package/mochawesome#adding-test-context" target="_blank" rel="nofollow noopener noreferrer">documentation page</a>.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">import</span> addContext <span class="token keyword">from</span> <span class="token string">&#x27;mochawesome/addContext&#x27;</span> Cypress<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">&quot;test:after:run&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">test<span class="token punctuation">,</span> runnable</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>test<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">&quot;failed&quot;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">const</span> imageUrl <span class="token operator">=</span> <span class="token string">&quot;?&quot;</span><span class="token punctuation">;</span> <span class="token function">addContext</span><span class="token punctuation">(</span><span class="token punctuation">{</span> test <span class="token punctuation">}</span><span class="token punctuation">,</span> imageUrl<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Ok - but how to define the <code>imageUrl</code>? This is a time for magic to happen.</p><p><img src="https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif" style="width:50%;margin:auto;display:block"/></p><p>Just kidding :) we will use the runnable object. As we saw earlier, Cypress generates the name of the screenshot based on the test suite structure. We need to re-create that.</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript">Cypress<span class="token punctuation">.</span><span class="token function">on</span><span class="token punctuation">(</span><span class="token string">&#x27;test:after:run&#x27;</span><span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">test<span class="token punctuation">,</span> runnable</span><span class="token punctuation">)</span> <span class="token operator">=&gt;</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>test<span class="token punctuation">.</span>state <span class="token operator">===</span> <span class="token string">&#x27;failed&#x27;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">let</span> item <span class="token operator">=</span> runnable <span class="token keyword">const</span> nameParts <span class="token operator">=</span> <span class="token punctuation">[</span>runnable<span class="token punctuation">.</span>title<span class="token punctuation">]</span> <span class="token comment">// Iterate through all parents and grab the titles</span> <span class="token keyword">while</span> <span class="token punctuation">(</span>item<span class="token punctuation">.</span>parent<span class="token punctuation">)</span> <span class="token punctuation">{</span> nameParts<span class="token punctuation">.</span><span class="token function">unshift</span><span class="token punctuation">(</span>item<span class="token punctuation">.</span>parent<span class="token punctuation">.</span>title<span class="token punctuation">)</span> item <span class="token operator">=</span> item<span class="token punctuation">.</span>parent <span class="token punctuation">}</span> <span class="token keyword">const</span> fullTestName <span class="token operator">=</span> nameParts <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>Boolean<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">&#x27; -- &#x27;</span><span class="token punctuation">)</span> <span class="token comment">// this is how cypress joins the test title fragments</span> <span class="token keyword">const</span> imageUrl <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">screenshots/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span> Cypress<span class="token punctuation">.</span>spec<span class="token punctuation">.</span>name <span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>fullTestName<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> (failed).png</span><span class="token template-punctuation string">`</span></span> <span class="token function">addContext</span><span class="token punctuation">(</span><span class="token punctuation">{</span> test <span class="token punctuation">}</span><span class="token punctuation">,</span> imageUrl<span class="token punctuation">)</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span></code></pre></div><p>From now on, if our test fails, a context field with a local URL to the image will appear in the JSON results file:</p><div class="gatsby-highlight" data-language="json"><pre class="language-json"><code class="language-json"><span class="token punctuation">{</span> <span class="token property">&quot;title&quot;</span><span class="token operator">:</span> <span class="token string">&quot;.type() - type into a DOM element&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;fullTitle&quot;</span><span class="token operator">:</span> <span class="token string">&quot;Actions .type() - type into a DOM element&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;timedOut&quot;</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span> <span class="token property">&quot;duration&quot;</span><span class="token operator">:</span> <span class="token number">10395</span><span class="token punctuation">,</span> <span class="token property">&quot;state&quot;</span><span class="token operator">:</span> <span class="token string">&quot;failed&quot;</span><span class="token punctuation">,</span> <span class="token property">&quot;speed&quot;</span><span class="token operator">:</span> <span class="token null keyword">null</span><span class="token punctuation">,</span> <span class="token property">&quot;pass&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;fail&quot;</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span> <span class="token property">&quot;pending&quot;</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> <span class="token property">&quot;context&quot;</span><span class="token operator">:</span> <span class="token string">&quot;screenshots/examples/actions.spec.js/Actions -- .type() - type into a DOM element (failed).png&quot;</span><span class="token punctuation">,</span> <span class="token punctuation">}</span></code></pre></div><p>What is more, the image itself will be attached to the HTML report.</p><p><figure class="gatsby-resp-image-figure"> <span class="gatsby-resp-image-wrapper" style="position:relative;display:block;margin-left:auto;margin-right:auto;max-width:1170px"> <a class="gatsby-resp-image-link" href="/static/8fd9e50f05c93dd52922645fba1b999e/29007/reporter-error.png" style="display:block" target="_blank" rel="noopener"> <span class="gatsby-resp-image-background-image" style="padding-bottom:55.631399317406135%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsTAAALEwEAmpwYAAABuklEQVQoz42SW0/CMBiG9/+v1QgoqIx4TUjgX6g3CEbB8zhstFu7tbC5vn7t0KByYZM3b7d8ffod6tXbbTQvL3F8fo6D0ybpFIe1Gg6bTdQv2jjxO2j4PhoUV6Pvett3OmqdufijVguNTse0KM6/8LUHY4CyhHO7rK/XyLPMqVAahdZVzO9lY51KY0xJW6M9xjk4yTpjds8QLZeYTJ/wMJl+6/XtHTyOEScJ0jR1YoxV5zg3vOJoL6GAb4kEMhFgUYhZNEfEIgIQRMYQUiCjjBVlu9lsoMntGSEEuTBCWIZYe/aH0xYqaR8uZniPArCEE0AhL/JKeaWiKH4BE2M9ilbmJ3DrgkpJs5Raqd1BRX2sMsuhqb9WmVJfsCoRKU0YhtgLVFKCCwmZKdez17c5luGKSla2LCpf0oXZLtAQEATcn2EmU0xmc0wDAq0YgtmSgMz1kCbptKYsd0rGFrg/Q81jJPePEHcTCMpMbwqUHz+fje3jv4GKXNIzMDSAcLHA1fUNbm9HGI/HGI1GGA6HCILAPZ2dHv4FutvorWkaiB2CXc8vL+j1ehgMBuj3+8673a6D2oHFFL8L/AQ++DLW9mYuQQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="HTML report with screenshot context" title="HTML report with screenshot context" src="/static/8fd9e50f05c93dd52922645fba1b999e/105d8/reporter-error.png" srcSet="/static/8fd9e50f05c93dd52922645fba1b999e/3cf3e/reporter-error.png 293w,/static/8fd9e50f05c93dd52922645fba1b999e/78a22/reporter-error.png 585w,/static/8fd9e50f05c93dd52922645fba1b999e/105d8/reporter-error.png 1170w,/static/8fd9e50f05c93dd52922645fba1b999e/29007/reporter-error.png 1600w" sizes="(max-width: 1170px) 100vw, 1170px" style="width:100%;height:100%;margin:0;vertical-align:middle;position:absolute;top:0;left:0" loading="lazy"/> </a> </span> <figcaption class="gatsby-resp-image-figcaption">HTML report with screenshot context</figcaption> </figure></p><p>TADA 🎉 We got it!</p><p>You can check all necessary code changes that we have done in the following pull request: <a href="https://github.com/przemuh/cypress-example-kitchensink/pull/1/files" target="_blank" rel="nofollow noopener noreferrer">https://github.com/przemuh/cypress-example-kitchensink/pull/1/files</a></p><h2>Optional steps</h2><p>You might want to add <code>cypress/results</code> and <code>cypress/reports</code> folders to your <code>.gitignore</code>.</p><p>It would be good to remove screenshots, results, and reports before the next test run. It can be done by a simple npm script. In our example repo, I&#x27;ve added:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;precy:run&quot;: &quot;rm -rf cypress/screenshots cypress/results cypress/reports&quot;</code></pre></div><p>&quot;Pre&quot; means that this script will be run before every <code>cy:run</code>. See <a href="https://docs.npmjs.com/misc/scripts" target="_blank" rel="nofollow noopener noreferrer">npm docs</a> for more details.</p><p>In the example repo, there is a <a href="https://www.npmjs.com/package/npm-run-all" target="_blank" rel="nofollow noopener noreferrer">npm-run-all</a> package installed. We could use it to run in sequence: merge, generate report and copy screenshots scripts in one command:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">&quot;report&quot;: &quot;run-s report:*&quot;, &quot;report:merge&quot;: &quot;mochawesome-merge --reportDir cypress/results/json &gt; cypress/results/mochawesome-bundle.json&quot;, &quot;report:generate&quot;: &quot;marge cypress/results/mochawesome-bundle.json -o cypress/reports/html&quot;, &quot;report:copyScreenshots&quot;: &quot;cp -r cypress/screenshots cypress/reports/html/screenshots&quot;</code></pre></div><p>There is also one caveat. The file name in most systems is limited to 255 characters. So what will happen when we have a very nested structure of a test suite with long descriptions? It&#x27;s simple - our file name will be truncated. Cypress truncates the full test name to 220 characters. So we could also do the same in our code:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token keyword">const</span> <span class="token constant">MAX_SPEC_NAME_LENGTH</span> <span class="token operator">=</span> <span class="token number">220</span><span class="token punctuation">;</span> <span class="token keyword">const</span> fullTestName <span class="token operator">=</span> nameParts <span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>Boolean<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">join</span><span class="token punctuation">(</span><span class="token string">&quot; -- &quot;</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">slice</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token constant">MAX_SPEC_NAME_LENGTH</span><span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>But this is an implementation detail. We don&#x27;t know whether Cypress devs are about to change that number. So, a better option would be to read an article from <a href="https://kentcdodds.com/" target="_blank" rel="nofollow noopener noreferrer">Kent C Dodds</a> about <a href="https://kentcdodds.com/blog/avoid-nesting-when-youre-testing" target="_blank" rel="nofollow noopener noreferrer">avoiding nesting when you are testing</a>.</p><h2>Wrap-up time</h2><p>I hope that this article will help you to set up awesome HTML reports in your project. It helps us a lot when it comes to quick investigations of why a given test is failing. Let&#x27;s recap what we did here:</p><ol><li>Install and set up the mochawesome reporter.</li><li>Collect test results and generate an HTML report based on the merged JSON file</li><li>Add screenshot context with an <code>addContext</code> function.</li></ol><p>You can check all code changes here in this <a href="https://github.com/przemuh/cypress-example-kitchensink/pull/1/files" target="_blank" rel="nofollow noopener noreferrer">pull request</a>. And of course, after you generate the HTML report you need to connect it somehow to your Continuous Integration tool. But this is a story for a separate post. :)</p><p>Now…are you ready to create your own Cypress HTML reports?</p></content:encoded>
</item>
</channel>
</rss>