<![CDATA[przemuh.dev [PL]]]>https://przemuh.devGatsbyJSFri, 01 Jan 2021 13:11:26 GMT<![CDATA[Krótka historia o optymalizacji]]>https://przemuh.dev/blog/tree-performance-improvement-case-studyhttps://przemuh.dev/blog/tree-performance-improvement-case-studySun, 08 Nov 2020 00:00:00 GMT<p>Jakiś czas temu na <a href="https://twitter.com/przemuh/status/1319595759935852544" target="_blank" rel="nofollow noopener noreferrer">Twitterze zamieściłem zrzut ekranu</a> pokazujący flame-chart z narzędzia Profiler. Pracowałem wtedy nad poprawą wydajności aplikacji, którą rozwijamy w Egnyte. Pewna funkcjonalność, dla dużej ilości danych, zajmowała strasznie dużo czasu - 3,5 minuty! Przez ten czas w aplikacji pokazywany był &quot;kręciołek&quot;, a użytkownik nie wiedział, czy coś się dzieje, czy może coś się zawiesiło. Po kilku dniach pracy z Profilerem udało mi się zaimplementować poprawki, które zredukowały czas potrzebny do obliczeń z 3,5 minuty do 35 sekund. W tym wpisie chciałbym opisać jak do tego doszedłem.</p><h2>Opis funkcjonalności</h2><p>Zacznijmy od opisu funkcjonalności, która krótko mówiąc kulała pod względem wydajności. Jednym z głównych widoków w naszej apce jest tzw. &quot;Sensitive Content&quot;. Pokazujemy tu listę folderów z plikami, które zawierają wrażliwe dane. To mogą być numery kart kredytowych, dane medyczne, personalne i nie tylko. Dane te mogą podchodzić pod jedną z kilkunastu wbudowanych polityk np. HIPA, GDPR, ale również dajemy możliwość użytkownikom zdefiniowana własnej polityki np. w oparciu o utworzony wcześniej słownik. Początkowo widok &quot;Sensitive Content&quot; pokazywał tylko płaską listę folderów. W zeszłym roku nasz Product Owner wraz z zespołem UX doszli do wniosku, że praca z płaską listą folderów może być nieefektywna. Zamiast tego dużo lepszym, i w zasadzie bardziej naturalnym sposobem reprezentacji danych, będzie drzewko folderów.</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.81779661016949%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABVElEQVQoz01R2U7EMBDr//8fLwiEuLTskftq0rTGk3YXIllJp45jz0yXm8ZVGdy0HZDz9aidQ8YpFqg0o7WOMjcEflufYGyAdRGGcCGN3biAyVqHmBJiTCQTIaKUGb2vqEtH6x25LThbD2Pc4NXaBjfngllQK7wPRMSktIY2li9aPM6EDwELBWVF7s907J0b/9uyjHrnYygVKWUopZFLwfR+0vg8G3xdLD5+NL4vvBRm2NTgckcsK66x4Ymcb3KEa2JFYN2wHnzBxSS8nxSuNmO6PRxSSBu6NIwTUVvD0neHiQ5f2V9HjrhvbMG2YSTYeh7RJZ20ao98ILGXYjvnPCI3isrKh6B30g5Dob/I21oHXymFeaagECx70w83/9d27OLwhYLBezhy772VBH3dhhER3B2q3Z0QJY5MeWOeO4ZDXnzjhJ3dB3Yfijjt64pEh/qI/AvbE2lTetVs3wAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Widok Sensitive Content List" title="Widok Sensitive Content List" 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">Widok Sensitive Content List</figcaption> </figure></p><h2>Algorytm budowania drzewa</h2><p>Podejść do drzewek było już u nas w projekcie kilka. Głównie opierały się one na własności folderu jakim było unikalne <code>folderId</code>. Niestety, w przypadku &quot;Sensitive Content&quot; nie mogliśmy tego użyć, ponieważ nie wszystkie foldery mogły zawierać wrażliwe dane, a tylko dla takich folderów dostawaliśmy <code>folderId</code>.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">/Shared/A/B/ W tym wypadku mamy 3 foldery (Shared, A, B), z czego tylko dla B dostajemy folderId</code></pre></div><p>Takich lokacji z SC (Sensitive Content) może być multum. Problem wydajności pojawiał się już przy 250 tysiącach lokacji. A że nie jest to wyjątek utwierdził nas klient, u którego znaleźliśmy prawie <strong>milion</strong> folderów. To właśnie dla 1M elementów listy, czas budowania drzewka wynosił 3,5 minuty. Dlatego też, moje zadanie polegało na tym, że drzewko dla 1M elementów ma się budować w mniej niż 60s.</p><p>Wracając do algorytmu. Prosty - jak budowa cepa - tak by się wydawało :)</p><ol><li>Weź całą ścieżkę i podziel ją na fragmenty wg. separatora np. <code>/</code></li><li>Każdy folder wsadź do dwóch struktur: &quot;drzewiastej&quot; i &quot;płaskiej&quot;</li><li>Jeśli folder nie ma <code>folderId</code> traktuj go jako meta-folder</li></ol><p>Dla uproszczenia pomijam kwestię tego, że wspieramy różne źródła danych i te separatory mogą się mocno różnić :) Co więcej, jak się później okazało, niektóre źródła danych mogą mieć dwa foldery o tej samej nazwie, na tym samym poziomie zagnieżdżenia 😱. I jak je rozróżnić? Pominę też kwestię tego, że wynikowe drzewo mieliśmy przedstawić w formie &quot;sparse-tree&quot;. W skrócie - chodzi o to, że jeśli folder zawiera tylko jeden sub-folder, to ścieżka rodzica powinna być zwinięta/scalona.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Lista: /Shared/A/B/C /Shared/A/B/D --&gt; Drzewo: /Shared/A/B /C /D</code></pre></div><h2>Pierwsza, a w zasadzie druga implementacja</h2><p>Jak już wspomniałem wcześniej, nie było to pierwsze drzewo, jakie mieliśmy wyświetlić w aplikacji. W zupełnie innym widoku, też mieliśmy zrobić sparse-tree i nie chcieliśmy mieć kilku różnych implementacji. Dlatego napisaliśmy prosty moduł do budowania i zarządzania drzewkiem. Oparty został o dwa małe komponenty:</p><ul><li>funkcję <code>buildTree</code>, która przyjmowała płaską tablicę węzłów i miejsce (ścieżkę w drzewie) od którego miała te węzły wstawiać</li><li>&quot;plasterek&quot; (slice) z <code>redux-toolkit</code>, który zarządzał strukturą drzewa (rozwijanie, zwijanie węzłów itp.)</li></ul><p>Całe drzewo, a w zasadzie te dwie struktury &quot;drzewiasta&quot; i &quot;płaska&quot;, trzymane były w reduxie w następujący sposób:</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>Taką strukturę otrzymujemy z pomocniczej funkcji <code>buildTree</code>.</p><p>Dzięki zastosowaniu <a href="https://redux-toolkit.js.org/" target="_blank" rel="nofollow noopener noreferrer">redux-toolkit</a>, a co za tym idzie biblioteki <a href="https://github.com/immerjs/immer" target="_blank" rel="nofollow noopener noreferrer">immer</a>, mogliśmy w bardzo prosty sposób wykonywać operacje na drzewie:</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> initialized<span class="token operator">:</span> <span class="token boolean">false</span><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> 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> paths<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> name<span class="token operator">:</span> treeName<span class="token punctuation">,</span> initialState<span class="token operator">:</span> initialTreeState<span class="token punctuation">,</span> reducers<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> payload<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>Pomocnicza funkcja <code>getNodeByPath</code> służy do wyszukiwania węzła po ścieżce. Potrafi też wyszukać węzeł w sparse-tree.</p><h2>Pierwsze podejścia do optymalizacji i pierwsze błędy</h2><p>No i wszystko pięknie-ładnie, ale przyszedł klient, 1 milion folderów i jebs... Product Owner zakłada Epic w Jirze pt. &quot;Support 1M folders on SC tree view&quot;. Szybka burza mózgów i od razu kosz pełen pomysłów:</p><ul><li>a może by tak budować drzewo w locie, jak parsujemy JSONa?</li><li>a może by tak budować drzewo w web-workerze, przynajmniej nie zablokujemy głównego wątku na 3,5 minuty?</li><li>a może by tak, pizgnąć to wszystko i wyjechać w Bieszczady? ⛰ 🐑</li></ul><p><img src="https://media.giphy.com/media/kPtv3UIPrv36cjxqLs/giphy.gif" alt="A może by tak..."/></p><p>Pierwszy błąd - nikt nawet nie odpalił Profilera, żeby zobaczyć co zajmuje tyle czasu. Każdy założył, że obecna implementacja drzewka wymiata i lepiej być nie może. Sam Profiler na pierwszy rzut oka nie jest prostym narzędziem i może to było powodem tego, że rzuciliśmy się wtedy na tego typu pomysły jak budowanie drzewa &quot;w locie&quot; czy przeniesienie tego do web-workera. Warto też podkreślić, że sam Epic w Jirze powstał już jakiś czas temu, a do samej implementacji usiadłem w połowie października.</p><p>Pewnie zastanawiacie się - ale jak to w locie? Przecież jak idzie request to dopiero jak przyjdzie odpowiedź to przeglądarka parsuje JSONa i daje odpowiedź. Tak, ale...w tym przypadku nasi backendowcy też musieli podziałać trochę w kwestii optymalizacji i zamiast zwracać pełne dane to zaczęli tę naszą listę SC zwracać na zasadzie stream&#x27;u. Dzięki temu mogliśmy np. wykorzystać bibliotekę <code>oboe.js</code> to parsowania JSONa w locie.</p><p>Oczywiście spróbowałem tego podejścia, no bo w końcu ktoś to wpisał do zadania w Jirze, trzeba było sprawdzić co nie? 😜 Fajnie, JSON parsował się &quot;w locie&quot;, ale stream zamiast 10s trwał 30s, a jeszcze nie zacząłem nawet budować drzewa. Dlatego odpuściłem i postanowiłem poszukać gdzie indziej.</p><h2>Web-worker</h2><p>Podejście z web-workerem też przetestowałem. Ale tu napotkałem zupełnie inny problem. Ok - mogę sobie pobrać 1M elementów i zbudować na tej podstawie drzewo, ale muszę je później przesłać z wątku web-workera do wątku głównego. Struktura drzewa jest dość obszerna, razem z danymi, które zapisywaliśmy w tej płaskiej strukturze <code>paths</code>. Jeśli chcemy przesłać tak duże dane z jednego wątku do drugiego, przeglądarka musi te dane zserializować, przesłać, a następnie ponownie sparsować. To też powodowało &quot;zamrożenie&quot; przeglądarki na czas przesyłania z jednego miejsca pamięci do drugiego. Oczywiście są sposoby na przesłanie &quot;bezpośrednie&quot; (bez kopiowania) poprzez tzw. Transferable Objects np. ArrayBuffer, ale uznałem, że na chwilę obecną to może być gra nie warta świeczki i postanowiłem sprawdzić, czy faktycznie ta nasza implementacja drzewka była tak zajebista jak myśleliśmy 😜</p><h2>Profiler podejście pierwsze</h2><p>Usiadłem przed ekranem komputera, odpaliłem dev-toolsy i wcisnąłem przycisk &quot;record&quot; w Profilerze. Po chwili dostałem taki kolorowy wykresik, który przypomniał mi czasy defregmentatora dysków z Windowsa 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.5%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAACOklEQVQ4y42TeW/aQBDF/ZGrtiSUw6Fqv1YVVYqapkkgKeEU4JgjGBx87YkPXscODUhVpP7x05ud3X07s14blVoNpZMyTkunqFNcr5+h8qmKWtWEaZoUn1BcRqV8gtL7d4V+bpiom1V8+dpA46yGRr0Ks1ZB6cNHGH7oYRMQvgsmQ3AtIWIFGWtwFUGLGRK5QKZWhINMOkjEEimR8KeXmHIsWMB1HRhaawjBCQHORaFZtiMAEiT4f6TSMISkipR6hdPY91YIwhW8pQ1/0odvDY7owytyR9A4sIeIgmcy1Cm1loEVpGS6g+V00N58w8PwHL2L75i2bmC3rmE3fxHXmDZvMGs1C83npve3sK+usB6Nc8MMx8jtDjxMEW1iOP0N2pcPGA8tPPanGC8eMVpN0Jn30G33MLq3MGiO0G8OYd8uEFn6X8MCqlbSBfJRDP/So5MdrAdLMKapiy08yWB3uljcD2HddTG9HsJtrhAtGQz5hqFIdoieNKJbutt2DP5bQ/Jd0YGKgfWFB+88gPfTQ/SDgbVo3Uq9YZgTU4XPCuGYI5qRzvIPlu7nUoQ9ynUkVaURWqQTCeVu32g5p7jLLdgsAnMUGJ0uXg2JeQLxRJXzhA6mTuYcMoipwu3hY+QcDAmZgW+24B498g291/wV/F0TUByRKRkKMmKugqKxkS/KnwuTdJJKD9CmosWQNjAah8khn2ueEy8IRnNkKsncWLvP8IMQYcgKGP0tjMu9EmxPJA654zw/mif9A0zFwtIb7Iv6AAAAAElFTkSuQmCC&#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>Pierwsze co rzuciło mi się w oczy to ten fioletowy kolor, który zanurkował bardzo, bardzo głęboko. Po bliższym spojrzeniu okazało się, że bardzo dużo tych fioletowych elementów to sprawka <code>immer.js</code>. Szybki rzut oka w dokumentację i boom! strzał w dziesiątkę. Okazuje się, że przy &quot;wkładaniu&quot; dużej ilości danych poprzez <code>immer</code> możemy przyspieszyć ten proces poprzez <code>Object.freeze</code> <a href="https://immerjs.github.io/immer/docs/performance#performance-tips" target="_blank" rel="nofollow noopener noreferrer">tutaj więcej info</a>. Zabieg ten pozwolił mi na zejście z 12,54s na 11,24s dla 54K elementów. Dla 1M skok był oczywiście proporcjonalnie większy. No ale to nadal nie było to...</p><h2>Od profilera, do źródeł</h2><p>Wiedzieliście, że jak klikniecie na dany blok w Profilerze, a następnie przeniesiecie się do pliku, to dostaniecie czasy dla poszczególnych bloków kodu? Nie!? 😎 To teraz już wiecie ;)</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.73170731707317%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACUElEQVQ4y4VUa5eaMBT0//+39kPrqa26qy4LKpDwSMgbpje40kWtyzEnYJLJ3Llz76IVEnnJwOsGQjokSYbt5oDdrsLbW4PDniHLzhBCw1mLEAL6Pozz59H3Pbz3WKiug2pPMLJEfAJtbniH/a7BdluMF7CyhlIe3nkMw4D/PRF0IaWEqFICPeO6d9CeQC04N7DWwxqLrnOQ0tDcEaN+BP48JkClFDr+AtMmuN7t6PCv5RHr9ZHY8ZHZeNENwO08AlrS5fqMCx+LjRBYbzZYrTLSMicN238AGGahzwCN0RDnn+jYetqgRYdjXiIrK7SyRtM044gHHjGbAWqtwZJvYOmPWVh1XaEoKuz3pLGwDwEeMlRKozkt0RZXhgN62pBmDWW6wOnIiZ2C1u4S7ICZjneAMWuyyaElnwDjjzMxejDPFYwOs8MDnoRsjCHDasqkmRYMvdddBWko3M7AeLJPuIxn2Z5sI8s/ZJ3dxMJqg/2hwPJ3gn1yQHpieKHvnNfwwd/pNwMUZA+tFZxzc23CgNMrozIssXvleE84+bKhyqnHJFkbHjOMgN65O00CMbFUbm3r0IpYKZYyb+j7UjVPASWFq9tsCsO7AEtAog3ExoymjsBKhbv6vQs5ZrlKv0MWq2kTq6g5pGTs/EQGrynTsYYVot7Rt0+TErMcG4ORbGoOhiQoOEdZMZRMwhjqND7qdhm3zO4qxRlB1ukuf9KiI6vwQiF9F6h59CKjqiGtffi6UiJDbxUCeS82zlgRmkTvKcvOXZqm9z0x6z9qeXga8l+P/3vSoyCcKQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Czasy przed optymalizacją dla buildTree" title="Czasy przed optymalizacją dla 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">Czasy przed optymalizacją dla buildTree</figcaption> </figure></p><p>To co się rzuca w oczy to 229ms dla zbudowania prostego ciągu znaków 🤯 jakim jest aktualna ścieżka. Okazało się, że to zwykłe niedopatrzenie można było zastąpić krótszym kawałkiem kodu, który koniec końców zajmuje 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.38510911424905%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAACtElEQVQ4y41UCXKjMBDM/3+XrdojTuIb29ygCx1AbyMn2HFc3hWeMqURrZ6ZnnnSWsO0B1h5hDYeSZJi8bbG66rELlFIU4X1ukR6KuCsRfAeIQT0fX/XnrrOwtQbiGKJcQSc86gKgdWyxvNzjsVii90uRVlpdMZhHHo8WgQ06HQTbQKMqx/gtEfdOGjdEcjC8yJjRkZh4L2Lx0Z+cGtPrdT8uESnigg4YY6uR5tbbLcSmn4hFNqmRVFUmFJU14zKhBn0C0OhOgSnyEDPzuAcDmmOZZLhVOaRVSs6hq2glCPDAT709wGnkEWxQnV6PR+YnmGEIZP9IcPLosHxKBmBh0wNLFPxuW7BPgA7aBalyd6/HFLM3W6bsCAK253k/hAvus3ZdS4joGN4482NPWXRNBZt+7iid0M2ZmK4onTWc8jTLy8r/HxfY01dJnmJQ3HE+pjwP0clKxjPVFnxnaHWBrp8h66WFycfpRWrKVgIi4omBN9LhaqkHqnda3Y3gDwgc/S2mTedt8iqCvuiQNHmaFSDjLJpuhqs7wcIvuXxHDKL0qa/UCY/Lg6abh3eXjL8+bPBZpNhuSxYHAJnilq0AMa7eYwMvVUsRDeDffp1aZBS3JudZi/XrHpLYEFgEYs2ibtnV31hqJRi/9qzLK5CmAaA8z3zFWCtp5gD33u+c5+dZMzZNww3gHHaNAk6ccCllQcETQAZIFoKWkpKSMfedi48lE7Moch+M7yXedN0HqeixiGrkbI4U5Xr2pGRj5FMYU52zW5mOElAlwvI/ALoOF0K9vJ6V2GbNMxvT5Zky36WUpCtiwMiBP8d0HJoOttF+9wcOCjrusN+ryGFY96mEWbIKvxPp5gI1gd/aT3myZDF6WQ4sjpOGDtP6X/28sQwHvxwTNVU0sZBEMIQbRjGhxPmev0FrkwWn/TxJDQAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Czasy po optymalizacji dla buildTree" title="Czasy po optymalizacji dla 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">Czasy po optymalizacji dla buildTree</figcaption> </figure></p><p>Pomyślicie - (ironicznie) wow... 227ms... &quot;brawo Ty&quot; 👏. Czym jest 227ms? Jeśli spojrzymy na to jako pojedynczą wartość - to fakt...mikro-optymalizacja. Ale pamiętajcie, że celem było obsłużenie 1M elementów, a operacja sklejania ścieżki dotyczyła każdego sub-folderu.</p><h2>Mój spread operator AKA Object.assign taki piękny</h2><p>Jak zrobić płytką kopię obiektu, bądź rozszerzyć inny obiekt - nic prostszego - spread operator <code>...</code>. Jeśli musicie wspierać przeglądarki takie jak IE11, to pewnie korzystacie z babel.js - tak jak my...no i taki spread operator, koniec końców jest tłumaczony na <code>Object.assign</code> (w wielkim uproszczeniu).</p><p><code>Object.assign</code> jest <a href="https://twitter.com/dan_abramov/status/980436488860196864" target="_blank" rel="nofollow noopener noreferrer">stosunkowo wolny</a> i przy większej skali może sprawiać problem. W tym przypadku postawiłem na zwykłe kopiowanie per klucz. Dzięki temu prostemu zabiegowi zbiłem 154ms do 44ms. I znowu, dla pojedynczych elementów to nie ma absolutnie żadnego znaczenia, ale kiedy iterujemy po dużym zbiorze danych takie optymalizacje mogą zdziałać cuda.</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.85382059800665%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAACuklEQVQ4y51U23LaQAz1//9EX/sTfehDp5k2MwkJ2MQ2EDBgx1eM7xc41dnYCcn0kunOyNrVylrp6OxqztLC9MdX7F0Pi+UKy9UKZVXhfD6jP51w+qDQv2k7aPrNNb58/oQkKxBGMYIwRtf3+J/BwFpelNh7T1ivN9hsHGy3O+x2e9FbZXOcLWzbhuu6UoUrPuLnOFg9Pirf9XoNPwhUQGapeZ6HyWQCXdcxm82UXi6XsCwLhmFgOp2KnuPu7h6mKbb5XPx03Iv99naCB9NE0zTPGTKg++Tj+maCqW7AeLCgmzbm1gKTqRzAtfEASzLkj7a9gCkH2YuFCsr1TBKwLBur1SOOxwzaNm3wc5djHpSY+hWMoIIZVrBEzKiGn7c4CaYj+P0wH3XXdUrGtVa2PZL6jLwDDlWPtutl8zTI+3n/zt4rJnB+Op2V1jg5RD6i4AlVWaJt25dT/yVt+zov5d+6bqARzOvvV7j6doVDekSapiiKAnleoKrqD1MmyzJUdQ2NnCvLSoKlCtRIuBgnCZLkoORwoKRIqOXA+FghFc7SHsWx0v3A20aq04gBjfu9C0/4mOe54NgpKlAIAWWcN4IxS73cJ//ko+ZaO3RoHGEYIhiIykEnHkSMuHeUShKpgBVdjvMYkCVXcnd931c/UFgym0WnWrIIo0gBH4mmL/dZyRjoTYYKQwHf9Xx4QvIgiGTdCMCtEnU1XYGiqGQ/ECyPyieKkz8EFEzYbt5HPwjVA0FnNoANyrJc2ZlhEEZqzQypLwM+vzZsipCRFNk4O3kI5LLLA7GTBq03W8FKKFTKbfFDybZRL1GWC5ZRglQY8X6wWRo/zLBu2mc9zJtB3tjry3nz4vPiS2Kzy4XwkDgym1HTlg/2egz8N5HD1APLUpI0QyE/UmdS/pHElZJiaUA53JZLrH4rg88vUpezjjrk5acAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Dan Abramov o Object.assign" title="Dan Abramov o 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 o Object.assign</figcaption> </figure></p><h2>Agregacja wartości</h2><p>Po &quot;podkręceniu&quot; immera i wyrzuceniu kliku Object.assign, bądź przepisaniu ich na prostą pętlę, skończyły mi się pomysły na &quot;proste&quot; optymalizacje. Trzeba było podkręcić sam sposób budowania drzewa.</p><p>Poprzednia implementacja, dzieliła sobie SC lokacje wg. źródła i dla każdego z nich budowała pod-drzewo. Dla każdego pod-drzewa liczone były zagregowane wartości (np. jeśli folder sam w sobie zawierał 10 SC, ale miał dodatkowo 50 sub-folderów, chcieliśmy pokazać zsumowane wartości). Dla każdego takiego pod-drzewa, wykonywane były sumowania, a następnie węzeł źródła był aktualizowany wg. zsumowanych wartości dla wszystkich folderów.</p><p>Każda taka operacja wrzucała coś do stanu w redux&#x27;ie. Pomyślałem - a na co to komu? A komu to potrzebne? Przecież nie pokazujemy drzewa dopóki wszystko nie jest policzone i zaktualizowane. Dlatego, zmieniłem kod tak, aby najpierw zbudować w pamięci całe drzewo wraz z policzonymi zagregowanymi wartościami, a następnie za pomocą jednej operacji, wsadzić zbudowane drzewo do reduxa.</p><p>Co więcej - w wymaganiach drzewka, było napisane, że pewne węzły miały być domyślnie rozwinięte np. pierwszy poziom + potencjalnie wcześniej zaznaczony element na liście (z listy do drzewka można przejść za pomocą prostego przycisku). Wcześniej operacje rozwijania były wyzwalane za pomocą akcji <code>toggleNode</code>. To też zmieniłem - zamiast odpalać reduxową akcję, po prostu zmieniam wartość <code>expanded</code> na <code>true</code> bezpośrednio w obiekcie węzła.</p><p>Można zapytać - co Ci to dało drogi Panie?</p><p>Dla 54K elementów zjechałem z czasu 12,25s na 2,4s 🚀</p><p>Łogień w szopie :) Product Owner cały w skowronkach.</p><p><img src="https://media.giphy.com/media/ciwIz38tlvDFH08Yuu/giphy.gif" alt="Wow"/></p><h2>Testy dla 1M</h2><p>Poprosiłem backendowców, żeby przygotowali mi środowisko do testów dla 1M elementów. Chciałem sprawdzić czy moje optymalizacje dla 54K znajdą uzasadnienie. No i uśmiech nie uciekł z mej twarzy :)</p><p>Przed optymalizacją czas budowania drzewa wynosił ~3,5 minuty. Po zaaplikowaniu wyżej wymienionych zmian udało się zjechać do 59s.</p><p>Mniej więcej ~70% oszczędności. W sumie można by powiedzieć - job done - miało się budować w mniej niż 60s... 59 to mniej niż 60 😅 Jest git...</p><p>Trochę byłem już zmęczony tym grzebaniem w de-facto w nie swoim kodzie...ale kumpel z zespołu słusznie stwierdził:</p><blockquote><p>No fajnie, fajnie, ale dla mnie nadal to jest wolno.</p></blockquote><p>Dodał też potem, że nic mi nie ujmuje i wg. niego wykonałem kawał dobrej roboty...ale trudno było się z nim nie zgodzić. Od momentu kliknięcia w element nawigacji, do czasu wyświetlenia widoku, użytkownik musiał poczekać łącznie 90s:</p><ul><li>25s pobieranie danych (streaming)</li><li>6s przeglądarka parsuje JSONa</li><li>59s budowanie drzewa</li></ul><p>Jako użytkownik, gdybym przez 90s widział tylko spinner (&quot;kręciołek&quot;) to by mnie szlag trafił :) Nie chcę myśleć, co czuli nasi użytkownicy jak musieli czekać 3,5 minuty...pewnie żaden nie wytrzymał 😅</p><p>...no...ale wracając...kumpel mówi: &quot;wolno&quot;...no to ja od razu: &quot;co!? wolno!? ja Ci pokażę!&quot; 🤣</p><h2>Generowanie ID</h2><p>Znowu zanurkowałem w Profiler. Dla meta-folderów generowny był <code>folderId</code>. Było to spowodowane tym, że inne miejsce w kodzie tego <code>id</code> potrzebowało (mniejsza o to). Koniec końców to generowane id nic nie znaczyło (nigdy nie było wysyłane do backendu). Ktoś jednak wymyślił, że to meta-folder-id ma być haszem ze ścieżki...</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>Funkcja <code>btoa</code> koduje ciąg znaków jako base64. Sama w sobie trwa średnio 0,25ms...czyli ułamek milisekund. Ale gdy mocniej się zastanowimy - a na co komu ten hasz? a na co komu to potrzebne?</p><p><img src="https://media.giphy.com/media/s239QJIh56sRW/giphy.gif" alt="Ale po co?"/></p><p>No właśnie! Jeśli meta-folder-id to tylko base64 ze ścieżki, która de facto zawierała też w sobie <code>id</code> źródła, więc była unikatowa względem całej listy, to po co to w ogóle ten cały 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>Ten jeden diff sprawił, że dla 1M elementów zszedłem z 59s na 35s, co dało ~40% zysku 🤯</p><p>Czyli teraz klient nie czekał już 90s a 66s - łącznie z pobieraniem i parsowaniem danych! Biorąc pod uwagę, że wymagania mówiły o budowaniu drzewka w czasie mniejszym niż 60s, to chyba Product Owner oraz klienci powinni być zadowoleni 😅</p><h2>Dalsze kroki</h2><p>Oczywiście nie spoczywamy na laurach. Blokowanie użytkownika na 60s nadal jest kiepskim pomysłem, dlatego dalej myślimy o ulepszeniu implementacji. Być może wyrzucimy to w końcu do web-workera. Kto wie? Może uda mi się dzięki temu zebrać materiał na następnego posta 😉.</p><h2>Podsumowanie</h2><p>Lekcja pierwsza - zamiast gdybać i sypać pomysłami z kapelusza o web-workerach, lepiej odpalić Profiler.</p><p>Lekcja druga - jeśli operujesz w dużej skali, iterujesz po dużym zbiorze danych, to optymalizacje na poziomie <code>ms</code> dla jednej iteracji potrafią czynić cuda 🚀</p><p>Lekcja trzecia - do reduxa wrzucaj dopiero wtedy, kiedy jesteś gotów 💪</p><p>Lekcja czwarta - jeśli nie ma potrzeby, to nie komplikuj sytuacji 😉 (patrz ID &amp; btoa).</p><p>Mam nadzieję, że dzięki tej historii sięgniecie wcześniej do Profilera i uda się Wam poprawić wydajność nie jednej aplikacji.</p><![CDATA[10 złotych rad dla junior developera]]>https://przemuh.dev/blog/10-golden-advices-for-junior-developerhttps://przemuh.dev/blog/10-golden-advices-for-junior-developerTue, 22 Sep 2020 00:00:00 GMT<p>Czy ten tytuł to clickbait? Oczywiście, że tak. Nie ma na świecie dwóch takich samych osób. To co sprawdziło się u mnie niekoniecznie musi się sprawdzić u Ciebie. Tak prawdę mówiąc, to tytuł tego posta powinien brzmieć &quot;Co chciałbym przekazać <strong>sobie</strong>, gdybym dzisiaj miał zaczynać jako junior developer&quot;. Ale uznałem, że to trochę zbyt długie. Kto wie, może niektóre z poniższych wskazówek przydadzą się Tobie. Czy niektóre z tych rad są kontrowesyjne? Pewnie tak. Ale wszystkie z nich są szczere, i właśnie takie chciałbym usłyszeć gdybym wchodził jeszcze raz w świat IT. Traktruj ten wpis, raczej jako taki list Przemka do Przemka :)</p><p><strong>TL;RD</strong></p><ol><li>nie buduj portfolio</li><li>załóż konto na Twitterze</li><li>uważaj na celebrytów-IT</li><li>nie kupuj książek</li><li>eksperymentuj, baw się kodem</li><li>naucz się świadomie zarządzać czasem</li><li>zainwestuj w umiejętności miękkie</li><li>znajdź mentora</li><li>kup notatnik</li><li>doszlifuj anglielski</li></ol><h2>Nie buduj portfolio</h2><p>Kiedy zaczynałem szukać swojej pierwszej pracy jako programista (2012 r.) nie wiedziałem, że wyląduję na frontendzie. Nie miałem konta na githubie. Nie miałem portfolio. I gdybym dzisiaj miał szukać pracy, to też bym takiego portfolio nie budował. A już na pewno nie wrzucałbym tam aplikacji typu ToDo List, albo Weather App. Nie zrozum mnie źle, pisanie takich aplikacji jest jak najbardziej ok. Dzięki temu uczysz się jak składać działającą całość. Ale wrzucanie tego do portfolio jak dla mnie nie ma sensu.</p><p>No dobra...ale co zamiast portfolio?</p><p>Odpowiedź jest prosta - Open Source. Na githubie jest mnóstwo bibliotek, do których możesz kontrybuować. Wybierz sobie swoją, przeczytaj dokumentację, spróbuj zbudować lokalnie, zobacz jak wygląda lista Issues - z czym ludzie mają problemy. Dzięki temu nauczysz się o wiele więcej niż przy ToDo czy Weather App. Czytanie cudzego kodu to jedna z najważniejszych umiejętności w byciu programistą. Mówiąc o kontrybucji do Open Source, nie mam na myśli np. nowych ficzerów Reacta (chociaż do odważnych świat należy). Czasami jedna linijka kodu potrafi rozwiązać czyjś problem. Ba! Nie musisz wcale kodować. Pisanie dokumentacji to też cegiełka do Open Source. Pierwszy commit Kent C Dodds&#x27;a w ramach Open Source to była literówka. Nie wierzysz? - <a href="https://kentcdodds.com/blog/how-getting-into-open-source-has-been-awesome-for-me" target="_blank" rel="nofollow noopener noreferrer">zobacz ten wpis</a>.</p><p>Podsumowując - jedno Twoje zdanie podczas rekrutacji - &quot;aktywnie udzielam się w świecie Open Source&quot; znaczy więcej niż portfolio z fajerwerkami.</p><h2>Załóż konto na Twitterze, ale ...</h2><p>Znowu - kiedy zaczynałem szukać pierwszej pracy, jedynym portalem typu social media był dla mnie Facebook. Dopiero po czasie zobaczyłem ile mnie omija. Jeśli chcesz być na bieżąco z informacjami dot. technologii to Twitter jest chyba najlepszą opcją. Facebook = rodzina i przyjaciele, sprawy for fun, śmieszki, heheszki, memy, zdjęcia. Twitter = nowinki technologiczne, ciekawi ludzie z branży, motywacja, inspiracje. Dlatego, jeśli jeszcze nie masz konta na Twitterze, to czym prędzej je zakładaj. Poszukaj kilku osób z branży, a potem poszerzaj listę obserwowanych. Nie bój się również usuwać ludzi z listy. To nie są Twoi przyjaciele - nikt się nie obrazi :) Jeśli nie podobają Ci się treści, jakie wrzuca obserwowana przez Ciebie osoba - po prostu przestań ją obserwować, albo wycisz jej tweety.</p><p>Z durgiej strony - nie obrażaj się, jak Twój &quot;idol&quot; wrzuci od czasu do czasu posta niekoniecznie związanego z programowaniem. Koniec końców jest to portal typu social media. Zrzut ekranu ze Spotify jeszcze nikomu krzywdy nie zrobił ;)</p><p>No ale, co z tym &quot;ale&quot;?</p><h2>Uważaj na celebrytów-IT</h2><p><em>pfff</em> &quot;że co!?&quot;. Wierz mi, albo nie, &quot;kiedyś to było&quot;. Może inaczej - kiedyś to NIE - było tylu blogów, tylu informacji, tylu kursów, tylu &quot;mentorów&quot;. Z czasem ludzie podchwycili temat pt. &quot;praca w IT = dużo hajsu&quot;. Zaczął się napływ ludzi do branży, bo to przecież &quot;łatwe pieniądze&quot;. Siedzi się tylko i klepie w klawisze. Jak grzyby po deszczu zaczęły wyskakiwać kolejne szkoły programowania, bootcampy, kursy, mentorzy. Zrobił się z tego niezły biznes. Przecież każdy chce zarobić - co nie? - co w tym złego?</p><p>Nic. Jeśli masz pieniądze to je wydajesz jak chcesz. Gdybym tylko miał sobie dawać w tej kwesti radę, to powiedziałbym &quot;uważaj na celebrytów w IT&quot;. Trochę takich ludzi szufladkuję, sorry. Naczytali się Aniserowicza i Szafrańskiego, jak to można zarabiać na blogu, kursach itp. Sami ledwo co skończyli bootcamp, liznęli pierwszej pracy a już wydają swój autorski kurs - &quot;Programowanie w HTML dla zaawansowanych&quot;. Brzmi jak dowcip? Niestety. Brzmi jak zazdrość - kto wie - być może. Niestety nikt nie uczy tego, jak odfiltrować dobry content od tego skopiowanego i nastawionego na szybki zysk. Czasami nie jesteśmy w stanie zweryfikować doświadczenia danej osoby. Kto wie - może naczytał się pierdół o żabach i teraz na siłę próbuje zainteresować tym innych :) Sam musisz sobie wyrobić czujnik na takie osoby - Przemku.</p><p>Dobra wiadomość jest taka, że tych dobrych &quot;dusz&quot; jest więcej. A takie celebryto-IT-pijawki zdarzają się sporadycznie.</p><p>PS. Nie zrozum mnie źle. Dzielenie się wiedzą na blogu, vlogu (whatever) nawet jeśli dopiero co nauczyłeś się &quot;czegoś&quot; jest SUPER! Ale natychmiastowa próba zarabiania na tym - już nie - przynajmniej nie dla mnie. Gdy płacę za kurs, to chcę mieć pewność, że dana osoba &quot;zęby na tym zjadła&quot;, a nie naczytała się pierdół o żabach ;)</p><h2>Nie kupuj książek</h2><p>A przynajmniej tych o technologiach. A już na pewno nie kupuj ich z myślą &quot;kiedyś przeczytam&quot;. Książki o technologiach szybko się starzeją. Zwłaszcza te o technologiach frontendowych, o frameworkach itp. Jeśli chcesz mieć fajną podstawkę pod monitor - spoko, your choice :) Mam takie dwie cegły pt. JAVA ^^ #naPóźniej.</p><p>Zamiast tego, skup się na książkach ponadczasowych. &quot;Clean Code&quot;, &quot;Clean Coder&quot;, &quot;Pragmatyczny Programista&quot;, &quot;Zawód Programista&quot;, &quot;Refaktoryzacja&quot; ... to są książki, które się nie starzeją. Po takie książki warto sięgać kilkukrotnie w swojej karierze. Za każdym razem wyciągniesz z niej coś innego, będziesz miał inny punkt widzenia, inny poziom doświadczenia.</p><p>I żeby było jasne - nie ma nic złego w książkach o samych technologiach/frameworkach - o ile kupisz aktualną wersję i przeczytasz ją zaraz po zakupie. W innym przypadku - &quot;daj se siana&quot; ;)</p><p>PS. Jeśli możesz - czytaj w oryginale, inaczej mówiąc - uważaj na tłumaczenia. Wiem, że oryginały są znacznie droższe w porównaniu z wydanymi nad Wisłą &quot;tłumaczeniami&quot; - ale warto. Obserwuj takie strony jak <a href="https://www.humblebundle.com/" target="_blank" rel="nofollow noopener noreferrer">HumbleBundle</a>, tam często pojawiają się &quot;paczki&quot; książek np. z wydawnictwa <a href="https://www.oreilly.com/" target="_blank" rel="nofollow noopener noreferrer">O&#x27;Reilly Media</a> i można je dostać za &quot;śmieszne&quot; pieniądze.</p><h2>Eksperymentuj, baw się kodem</h2><p>Często na grupach dla początkujących czytam: &quot;najpierw skup się na podstawach, dopiero potem zajmij się Reaktem&quot;. Ding-dong - Bullshit dector - Ding Dong. Owszem - podstawy pt. zmienne, pętle, funkcje wypadałoby ogarnąć przed frameworkiem. Ale jak już łykniesz podstawowej składni to śmiało wypływaj na głębie. Podczas nauki samego Reakta otrzesz się o funkcje wyższego rzędu, kompozycje, destrukturyzację i to w takiej praktycznej formie. Nie ma sensu czekać!</p><blockquote class="twitter-tweet"><p lang="en" dir="ltr">It&#x27;s fine to learn React while you&#x27;re learning JavaScript. Frameworks can teach you closures, higher order functions, ternaries, etc...</p>— Eric Elliott (@_ericelliott) <a href="https://twitter.com/_ericelliott/status/1269115558479495168?ref_src=twsrc%5Etfw">June 6, 2020</a></blockquote><script async="" src="https://platform.twitter.com/widgets.js" charSet="utf-8"></script><p>Inny wymiar tej rady mówi o tym, że programowanie powinno sprawiać Ci frajdę. Nie bój się napisać &quot;brudnego kodu&quot;, eksperymentuj, baw się. Wykorzystaj różne podejścia. Sprawdź w czym najlepiej się czujesz.</p><p>W programowaniu chodzi o rozwiązywanie problemów. A najpiękniejsze w tym wszystkim jest to, że wiele problemów można rozwiązać na wiele, wiele, wiele różnych sposobów.</p><p>I na koniec - jeśli masz już za sobą ciężki bój przez te &quot;podstawy&quot;, a pisanie każdej kolejnej linijki sprawia Ci ból - to zastanów się czy to na pewno dla Ciebie. Praca programisty nie jest usłana różami, czasami trzeba zanurkować w niemałe szambo i nikt Cię za to po plecach nie poklepie. Z drugiej strony - nie samymi programistami IT stoi. Do branży można wejść na różne sposoby ;)</p><h2>Naucz się świadomie zarządzać czasem</h2><p>Z niecierpliwością czekam, aż mój kolega Radomir, zbierze się w sobie i wygłosi prezentację na temat zarządzania czasem. Sprzedał mi ten temat kiedyś przy jakiejś kawie. Główny przekaz jest mniej więcej taki - jeśli jesteś na studiach to nawet nie wiesz ile masz wolnego czasu. Dopiero (o zgrozo) po czasie dochodzi do nas, ile cennych minut przepaliliśmy. I żebyśmy się dobrze zrozumieli - nie ma nic złego w naparzaniu po nocach w Counter Strike&#x27;a (gra się jeszcze w to?). Nie ma nic złego w imprezowaniu. Jest taki okres w życiu człowieka - studia - gdzie dopiero poznaje się &quot;co to życie&quot;. Korzystaj póki możesz :) Ale pamiętaj - już nigdy nie będziesz miał tyle czasu co teraz. Każdy z nas ma tyle samo czasu. Doba ma 24 godziny. Różnimy się tym, jak ten czas wykorzystujemy. Naucz się swoich nawyków. Swojego organizmu. Sprawdź kiedy jesteś najbardziej produktywny, kiedy najlepiej się uczysz. Wykorzystuj to. Naucz się świadomie zarządzać czasem.</p><p>Ostatnio na Twitterze napisałem, że praca programisty to nieustanna nauka. Tak jest. Na tę naukę też trzeba umieć znaleźć czas. Chcesz prowadzić bloga? Sprawdź ile czasu zajmuje napisanie posta. Kanał na Youtube? Kurs gita, czy babela nagrywam już chyba od lutego :) To nie jest proste. Na wszystko trzeba znaleźć ten cholerny czas. Dlatego Przemku - naucz się świadomie zarządzać czasem.</p><h2>Zainwesuj w umiejętności miękkie</h2><p>Do pierwszej pracy dostałem się na staż. To nawet nie było stanowisko juniorskie. Kiedyś staż kojarzył mi się z parzeniem kawy i wpinaniu kartek do segregatora (tak tak, i jeszcze za to Unia płaciła...ale ciiiiii 🙊). Ale w Samsungu było inaczej. Konkretny projekt, konkretni ludzie, ogrom wiedzy. Dacie wiarę, że startowałem do zespołu C++ (myślałem, że jak kodowałem w tym języku na studiach to się uda) a dostałem się do zespołu SmartTV, w którym pisaliśmy we frontendowych technologiach? W życiu bym nie powiedział. Dla mnie JS kojarzył się tylko z jQuery i śnieżynkami na stronach. No ale nie o tym chciałem pisać.</p><p>Byłem zielony - to fakt. Szybko musiałem nadrobić wiedzę. Udało mi się to dzięki wspaniałym ludziom, z którymi miałem okazję pracować - ale o tym będzie następna rada. Pomimo braku w wielu kwestiach techniczncyh - dosyć szybko awansowałem. Dostałem też możliwość poprowadzenia małego zespołu. Mój szef coś we mnie dostrzegł. Od liceum bardzo lubiłem nawiązywać nowe kontakty. Uważałem się za duszę towarzystwa. Tu coś zagrać na gitarce, tu zagadać, tu się pośmiać. Spotkałem świetnych ludzi na swojej drodze. Byłem szczery, życzliwy, pracowity (teraz też jestem, żeby nie było 😜). Z perspektywy czasu widzę, że to właśnie relacje jakie budowałem z ludźmi pozwoliły mi tak szybko awansować, tak szybko stanąć na czele zespołu.</p><p>Bardzo często nie doceniamy umiejętności miękkich w IT (chociaż zauważam zmieniający się trend). Zdradzę Ci teraz pewien sekret - Przemku - pisanie kodu to nie wszystko. Ba! Umiejętna komunikacja i zdolność do budowania relacji znaczą o wiele więcej niż klepanie kodu.</p><p>Dlatego - zainwestuj w umiejętności miękkie. Sprawdź jak dobrze się komunikować - bo to wcale nie jest takie łatwe.</p><h2>Znajdź mentora</h2><p>W poprzednim punkcie pisałem o świetnych ludziach, z którymi miałem przyjemność współpracować. Szymon K., Grzegorz D., Bartek K., Maciej W. (trochę jak z kryminału :D...no ale nie wiem czy chcieliby się znaleźć tu z nazwiska więc dla pewności zostawiam tak :))...wiele by wymieniać. Wszysktich ich łączy jedno - dążyłem do tego, by być na zbliżonym do nich poziomie. Nie porównywałem się, bo to nie ma sensu. Każdy ma swoją drogę, swoje przeżycia, doświadczenia. Ale możemy się inspirować, czerpać wiedzę, pytać o przeżycia i bazować na doświadzczeniu innych. Dzięki temu zwiększałem prędkość nauki i wchodzenia na wyższy level. Dzisiaj nie wyobrażam sobie pracy w środowisku, w którym nie mogę uczyć się od lepszych od siebie. Zawsze szukam takiej osoby i staram się inspirować.</p><p>PS. Pamiętaj, że to może działać w dwie strony. U kogoś widzisz rzecz, której Tobie brakuje...za to ta druga strona widzi w Tobie rzeczy, które ją inspirują do działania. Dzięki temu wzajemnie się napędzacie.</p><h2>Kup notatnik</h2><p>Wszyscy mówią - załóż bloga, załóż kanał na YT. A ja mówię - kup notatnik. Taki zwykły zeszycik A5 może być. Zapisuj tam skrzętnie to, czego się nauczyłeś. Czytając książki, bądź oglądając kursy - rób notatki. Spisuj swoje osiągnięcia. Zapisuj przemyślenia. Rysuj mapy myśli. To pomaga - serio. Pomaga - ale z czasem. Np. podczas rozmowy o podwyżkę. Takie rozmowy często odbywają się raz do roku. Czy jesteś w stanie z pamięci wyrecytować wszystkie swoje osiągnięcia. Podpowiem Ci - NIE!. Czasami robimy małą rzecz dla siebie, ale wielką dla projektu. Zapisując swoje wszystkie sukcesy i porażki jesteś w stanie popłynąć na takiej ocenie rocznej i wywalczyć upragnioną podwyżkę. Ale nie rób notatek dla pieniędzy :) rób je dla siebie i dla swojego mózgu, który nie zawsze jest w stanie wszystko spamiętać ;)</p><p>PS. Bloga też sobie załóż jak chcesz - dziel się tam swoją nowo-zdobytą wiedzą i doświadczeniem. Tylko proszę - nie nastawiaj się od początku na &quot;zarabianie z bloga&quot;. Nie każ mi akceptować powiadomień i zapisywać się do newslettera :) Na początku bloga prowadź go dla siebie. Sława i splendor przyjdą z czasem....albo i nie :P</p><h2>Doszlifuj angielski</h2><p>W liceum nie byłem jakoś specjalnie zainteresowany angielskim. Tu trójeczka, tam czwóreczka, tu coś ściągnąć, tam zagadać i &quot;jakoś to będzie&quot;. Szybko tego pożałowałem. I choć nie miałem problemu w zrozumieniu słowa pisanego (dokumentacja, książki, maile) to szybko zarobiłem &quot;plaskacza&quot; w momencie, kiedy miałem poprowadzić spotkanie czy prezentację po angielsku. Mówi się, że podstawowym językiem programisty jest język angielski - to prawda. I chociaż coraz więcej publikacji powstaje w języku polskim, tłumaczone są dokumentacje (patrz React) to nadal - posługiwanie się językiem angielskim to podstawa. Bardzo dużo dają tzw. English-Days. W Suncrapers, w którym miałem okazję pracować, był taki jeden dzień - chyba czwartek z tego co pamiętam. W czwartki rozmawiało się tylko po angielsku. To nic, że w projekcie sami polacy. Trzeba było tłumaczyć i szprechać po inglishu. Wiadomo - dla ludzi, którzy dużo się oczytali, ale mało mówili to był problem. Wytłumaczenie prostej rzeczy zajmowało x2, x3 albo x4 tyle co po polsku. Z drugiej strony dzięki takim zabiegom później było łatwiej :)</p><p>Dzisiaj nie mam już problemu z posługiwaniem się językiem angielskim. Cały czas pracuję nad wzbogacaniem słownictwa (aktualnie trenuję z dziećmi Ba-Ba-Black Sheep ;))</p><p>Dlatego Przemku, jeśli to czytasz i chcesz wejść do IT - to wiedz, że bez angielskiego - ani rusz!</p><h2>Podsumowanie</h2><p>I to by było na tyle. Takie rady dałbym <strong>sobie</strong> dzisiaj, gdybym miał zaczynać jeszcze raz w IT. Banalne? Kontrowesyjne? Nudne? Dajcie znać w komentarzach ;)</p><![CDATA[Uwaga na fixtury w cypress.io]]>https://przemuh.dev/blog/watch-out-for-fixtures-in-cypresshttps://przemuh.dev/blog/watch-out-for-fixtures-in-cypressFri, 26 Jun 2020 00:00:00 GMT<p>Dzisiaj opowiem wam krótką historię o błędzie, który kosztował mnie dwa dni poszukiwań. Błędzie, który koniec końców okazał się czymś bardzo trywialnym, a czas który spędziłem na debugowaniu spowodowany był niedokładnym komunikatem o błędzie.</p><h2>Hej Przemek! Czy mógłbyś mi pomóc?</h2><p>Kilka dni temu, zauważyłem, że nasze testy VRT (Visual Regression Tests) zaczęły się sypać dla jednego przypadku. Poprosiłem moją koleżankę z zespołu, Monikę, aby rzuciła na to okiem. Monika przyjęła wyzwanie i bezzwłocznie zaczęła szukać rozwiązania. Po całym dniu bezowocnych poszukiwań powiedziała, że nie ma pojęcia dlaczego to nie działa. Lokalnie test przechodził za każdym razem, niestety na naszym GitlabCI było zupełnie odwrotnie. Dziwna sprawa co nie? Jak to jest, że &quot;u mnie działa&quot; a na CI już nie? Zrezygnowana Monika poprosiła mnie o pomoc. Po dwóch dniach kombinowania, commitowania, wypychania, czekania i sprawdzania w końcu się udało.</p><h2>Fake server</h2><p>W naszej aplikacji do testów wykorzystujemy różne narzędzia. Do unit testów mamy <a href="https://jestjs.io/" target="_blank" rel="nofollow noopener noreferrer">jest</a>. Do testów E2E mamy <a href="https://docs.pytest.org/en/stable/" target="_blank" rel="nofollow noopener noreferrer">py.testa</a> z bindigami do webdrivera. Mamy też testy UI, które sprawdzają naszą aplikację pod kątem integracji pomiędzy komponentami, stronami czy widokami. Niedawno wprowadziliśmy także VRT - wizualne testy regresyjne. Te dwa ostatnie rodzaje testów wykorzystują <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. Jest to świetne narzędzie do pisania wszelakich testów - od unitów do E2E.</p><p>Nasza aplikacja od strony backendowej jest bardzo skomplikowana i ciężko ją postawić w całości na lokalnym komputerze, a nawet jeśli jest to możliwe - kosztuje to wiele pracy i zasobów komputera. Dlatego do testów UI i VRT wykorzystujemy jeden z killer-featureów Cypressa, który pozwala mockować zapytania do API. Cypress wpina się pomiędzy aplikację i żądanie do serwera, a my możemy zdecydować o tym jaką odpowiedź dostanie nasza aplikacja.</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">// Po pierwsze musimy powiedzieć, że stawiamy 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">// Dalej deklarujemy jakie ścieżki chcemy mockować</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> value<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>Więcej o tym przeczytacie w <a href="https://docs.cypress.io/guides/guides/network-requests.html#Stub-Responses" target="_blank" rel="nofollow noopener noreferrer">oficjalnej dokumentacji Cypressa</a>.</p><h2>Fixtury</h2><p>Fixtury to kolejna z funkcjonalności <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>, z której korzystamy - szczególnie w testach VRT. Fixtura to nic innego jak pewne dane zapisane w pliku, które możemy wykorzystywać wiele razy. Pomaga to w organizacji testów i zarządzaniu odpowiedziami z naszego <code>cy.route</code>. Żeby załadować fixturę korzystamy z komendy <code>cy.fixture</code>. Jako argument przyjmuje ona relatywną ścieżkę do pliku, znajdującego się w katalogu z naszymi fixturami. Domyślnie katalog na fixtury nazywa się po prostu <code>fixtures</code>, a same pliki z fixturami mogą mieć różne rozszerzenia. Nie muszą też być używane w kontekście <code>cy.route</code>. Więcej o fixturach znajdziecie w <a href="https://docs.cypress.io/api/commands/fixture.html#Syntax" target="_blank" rel="nofollow noopener noreferrer">oficjalnej dokumentacji cypressa</a>. </p><p>Załóżmy, że mamy następującą strukturę plików:</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>Kod, który wykorzystuje fixtury:</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">// Nie musimy deklarować rozszerzenia pliku</span> <span class="token comment">// Cypress spróbuje sam je odgadnąć</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">// Tutaj możemy odczytać dane</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token comment">// Możemy zapisać dane w postaci aliasu ...</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">// Żeby móc go wykorzystać przy mockowaniu network requesta</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>Twórcy cypressa zadbali też o to, żebyśmy nie musieli pisać tak dużo boilerplateu 🔥🔥🔥. Do komendy <code>cy.route</code> jako parametr reprezentujący odpowiedź z serwera możemy podać skrót do fixtury <code>fixture</code> albo <code>fx</code></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>W ten sposób zamockowaliśmy sobie odpowiedzi z serwera, a dane trzymane są w re-używalnych plikach z fixturami. Rewelacja!</p><h2>Gdzie się podział główny bohater opowieści?</h2><p>No dobra, ale gdzie ten błąd, z którym walczyliśmy dwa dni?</p><p>Na potrzeby reprodukcji utworzyłem bardzo prostą aplikację. Na początku wyświetla ona napis <code>Loading…</code>, wykonuje zapytanie do serwera, a następnie podmienia tekst na to, co zwrócił backend.</p><p>Pobieranie danych w starym, dobrym stylu XHR 😎</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">=</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>Do tego napisałem 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>Oraz utworzyłem plik z fixturą w pliku <code>fixtures/exampleFixture.json</code> z taką zawartością:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Hello</code></pre></div><p>Czy widzisz już gdzie leży błąd?</p><p>W odnalezieniu przyczyny problemu pomógł mi zrzut ekranu, który cypress wykonuje w momencie kiedy test nie przejdzie. Kolejny killer feature 🔥.</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.25%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAACN0lEQVQoz22SXU/TYBiG+VXCgMFW1nbA9A8YQYmRA6Iy2JgmfhxwQgiHgBxs3UKi8Zd4akKCUT7GBDrGhHVdP1YYePm2AhtokytP3pMrz93n7jIaLsWjU/YrVQ4Eh9VTyhWdsl4im82Sy+VIJpMMRCJIsooUkwNiiooSj9MXHuT5i5fk83mWl1foatVN7I1vuJtfaW5+wdvaoFUqYR78JKtpaIJUKoUsywyPJBgRjI7eJ5F4IGaCqBRjZmaWQqHA6uoqXWcNB2Nnn3pRxzo8wT6u4wiMykmwnS9Mp9MoikJMbBjq7Q/oCfUxFFOIRIdEgpkOoWFS/17E3NqjUdzH1o9xBIZeIesLcxpzc3NIksTY+BO0wjpafp2Pnz4zMfGUvv6B2xteisiXO0UutnfF3KX1Yxt2i2LL6o0wk8kQDofJvHpN5/f23XvudYeYTaXbwgvLxiz/wjyu0bjCrlmYNYOcOIof+a7w95XwzX+FbhPXdjFsD8NyqVsODUdMoy28jvxo7DE5rRDwQfBwfIL+fyK7Lpeug930sGxbyGws174lbB9Fobunl+5QLz1ixq6OMt15lJaIbOhlatUqXsPiXEjPHQfz5DToYacwPjwiqjN6g/8ejESFMNkWepZFvXwk/l2VM0NURohcgb5XYm1tLajOtVCNDyP7hVbjAf47EE5PB8VeWRHFdkTkpufhCpymi/8+a7U41HUWFhZYXFxkamoKVVVvCWXlrzASlXg2OcnS0hLz8/P8AXOxcxI9R2CUAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Zrzut ekranu dla testu" title="Zrzut ekranu dla testu" 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">Zrzut ekranu dla testu</figcaption> </figure></p><p>Czy teraz domyślasz się gdzie był błąd?</p><p>Moją uwagę przykuł komunikat o tym, że route został co prawda zestubowany, ale odpowiedział kodem <code>400</code>, a nie <code>200</code>, co spowodowało błąd w kolejnej komendzie oczekującej na element z tekstem &quot;FolderA&quot;. Przypominam, że &quot;lokalnie&quot; ten test nie miał z tym problemu 😉.</p><h2>Literówka i systemy plików</h2><p>Nasz błąd, który próbowaliśmy rozwiązać z Moniką, polegał na banalnej literówce. Nazwa pliku z fixturą zapisana była camelCase, natomiast w kodzie testu mieliśmy wszystko małymi literami.</p><p><code>exampleFixture.json</code> vs <code>cy.route(&quot;/api&quot;, &quot;fixture:examplefixture&quot;)</code></p><p>No ok, ale dlaczego to działało lokalnie, a nie działało na CI?</p><p>99% naszego zespołu frontendowego pracuje na MacBookach podczas gdy GitlabCi odpala testy w kontenerze dockerowym, który opiera się na Linuxie. Co to ma wspólnego z fixturami i naszą literówką? Otóż system plików używany domyślnie w Linuxie jest caseSensitive (zwraca uwagę na wielkość liter w nazwach plików). MacOS oraz Windows domyślnie opierają się o system plików, który caseSensitive nie jest. Co to oznacza w praktyce?</p><p>Na Linuxie możemy utworzyć pliki o tej samej nazwie, ale pisane nieco inaczej np.</p><ul><li>myAwesomeFile.js</li><li>myawesomefile.js</li></ul><p>Linux potraktuje je jako dwa osobne pliki. Natomiast Windows i MacOS nie pozwolą na stworzenie drugiego pliku o tej samej nazwie (pisanej np. camelCase). Przy próbie odczytu pliku np. w node.js na MacOS nie ma znaczenia czy wpiszemy myFixture czy mYFiXtURe - plik zostanie załadowany. Na Linuxie natomiast dostaniemy błąd odczytu - plik nie został odnaleziony.</p><h2>Sprawdzam</h2><p>I faktycznie tak jest. Gdy zmodyfikujemy kod naszego testu w ten sposób:</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>Test na Macu przechodzi zawsze. Na Linuxie natomiast, cypress logger pokaże nam stuba z kodem <code>400</code>:</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.9254327563249%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAABBElEQVQoz52R3W6DMAxG8/5XILjlKRBXPEq10VGtLeWvZMQJlPCNqNDSat1glo4sJfaR47AkLXDIKmQZR5Jz5EUFUZ9RFDk45xBCoK5rKKXQti2apnnAnJk7U2dgaVbiY3dEyQm1vECqoam90nUdpuj7/lemYF03SCRB6wt6rYes73ksnDe8iknMiAscNzHSaIdT9Ikk2iPdHnBOigfhUljDvyDftqAovvMeQ50y6BUT3p4siECNAqk5EjSsYdrhKiGNv/OMOZ8LFz+ZxuYbZuJRaoRrZFehETwhRvF/gv0kk1KiqiqEYQjf9xEEwSJM7UthWZbwPA+2bcNxnD9xXReWZeEbYpJEWPqWc/gAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Zrzut ekranu z kodem 400" title="Zrzut ekranu z kodem 400" 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">Zrzut ekranu z kodem 400</figcaption> </figure></p><p>a w konsoli dostaniemy błąd:</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>Ale jak to “błąd nie jest w cypressie tylko w mojej apce”? Przecież apka działa, test na Macu też przechodzi - więc o co kaman?</p><p>Spróbujmy skorzystać z naszej fixtury za pomocą <code>cy.fixture</code> zamiast skrótu:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token comment">// Celowy błąd w nazwie fixtury</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;fixture:examplefixture&quot;</span><span class="token punctuation">)</span> <span class="token comment">// Bonusem takiego podejścia jest możliwość użycia aliasu do odczytu danych</span> <span class="token comment">// zamiast hardcodować &quot;Hello&quot; w teście</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>Jaki teraz dostaniemy błąd?</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>👏 no i taki komunikat błędu jest o wiele lepszy. Od razu wiemy gdzie należy szukać 😎</p><h2>Podsumowanie</h2><p>Z tej historii można wyciągnąć dwa wnioski:</p><ul><li>mała literówka może przysporzyć Ci kilka dni debugowania</li><li>jesteś tak dobry jak komunikaty o błędzie z twojego test runnera 😉</li></ul><p>Myślę, że cypress mógłby zwracać “poprawny” komunikat o błędnej fixturze zamiast <code>CypressError</code>- dlatego zgłosiłem issue, którego <a href="https://github.com/cypress-io/cypress/issues/7818" target="_blank" rel="nofollow noopener noreferrer">status możecie śledzić tutaj</a>.</p><p>Dzięki, że wytrwaliście ze mną w tej historii do końca. Lecę spróbować naprawić issue, które sam zgłosiłem 😉. Może uda mi się dołożyć kolejną cegiełkę do OpenSource i sprawdzić by Cypress, który jest niesamowitym narzędziem, stał się jeszcze lepszy 😁</p><![CDATA[Zatrzymaj czas z cy.clock]]>https://przemuh.dev/blog/stop-the-time-with-cyclockhttps://przemuh.dev/blog/stop-the-time-with-cyclockWed, 22 Apr 2020 17:00:00 GMT<p>Dziś pokażę Wam jak za pomocą jednej komendy zatrzymać czas. Niestety, tylko w testach napisanych w <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io.</a> Jeśli znasz skuteczną metodę na zatrzymanie czasu tak ogólnie - w życiu, to napisz do mnie. Ostatnio krucho u mnie z tym czasem, dlatego każda wskazówka jest na wagę złota. Dobra 🙂 koniec z tym śmieszkowaniem, bierzmy się do roboty.</p><h2>Opis testowanej aplikacji</h2><p>Na początek trzeba mieć co testować. Nasza aplikacja będzie do bólu prosta. Wyświetlamy czas wejścia na stronę oraz licznik, pokazujący ile czasu na niej spędziliśmy.</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>Możemy przejść teraz do napisania testu.</p><h2>Testujemy!</h2><p>Dla naszej aplikacji chcemy napisać prosty test case sprawdzający:</p><ul><li>czy poprawnie wyświetlamy czas wejścia,</li><li>czy licznik zwiększa swoją wartość co sekundę.</li></ul><p>Spróbujmy w ten sposób:</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>Na pierwszy rzut oka kod testu wygląda w miarę ok. Niestety nie przechodzi on weryfikacji 😢</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.08787346221442%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAABxklEQVQoz42S627TQBCF8/4PUZWW8g4gIQFtkFIgKGnTxEnbOHHs3G92fNnr4Xg3CIk/YOnTWY9nxme00/hyeY35ZoP8dIK1FlpraGOcmrP+C6WUy83zHI2vF1cQUiA7HlGVJYqicFpSCyYYFvzPI0SF3W6HRvPiGnXJ7phin2UopUJBSqXdudIGwlhiPPyBQ535/c687ESHzcu3UEJiO3hG/NBHsdoimyZIw4gaI62ZeM2iBPly42Geoz4vNiio2XbPhlfvYHYH6M4T5MdbiA+foTo9lHctyO8dVO8/Qd7do2reQ7Ta0I8BdOsnFGPqtgXdfoB6HMD2higXKzZ8cwOTnaCCF6ghoVM5GEH2R1CjMeSPrudbB6r/DEP3ZhyRKcwrCWeMxTDMLZYr71DvDxDtLkT3yanqBcB8RdYAxwLHwWINGy9gZ3Oy8MRnTVacoIXTJDo7zAuIIZ2NJ1CzBPI1dIhghKofuLgMJ9B0oGYxVJz4vHDqvomXMW91j5KX6hvydiTHlePQF7OZL/CNVDSD5RSWm2APxz/sjz5OkHJD0tTfsq0XeLmGnEaQtC2jGJaLjnoHuTqQEqgqoPyLymPdWaBkzS92HfDmxAi65wAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Błąd przy pierwszej iteracji testu" title="Błąd przy pierwszej iteracji testu" 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">Błąd przy pierwszej iteracji testu</figcaption> </figure></p><p>Gdybyśmy zamiast liczby sprawdzali sformatowaną datę np. 22-04-2020 pewnie nie byłoby problemu. No ale nasz klient zażyczył sobie formę milisekund - #jakŻyć? W takim wypadku sprawa się nieco komplikuje.</p><p>Z pomocą przychodzi nam komenda <code>cy.clock</code>. Nadpisuje ona globalne funkcje zwiazane z czasem i pozwala na kontrolowanie ich poprzez np. <code>cy.tick()</code>.</p><p>Funkcjonalności na które wpływa <code>cy.clock</code> to:</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>Więcej informacji znajdziecie w <a href="https://docs.cypress.io/api/commands/clock.html" target="_blank" rel="nofollow noopener noreferrer">oficjalnej dokumentacji cypress.io</a>.</p><p>Spróbujmy teraz dodać <code>cy.clock</code> do naszego testu:</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>Nadal dostajemy błąd, ale kiedy wczytamy się w jego treść zauważymy różnicę:</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>O co chodzi z tym zerem? Ponieważ czas reprezentowany jako timestamp to liczba sekund od rozpoczęcia epoki Unixa (unix epoch), dlatego wartość <code>0</code> reprezentuje 1 stycznia 1970. Można by zapytać co stanie się w momencie przekręcenia licznika, czyli 19 Stycznia 2038 roku - ale to już rozkmina na inny wpis 🙂.</p><p>Wywołanie komendy <code>cy.clock()</code> bez argumentów ustawia czas dla naszej aplikacji na 1 Stycznia 1970. Dlatego po prawej stronie asercji otrzymaliśmy <code>0</code>. Żeby to zmienić możemy podać porządaną wartość daty jaką chcemy ustawić:</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>Dzięki temu być może magicznym trafem uda nam się zgrać z datą ustawioną w linijce odpowiadającej za asercję <code>should(&quot;have.text&quot;, Date.now().toString());</code>. To zależy jaki szybki mamy komputer 😄. Żeby poprawnie napisać test należy pamiętać o tym, że <code>cy.clock</code> nadpisuje czas w naszej aplikacji a nie w testach. Dlatego wywołanie <code>Date.now()</code> w momencie asercji powinniśmy zamienić na wartość zapisaną wcześniej w clock.</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>Test przechodzi - zawsze! - sukces!. Jest tylko jedna różnica w działaniu aplikacji. Przed wykorzystaniem <code>cy.clock</code> nasz licznik w testach zmieniał się co sekundę. Po wykorzystaniu <code>cy.clock</code> licznik zatrzymał się na <code>0</code>. Akurat dla nas jest to porządany efekt. Nie tylko ustawiliśmy porządaną datę ale też zatrzymaliśmy czas.</p><p>Aby ruszyć czas o porządaną wartość wykorzystamy komendę <code>cy.tick</code>. Jako argument przyjmuje ona ilość milisekund, o które chcemy przesunąć nasz czas.</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>Sukces 🎉! Mamy test sprawdzający czas wejścia oraz działanie licznika.</p><h2>A co jeśli chcielibyśmy ustawić tylko datę? 🤔</h2><p>Bardzo dobre pytanie. Czasami może jednak zdarzyć się tak, że chcemy wpłynąć tylko na obiekt <code>Date</code> pozostawiając w spokoju <code>setTimeout</code> i inne funkcje związane z czasem. W takim wypadku należy do wywołania <code>cy.clock</code> dodać drugi argument - tablicę funkcji czasu, które chcemy nadpisać</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>W tym przypadku ustawiamy datę/godzinę na 22 kwietnia 2020 00:00 UTC (miesiące w <code>Date</code> liczymy od <code>0</code>, dlatego <code>3</code> to Kwiecień 🙂). Przy takim wywołaniu nie wpływamy na pozostałe funkcje czasu takie jak <code>setTimeout</code> itp.</p><hr/><p>To tyle na dziś. Uzbrojeni w tę wiedzę możecie teraz zatrzymać czas w swoich testach 😉</p><p>Powodzenia!</p><![CDATA[3 kroki do niesamowitych raportów w cypress.io]]>https://przemuh.dev/blog/3-steps-to-awesome-test-reports-with-cypresshttps://przemuh.dev/blog/3-steps-to-awesome-test-reports-with-cypressWed, 18 Dec 2019 00:00:00 GMT<p>W tym wpisie nauczymy się jak wygenerować rozbudowane raporty z testów napisanych w cypress.io. Wzbogacimy je także o zrzuty ekranu, co powinno pomóc w znacznie szybszej naprawie potencjalnych bugów 😄 Wszystko czego potrzebujemy to trzy proste kroki.</p><h2>Egnyte + testy = ❤️</h2><p>Utrzymanie możliwie jak najwyższej jakości produktu to jedno z naszych priorytetów w Egnyte. Dlatego kochamy testować 😍. Nasze aplikacje do małych nie należą, dlatego ich ręczne testowanie było by dla nas bardzo wyczerpujące. W związku z tym, automatyzacja testów i technika Continous Integration są naszymi najlepszymi przyjaciółmi. Piszemy wiele rodzajów testów: unit, integration, end-to-end, modułowe itd. Ale to nie nazwy są tu najważniejsze. To co się liczy najbardziej, to pewność, że gdy widzisz zielony pipeline na Jenkinsie, to potencjalnie nie popsułeś żadnej części systemu.</p><p>Więc w czym problem? Wiedzieliście, że testy nie zawsze przechodzą? 😱 I to jest jak najbardziej ok :) (przynajmniej do czasu znalezienia przyczyny). Nie musimy od razu panikować. Po pierwsze, spokój. Tylko spokój może nas uratować. Widzimy czerwony pipeline - wchodzimy na Jenkinsa, sprawdzamy co jest nie tak i fixujemy. To tyle. <strong>Problem polega na tym, że raport z testów na Jenkinsie to bardzo często, zwykły tekst wraz ze zrzutem ze stack-trace.</strong> Taki raport jest jak najbardziej ok dla unit testów albo testów integracyjnych dla naszych komponentów Reactowych, połączeń z Reduxem, itd. Z drugiej strony, taka ściana tekstu nie zawsze jest przydatna dla testów uruchamianych w przeglądarce (full UI). Spójrzmy na fragment raportu dla przykładowego testu:</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.3125%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsSAAALEgHS3X78AAABDUlEQVQoz2VQi3KDIBD0/3+x8YGiBwfFaKNoFNJVO7Y2OzfLzTJ7r8Q5R0Te+xDCuuNIlmVZ3xCuSNq27bruuQOGk89knud38aiVWGvR3PsR8H7aMG84HiiHf/+YzloHEniMMUopIkXakW4lOeJ7rVyjXU2bQtyC4YoxXsZGAWZuiIuSb7lOC05LcysQ/JFzJiyYbT/657KGGF+IE5sZnbVmWatKKlFpIS1KZMKkYuOjImbhz/4xTLjjrxmbwMz75Jq5JlNKjWiUrWpGsL279su63rg+/u0LM9RSci6UkDovVVpQJhSSosIiyliHA4cQ1/Vi+zFjbzTHmXHYYZwRj2Eed8aQYChYGEqI//3fzUwBr8ba6BcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Raport z testów na Jenkinsie" title="Raport z testów na Jenkinsie" 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">Raport z testów na Jenkinsie</figcaption> </figure></p><p>Tak wygląda podstawowy raport z testu na Jenkinsie dla jednego z naszych produktów (<a href="https://www.egnyte.com/protect/content-governance-solution.html" target="_blank" rel="nofollow noopener noreferrer">Egnyte Protect</a>). Do pisania testów integracyjnych-UI, korzystamy z bombowego 💣 narzędzia <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. Muszę przyznać, że Cypress wraz z rozszerzeniem <a href="https://github.com/testing-library/cypress-testing-library" target="_blank" rel="nofollow noopener noreferrer">cypress-testing-library</a> robią wspaniałą robotę jeśli chodzi o tzw. error messages. Na pierwszy rzut oka widać, że cypress nie potrafił znaleźć elementu z zadaną wartością tekstową. <code>Timed out retrying: Expect to find element: findByText(&quot;...&quot;), but never found it</code>. Ok, ale jaki był stan wizualny naszej aplikacji? Jako developer biorący udział w projekcie Egnyte Protect, wiem, że element którego szukamy powinien znajdować się w dialogu. Ale czy dialog został otwarty? A może to tylko literówka? Tyle pytań a tak mało odpowiedzi. Jeśli teraz chcielibyśmy sprawdzić o co biega, musielibyśmy uruchomić testy lokalnie i zobaczyć stan wizualny naszej aplikacji. Dopiero wtedy dowiedzielibyśmy się, że mamy doczynienia (spoiler alert) z literówką :)</p><p>A co by było, gdyby zamiast ściany tekstu, pokazać zrzut ekranu z aplikacji?</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.487179487179496%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsSAAALEgHS3X78AAABYElEQVQoz42Q2U7CQBSG+yosGr0FSmm8o8x0ppoobVktEhCe0bfQxDsiILK0oJUlYLWrDhaIgYjOfHMuTv4vZ2aoY5Y9YtkwwxwwzGEiQWooFgtEo8E1gUgkTNNB0qTpUDxOMhsoxAsICRh9VywIWEhfpCVR8pFlOUO2JOPzixOIIMJ+0leoFEQreMwBKIri7d19t6c12912pz9Qn7Wh/vI61UZ6uVJJcinA441CAYh8IOkCmMlkHvsD0/Fcx/38sSzbrtfrHJcisY2yLZPLqqpK0p67kj3PW8qWVavX/pAlSdK04cb5rwwgBoDP5XK97pNpWfPF24dp2Y5LjuO4hvFeq+2RSRfAfD7f6zT18Yz80GS2WBimabtktG3tffNyMuSz2WzjoTVfGOPJjKCPp9PJtKmNbhqtavWafPZv8hKEhUJRUUrlS+VKWVMoluSCIpyebYW3ZUKSA7twHNhNfgEqfifvvlhbZQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Zrzut ekranu z testowanej aplikacji" title="Zrzut ekranu z testowanej aplikacji" 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">Zrzut ekranu z testowanej aplikacji</figcaption> </figure></p><p>Wow! Teraz wiemy, że dialog został otwarty, i że tekst w nagłówku jest niepoprawny! Dostaliśmy nieoceniony kontekst, potrzebny do zdiagnozowania błędu, od razu w raporcie wzbogaconym o zrzut ekranu.</p><p>Zatem, jak to osiągnąć? Jak dodać zrzuty ekranu do raportu z testów? I w końcu - jak taki raport wygenerować? Jedziemy z koksem!</p><h2>Raport HTML pędzi z pomocą!</h2><p>Cypress bazuje na frameworku <a href="https://mochajs.org/" target="_blank" rel="nofollow noopener noreferrer">mocha.js</a>. To dla nas świetna wiadomość - ponieważ mocha.js to bardzo dojrzały projekt, posiadający mnóstwo rozszerzeń. Rezultaty testów generowane są w mocha.js za pomocą tzw. reporterów. Taki reporter możemy napisać samemu, albo możemy skorzystać już z istniejącego np. <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a>. Jak sama nazwa wskazuje, generuje on <strong>AWESOME</strong> raporty! Badum tsss.</p><p>Zobaczmy teraz, jak możemy zintegrować <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a> z <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress</a> w celu wygenerowania raportu HTML wraz ze zrzutem ekranu dla testów zakończonych błędem. Aby zwizualizować wszystkie zmiany, które wprowadziłem na potrzeby tego wpisu, skorzystałem z przykładowego repozytorium <a href="https://github.com/cypress-io/cypress-example-kitchensink" target="_blank" rel="nofollow noopener noreferrer">cypress-example-kitchensink</a>. Integrację przeprowadzimy w trzech prostych krokach. Do dzieła!</p><h2>Krok 1 - setup reportera</h2><p>Po pierwsze, musimy zainstalować odpowiednie reportery. Tak! Dokładnie - liczba mnoga - reportery. Oprócz generowania raportów HTML, nadal chcielibyśmy wyświetlać wyniki w konsoli, a być może także w specjalnym formacie JUnit XML. Dla każdego, z tego typu wyników, musimy mieć osobny reporter. W tym celu skorzystamy z paczki <a href="https://www.npmjs.com/package/cypress-multi-reporters" target="_blank" rel="nofollow noopener noreferrer">cypress-multi-reporters</a>, która umożliwia skorzystanie z wielu reporterów dla cypress.io. Oprócz tego musimy zainstalować jeszcze pakiet <code>mocha</code>, oraz oczywiście <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>Lub jeśli korzystacie z <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> -D mocha cypress-multi-reporters mochawesome</code></pre></div><p>Następnie w pliku konfiguracyjnym <code>cypress.config</code>, musimy wskazać reporter, z którego będziemy korzystać:</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>Pole <code>configFile</code> wskazuje na plik konfiguracyjny <code>reporter-config.json</code>, który zawiera opcje dla poszczególnych reporterów. Plik ten powinniśmy dodać do repozytorium. Zobaczmy teraz jak wygląda jego zawartość:</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>Ustawiamy folder docelowy, do którego trafią nasze wyniki. Chcemy, aby zostały one zapisane w formacie JSON, osobno dla każdego pliku z testami. Dlatego ustawiamy flagę <code>html</code> na wartość <code>false</code>. Cypress potrafi uruchamiać testy równolegle, dlatego aby nie nadpisać wygenerowanych już wyników, ustawiamy flagę <code>overwrite</code> na <code>false</code>.</p><p>Spróbujemy teraz uruchomić nasze testy za pomocą komendy <code>npm run local:run</code>.</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>Jak widzicie, w konsoli dostaliśy wyniki dla poszczególnych testów (domyślny spec-reporter). Zaraz pod nimi widzimy informację na temat utworzonego pliku <code>mochawesome_008.json</code>. Każdy plik z testami wygeneruje nam osobny plik z wynikami.</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:142.0245398773006%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAcCAIAAADuuAg3AAAACXBIWXMAAAsSAAALEgHS3X78AAAB90lEQVQ4y5WU227TQBCGc1PUCiHuUGkpEHsTJz7Eu7PrdcINRa3UCNRKXPEMiEtS1Mu+QHlmfnuSSULSrjsarSaHz/+cvL2XX/+8+nb77sd9/euv+/nw+vruxdXvw/niCT+aLw6uFm9ubntn7uJ9dalmc33xPf18fUpf3urzE9Oc7Ih3/bg8H36a98aDKB2qIh04M5lk40T1RypqvY+f4AiSuD+MPmy6+nimi7SXJKMkGWudkM0MuaqqfF27ylvr+Ky8z/I8jpVSA/EoistSA4aNAFubE1kYeJzUfnDOGYOUJnFLi0VRVJblEjYGcEaEPzfizOCs6xqPCMArZSdG7QfOYi+ML9cwUW4MSdpQ5hhP6ahsuU6p2XuPoCiKXTjP8yVclksYIADmWRnxrjLbVsM4bTatNbDpdLpXeQtG2s4VzWRXxm3jzPNmzk/CxmRaGyJ4Y1DmtNG5MMyjYikeFQzx3lFtwWgY0bJmzlZGhTNcM5Q3YVbmOMuycLdlPXk32BCnadpFWSZlBcbjOsHtkjhZEl4yKAe6LQ2TOk1rzD9DGa+hKHMLAsqr9bT/jYr3LNBtXs+2wGZD5TLhFoQ3DDVrTVwkTi6Ys+hY8/oa2sw8AMv7LO+wiHdSXu32Om2+A8M1o9veFx5Xdl3PZjPcAdw5PEg9bv8A9dAma9wEJYgAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Lista wygenerowanych plików z wynikami testów" title="Lista wygenerowanych plików z wynikami testów" 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">Lista wygenerowanych plików z wynikami testów</figcaption> </figure></p><p>Jesteśmy gotowi do implementacji następnego kroku.</p><h2>Krok 2 - generowanie raportu</h2><p>Zebraliśmy pliki z wynikami testów. Teraz, musimy połączyć je w jeden plik i na jego podstawie wygenerować raport HTML. W tym celu skorzysamy z narzędzia <a href="https://www.npmjs.com/package/mochawesome-merge" target="_blank" rel="nofollow noopener noreferrer">mochawesome-merge</a>. Zainstalujmy je!</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> -D mochawesome-merge</code></pre></div><p>Teraz, dodajmy skrypt npm, który będzie odpowiedzialny za uruchamianie narzędzia do łączenia wyników.</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>Flaga <code>reportDir</code> mówi o tym, w jakim folderze znajdują się pliki do połączenia. Wynik łączenia wyrzucony będzie na standardowe wyjście (w naszym przypadku będzie to konsola) dlatego, przekierowujemy wyjście do pliku <code>mochawesome-bundle.json</code>. Jedna uwaga: wynik łączenia powinien znajdować się w innym folderze niż pliki z poszczególnymi wynikami testów. W przeciwnym wypadku dostaniemy błąd.</p><p>Po zmergowaniu wyników jesteśmy gotowi do wygenerowania raportu HTML. Potrzebujemy do tego jeszcze jednej paczki <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> -D mochawesome-report-generator</code></pre></div><p>Tak jak w przypadku łączenia, utworzymy sobie teraz skrypt, który będzie uruchamiał narzędzie do generowania raportu HTML:</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>Słówko marge to skrót od <strong>M</strong>och<strong>a</strong>wesome<strong>R</strong>eport<strong>GE</strong>nerator - to tak w ramach gdybyście się zastanawiali :)</p><p>Po poprawym wykonaniu skryptu, nasz raport HTML powinien pojawić się w katalogu <code>cypress/results/html</code>.</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.6875%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsSAAALEgHS3X78AAAA80lEQVQoz5WR227CMAyG+/IDaRcUuoNE+niM0m6Q2M45abd5RWJotEL7ZOUi8qf8dorHl9e1qFdbsdhUD+vNonpaVs/Ln3OiuLkUotyKsq4rIYqcU0ohpxhz0J6+7pFS5H6umGIBgGcUwOHUdvL9qI6oUTtDlqyzbiSOBO/xAmBBFzQ13f7tY8eyQvDehxjiFZyQT7riV0YiCScyZJzxwecppmVNWqJssbOGc9rPGTj2rNzIhiex1g7DcGvylbdWz8l72RAQ76Lv+0k5ODcfG1qOzTKPx7/yj9gKVTfOjIS82Ek5hjD78kG1hjQAnMe+5U/sb8tlYyeUKfRVAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Widok raportu HTML" title="Widok raportu HTML" 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">Widok raportu HTML</figcaption> </figure></p><p>Została tylko jedna, mała rzecz. Dodanie zrzutu ekranu do wyniku testu zakończonego błędem.</p><h2>Krok 3 - zrzut ekranu</h2><p>Cypress automatycznie generuje zrzuty ekranu dla testów, które z jakiegoś powodu nie przeszły. Jest to domyślne zachowanie, które można wyłączyć. Wygenerowane obrazki zbierane są w folderach, które przyjmują następujacą strukturę:</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>Rozważmy sobie test, który znajduje się w katalogu <code>examples/actions.spec.js</code>:</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>Wygeneruje on zrzut ekranu, który znajdzie się w następującej strukturze folderów:</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.424242424242426%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAIAAAAcOLh5AAAACXBIWXMAAAsSAAALEgHS3X78AAAAVklEQVQI15WOUQrAMAhDe5Y6LLaKrNr7X20ZZfvbx95HEDGJZYwx51xrmRkRHX8orTV3jwikMDN9U2t9ddfc5tPRnPBnpqoipfcuIhi2Auzxmj7gAOYL0ZwcBDCNqUcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Struktura folderów ze zrzutem ekranu" title="Struktura folderów ze zrzutem ekranu" 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">Struktura folderów ze zrzutem ekranu</figcaption> </figure></p><p>Ok, to jak połączyć te dwa elementy: zrzut ekranu wygenerowany przez Cypress i wynik testu wygenerowany przez mochawesome reporter?</p><p>Po pierwsze, skopiujmy sobie nasze zrzuty ekranu do folderu, w którym trzymamy nasz raport HTML. W tym celu napiszemy sobie kolejny skrypt npm:</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>Następnie, w pliku <code>cypress/support/index.js</code>, napiszmy kawałek kodu odpowiedzialny za nasłuchiwanie na event <code>test:after:run</code></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>Aby dodać zrzut ekranu do wyniku testu, musimy skorzystać z metody <code>addContext</code> z pakietu <code>mochawesome</code>. Metoda ta, przyjmuje dwa argumenty: obiekt z testem oraz tzw. context. Jeśli context jest poprawnym adresem URL (może być lokalną ścieżką) do obrazka, wtedy obrazek ten będzie wyświetlany pod wynikiem testu. Oczywiście to nie musi być tylko obrazek. Po więcej info odsyłam do <a href="https://www.npmjs.com/package/mochawesome#adding-test-context" target="_blank" rel="nofollow noopener noreferrer">dokumentacji</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>Wszystko spoko, ale skąd mamy wziąć ten <code>imageUrl</code> ? ...</p><p>Pora na odrobinę magii</p><p><img src="https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif" style="width:50%;margin:auto;display:block"/></p><p>Żartuję :) Skorzystamy z API jakie daje nam mocha, a dokładniej z tzw. runnable object. Jak widzieliśmy wcześniej, Cypress generuje nazwy obrazków na podstawie struktury testu. Musimy to teraz odtworzyć:</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>Od tej chwili, jeśli nasz test zawiedzie, w naszym pliku wynikowym zostanie doklejony context z adresem do wygenerowanego obrazka:</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>Co więcej - obrazek ten zostanie dodany do raportu HTML.</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.49999999999999%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAABuklEQVQoz41S21LCMBTs/z+rI6CgUvSZYQb+Qn1BcBS8l0tL0iZtAq1kPUmRQeXBzOxs0p5s9ly8arOJ+tUVjs/PcXBaJ5zisFLBYb2O6kUTJ34LNd9HjeIqdK42fYejxpmLP2o0UGu1zFnrEv6Frz0YA6zXcGyX5eUSeZY5FEqj0LqM+b1srMPalEejPcY5OMEyY3bPEM3nGI2f8Tgab/H2/gEex4iTBGmaOjDGynucm5j+kY72EgrYQiSQiQCLQkyiKSIWkQCJyBhCCmTkWJHb1WoFTWzvCCGIhZFSWl569oPDRlTSPpxN8BEFYAknAYW8yEvkJYqi+CWYGPtgFC3MT8ENC0olzVIqpXYXFdWxdJZDU30tMqW+xUojUpowDLFXUJF9LiRkplzN3t6nmIcLSlnZtCh9SQ9mu4IuZRLc7zCTKUaTKcYBCS0YgsmcBJmrIXXSYUkud1LGRnC/Q81jJA9PEPcjCHKmVwXWnz/Hxtbx34KKWNI4GGpAOJvh+uYWd3cDDIdDDAYD9Pt9BEHgRmenhn8F3Ws0T5oaYptg18vrKzqdDnq9HrrdruN2u+1EbcPs/O0KfgFmmzLlPg5ZHQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Raport HTML wraz ze zrzutem ekranu" title="Raport HTML wraz ze zrzutem ekranu" 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">Raport HTML wraz ze zrzutem ekranu</figcaption> </figure></p><p>TADA 🎉 Mamy to!</p><p>Wszystkie zmiany, których dokonaliśmy, możecie zobaczyć w jednym miejscu: <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>Kroki opcjonalne</h2><p>Dobrym pomysłem jest dodanie do <code>.gitignore</code> folderów <code>cypress/results</code> i <code>cypress/reports</code>.</p><p>Fajnie było by też &quot;czyścić&quot; foldery z generowanymi assetami (wyniki, zrzuty, itp.). Jak to zrobić? Zgodnie z &quot;tradycją&quot; dodamy teraz skrypt npm odpowiedzialny za tę akcję:</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>Te trzy literki &quot;pre&quot;, zanczą ni mniej ni więcej, jak to, że skrypt ten odpalony będzie przed wywołaniem skrypu <code>cy:run</code>. Więcej info w <a href="https://docs.npmjs.com/misc/scripts" target="_blank" rel="nofollow noopener noreferrer">dokumentacji npma</a>.</p><p>W użytym, przykładowym repo, zainstalowany został pakiet <a href="https://www.npmjs.com/package/npm-run-all" target="_blank" rel="nofollow noopener noreferrer">npm-run-all</a>. Możemy z niego skorzystać aby uruchomić wszystkie, stworzone do tej pory skrypty w zadanej kolejności (sekwencyjnie):</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>Pozostała jeszcze jedna sprawa do rozważenia. W większości systemów operacyjnych, nazwa pliku ograniczona jest do 255 znaków. Co się stanie jeśli zagnieździmy nasz test wielopoziomo? To proste! Zostanie przycięty :) Cypress sam przycina nazwy wygenerowanych obrazków do 220 znaków. Powinniśmy to odzwierciedlić w naszym kodzie:</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>Niestety jest to tzw. szczegół implementacyjny. Nie wiemy czy programiści cypressa zmienią kiedyś ten numer. Dlatego, lepszą opcją będzie nie zagnieżdżanie testów na tak głębokich poziomach. Polecam lekturę posta od Kent C Doddsa - <a href="https://kentcdodds.com/blog/avoid-nesting-when-youre-testing" target="_blank" rel="nofollow noopener noreferrer">Avoiding nesting when you are testing</a>, w którym pokazuje, do jakich problemów może prowadzić zagnieżdżanie w testach.</p><h2>Podsumowanie</h2><p>Mam nadzieję, że ten artykuł pomógł Wam w poprawnym skonfigurowaniu środowiska pod generowanie raportów HTML z cypressa. Tego typu raporty bardzo pomagają nam, w Egnyte, w szybkim sprawdzeniu gdzie leży źródło problemu - wierzę, że pomogą i Wam :)</p><p>Podsumujmy nasze 3 proste kroki:</p><ol><li>Instalacja i konfiguracja reporterów</li><li>Zebranie wyników i wygenerowanie raportu HTML</li><li>Dekoracja raportu kontekstem - zrzutem ekranu - za pomocą metody <code>addContext</code>.</li></ol><p>Wszystkie niezbędne zmiany, możecie zobaczyć w przystępnej formie pull requesta: <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>. Oczywiście po wygenerowaniu raportu musicie jeszcze go podpiąć pod narzędzie CI. Ale to już historia na osobnego posta. :)</p><p>To jak? Jesteście gotowi na stworzenie swojego raportu HTML z cypress.io?</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 [PL] ]]>
</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>Fri, 01 Jan 2021 13:11:26 GMT</lastBuildDate>
<item>
<title>
<![CDATA[ Krótka historia o optymalizacji ]]>
</title>
<description>
<![CDATA[ Jakiś czas temu na Twitterze zamieściłem zrzut ekranu pokazujący flame-chart z narzędzia Profiler. Pracowałem wtedy nad poprawą wydajności aplikacji, którą rozwijamy w Egnyte. Pewna funkcjonalność… ]]>
</description>
<link>https://przemuh.dev/blog/tree-performance-improvement-case-study</link>
<guid isPermaLink="false">https://przemuh.dev/blog/tree-performance-improvement-case-study</guid>
<pubDate>Sun, 08 Nov 2020 00:00:00 GMT</pubDate>
<content:encoded><p>Jakiś czas temu na <a href="https://twitter.com/przemuh/status/1319595759935852544" target="_blank" rel="nofollow noopener noreferrer">Twitterze zamieściłem zrzut ekranu</a> pokazujący flame-chart z narzędzia Profiler. Pracowałem wtedy nad poprawą wydajności aplikacji, którą rozwijamy w Egnyte. Pewna funkcjonalność, dla dużej ilości danych, zajmowała strasznie dużo czasu - 3,5 minuty! Przez ten czas w aplikacji pokazywany był &quot;kręciołek&quot;, a użytkownik nie wiedział, czy coś się dzieje, czy może coś się zawiesiło. Po kilku dniach pracy z Profilerem udało mi się zaimplementować poprawki, które zredukowały czas potrzebny do obliczeń z 3,5 minuty do 35 sekund. W tym wpisie chciałbym opisać jak do tego doszedłem.</p><h2>Opis funkcjonalności</h2><p>Zacznijmy od opisu funkcjonalności, która krótko mówiąc kulała pod względem wydajności. Jednym z głównych widoków w naszej apce jest tzw. &quot;Sensitive Content&quot;. Pokazujemy tu listę folderów z plikami, które zawierają wrażliwe dane. To mogą być numery kart kredytowych, dane medyczne, personalne i nie tylko. Dane te mogą podchodzić pod jedną z kilkunastu wbudowanych polityk np. HIPA, GDPR, ale również dajemy możliwość użytkownikom zdefiniowana własnej polityki np. w oparciu o utworzony wcześniej słownik. Początkowo widok &quot;Sensitive Content&quot; pokazywał tylko płaską listę folderów. W zeszłym roku nasz Product Owner wraz z zespołem UX doszli do wniosku, że praca z płaską listą folderów może być nieefektywna. Zamiast tego dużo lepszym, i w zasadzie bardziej naturalnym sposobem reprezentacji danych, będzie drzewko folderów.</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.81779661016949%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAICAYAAAD5nd/tAAAACXBIWXMAAAsSAAALEgHS3X78AAABVElEQVQoz01R2U7EMBDr//8fLwiEuLTskftq0rTGk3YXIllJp45jz0yXm8ZVGdy0HZDz9aidQ8YpFqg0o7WOMjcEflufYGyAdRGGcCGN3biAyVqHmBJiTCQTIaKUGb2vqEtH6x25LThbD2Pc4NXaBjfngllQK7wPRMSktIY2li9aPM6EDwELBWVF7s907J0b/9uyjHrnYygVKWUopZFLwfR+0vg8G3xdLD5+NL4vvBRm2NTgckcsK66x4Ymcb3KEa2JFYN2wHnzBxSS8nxSuNmO6PRxSSBu6NIwTUVvD0neHiQ5f2V9HjrhvbMG2YSTYeh7RJZ20ao98ILGXYjvnPCI3isrKh6B30g5Dob/I21oHXymFeaagECx70w83/9d27OLwhYLBezhy772VBH3dhhER3B2q3Z0QJY5MeWOeO4ZDXnzjhJ3dB3Yfijjt64pEh/qI/AvbE2lTetVs3wAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Widok Sensitive Content List" title="Widok Sensitive Content List" 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">Widok Sensitive Content List</figcaption> </figure></p><h2>Algorytm budowania drzewa</h2><p>Podejść do drzewek było już u nas w projekcie kilka. Głównie opierały się one na własności folderu jakim było unikalne <code>folderId</code>. Niestety, w przypadku &quot;Sensitive Content&quot; nie mogliśmy tego użyć, ponieważ nie wszystkie foldery mogły zawierać wrażliwe dane, a tylko dla takich folderów dostawaliśmy <code>folderId</code>.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">/Shared/A/B/ W tym wypadku mamy 3 foldery (Shared, A, B), z czego tylko dla B dostajemy folderId</code></pre></div><p>Takich lokacji z SC (Sensitive Content) może być multum. Problem wydajności pojawiał się już przy 250 tysiącach lokacji. A że nie jest to wyjątek utwierdził nas klient, u którego znaleźliśmy prawie <strong>milion</strong> folderów. To właśnie dla 1M elementów listy, czas budowania drzewka wynosił 3,5 minuty. Dlatego też, moje zadanie polegało na tym, że drzewko dla 1M elementów ma się budować w mniej niż 60s.</p><p>Wracając do algorytmu. Prosty - jak budowa cepa - tak by się wydawało :)</p><ol><li>Weź całą ścieżkę i podziel ją na fragmenty wg. separatora np. <code>/</code></li><li>Każdy folder wsadź do dwóch struktur: &quot;drzewiastej&quot; i &quot;płaskiej&quot;</li><li>Jeśli folder nie ma <code>folderId</code> traktuj go jako meta-folder</li></ol><p>Dla uproszczenia pomijam kwestię tego, że wspieramy różne źródła danych i te separatory mogą się mocno różnić :) Co więcej, jak się później okazało, niektóre źródła danych mogą mieć dwa foldery o tej samej nazwie, na tym samym poziomie zagnieżdżenia 😱. I jak je rozróżnić? Pominę też kwestię tego, że wynikowe drzewo mieliśmy przedstawić w formie &quot;sparse-tree&quot;. W skrócie - chodzi o to, że jeśli folder zawiera tylko jeden sub-folder, to ścieżka rodzica powinna być zwinięta/scalona.</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Lista: /Shared/A/B/C /Shared/A/B/D --&gt; Drzewo: /Shared/A/B /C /D</code></pre></div><h2>Pierwsza, a w zasadzie druga implementacja</h2><p>Jak już wspomniałem wcześniej, nie było to pierwsze drzewo, jakie mieliśmy wyświetlić w aplikacji. W zupełnie innym widoku, też mieliśmy zrobić sparse-tree i nie chcieliśmy mieć kilku różnych implementacji. Dlatego napisaliśmy prosty moduł do budowania i zarządzania drzewkiem. Oparty został o dwa małe komponenty:</p><ul><li>funkcję <code>buildTree</code>, która przyjmowała płaską tablicę węzłów i miejsce (ścieżkę w drzewie) od którego miała te węzły wstawiać</li><li>&quot;plasterek&quot; (slice) z <code>redux-toolkit</code>, który zarządzał strukturą drzewa (rozwijanie, zwijanie węzłów itp.)</li></ul><p>Całe drzewo, a w zasadzie te dwie struktury &quot;drzewiasta&quot; i &quot;płaska&quot;, trzymane były w reduxie w następujący sposób:</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>Taką strukturę otrzymujemy z pomocniczej funkcji <code>buildTree</code>.</p><p>Dzięki zastosowaniu <a href="https://redux-toolkit.js.org/" target="_blank" rel="nofollow noopener noreferrer">redux-toolkit</a>, a co za tym idzie biblioteki <a href="https://github.com/immerjs/immer" target="_blank" rel="nofollow noopener noreferrer">immer</a>, mogliśmy w bardzo prosty sposób wykonywać operacje na drzewie:</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> initialized<span class="token operator">:</span> <span class="token boolean">false</span><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> 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> paths<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> name<span class="token operator">:</span> treeName<span class="token punctuation">,</span> initialState<span class="token operator">:</span> initialTreeState<span class="token punctuation">,</span> reducers<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> payload<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>Pomocnicza funkcja <code>getNodeByPath</code> służy do wyszukiwania węzła po ścieżce. Potrafi też wyszukać węzeł w sparse-tree.</p><h2>Pierwsze podejścia do optymalizacji i pierwsze błędy</h2><p>No i wszystko pięknie-ładnie, ale przyszedł klient, 1 milion folderów i jebs... Product Owner zakłada Epic w Jirze pt. &quot;Support 1M folders on SC tree view&quot;. Szybka burza mózgów i od razu kosz pełen pomysłów:</p><ul><li>a może by tak budować drzewo w locie, jak parsujemy JSONa?</li><li>a może by tak budować drzewo w web-workerze, przynajmniej nie zablokujemy głównego wątku na 3,5 minuty?</li><li>a może by tak, pizgnąć to wszystko i wyjechać w Bieszczady? ⛰ 🐑</li></ul><p><img src="https://media.giphy.com/media/kPtv3UIPrv36cjxqLs/giphy.gif" alt="A może by tak..."/></p><p>Pierwszy błąd - nikt nawet nie odpalił Profilera, żeby zobaczyć co zajmuje tyle czasu. Każdy założył, że obecna implementacja drzewka wymiata i lepiej być nie może. Sam Profiler na pierwszy rzut oka nie jest prostym narzędziem i może to było powodem tego, że rzuciliśmy się wtedy na tego typu pomysły jak budowanie drzewa &quot;w locie&quot; czy przeniesienie tego do web-workera. Warto też podkreślić, że sam Epic w Jirze powstał już jakiś czas temu, a do samej implementacji usiadłem w połowie października.</p><p>Pewnie zastanawiacie się - ale jak to w locie? Przecież jak idzie request to dopiero jak przyjdzie odpowiedź to przeglądarka parsuje JSONa i daje odpowiedź. Tak, ale...w tym przypadku nasi backendowcy też musieli podziałać trochę w kwestii optymalizacji i zamiast zwracać pełne dane to zaczęli tę naszą listę SC zwracać na zasadzie stream&#x27;u. Dzięki temu mogliśmy np. wykorzystać bibliotekę <code>oboe.js</code> to parsowania JSONa w locie.</p><p>Oczywiście spróbowałem tego podejścia, no bo w końcu ktoś to wpisał do zadania w Jirze, trzeba było sprawdzić co nie? 😜 Fajnie, JSON parsował się &quot;w locie&quot;, ale stream zamiast 10s trwał 30s, a jeszcze nie zacząłem nawet budować drzewa. Dlatego odpuściłem i postanowiłem poszukać gdzie indziej.</p><h2>Web-worker</h2><p>Podejście z web-workerem też przetestowałem. Ale tu napotkałem zupełnie inny problem. Ok - mogę sobie pobrać 1M elementów i zbudować na tej podstawie drzewo, ale muszę je później przesłać z wątku web-workera do wątku głównego. Struktura drzewa jest dość obszerna, razem z danymi, które zapisywaliśmy w tej płaskiej strukturze <code>paths</code>. Jeśli chcemy przesłać tak duże dane z jednego wątku do drugiego, przeglądarka musi te dane zserializować, przesłać, a następnie ponownie sparsować. To też powodowało &quot;zamrożenie&quot; przeglądarki na czas przesyłania z jednego miejsca pamięci do drugiego. Oczywiście są sposoby na przesłanie &quot;bezpośrednie&quot; (bez kopiowania) poprzez tzw. Transferable Objects np. ArrayBuffer, ale uznałem, że na chwilę obecną to może być gra nie warta świeczki i postanowiłem sprawdzić, czy faktycznie ta nasza implementacja drzewka była tak zajebista jak myśleliśmy 😜</p><h2>Profiler podejście pierwsze</h2><p>Usiadłem przed ekranem komputera, odpaliłem dev-toolsy i wcisnąłem przycisk &quot;record&quot; w Profilerze. Po chwili dostałem taki kolorowy wykresik, który przypomniał mi czasy defregmentatora dysków z Windowsa 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.5%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAANCAYAAACpUE5eAAAACXBIWXMAABYlAAAWJQFJUiTwAAACOklEQVQ4y42TeW/aQBDF/ZGrtiSUw6Fqv1YVVYqapkkgKeEU4JgjGBx87YkPXscODUhVpP7x05ud3X07s14blVoNpZMyTkunqFNcr5+h8qmKWtWEaZoUn1BcRqV8gtL7d4V+bpiom1V8+dpA46yGRr0Ks1ZB6cNHGH7oYRMQvgsmQ3AtIWIFGWtwFUGLGRK5QKZWhINMOkjEEimR8KeXmHIsWMB1HRhaawjBCQHORaFZtiMAEiT4f6TSMISkipR6hdPY91YIwhW8pQ1/0odvDY7owytyR9A4sIeIgmcy1Cm1loEVpGS6g+V00N58w8PwHL2L75i2bmC3rmE3fxHXmDZvMGs1C83npve3sK+usB6Nc8MMx8jtDjxMEW1iOP0N2pcPGA8tPPanGC8eMVpN0Jn30G33MLq3MGiO0G8OYd8uEFn6X8MCqlbSBfJRDP/So5MdrAdLMKapiy08yWB3uljcD2HddTG9HsJtrhAtGQz5hqFIdoieNKJbutt2DP5bQ/Jd0YGKgfWFB+88gPfTQ/SDgbVo3Uq9YZgTU4XPCuGYI5qRzvIPlu7nUoQ9ynUkVaURWqQTCeVu32g5p7jLLdgsAnMUGJ0uXg2JeQLxRJXzhA6mTuYcMoipwu3hY+QcDAmZgW+24B498g291/wV/F0TUByRKRkKMmKugqKxkS/KnwuTdJJKD9CmosWQNjAah8khn2ueEy8IRnNkKsncWLvP8IMQYcgKGP0tjMu9EmxPJA654zw/mif9A0zFwtIb7Iv6AAAAAElFTkSuQmCC&#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>Pierwsze co rzuciło mi się w oczy to ten fioletowy kolor, który zanurkował bardzo, bardzo głęboko. Po bliższym spojrzeniu okazało się, że bardzo dużo tych fioletowych elementów to sprawka <code>immer.js</code>. Szybki rzut oka w dokumentację i boom! strzał w dziesiątkę. Okazuje się, że przy &quot;wkładaniu&quot; dużej ilości danych poprzez <code>immer</code> możemy przyspieszyć ten proces poprzez <code>Object.freeze</code> <a href="https://immerjs.github.io/immer/docs/performance#performance-tips" target="_blank" rel="nofollow noopener noreferrer">tutaj więcej info</a>. Zabieg ten pozwolił mi na zejście z 12,54s na 11,24s dla 54K elementów. Dla 1M skok był oczywiście proporcjonalnie większy. No ale to nadal nie było to...</p><h2>Od profilera, do źródeł</h2><p>Wiedzieliście, że jak klikniecie na dany blok w Profilerze, a następnie przeniesiecie się do pliku, to dostaniecie czasy dla poszczególnych bloków kodu? Nie!? 😎 To teraz już wiecie ;)</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.73170731707317%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAASCAYAAABb0P4QAAAACXBIWXMAABYlAAAWJQFJUiTwAAACUElEQVQ4y4VUa5eaMBT0//+39kPrqa26qy4LKpDwSMgbpje40kWtyzEnYJLJ3Llz76IVEnnJwOsGQjokSYbt5oDdrsLbW4PDniHLzhBCw1mLEAL6Pozz59H3Pbz3WKiug2pPMLJEfAJtbniH/a7BdluMF7CyhlIe3nkMw4D/PRF0IaWEqFICPeO6d9CeQC04N7DWwxqLrnOQ0tDcEaN+BP48JkClFDr+AtMmuN7t6PCv5RHr9ZHY8ZHZeNENwO08AlrS5fqMCx+LjRBYbzZYrTLSMicN238AGGahzwCN0RDnn+jYetqgRYdjXiIrK7SyRtM044gHHjGbAWqtwZJvYOmPWVh1XaEoKuz3pLGwDwEeMlRKozkt0RZXhgN62pBmDWW6wOnIiZ2C1u4S7ICZjneAMWuyyaElnwDjjzMxejDPFYwOs8MDnoRsjCHDasqkmRYMvdddBWko3M7AeLJPuIxn2Z5sI8s/ZJ3dxMJqg/2hwPJ3gn1yQHpieKHvnNfwwd/pNwMUZA+tFZxzc23CgNMrozIssXvleE84+bKhyqnHJFkbHjOMgN65O00CMbFUbm3r0IpYKZYyb+j7UjVPASWFq9tsCsO7AEtAog3ExoymjsBKhbv6vQs5ZrlKv0MWq2kTq6g5pGTs/EQGrynTsYYVot7Rt0+TErMcG4ORbGoOhiQoOEdZMZRMwhjqND7qdhm3zO4qxRlB1ukuf9KiI6vwQiF9F6h59CKjqiGtffi6UiJDbxUCeS82zlgRmkTvKcvOXZqm9z0x6z9qeXga8l+P/3vSoyCcKQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Czasy przed optymalizacją dla buildTree" title="Czasy przed optymalizacją dla 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">Czasy przed optymalizacją dla buildTree</figcaption> </figure></p><p>To co się rzuca w oczy to 229ms dla zbudowania prostego ciągu znaków 🤯 jakim jest aktualna ścieżka. Okazało się, że to zwykłe niedopatrzenie można było zastąpić krótszym kawałkiem kodu, który koniec końców zajmuje 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.38510911424905%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAAACXBIWXMAABYlAAAWJQFJUiTwAAACtElEQVQ4y41UCXKjMBDM/3+XrdojTuIb29ygCx1AbyMn2HFc3hWeMqURrZ6ZnnnSWsO0B1h5hDYeSZJi8bbG66rELlFIU4X1ukR6KuCsRfAeIQT0fX/XnrrOwtQbiGKJcQSc86gKgdWyxvNzjsVii90uRVlpdMZhHHo8WgQ06HQTbQKMqx/gtEfdOGjdEcjC8yJjRkZh4L2Lx0Z+cGtPrdT8uESnigg4YY6uR5tbbLcSmn4hFNqmRVFUmFJU14zKhBn0C0OhOgSnyEDPzuAcDmmOZZLhVOaRVSs6hq2glCPDAT709wGnkEWxQnV6PR+YnmGEIZP9IcPLosHxKBmBh0wNLFPxuW7BPgA7aBalyd6/HFLM3W6bsCAK253k/hAvus3ZdS4joGN4482NPWXRNBZt+7iid0M2ZmK4onTWc8jTLy8r/HxfY01dJnmJQ3HE+pjwP0clKxjPVFnxnaHWBrp8h66WFycfpRWrKVgIi4omBN9LhaqkHqnda3Y3gDwgc/S2mTedt8iqCvuiQNHmaFSDjLJpuhqs7wcIvuXxHDKL0qa/UCY/Lg6abh3eXjL8+bPBZpNhuSxYHAJnilq0AMa7eYwMvVUsRDeDffp1aZBS3JudZi/XrHpLYEFgEYs2ibtnV31hqJRi/9qzLK5CmAaA8z3zFWCtp5gD33u+c5+dZMzZNww3gHHaNAk6ccCllQcETQAZIFoKWkpKSMfedi48lE7Moch+M7yXedN0HqeixiGrkbI4U5Xr2pGRj5FMYU52zW5mOElAlwvI/ALoOF0K9vJ6V2GbNMxvT5Zky36WUpCtiwMiBP8d0HJoOttF+9wcOCjrusN+ryGFY96mEWbIKvxPp5gI1gd/aT3myZDF6WQ4sjpOGDtP6X/28sQwHvxwTNVU0sZBEMIQbRjGhxPmev0FrkwWn/TxJDQAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Czasy po optymalizacji dla buildTree" title="Czasy po optymalizacji dla 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">Czasy po optymalizacji dla buildTree</figcaption> </figure></p><p>Pomyślicie - (ironicznie) wow... 227ms... &quot;brawo Ty&quot; 👏. Czym jest 227ms? Jeśli spojrzymy na to jako pojedynczą wartość - to fakt...mikro-optymalizacja. Ale pamiętajcie, że celem było obsłużenie 1M elementów, a operacja sklejania ścieżki dotyczyła każdego sub-folderu.</p><h2>Mój spread operator AKA Object.assign taki piękny</h2><p>Jak zrobić płytką kopię obiektu, bądź rozszerzyć inny obiekt - nic prostszego - spread operator <code>...</code>. Jeśli musicie wspierać przeglądarki takie jak IE11, to pewnie korzystacie z babel.js - tak jak my...no i taki spread operator, koniec końców jest tłumaczony na <code>Object.assign</code> (w wielkim uproszczeniu).</p><p><code>Object.assign</code> jest <a href="https://twitter.com/dan_abramov/status/980436488860196864" target="_blank" rel="nofollow noopener noreferrer">stosunkowo wolny</a> i przy większej skali może sprawiać problem. W tym przypadku postawiłem na zwykłe kopiowanie per klucz. Dzięki temu prostemu zabiegowi zbiłem 154ms do 44ms. I znowu, dla pojedynczych elementów to nie ma absolutnie żadnego znaczenia, ale kiedy iterujemy po dużym zbiorze danych takie optymalizacje mogą zdziałać cuda.</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.85382059800665%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAATCAYAAACQjC21AAAACXBIWXMAABYlAAAWJQFJUiTwAAACuklEQVQ4y51U23LaQAz1//9EX/sTfehDp5k2MwkJ2MQ2EDBgx1eM7xc41dnYCcn0kunOyNrVylrp6OxqztLC9MdX7F0Pi+UKy9UKZVXhfD6jP51w+qDQv2k7aPrNNb58/oQkKxBGMYIwRtf3+J/BwFpelNh7T1ivN9hsHGy3O+x2e9FbZXOcLWzbhuu6UoUrPuLnOFg9Pirf9XoNPwhUQGapeZ6HyWQCXdcxm82UXi6XsCwLhmFgOp2KnuPu7h6mKbb5XPx03Iv99naCB9NE0zTPGTKg++Tj+maCqW7AeLCgmzbm1gKTqRzAtfEASzLkj7a9gCkH2YuFCsr1TBKwLBur1SOOxwzaNm3wc5djHpSY+hWMoIIZVrBEzKiGn7c4CaYj+P0wH3XXdUrGtVa2PZL6jLwDDlWPtutl8zTI+3n/zt4rJnB+Op2V1jg5RD6i4AlVWaJt25dT/yVt+zov5d+6bqARzOvvV7j6doVDekSapiiKAnleoKrqD1MmyzJUdQ2NnCvLSoKlCtRIuBgnCZLkoORwoKRIqOXA+FghFc7SHsWx0v3A20aq04gBjfu9C0/4mOe54NgpKlAIAWWcN4IxS73cJ//ko+ZaO3RoHGEYIhiIykEnHkSMuHeUShKpgBVdjvMYkCVXcnd931c/UFgym0WnWrIIo0gBH4mmL/dZyRjoTYYKQwHf9Xx4QvIgiGTdCMCtEnU1XYGiqGQ/ECyPyieKkz8EFEzYbt5HPwjVA0FnNoANyrJc2ZlhEEZqzQypLwM+vzZsipCRFNk4O3kI5LLLA7GTBq03W8FKKFTKbfFDybZRL1GWC5ZRglQY8X6wWRo/zLBu2mc9zJtB3tjry3nz4vPiS2Kzy4XwkDgym1HTlg/2egz8N5HD1APLUpI0QyE/UmdS/pHElZJiaUA53JZLrH4rg88vUpezjjrk5acAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Dan Abramov o Object.assign" title="Dan Abramov o 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 o Object.assign</figcaption> </figure></p><h2>Agregacja wartości</h2><p>Po &quot;podkręceniu&quot; immera i wyrzuceniu kliku Object.assign, bądź przepisaniu ich na prostą pętlę, skończyły mi się pomysły na &quot;proste&quot; optymalizacje. Trzeba było podkręcić sam sposób budowania drzewa.</p><p>Poprzednia implementacja, dzieliła sobie SC lokacje wg. źródła i dla każdego z nich budowała pod-drzewo. Dla każdego pod-drzewa liczone były zagregowane wartości (np. jeśli folder sam w sobie zawierał 10 SC, ale miał dodatkowo 50 sub-folderów, chcieliśmy pokazać zsumowane wartości). Dla każdego takiego pod-drzewa, wykonywane były sumowania, a następnie węzeł źródła był aktualizowany wg. zsumowanych wartości dla wszystkich folderów.</p><p>Każda taka operacja wrzucała coś do stanu w redux&#x27;ie. Pomyślałem - a na co to komu? A komu to potrzebne? Przecież nie pokazujemy drzewa dopóki wszystko nie jest policzone i zaktualizowane. Dlatego, zmieniłem kod tak, aby najpierw zbudować w pamięci całe drzewo wraz z policzonymi zagregowanymi wartościami, a następnie za pomocą jednej operacji, wsadzić zbudowane drzewo do reduxa.</p><p>Co więcej - w wymaganiach drzewka, było napisane, że pewne węzły miały być domyślnie rozwinięte np. pierwszy poziom + potencjalnie wcześniej zaznaczony element na liście (z listy do drzewka można przejść za pomocą prostego przycisku). Wcześniej operacje rozwijania były wyzwalane za pomocą akcji <code>toggleNode</code>. To też zmieniłem - zamiast odpalać reduxową akcję, po prostu zmieniam wartość <code>expanded</code> na <code>true</code> bezpośrednio w obiekcie węzła.</p><p>Można zapytać - co Ci to dało drogi Panie?</p><p>Dla 54K elementów zjechałem z czasu 12,25s na 2,4s 🚀</p><p>Łogień w szopie :) Product Owner cały w skowronkach.</p><p><img src="https://media.giphy.com/media/ciwIz38tlvDFH08Yuu/giphy.gif" alt="Wow"/></p><h2>Testy dla 1M</h2><p>Poprosiłem backendowców, żeby przygotowali mi środowisko do testów dla 1M elementów. Chciałem sprawdzić czy moje optymalizacje dla 54K znajdą uzasadnienie. No i uśmiech nie uciekł z mej twarzy :)</p><p>Przed optymalizacją czas budowania drzewa wynosił ~3,5 minuty. Po zaaplikowaniu wyżej wymienionych zmian udało się zjechać do 59s.</p><p>Mniej więcej ~70% oszczędności. W sumie można by powiedzieć - job done - miało się budować w mniej niż 60s... 59 to mniej niż 60 😅 Jest git...</p><p>Trochę byłem już zmęczony tym grzebaniem w de-facto w nie swoim kodzie...ale kumpel z zespołu słusznie stwierdził:</p><blockquote><p>No fajnie, fajnie, ale dla mnie nadal to jest wolno.</p></blockquote><p>Dodał też potem, że nic mi nie ujmuje i wg. niego wykonałem kawał dobrej roboty...ale trudno było się z nim nie zgodzić. Od momentu kliknięcia w element nawigacji, do czasu wyświetlenia widoku, użytkownik musiał poczekać łącznie 90s:</p><ul><li>25s pobieranie danych (streaming)</li><li>6s przeglądarka parsuje JSONa</li><li>59s budowanie drzewa</li></ul><p>Jako użytkownik, gdybym przez 90s widział tylko spinner (&quot;kręciołek&quot;) to by mnie szlag trafił :) Nie chcę myśleć, co czuli nasi użytkownicy jak musieli czekać 3,5 minuty...pewnie żaden nie wytrzymał 😅</p><p>...no...ale wracając...kumpel mówi: &quot;wolno&quot;...no to ja od razu: &quot;co!? wolno!? ja Ci pokażę!&quot; 🤣</p><h2>Generowanie ID</h2><p>Znowu zanurkowałem w Profiler. Dla meta-folderów generowny był <code>folderId</code>. Było to spowodowane tym, że inne miejsce w kodzie tego <code>id</code> potrzebowało (mniejsza o to). Koniec końców to generowane id nic nie znaczyło (nigdy nie było wysyłane do backendu). Ktoś jednak wymyślił, że to meta-folder-id ma być haszem ze ścieżki...</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>Funkcja <code>btoa</code> koduje ciąg znaków jako base64. Sama w sobie trwa średnio 0,25ms...czyli ułamek milisekund. Ale gdy mocniej się zastanowimy - a na co komu ten hasz? a na co komu to potrzebne?</p><p><img src="https://media.giphy.com/media/s239QJIh56sRW/giphy.gif" alt="Ale po co?"/></p><p>No właśnie! Jeśli meta-folder-id to tylko base64 ze ścieżki, która de facto zawierała też w sobie <code>id</code> źródła, więc była unikatowa względem całej listy, to po co to w ogóle ten cały 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>Ten jeden diff sprawił, że dla 1M elementów zszedłem z 59s na 35s, co dało ~40% zysku 🤯</p><p>Czyli teraz klient nie czekał już 90s a 66s - łącznie z pobieraniem i parsowaniem danych! Biorąc pod uwagę, że wymagania mówiły o budowaniu drzewka w czasie mniejszym niż 60s, to chyba Product Owner oraz klienci powinni być zadowoleni 😅</p><h2>Dalsze kroki</h2><p>Oczywiście nie spoczywamy na laurach. Blokowanie użytkownika na 60s nadal jest kiepskim pomysłem, dlatego dalej myślimy o ulepszeniu implementacji. Być może wyrzucimy to w końcu do web-workera. Kto wie? Może uda mi się dzięki temu zebrać materiał na następnego posta 😉.</p><h2>Podsumowanie</h2><p>Lekcja pierwsza - zamiast gdybać i sypać pomysłami z kapelusza o web-workerach, lepiej odpalić Profiler.</p><p>Lekcja druga - jeśli operujesz w dużej skali, iterujesz po dużym zbiorze danych, to optymalizacje na poziomie <code>ms</code> dla jednej iteracji potrafią czynić cuda 🚀</p><p>Lekcja trzecia - do reduxa wrzucaj dopiero wtedy, kiedy jesteś gotów 💪</p><p>Lekcja czwarta - jeśli nie ma potrzeby, to nie komplikuj sytuacji 😉 (patrz ID &amp; btoa).</p><p>Mam nadzieję, że dzięki tej historii sięgniecie wcześniej do Profilera i uda się Wam poprawić wydajność nie jednej aplikacji.</p></content:encoded>
</item>
<item>
<title>
<![CDATA[ 10 złotych rad dla junior developera ]]>
</title>
<description>
<![CDATA[ Czy ten tytuł to clickbait? Oczywiście, że tak. Nie ma na świecie dwóch takich samych osób. To co sprawdziło się u mnie niekoniecznie musi się sprawdzić u Ciebie. Tak prawdę mówiąc, to tytuł tego… ]]>
</description>
<link>https://przemuh.dev/blog/10-golden-advices-for-junior-developer</link>
<guid isPermaLink="false">https://przemuh.dev/blog/10-golden-advices-for-junior-developer</guid>
<pubDate>Tue, 22 Sep 2020 00:00:00 GMT</pubDate>
<content:encoded><p>Czy ten tytuł to clickbait? Oczywiście, że tak. Nie ma na świecie dwóch takich samych osób. To co sprawdziło się u mnie niekoniecznie musi się sprawdzić u Ciebie. Tak prawdę mówiąc, to tytuł tego posta powinien brzmieć &quot;Co chciałbym przekazać <strong>sobie</strong>, gdybym dzisiaj miał zaczynać jako junior developer&quot;. Ale uznałem, że to trochę zbyt długie. Kto wie, może niektóre z poniższych wskazówek przydadzą się Tobie. Czy niektóre z tych rad są kontrowesyjne? Pewnie tak. Ale wszystkie z nich są szczere, i właśnie takie chciałbym usłyszeć gdybym wchodził jeszcze raz w świat IT. Traktruj ten wpis, raczej jako taki list Przemka do Przemka :)</p><p><strong>TL;RD</strong></p><ol><li>nie buduj portfolio</li><li>załóż konto na Twitterze</li><li>uważaj na celebrytów-IT</li><li>nie kupuj książek</li><li>eksperymentuj, baw się kodem</li><li>naucz się świadomie zarządzać czasem</li><li>zainwestuj w umiejętności miękkie</li><li>znajdź mentora</li><li>kup notatnik</li><li>doszlifuj anglielski</li></ol><h2>Nie buduj portfolio</h2><p>Kiedy zaczynałem szukać swojej pierwszej pracy jako programista (2012 r.) nie wiedziałem, że wyląduję na frontendzie. Nie miałem konta na githubie. Nie miałem portfolio. I gdybym dzisiaj miał szukać pracy, to też bym takiego portfolio nie budował. A już na pewno nie wrzucałbym tam aplikacji typu ToDo List, albo Weather App. Nie zrozum mnie źle, pisanie takich aplikacji jest jak najbardziej ok. Dzięki temu uczysz się jak składać działającą całość. Ale wrzucanie tego do portfolio jak dla mnie nie ma sensu.</p><p>No dobra...ale co zamiast portfolio?</p><p>Odpowiedź jest prosta - Open Source. Na githubie jest mnóstwo bibliotek, do których możesz kontrybuować. Wybierz sobie swoją, przeczytaj dokumentację, spróbuj zbudować lokalnie, zobacz jak wygląda lista Issues - z czym ludzie mają problemy. Dzięki temu nauczysz się o wiele więcej niż przy ToDo czy Weather App. Czytanie cudzego kodu to jedna z najważniejszych umiejętności w byciu programistą. Mówiąc o kontrybucji do Open Source, nie mam na myśli np. nowych ficzerów Reacta (chociaż do odważnych świat należy). Czasami jedna linijka kodu potrafi rozwiązać czyjś problem. Ba! Nie musisz wcale kodować. Pisanie dokumentacji to też cegiełka do Open Source. Pierwszy commit Kent C Dodds&#x27;a w ramach Open Source to była literówka. Nie wierzysz? - <a href="https://kentcdodds.com/blog/how-getting-into-open-source-has-been-awesome-for-me" target="_blank" rel="nofollow noopener noreferrer">zobacz ten wpis</a>.</p><p>Podsumowując - jedno Twoje zdanie podczas rekrutacji - &quot;aktywnie udzielam się w świecie Open Source&quot; znaczy więcej niż portfolio z fajerwerkami.</p><h2>Załóż konto na Twitterze, ale ...</h2><p>Znowu - kiedy zaczynałem szukać pierwszej pracy, jedynym portalem typu social media był dla mnie Facebook. Dopiero po czasie zobaczyłem ile mnie omija. Jeśli chcesz być na bieżąco z informacjami dot. technologii to Twitter jest chyba najlepszą opcją. Facebook = rodzina i przyjaciele, sprawy for fun, śmieszki, heheszki, memy, zdjęcia. Twitter = nowinki technologiczne, ciekawi ludzie z branży, motywacja, inspiracje. Dlatego, jeśli jeszcze nie masz konta na Twitterze, to czym prędzej je zakładaj. Poszukaj kilku osób z branży, a potem poszerzaj listę obserwowanych. Nie bój się również usuwać ludzi z listy. To nie są Twoi przyjaciele - nikt się nie obrazi :) Jeśli nie podobają Ci się treści, jakie wrzuca obserwowana przez Ciebie osoba - po prostu przestań ją obserwować, albo wycisz jej tweety.</p><p>Z durgiej strony - nie obrażaj się, jak Twój &quot;idol&quot; wrzuci od czasu do czasu posta niekoniecznie związanego z programowaniem. Koniec końców jest to portal typu social media. Zrzut ekranu ze Spotify jeszcze nikomu krzywdy nie zrobił ;)</p><p>No ale, co z tym &quot;ale&quot;?</p><h2>Uważaj na celebrytów-IT</h2><p><em>pfff</em> &quot;że co!?&quot;. Wierz mi, albo nie, &quot;kiedyś to było&quot;. Może inaczej - kiedyś to NIE - było tylu blogów, tylu informacji, tylu kursów, tylu &quot;mentorów&quot;. Z czasem ludzie podchwycili temat pt. &quot;praca w IT = dużo hajsu&quot;. Zaczął się napływ ludzi do branży, bo to przecież &quot;łatwe pieniądze&quot;. Siedzi się tylko i klepie w klawisze. Jak grzyby po deszczu zaczęły wyskakiwać kolejne szkoły programowania, bootcampy, kursy, mentorzy. Zrobił się z tego niezły biznes. Przecież każdy chce zarobić - co nie? - co w tym złego?</p><p>Nic. Jeśli masz pieniądze to je wydajesz jak chcesz. Gdybym tylko miał sobie dawać w tej kwesti radę, to powiedziałbym &quot;uważaj na celebrytów w IT&quot;. Trochę takich ludzi szufladkuję, sorry. Naczytali się Aniserowicza i Szafrańskiego, jak to można zarabiać na blogu, kursach itp. Sami ledwo co skończyli bootcamp, liznęli pierwszej pracy a już wydają swój autorski kurs - &quot;Programowanie w HTML dla zaawansowanych&quot;. Brzmi jak dowcip? Niestety. Brzmi jak zazdrość - kto wie - być może. Niestety nikt nie uczy tego, jak odfiltrować dobry content od tego skopiowanego i nastawionego na szybki zysk. Czasami nie jesteśmy w stanie zweryfikować doświadczenia danej osoby. Kto wie - może naczytał się pierdół o żabach i teraz na siłę próbuje zainteresować tym innych :) Sam musisz sobie wyrobić czujnik na takie osoby - Przemku.</p><p>Dobra wiadomość jest taka, że tych dobrych &quot;dusz&quot; jest więcej. A takie celebryto-IT-pijawki zdarzają się sporadycznie.</p><p>PS. Nie zrozum mnie źle. Dzielenie się wiedzą na blogu, vlogu (whatever) nawet jeśli dopiero co nauczyłeś się &quot;czegoś&quot; jest SUPER! Ale natychmiastowa próba zarabiania na tym - już nie - przynajmniej nie dla mnie. Gdy płacę za kurs, to chcę mieć pewność, że dana osoba &quot;zęby na tym zjadła&quot;, a nie naczytała się pierdół o żabach ;)</p><h2>Nie kupuj książek</h2><p>A przynajmniej tych o technologiach. A już na pewno nie kupuj ich z myślą &quot;kiedyś przeczytam&quot;. Książki o technologiach szybko się starzeją. Zwłaszcza te o technologiach frontendowych, o frameworkach itp. Jeśli chcesz mieć fajną podstawkę pod monitor - spoko, your choice :) Mam takie dwie cegły pt. JAVA ^^ #naPóźniej.</p><p>Zamiast tego, skup się na książkach ponadczasowych. &quot;Clean Code&quot;, &quot;Clean Coder&quot;, &quot;Pragmatyczny Programista&quot;, &quot;Zawód Programista&quot;, &quot;Refaktoryzacja&quot; ... to są książki, które się nie starzeją. Po takie książki warto sięgać kilkukrotnie w swojej karierze. Za każdym razem wyciągniesz z niej coś innego, będziesz miał inny punkt widzenia, inny poziom doświadczenia.</p><p>I żeby było jasne - nie ma nic złego w książkach o samych technologiach/frameworkach - o ile kupisz aktualną wersję i przeczytasz ją zaraz po zakupie. W innym przypadku - &quot;daj se siana&quot; ;)</p><p>PS. Jeśli możesz - czytaj w oryginale, inaczej mówiąc - uważaj na tłumaczenia. Wiem, że oryginały są znacznie droższe w porównaniu z wydanymi nad Wisłą &quot;tłumaczeniami&quot; - ale warto. Obserwuj takie strony jak <a href="https://www.humblebundle.com/" target="_blank" rel="nofollow noopener noreferrer">HumbleBundle</a>, tam często pojawiają się &quot;paczki&quot; książek np. z wydawnictwa <a href="https://www.oreilly.com/" target="_blank" rel="nofollow noopener noreferrer">O&#x27;Reilly Media</a> i można je dostać za &quot;śmieszne&quot; pieniądze.</p><h2>Eksperymentuj, baw się kodem</h2><p>Często na grupach dla początkujących czytam: &quot;najpierw skup się na podstawach, dopiero potem zajmij się Reaktem&quot;. Ding-dong - Bullshit dector - Ding Dong. Owszem - podstawy pt. zmienne, pętle, funkcje wypadałoby ogarnąć przed frameworkiem. Ale jak już łykniesz podstawowej składni to śmiało wypływaj na głębie. Podczas nauki samego Reakta otrzesz się o funkcje wyższego rzędu, kompozycje, destrukturyzację i to w takiej praktycznej formie. Nie ma sensu czekać!</p><blockquote class="twitter-tweet"><p lang="en" dir="ltr">It&#x27;s fine to learn React while you&#x27;re learning JavaScript. Frameworks can teach you closures, higher order functions, ternaries, etc...</p>— Eric Elliott (@_ericelliott) <a href="https://twitter.com/_ericelliott/status/1269115558479495168?ref_src=twsrc%5Etfw">June 6, 2020</a></blockquote><script async="" src="https://platform.twitter.com/widgets.js" charSet="utf-8"></script><p>Inny wymiar tej rady mówi o tym, że programowanie powinno sprawiać Ci frajdę. Nie bój się napisać &quot;brudnego kodu&quot;, eksperymentuj, baw się. Wykorzystaj różne podejścia. Sprawdź w czym najlepiej się czujesz.</p><p>W programowaniu chodzi o rozwiązywanie problemów. A najpiękniejsze w tym wszystkim jest to, że wiele problemów można rozwiązać na wiele, wiele, wiele różnych sposobów.</p><p>I na koniec - jeśli masz już za sobą ciężki bój przez te &quot;podstawy&quot;, a pisanie każdej kolejnej linijki sprawia Ci ból - to zastanów się czy to na pewno dla Ciebie. Praca programisty nie jest usłana różami, czasami trzeba zanurkować w niemałe szambo i nikt Cię za to po plecach nie poklepie. Z drugiej strony - nie samymi programistami IT stoi. Do branży można wejść na różne sposoby ;)</p><h2>Naucz się świadomie zarządzać czasem</h2><p>Z niecierpliwością czekam, aż mój kolega Radomir, zbierze się w sobie i wygłosi prezentację na temat zarządzania czasem. Sprzedał mi ten temat kiedyś przy jakiejś kawie. Główny przekaz jest mniej więcej taki - jeśli jesteś na studiach to nawet nie wiesz ile masz wolnego czasu. Dopiero (o zgrozo) po czasie dochodzi do nas, ile cennych minut przepaliliśmy. I żebyśmy się dobrze zrozumieli - nie ma nic złego w naparzaniu po nocach w Counter Strike&#x27;a (gra się jeszcze w to?). Nie ma nic złego w imprezowaniu. Jest taki okres w życiu człowieka - studia - gdzie dopiero poznaje się &quot;co to życie&quot;. Korzystaj póki możesz :) Ale pamiętaj - już nigdy nie będziesz miał tyle czasu co teraz. Każdy z nas ma tyle samo czasu. Doba ma 24 godziny. Różnimy się tym, jak ten czas wykorzystujemy. Naucz się swoich nawyków. Swojego organizmu. Sprawdź kiedy jesteś najbardziej produktywny, kiedy najlepiej się uczysz. Wykorzystuj to. Naucz się świadomie zarządzać czasem.</p><p>Ostatnio na Twitterze napisałem, że praca programisty to nieustanna nauka. Tak jest. Na tę naukę też trzeba umieć znaleźć czas. Chcesz prowadzić bloga? Sprawdź ile czasu zajmuje napisanie posta. Kanał na Youtube? Kurs gita, czy babela nagrywam już chyba od lutego :) To nie jest proste. Na wszystko trzeba znaleźć ten cholerny czas. Dlatego Przemku - naucz się świadomie zarządzać czasem.</p><h2>Zainwesuj w umiejętności miękkie</h2><p>Do pierwszej pracy dostałem się na staż. To nawet nie było stanowisko juniorskie. Kiedyś staż kojarzył mi się z parzeniem kawy i wpinaniu kartek do segregatora (tak tak, i jeszcze za to Unia płaciła...ale ciiiiii 🙊). Ale w Samsungu było inaczej. Konkretny projekt, konkretni ludzie, ogrom wiedzy. Dacie wiarę, że startowałem do zespołu C++ (myślałem, że jak kodowałem w tym języku na studiach to się uda) a dostałem się do zespołu SmartTV, w którym pisaliśmy we frontendowych technologiach? W życiu bym nie powiedział. Dla mnie JS kojarzył się tylko z jQuery i śnieżynkami na stronach. No ale nie o tym chciałem pisać.</p><p>Byłem zielony - to fakt. Szybko musiałem nadrobić wiedzę. Udało mi się to dzięki wspaniałym ludziom, z którymi miałem okazję pracować - ale o tym będzie następna rada. Pomimo braku w wielu kwestiach techniczncyh - dosyć szybko awansowałem. Dostałem też możliwość poprowadzenia małego zespołu. Mój szef coś we mnie dostrzegł. Od liceum bardzo lubiłem nawiązywać nowe kontakty. Uważałem się za duszę towarzystwa. Tu coś zagrać na gitarce, tu zagadać, tu się pośmiać. Spotkałem świetnych ludzi na swojej drodze. Byłem szczery, życzliwy, pracowity (teraz też jestem, żeby nie było 😜). Z perspektywy czasu widzę, że to właśnie relacje jakie budowałem z ludźmi pozwoliły mi tak szybko awansować, tak szybko stanąć na czele zespołu.</p><p>Bardzo często nie doceniamy umiejętności miękkich w IT (chociaż zauważam zmieniający się trend). Zdradzę Ci teraz pewien sekret - Przemku - pisanie kodu to nie wszystko. Ba! Umiejętna komunikacja i zdolność do budowania relacji znaczą o wiele więcej niż klepanie kodu.</p><p>Dlatego - zainwestuj w umiejętności miękkie. Sprawdź jak dobrze się komunikować - bo to wcale nie jest takie łatwe.</p><h2>Znajdź mentora</h2><p>W poprzednim punkcie pisałem o świetnych ludziach, z którymi miałem przyjemność współpracować. Szymon K., Grzegorz D., Bartek K., Maciej W. (trochę jak z kryminału :D...no ale nie wiem czy chcieliby się znaleźć tu z nazwiska więc dla pewności zostawiam tak :))...wiele by wymieniać. Wszysktich ich łączy jedno - dążyłem do tego, by być na zbliżonym do nich poziomie. Nie porównywałem się, bo to nie ma sensu. Każdy ma swoją drogę, swoje przeżycia, doświadczenia. Ale możemy się inspirować, czerpać wiedzę, pytać o przeżycia i bazować na doświadzczeniu innych. Dzięki temu zwiększałem prędkość nauki i wchodzenia na wyższy level. Dzisiaj nie wyobrażam sobie pracy w środowisku, w którym nie mogę uczyć się od lepszych od siebie. Zawsze szukam takiej osoby i staram się inspirować.</p><p>PS. Pamiętaj, że to może działać w dwie strony. U kogoś widzisz rzecz, której Tobie brakuje...za to ta druga strona widzi w Tobie rzeczy, które ją inspirują do działania. Dzięki temu wzajemnie się napędzacie.</p><h2>Kup notatnik</h2><p>Wszyscy mówią - załóż bloga, załóż kanał na YT. A ja mówię - kup notatnik. Taki zwykły zeszycik A5 może być. Zapisuj tam skrzętnie to, czego się nauczyłeś. Czytając książki, bądź oglądając kursy - rób notatki. Spisuj swoje osiągnięcia. Zapisuj przemyślenia. Rysuj mapy myśli. To pomaga - serio. Pomaga - ale z czasem. Np. podczas rozmowy o podwyżkę. Takie rozmowy często odbywają się raz do roku. Czy jesteś w stanie z pamięci wyrecytować wszystkie swoje osiągnięcia. Podpowiem Ci - NIE!. Czasami robimy małą rzecz dla siebie, ale wielką dla projektu. Zapisując swoje wszystkie sukcesy i porażki jesteś w stanie popłynąć na takiej ocenie rocznej i wywalczyć upragnioną podwyżkę. Ale nie rób notatek dla pieniędzy :) rób je dla siebie i dla swojego mózgu, który nie zawsze jest w stanie wszystko spamiętać ;)</p><p>PS. Bloga też sobie załóż jak chcesz - dziel się tam swoją nowo-zdobytą wiedzą i doświadczeniem. Tylko proszę - nie nastawiaj się od początku na &quot;zarabianie z bloga&quot;. Nie każ mi akceptować powiadomień i zapisywać się do newslettera :) Na początku bloga prowadź go dla siebie. Sława i splendor przyjdą z czasem....albo i nie :P</p><h2>Doszlifuj angielski</h2><p>W liceum nie byłem jakoś specjalnie zainteresowany angielskim. Tu trójeczka, tam czwóreczka, tu coś ściągnąć, tam zagadać i &quot;jakoś to będzie&quot;. Szybko tego pożałowałem. I choć nie miałem problemu w zrozumieniu słowa pisanego (dokumentacja, książki, maile) to szybko zarobiłem &quot;plaskacza&quot; w momencie, kiedy miałem poprowadzić spotkanie czy prezentację po angielsku. Mówi się, że podstawowym językiem programisty jest język angielski - to prawda. I chociaż coraz więcej publikacji powstaje w języku polskim, tłumaczone są dokumentacje (patrz React) to nadal - posługiwanie się językiem angielskim to podstawa. Bardzo dużo dają tzw. English-Days. W Suncrapers, w którym miałem okazję pracować, był taki jeden dzień - chyba czwartek z tego co pamiętam. W czwartki rozmawiało się tylko po angielsku. To nic, że w projekcie sami polacy. Trzeba było tłumaczyć i szprechać po inglishu. Wiadomo - dla ludzi, którzy dużo się oczytali, ale mało mówili to był problem. Wytłumaczenie prostej rzeczy zajmowało x2, x3 albo x4 tyle co po polsku. Z drugiej strony dzięki takim zabiegom później było łatwiej :)</p><p>Dzisiaj nie mam już problemu z posługiwaniem się językiem angielskim. Cały czas pracuję nad wzbogacaniem słownictwa (aktualnie trenuję z dziećmi Ba-Ba-Black Sheep ;))</p><p>Dlatego Przemku, jeśli to czytasz i chcesz wejść do IT - to wiedz, że bez angielskiego - ani rusz!</p><h2>Podsumowanie</h2><p>I to by było na tyle. Takie rady dałbym <strong>sobie</strong> dzisiaj, gdybym miał zaczynać jeszcze raz w IT. Banalne? Kontrowesyjne? Nudne? Dajcie znać w komentarzach ;)</p></content:encoded>
</item>
<item>
<title>
<![CDATA[ Uwaga na fixtury w cypress.io ]]>
</title>
<description>
<![CDATA[ Dzisiaj opowiem wam krótką historię o błędzie, który kosztował mnie dwa dni poszukiwań. Błędzie, który koniec końców okazał się czymś bardzo trywialnym, a czas który spędziłem na debugowaniu… ]]>
</description>
<link>https://przemuh.dev/blog/watch-out-for-fixtures-in-cypress</link>
<guid isPermaLink="false">https://przemuh.dev/blog/watch-out-for-fixtures-in-cypress</guid>
<pubDate>Fri, 26 Jun 2020 00:00:00 GMT</pubDate>
<content:encoded><p>Dzisiaj opowiem wam krótką historię o błędzie, który kosztował mnie dwa dni poszukiwań. Błędzie, który koniec końców okazał się czymś bardzo trywialnym, a czas który spędziłem na debugowaniu spowodowany był niedokładnym komunikatem o błędzie.</p><h2>Hej Przemek! Czy mógłbyś mi pomóc?</h2><p>Kilka dni temu, zauważyłem, że nasze testy VRT (Visual Regression Tests) zaczęły się sypać dla jednego przypadku. Poprosiłem moją koleżankę z zespołu, Monikę, aby rzuciła na to okiem. Monika przyjęła wyzwanie i bezzwłocznie zaczęła szukać rozwiązania. Po całym dniu bezowocnych poszukiwań powiedziała, że nie ma pojęcia dlaczego to nie działa. Lokalnie test przechodził za każdym razem, niestety na naszym GitlabCI było zupełnie odwrotnie. Dziwna sprawa co nie? Jak to jest, że &quot;u mnie działa&quot; a na CI już nie? Zrezygnowana Monika poprosiła mnie o pomoc. Po dwóch dniach kombinowania, commitowania, wypychania, czekania i sprawdzania w końcu się udało.</p><h2>Fake server</h2><p>W naszej aplikacji do testów wykorzystujemy różne narzędzia. Do unit testów mamy <a href="https://jestjs.io/" target="_blank" rel="nofollow noopener noreferrer">jest</a>. Do testów E2E mamy <a href="https://docs.pytest.org/en/stable/" target="_blank" rel="nofollow noopener noreferrer">py.testa</a> z bindigami do webdrivera. Mamy też testy UI, które sprawdzają naszą aplikację pod kątem integracji pomiędzy komponentami, stronami czy widokami. Niedawno wprowadziliśmy także VRT - wizualne testy regresyjne. Te dwa ostatnie rodzaje testów wykorzystują <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. Jest to świetne narzędzie do pisania wszelakich testów - od unitów do E2E.</p><p>Nasza aplikacja od strony backendowej jest bardzo skomplikowana i ciężko ją postawić w całości na lokalnym komputerze, a nawet jeśli jest to możliwe - kosztuje to wiele pracy i zasobów komputera. Dlatego do testów UI i VRT wykorzystujemy jeden z killer-featureów Cypressa, który pozwala mockować zapytania do API. Cypress wpina się pomiędzy aplikację i żądanie do serwera, a my możemy zdecydować o tym jaką odpowiedź dostanie nasza aplikacja.</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">// Po pierwsze musimy powiedzieć, że stawiamy 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">// Dalej deklarujemy jakie ścieżki chcemy mockować</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> value<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>Więcej o tym przeczytacie w <a href="https://docs.cypress.io/guides/guides/network-requests.html#Stub-Responses" target="_blank" rel="nofollow noopener noreferrer">oficjalnej dokumentacji Cypressa</a>.</p><h2>Fixtury</h2><p>Fixtury to kolejna z funkcjonalności <a href="https://www.cypress.io/" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>, z której korzystamy - szczególnie w testach VRT. Fixtura to nic innego jak pewne dane zapisane w pliku, które możemy wykorzystywać wiele razy. Pomaga to w organizacji testów i zarządzaniu odpowiedziami z naszego <code>cy.route</code>. Żeby załadować fixturę korzystamy z komendy <code>cy.fixture</code>. Jako argument przyjmuje ona relatywną ścieżkę do pliku, znajdującego się w katalogu z naszymi fixturami. Domyślnie katalog na fixtury nazywa się po prostu <code>fixtures</code>, a same pliki z fixturami mogą mieć różne rozszerzenia. Nie muszą też być używane w kontekście <code>cy.route</code>. Więcej o fixturach znajdziecie w <a href="https://docs.cypress.io/api/commands/fixture.html#Syntax" target="_blank" rel="nofollow noopener noreferrer">oficjalnej dokumentacji cypressa</a>. </p><p>Załóżmy, że mamy następującą strukturę plików:</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>Kod, który wykorzystuje fixtury:</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">// Nie musimy deklarować rozszerzenia pliku</span> <span class="token comment">// Cypress spróbuje sam je odgadnąć</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">// Tutaj możemy odczytać dane</span> <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token comment">// Możemy zapisać dane w postaci aliasu ...</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">// Żeby móc go wykorzystać przy mockowaniu network requesta</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>Twórcy cypressa zadbali też o to, żebyśmy nie musieli pisać tak dużo boilerplateu 🔥🔥🔥. Do komendy <code>cy.route</code> jako parametr reprezentujący odpowiedź z serwera możemy podać skrót do fixtury <code>fixture</code> albo <code>fx</code></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>W ten sposób zamockowaliśmy sobie odpowiedzi z serwera, a dane trzymane są w re-używalnych plikach z fixturami. Rewelacja!</p><h2>Gdzie się podział główny bohater opowieści?</h2><p>No dobra, ale gdzie ten błąd, z którym walczyliśmy dwa dni?</p><p>Na potrzeby reprodukcji utworzyłem bardzo prostą aplikację. Na początku wyświetla ona napis <code>Loading…</code>, wykonuje zapytanie do serwera, a następnie podmienia tekst na to, co zwrócił backend.</p><p>Pobieranie danych w starym, dobrym stylu XHR 😎</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">=</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>Do tego napisałem 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>Oraz utworzyłem plik z fixturą w pliku <code>fixtures/exampleFixture.json</code> z taką zawartością:</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">Hello</code></pre></div><p>Czy widzisz już gdzie leży błąd?</p><p>W odnalezieniu przyczyny problemu pomógł mi zrzut ekranu, który cypress wykonuje w momencie kiedy test nie przejdzie. Kolejny killer feature 🔥.</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.25%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAACN0lEQVQoz22SXU/TYBiG+VXCgMFW1nbA9A8YQYmRA6Iy2JgmfhxwQgiHgBxs3UKi8Zd4akKCUT7GBDrGhHVdP1YYePm2AhtokytP3pMrz93n7jIaLsWjU/YrVQ4Eh9VTyhWdsl4im82Sy+VIJpMMRCJIsooUkwNiiooSj9MXHuT5i5fk83mWl1foatVN7I1vuJtfaW5+wdvaoFUqYR78JKtpaIJUKoUsywyPJBgRjI7eJ5F4IGaCqBRjZmaWQqHA6uoqXWcNB2Nnn3pRxzo8wT6u4wiMykmwnS9Mp9MoikJMbBjq7Q/oCfUxFFOIRIdEgpkOoWFS/17E3NqjUdzH1o9xBIZeIesLcxpzc3NIksTY+BO0wjpafp2Pnz4zMfGUvv6B2xteisiXO0UutnfF3KX1Yxt2i2LL6o0wk8kQDofJvHpN5/f23XvudYeYTaXbwgvLxiz/wjyu0bjCrlmYNYOcOIof+a7w95XwzX+FbhPXdjFsD8NyqVsODUdMoy28jvxo7DE5rRDwQfBwfIL+fyK7Lpeug930sGxbyGws174lbB9Fobunl+5QLz1ixq6OMt15lJaIbOhlatUqXsPiXEjPHQfz5DToYacwPjwiqjN6g/8ejESFMNkWepZFvXwk/l2VM0NURohcgb5XYm1tLajOtVCNDyP7hVbjAf47EE5PB8VeWRHFdkTkpufhCpymi/8+a7U41HUWFhZYXFxkamoKVVVvCWXlrzASlXg2OcnS0hLz8/P8AXOxcxI9R2CUAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Zrzut ekranu dla testu" title="Zrzut ekranu dla testu" 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">Zrzut ekranu dla testu</figcaption> </figure></p><p>Czy teraz domyślasz się gdzie był błąd?</p><p>Moją uwagę przykuł komunikat o tym, że route został co prawda zestubowany, ale odpowiedział kodem <code>400</code>, a nie <code>200</code>, co spowodowało błąd w kolejnej komendzie oczekującej na element z tekstem &quot;FolderA&quot;. Przypominam, że &quot;lokalnie&quot; ten test nie miał z tym problemu 😉.</p><h2>Literówka i systemy plików</h2><p>Nasz błąd, który próbowaliśmy rozwiązać z Moniką, polegał na banalnej literówce. Nazwa pliku z fixturą zapisana była camelCase, natomiast w kodzie testu mieliśmy wszystko małymi literami.</p><p><code>exampleFixture.json</code> vs <code>cy.route(&quot;/api&quot;, &quot;fixture:examplefixture&quot;)</code></p><p>No ok, ale dlaczego to działało lokalnie, a nie działało na CI?</p><p>99% naszego zespołu frontendowego pracuje na MacBookach podczas gdy GitlabCi odpala testy w kontenerze dockerowym, który opiera się na Linuxie. Co to ma wspólnego z fixturami i naszą literówką? Otóż system plików używany domyślnie w Linuxie jest caseSensitive (zwraca uwagę na wielkość liter w nazwach plików). MacOS oraz Windows domyślnie opierają się o system plików, który caseSensitive nie jest. Co to oznacza w praktyce?</p><p>Na Linuxie możemy utworzyć pliki o tej samej nazwie, ale pisane nieco inaczej np.</p><ul><li>myAwesomeFile.js</li><li>myawesomefile.js</li></ul><p>Linux potraktuje je jako dwa osobne pliki. Natomiast Windows i MacOS nie pozwolą na stworzenie drugiego pliku o tej samej nazwie (pisanej np. camelCase). Przy próbie odczytu pliku np. w node.js na MacOS nie ma znaczenia czy wpiszemy myFixture czy mYFiXtURe - plik zostanie załadowany. Na Linuxie natomiast dostaniemy błąd odczytu - plik nie został odnaleziony.</p><h2>Sprawdzam</h2><p>I faktycznie tak jest. Gdy zmodyfikujemy kod naszego testu w ten sposób:</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>Test na Macu przechodzi zawsze. Na Linuxie natomiast, cypress logger pokaże nam stuba z kodem <code>400</code>:</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.9254327563249%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAABBElEQVQoz52R3W6DMAxG8/5XILjlKRBXPEq10VGtLeWvZMQJlPCNqNDSat1glo4sJfaR47AkLXDIKmQZR5Jz5EUFUZ9RFDk45xBCoK5rKKXQti2apnnAnJk7U2dgaVbiY3dEyQm1vECqoam90nUdpuj7/lemYF03SCRB6wt6rYes73ksnDe8iknMiAscNzHSaIdT9Ikk2iPdHnBOigfhUljDvyDftqAovvMeQ50y6BUT3p4siECNAqk5EjSsYdrhKiGNv/OMOZ8LFz+ZxuYbZuJRaoRrZFehETwhRvF/gv0kk1KiqiqEYQjf9xEEwSJM7UthWZbwPA+2bcNxnD9xXReWZeEbYpJEWPqWc/gAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Zrzut ekranu z kodem 400" title="Zrzut ekranu z kodem 400" 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">Zrzut ekranu z kodem 400</figcaption> </figure></p><p>a w konsoli dostaniemy błąd:</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>Ale jak to “błąd nie jest w cypressie tylko w mojej apce”? Przecież apka działa, test na Macu też przechodzi - więc o co kaman?</p><p>Spróbujmy skorzystać z naszej fixtury za pomocą <code>cy.fixture</code> zamiast skrótu:</p><div class="gatsby-highlight" data-language="javascript"><pre class="language-javascript"><code class="language-javascript"><span class="token comment">// Celowy błąd w nazwie fixtury</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;fixture:examplefixture&quot;</span><span class="token punctuation">)</span> <span class="token comment">// Bonusem takiego podejścia jest możliwość użycia aliasu do odczytu danych</span> <span class="token comment">// zamiast hardcodować &quot;Hello&quot; w teście</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>Jaki teraz dostaniemy błąd?</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>👏 no i taki komunikat błędu jest o wiele lepszy. Od razu wiemy gdzie należy szukać 😎</p><h2>Podsumowanie</h2><p>Z tej historii można wyciągnąć dwa wnioski:</p><ul><li>mała literówka może przysporzyć Ci kilka dni debugowania</li><li>jesteś tak dobry jak komunikaty o błędzie z twojego test runnera 😉</li></ul><p>Myślę, że cypress mógłby zwracać “poprawny” komunikat o błędnej fixturze zamiast <code>CypressError</code>- dlatego zgłosiłem issue, którego <a href="https://github.com/cypress-io/cypress/issues/7818" target="_blank" rel="nofollow noopener noreferrer">status możecie śledzić tutaj</a>.</p><p>Dzięki, że wytrwaliście ze mną w tej historii do końca. Lecę spróbować naprawić issue, które sam zgłosiłem 😉. Może uda mi się dołożyć kolejną cegiełkę do OpenSource i sprawdzić by Cypress, który jest niesamowitym narzędziem, stał się jeszcze lepszy 😁</p></content:encoded>
</item>
<item>
<title>
<![CDATA[ Zatrzymaj czas z cy.clock ]]>
</title>
<description>
<![CDATA[ Dziś pokażę Wam jak za pomocą jednej komendy zatrzymać czas. Niestety, tylko w testach napisanych w cypress.io. Jeśli znasz skuteczną metodę na zatrzymanie czasu tak ogólnie - w życiu, to napisz do… ]]>
</description>
<link>https://przemuh.dev/blog/stop-the-time-with-cyclock</link>
<guid isPermaLink="false">https://przemuh.dev/blog/stop-the-time-with-cyclock</guid>
<pubDate>Wed, 22 Apr 2020 17:00:00 GMT</pubDate>
<content:encoded><p>Dziś pokażę Wam jak za pomocą jednej komendy zatrzymać czas. Niestety, tylko w testach napisanych w <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io.</a> Jeśli znasz skuteczną metodę na zatrzymanie czasu tak ogólnie - w życiu, to napisz do mnie. Ostatnio krucho u mnie z tym czasem, dlatego każda wskazówka jest na wagę złota. Dobra 🙂 koniec z tym śmieszkowaniem, bierzmy się do roboty.</p><h2>Opis testowanej aplikacji</h2><p>Na początek trzeba mieć co testować. Nasza aplikacja będzie do bólu prosta. Wyświetlamy czas wejścia na stronę oraz licznik, pokazujący ile czasu na niej spędziliśmy.</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>Możemy przejść teraz do napisania testu.</p><h2>Testujemy!</h2><p>Dla naszej aplikacji chcemy napisać prosty test case sprawdzający:</p><ul><li>czy poprawnie wyświetlamy czas wejścia,</li><li>czy licznik zwiększa swoją wartość co sekundę.</li></ul><p>Spróbujmy w ten sposób:</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>Na pierwszy rzut oka kod testu wygląda w miarę ok. Niestety nie przechodzi on weryfikacji 😢</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.08787346221442%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAsSAAALEgHS3X78AAABxklEQVQoz42S627TQBCF8/4PUZWW8g4gIQFtkFIgKGnTxEnbOHHs3G92fNnr4Xg3CIk/YOnTWY9nxme00/hyeY35ZoP8dIK1FlpraGOcmrP+C6WUy83zHI2vF1cQUiA7HlGVJYqicFpSCyYYFvzPI0SF3W6HRvPiGnXJ7phin2UopUJBSqXdudIGwlhiPPyBQ535/c687ESHzcu3UEJiO3hG/NBHsdoimyZIw4gaI62ZeM2iBPly42Geoz4vNiio2XbPhlfvYHYH6M4T5MdbiA+foTo9lHctyO8dVO8/Qd7do2reQ7Ta0I8BdOsnFGPqtgXdfoB6HMD2higXKzZ8cwOTnaCCF6ghoVM5GEH2R1CjMeSPrudbB6r/DEP3ZhyRKcwrCWeMxTDMLZYr71DvDxDtLkT3yanqBcB8RdYAxwLHwWINGy9gZ3Oy8MRnTVacoIXTJDo7zAuIIZ2NJ1CzBPI1dIhghKofuLgMJ9B0oGYxVJz4vHDqvomXMW91j5KX6hvydiTHlePQF7OZL/CNVDSD5RSWm2APxz/sjz5OkHJD0tTfsq0XeLmGnEaQtC2jGJaLjnoHuTqQEqgqoPyLymPdWaBkzS92HfDmxAi65wAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Błąd przy pierwszej iteracji testu" title="Błąd przy pierwszej iteracji testu" 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">Błąd przy pierwszej iteracji testu</figcaption> </figure></p><p>Gdybyśmy zamiast liczby sprawdzali sformatowaną datę np. 22-04-2020 pewnie nie byłoby problemu. No ale nasz klient zażyczył sobie formę milisekund - #jakŻyć? W takim wypadku sprawa się nieco komplikuje.</p><p>Z pomocą przychodzi nam komenda <code>cy.clock</code>. Nadpisuje ona globalne funkcje zwiazane z czasem i pozwala na kontrolowanie ich poprzez np. <code>cy.tick()</code>.</p><p>Funkcjonalności na które wpływa <code>cy.clock</code> to:</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>Więcej informacji znajdziecie w <a href="https://docs.cypress.io/api/commands/clock.html" target="_blank" rel="nofollow noopener noreferrer">oficjalnej dokumentacji cypress.io</a>.</p><p>Spróbujmy teraz dodać <code>cy.clock</code> do naszego testu:</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>Nadal dostajemy błąd, ale kiedy wczytamy się w jego treść zauważymy różnicę:</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>O co chodzi z tym zerem? Ponieważ czas reprezentowany jako timestamp to liczba sekund od rozpoczęcia epoki Unixa (unix epoch), dlatego wartość <code>0</code> reprezentuje 1 stycznia 1970. Można by zapytać co stanie się w momencie przekręcenia licznika, czyli 19 Stycznia 2038 roku - ale to już rozkmina na inny wpis 🙂.</p><p>Wywołanie komendy <code>cy.clock()</code> bez argumentów ustawia czas dla naszej aplikacji na 1 Stycznia 1970. Dlatego po prawej stronie asercji otrzymaliśmy <code>0</code>. Żeby to zmienić możemy podać porządaną wartość daty jaką chcemy ustawić:</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>Dzięki temu być może magicznym trafem uda nam się zgrać z datą ustawioną w linijce odpowiadającej za asercję <code>should(&quot;have.text&quot;, Date.now().toString());</code>. To zależy jaki szybki mamy komputer 😄. Żeby poprawnie napisać test należy pamiętać o tym, że <code>cy.clock</code> nadpisuje czas w naszej aplikacji a nie w testach. Dlatego wywołanie <code>Date.now()</code> w momencie asercji powinniśmy zamienić na wartość zapisaną wcześniej w clock.</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>Test przechodzi - zawsze! - sukces!. Jest tylko jedna różnica w działaniu aplikacji. Przed wykorzystaniem <code>cy.clock</code> nasz licznik w testach zmieniał się co sekundę. Po wykorzystaniu <code>cy.clock</code> licznik zatrzymał się na <code>0</code>. Akurat dla nas jest to porządany efekt. Nie tylko ustawiliśmy porządaną datę ale też zatrzymaliśmy czas.</p><p>Aby ruszyć czas o porządaną wartość wykorzystamy komendę <code>cy.tick</code>. Jako argument przyjmuje ona ilość milisekund, o które chcemy przesunąć nasz czas.</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>Sukces 🎉! Mamy test sprawdzający czas wejścia oraz działanie licznika.</p><h2>A co jeśli chcielibyśmy ustawić tylko datę? 🤔</h2><p>Bardzo dobre pytanie. Czasami może jednak zdarzyć się tak, że chcemy wpłynąć tylko na obiekt <code>Date</code> pozostawiając w spokoju <code>setTimeout</code> i inne funkcje związane z czasem. W takim wypadku należy do wywołania <code>cy.clock</code> dodać drugi argument - tablicę funkcji czasu, które chcemy nadpisać</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>W tym przypadku ustawiamy datę/godzinę na 22 kwietnia 2020 00:00 UTC (miesiące w <code>Date</code> liczymy od <code>0</code>, dlatego <code>3</code> to Kwiecień 🙂). Przy takim wywołaniu nie wpływamy na pozostałe funkcje czasu takie jak <code>setTimeout</code> itp.</p><hr/><p>To tyle na dziś. Uzbrojeni w tę wiedzę możecie teraz zatrzymać czas w swoich testach 😉</p><p>Powodzenia!</p></content:encoded>
</item>
<item>
<title>
<![CDATA[ 3 kroki do niesamowitych raportów w cypress.io ]]>
</title>
<description>
<![CDATA[ W tym wpisie nauczymy się jak wygenerować rozbudowane raporty z testów napisanych w cypress.io. Wzbogacimy je także o zrzuty ekranu, co powinno pomóc w znacznie szybszej naprawie potencjalnych bugów… ]]>
</description>
<link>https://przemuh.dev/blog/3-steps-to-awesome-test-reports-with-cypress</link>
<guid isPermaLink="false">https://przemuh.dev/blog/3-steps-to-awesome-test-reports-with-cypress</guid>
<pubDate>Wed, 18 Dec 2019 00:00:00 GMT</pubDate>
<content:encoded><p>W tym wpisie nauczymy się jak wygenerować rozbudowane raporty z testów napisanych w cypress.io. Wzbogacimy je także o zrzuty ekranu, co powinno pomóc w znacznie szybszej naprawie potencjalnych bugów 😄 Wszystko czego potrzebujemy to trzy proste kroki.</p><h2>Egnyte + testy = ❤️</h2><p>Utrzymanie możliwie jak najwyższej jakości produktu to jedno z naszych priorytetów w Egnyte. Dlatego kochamy testować 😍. Nasze aplikacje do małych nie należą, dlatego ich ręczne testowanie było by dla nas bardzo wyczerpujące. W związku z tym, automatyzacja testów i technika Continous Integration są naszymi najlepszymi przyjaciółmi. Piszemy wiele rodzajów testów: unit, integration, end-to-end, modułowe itd. Ale to nie nazwy są tu najważniejsze. To co się liczy najbardziej, to pewność, że gdy widzisz zielony pipeline na Jenkinsie, to potencjalnie nie popsułeś żadnej części systemu.</p><p>Więc w czym problem? Wiedzieliście, że testy nie zawsze przechodzą? 😱 I to jest jak najbardziej ok :) (przynajmniej do czasu znalezienia przyczyny). Nie musimy od razu panikować. Po pierwsze, spokój. Tylko spokój może nas uratować. Widzimy czerwony pipeline - wchodzimy na Jenkinsa, sprawdzamy co jest nie tak i fixujemy. To tyle. <strong>Problem polega na tym, że raport z testów na Jenkinsie to bardzo często, zwykły tekst wraz ze zrzutem ze stack-trace.</strong> Taki raport jest jak najbardziej ok dla unit testów albo testów integracyjnych dla naszych komponentów Reactowych, połączeń z Reduxem, itd. Z drugiej strony, taka ściana tekstu nie zawsze jest przydatna dla testów uruchamianych w przeglądarce (full UI). Spójrzmy na fragment raportu dla przykładowego testu:</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.3125%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAJCAIAAAC9o5sfAAAACXBIWXMAAAsSAAALEgHS3X78AAABDUlEQVQoz2VQi3KDIBD0/3+x8YGiBwfFaKNoFNJVO7Y2OzfLzTJ7r8Q5R0Te+xDCuuNIlmVZ3xCuSNq27bruuQOGk89knud38aiVWGvR3PsR8H7aMG84HiiHf/+YzloHEniMMUopIkXakW4lOeJ7rVyjXU2bQtyC4YoxXsZGAWZuiIuSb7lOC05LcysQ/JFzJiyYbT/657KGGF+IE5sZnbVmWatKKlFpIS1KZMKkYuOjImbhz/4xTLjjrxmbwMz75Jq5JlNKjWiUrWpGsL279su63rg+/u0LM9RSci6UkDovVVpQJhSSosIiyliHA4cQ1/Vi+zFjbzTHmXHYYZwRj2Eed8aQYChYGEqI//3fzUwBr8ba6BcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Raport z testów na Jenkinsie" title="Raport z testów na Jenkinsie" 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">Raport z testów na Jenkinsie</figcaption> </figure></p><p>Tak wygląda podstawowy raport z testu na Jenkinsie dla jednego z naszych produktów (<a href="https://www.egnyte.com/protect/content-governance-solution.html" target="_blank" rel="nofollow noopener noreferrer">Egnyte Protect</a>). Do pisania testów integracyjnych-UI, korzystamy z bombowego 💣 narzędzia <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress.io</a>. Muszę przyznać, że Cypress wraz z rozszerzeniem <a href="https://github.com/testing-library/cypress-testing-library" target="_blank" rel="nofollow noopener noreferrer">cypress-testing-library</a> robią wspaniałą robotę jeśli chodzi o tzw. error messages. Na pierwszy rzut oka widać, że cypress nie potrafił znaleźć elementu z zadaną wartością tekstową. <code>Timed out retrying: Expect to find element: findByText(&quot;...&quot;), but never found it</code>. Ok, ale jaki był stan wizualny naszej aplikacji? Jako developer biorący udział w projekcie Egnyte Protect, wiem, że element którego szukamy powinien znajdować się w dialogu. Ale czy dialog został otwarty? A może to tylko literówka? Tyle pytań a tak mało odpowiedzi. Jeśli teraz chcielibyśmy sprawdzić o co biega, musielibyśmy uruchomić testy lokalnie i zobaczyć stan wizualny naszej aplikacji. Dopiero wtedy dowiedzielibyśmy się, że mamy doczynienia (spoiler alert) z literówką :)</p><p>A co by było, gdyby zamiast ściany tekstu, pokazać zrzut ekranu z aplikacji?</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.487179487179496%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsSAAALEgHS3X78AAABYElEQVQoz42Q2U7CQBSG+yosGr0FSmm8o8x0ppoobVktEhCe0bfQxDsiILK0oJUlYLWrDhaIgYjOfHMuTv4vZ2aoY5Y9YtkwwxwwzGEiQWooFgtEo8E1gUgkTNNB0qTpUDxOMhsoxAsICRh9VywIWEhfpCVR8pFlOUO2JOPzixOIIMJ+0leoFEQreMwBKIri7d19t6c12912pz9Qn7Wh/vI61UZ6uVJJcinA441CAYh8IOkCmMlkHvsD0/Fcx/38sSzbrtfrHJcisY2yLZPLqqpK0p67kj3PW8qWVavX/pAlSdK04cb5rwwgBoDP5XK97pNpWfPF24dp2Y5LjuO4hvFeq+2RSRfAfD7f6zT18Yz80GS2WBimabtktG3tffNyMuSz2WzjoTVfGOPJjKCPp9PJtKmNbhqtavWafPZv8hKEhUJRUUrlS+VKWVMoluSCIpyebYW3ZUKSA7twHNhNfgEqfifvvlhbZQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Zrzut ekranu z testowanej aplikacji" title="Zrzut ekranu z testowanej aplikacji" 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">Zrzut ekranu z testowanej aplikacji</figcaption> </figure></p><p>Wow! Teraz wiemy, że dialog został otwarty, i że tekst w nagłówku jest niepoprawny! Dostaliśmy nieoceniony kontekst, potrzebny do zdiagnozowania błędu, od razu w raporcie wzbogaconym o zrzut ekranu.</p><p>Zatem, jak to osiągnąć? Jak dodać zrzuty ekranu do raportu z testów? I w końcu - jak taki raport wygenerować? Jedziemy z koksem!</p><h2>Raport HTML pędzi z pomocą!</h2><p>Cypress bazuje na frameworku <a href="https://mochajs.org/" target="_blank" rel="nofollow noopener noreferrer">mocha.js</a>. To dla nas świetna wiadomość - ponieważ mocha.js to bardzo dojrzały projekt, posiadający mnóstwo rozszerzeń. Rezultaty testów generowane są w mocha.js za pomocą tzw. reporterów. Taki reporter możemy napisać samemu, albo możemy skorzystać już z istniejącego np. <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a>. Jak sama nazwa wskazuje, generuje on <strong>AWESOME</strong> raporty! Badum tsss.</p><p>Zobaczmy teraz, jak możemy zintegrować <a href="https://www.npmjs.com/package/mochawesome" target="_blank" rel="nofollow noopener noreferrer">mochawesome</a> z <a href="https://cypress.io" target="_blank" rel="nofollow noopener noreferrer">cypress</a> w celu wygenerowania raportu HTML wraz ze zrzutem ekranu dla testów zakończonych błędem. Aby zwizualizować wszystkie zmiany, które wprowadziłem na potrzeby tego wpisu, skorzystałem z przykładowego repozytorium <a href="https://github.com/cypress-io/cypress-example-kitchensink" target="_blank" rel="nofollow noopener noreferrer">cypress-example-kitchensink</a>. Integrację przeprowadzimy w trzech prostych krokach. Do dzieła!</p><h2>Krok 1 - setup reportera</h2><p>Po pierwsze, musimy zainstalować odpowiednie reportery. Tak! Dokładnie - liczba mnoga - reportery. Oprócz generowania raportów HTML, nadal chcielibyśmy wyświetlać wyniki w konsoli, a być może także w specjalnym formacie JUnit XML. Dla każdego, z tego typu wyników, musimy mieć osobny reporter. W tym celu skorzystamy z paczki <a href="https://www.npmjs.com/package/cypress-multi-reporters" target="_blank" rel="nofollow noopener noreferrer">cypress-multi-reporters</a>, która umożliwia skorzystanie z wielu reporterów dla cypress.io. Oprócz tego musimy zainstalować jeszcze pakiet <code>mocha</code>, oraz oczywiście <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>Lub jeśli korzystacie z <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> -D mocha cypress-multi-reporters mochawesome</code></pre></div><p>Następnie w pliku konfiguracyjnym <code>cypress.config</code>, musimy wskazać reporter, z którego będziemy korzystać:</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>Pole <code>configFile</code> wskazuje na plik konfiguracyjny <code>reporter-config.json</code>, który zawiera opcje dla poszczególnych reporterów. Plik ten powinniśmy dodać do repozytorium. Zobaczmy teraz jak wygląda jego zawartość:</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>Ustawiamy folder docelowy, do którego trafią nasze wyniki. Chcemy, aby zostały one zapisane w formacie JSON, osobno dla każdego pliku z testami. Dlatego ustawiamy flagę <code>html</code> na wartość <code>false</code>. Cypress potrafi uruchamiać testy równolegle, dlatego aby nie nadpisać wygenerowanych już wyników, ustawiamy flagę <code>overwrite</code> na <code>false</code>.</p><p>Spróbujemy teraz uruchomić nasze testy za pomocą komendy <code>npm run local:run</code>.</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>Jak widzicie, w konsoli dostaliśy wyniki dla poszczególnych testów (domyślny spec-reporter). Zaraz pod nimi widzimy informację na temat utworzonego pliku <code>mochawesome_008.json</code>. Każdy plik z testami wygeneruje nam osobny plik z wynikami.</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:142.0245398773006%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAcCAIAAADuuAg3AAAACXBIWXMAAAsSAAALEgHS3X78AAAB90lEQVQ4y5WU227TQBCGc1PUCiHuUGkpEHsTJz7Eu7PrdcINRa3UCNRKXPEMiEtS1Mu+QHlmfnuSSULSrjsarSaHz/+cvL2XX/+8+nb77sd9/euv+/nw+vruxdXvw/niCT+aLw6uFm9ubntn7uJ9dalmc33xPf18fUpf3urzE9Oc7Ih3/bg8H36a98aDKB2qIh04M5lk40T1RypqvY+f4AiSuD+MPmy6+nimi7SXJKMkGWudkM0MuaqqfF27ylvr+Ky8z/I8jpVSA/EoistSA4aNAFubE1kYeJzUfnDOGYOUJnFLi0VRVJblEjYGcEaEPzfizOCs6xqPCMArZSdG7QfOYi+ML9cwUW4MSdpQ5hhP6ahsuU6p2XuPoCiKXTjP8yVclksYIADmWRnxrjLbVsM4bTatNbDpdLpXeQtG2s4VzWRXxm3jzPNmzk/CxmRaGyJ4Y1DmtNG5MMyjYikeFQzx3lFtwWgY0bJmzlZGhTNcM5Q3YVbmOMuycLdlPXk32BCnadpFWSZlBcbjOsHtkjhZEl4yKAe6LQ2TOk1rzD9DGa+hKHMLAsqr9bT/jYr3LNBtXs+2wGZD5TLhFoQ3DDVrTVwkTi6Ys+hY8/oa2sw8AMv7LO+wiHdSXu32Om2+A8M1o9veFx5Xdl3PZjPcAdw5PEg9bv8A9dAma9wEJYgAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Lista wygenerowanych plików z wynikami testów" title="Lista wygenerowanych plików z wynikami testów" 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">Lista wygenerowanych plików z wynikami testów</figcaption> </figure></p><p>Jesteśmy gotowi do implementacji następnego kroku.</p><h2>Krok 2 - generowanie raportu</h2><p>Zebraliśmy pliki z wynikami testów. Teraz, musimy połączyć je w jeden plik i na jego podstawie wygenerować raport HTML. W tym celu skorzysamy z narzędzia <a href="https://www.npmjs.com/package/mochawesome-merge" target="_blank" rel="nofollow noopener noreferrer">mochawesome-merge</a>. Zainstalujmy je!</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> -D mochawesome-merge</code></pre></div><p>Teraz, dodajmy skrypt npm, który będzie odpowiedzialny za uruchamianie narzędzia do łączenia wyników.</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>Flaga <code>reportDir</code> mówi o tym, w jakim folderze znajdują się pliki do połączenia. Wynik łączenia wyrzucony będzie na standardowe wyjście (w naszym przypadku będzie to konsola) dlatego, przekierowujemy wyjście do pliku <code>mochawesome-bundle.json</code>. Jedna uwaga: wynik łączenia powinien znajdować się w innym folderze niż pliki z poszczególnymi wynikami testów. W przeciwnym wypadku dostaniemy błąd.</p><p>Po zmergowaniu wyników jesteśmy gotowi do wygenerowania raportu HTML. Potrzebujemy do tego jeszcze jednej paczki <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> -D mochawesome-report-generator</code></pre></div><p>Tak jak w przypadku łączenia, utworzymy sobie teraz skrypt, który będzie uruchamiał narzędzie do generowania raportu HTML:</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>Słówko marge to skrót od <strong>M</strong>och<strong>a</strong>wesome<strong>R</strong>eport<strong>GE</strong>nerator - to tak w ramach gdybyście się zastanawiali :)</p><p>Po poprawym wykonaniu skryptu, nasz raport HTML powinien pojawić się w katalogu <code>cypress/results/html</code>.</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.6875%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAIAAADwazoUAAAACXBIWXMAAAsSAAALEgHS3X78AAAA80lEQVQoz5WR227CMAyG+/IDaRcUuoNE+niM0m6Q2M45abd5RWJotEL7ZOUi8qf8dorHl9e1qFdbsdhUD+vNonpaVs/Ln3OiuLkUotyKsq4rIYqcU0ohpxhz0J6+7pFS5H6umGIBgGcUwOHUdvL9qI6oUTtDlqyzbiSOBO/xAmBBFzQ13f7tY8eyQvDehxjiFZyQT7riV0YiCScyZJzxwecppmVNWqJssbOGc9rPGTj2rNzIhiex1g7DcGvylbdWz8l72RAQ76Lv+0k5ODcfG1qOzTKPx7/yj9gKVTfOjIS82Ek5hjD78kG1hjQAnMe+5U/sb8tlYyeUKfRVAAAAAElFTkSuQmCC&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Widok raportu HTML" title="Widok raportu HTML" 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">Widok raportu HTML</figcaption> </figure></p><p>Została tylko jedna, mała rzecz. Dodanie zrzutu ekranu do wyniku testu zakończonego błędem.</p><h2>Krok 3 - zrzut ekranu</h2><p>Cypress automatycznie generuje zrzuty ekranu dla testów, które z jakiegoś powodu nie przeszły. Jest to domyślne zachowanie, które można wyłączyć. Wygenerowane obrazki zbierane są w folderach, które przyjmują następujacą strukturę:</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>Rozważmy sobie test, który znajduje się w katalogu <code>examples/actions.spec.js</code>:</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>Wygeneruje on zrzut ekranu, który znajdzie się w następującej strukturze folderów:</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.424242424242426%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAADCAIAAAAcOLh5AAAACXBIWXMAAAsSAAALEgHS3X78AAAAVklEQVQI15WOUQrAMAhDe5Y6LLaKrNr7X20ZZfvbx95HEDGJZYwx51xrmRkRHX8orTV3jwikMDN9U2t9ddfc5tPRnPBnpqoipfcuIhi2Auzxmj7gAOYL0ZwcBDCNqUcAAAAASUVORK5CYII=&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Struktura folderów ze zrzutem ekranu" title="Struktura folderów ze zrzutem ekranu" 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">Struktura folderów ze zrzutem ekranu</figcaption> </figure></p><p>Ok, to jak połączyć te dwa elementy: zrzut ekranu wygenerowany przez Cypress i wynik testu wygenerowany przez mochawesome reporter?</p><p>Po pierwsze, skopiujmy sobie nasze zrzuty ekranu do folderu, w którym trzymamy nasz raport HTML. W tym celu napiszemy sobie kolejny skrypt npm:</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>Następnie, w pliku <code>cypress/support/index.js</code>, napiszmy kawałek kodu odpowiedzialny za nasłuchiwanie na event <code>test:after:run</code></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>Aby dodać zrzut ekranu do wyniku testu, musimy skorzystać z metody <code>addContext</code> z pakietu <code>mochawesome</code>. Metoda ta, przyjmuje dwa argumenty: obiekt z testem oraz tzw. context. Jeśli context jest poprawnym adresem URL (może być lokalną ścieżką) do obrazka, wtedy obrazek ten będzie wyświetlany pod wynikiem testu. Oczywiście to nie musi być tylko obrazek. Po więcej info odsyłam do <a href="https://www.npmjs.com/package/mochawesome#adding-test-context" target="_blank" rel="nofollow noopener noreferrer">dokumentacji</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>Wszystko spoko, ale skąd mamy wziąć ten <code>imageUrl</code> ? ...</p><p>Pora na odrobinę magii</p><p><img src="https://media.giphy.com/media/12NUbkX6p4xOO4/giphy.gif" style="width:50%;margin:auto;display:block"/></p><p>Żartuję :) Skorzystamy z API jakie daje nam mocha, a dokładniej z tzw. runnable object. Jak widzieliśmy wcześniej, Cypress generuje nazwy obrazków na podstawie struktury testu. Musimy to teraz odtworzyć:</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>Od tej chwili, jeśli nasz test zawiedzie, w naszym pliku wynikowym zostanie doklejony context z adresem do wygenerowanego obrazka:</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>Co więcej - obrazek ten zostanie dodany do raportu HTML.</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.49999999999999%;position:relative;bottom:0;left:0;background-image:url(&#x27;data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAALCAYAAAB/Ca1DAAAACXBIWXMAAAsSAAALEgHS3X78AAABuklEQVQoz41S21LCMBTs/z+rI6CgUvSZYQb+Qn1BcBS8l0tL0iZtAq1kPUmRQeXBzOxs0p5s9ly8arOJ+tUVjs/PcXBaJ5zisFLBYb2O6kUTJ34LNd9HjeIqdK42fYejxpmLP2o0UGu1zFnrEv6Frz0YA6zXcGyX5eUSeZY5FEqj0LqM+b1srMPalEejPcY5OMEyY3bPEM3nGI2f8Tgab/H2/gEex4iTBGmaOjDGynucm5j+kY72EgrYQiSQiQCLQkyiKSIWkQCJyBhCCmTkWJHb1WoFTWzvCCGIhZFSWl569oPDRlTSPpxN8BEFYAknAYW8yEvkJYqi+CWYGPtgFC3MT8ENC0olzVIqpXYXFdWxdJZDU30tMqW+xUojUpowDLFXUJF9LiRkplzN3t6nmIcLSlnZtCh9SQ9mu4IuZRLc7zCTKUaTKcYBCS0YgsmcBJmrIXXSYUkud1LGRnC/Q81jJA9PEPcjCHKmVwXWnz/Hxtbx34KKWNI4GGpAOJvh+uYWd3cDDIdDDAYD9Pt9BEHgRmenhn8F3Ws0T5oaYptg18vrKzqdDnq9HrrdruN2u+1EbcPs/O0KfgFmmzLlPg5ZHQAAAABJRU5ErkJggg==&#x27;);background-size:cover;display:block"></span> <img class="gatsby-resp-image-image" alt="Raport HTML wraz ze zrzutem ekranu" title="Raport HTML wraz ze zrzutem ekranu" 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">Raport HTML wraz ze zrzutem ekranu</figcaption> </figure></p><p>TADA 🎉 Mamy to!</p><p>Wszystkie zmiany, których dokonaliśmy, możecie zobaczyć w jednym miejscu: <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>Kroki opcjonalne</h2><p>Dobrym pomysłem jest dodanie do <code>.gitignore</code> folderów <code>cypress/results</code> i <code>cypress/reports</code>.</p><p>Fajnie było by też &quot;czyścić&quot; foldery z generowanymi assetami (wyniki, zrzuty, itp.). Jak to zrobić? Zgodnie z &quot;tradycją&quot; dodamy teraz skrypt npm odpowiedzialny za tę akcję:</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>Te trzy literki &quot;pre&quot;, zanczą ni mniej ni więcej, jak to, że skrypt ten odpalony będzie przed wywołaniem skrypu <code>cy:run</code>. Więcej info w <a href="https://docs.npmjs.com/misc/scripts" target="_blank" rel="nofollow noopener noreferrer">dokumentacji npma</a>.</p><p>W użytym, przykładowym repo, zainstalowany został pakiet <a href="https://www.npmjs.com/package/npm-run-all" target="_blank" rel="nofollow noopener noreferrer">npm-run-all</a>. Możemy z niego skorzystać aby uruchomić wszystkie, stworzone do tej pory skrypty w zadanej kolejności (sekwencyjnie):</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>Pozostała jeszcze jedna sprawa do rozważenia. W większości systemów operacyjnych, nazwa pliku ograniczona jest do 255 znaków. Co się stanie jeśli zagnieździmy nasz test wielopoziomo? To proste! Zostanie przycięty :) Cypress sam przycina nazwy wygenerowanych obrazków do 220 znaków. Powinniśmy to odzwierciedlić w naszym kodzie:</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>Niestety jest to tzw. szczegół implementacyjny. Nie wiemy czy programiści cypressa zmienią kiedyś ten numer. Dlatego, lepszą opcją będzie nie zagnieżdżanie testów na tak głębokich poziomach. Polecam lekturę posta od Kent C Doddsa - <a href="https://kentcdodds.com/blog/avoid-nesting-when-youre-testing" target="_blank" rel="nofollow noopener noreferrer">Avoiding nesting when you are testing</a>, w którym pokazuje, do jakich problemów może prowadzić zagnieżdżanie w testach.</p><h2>Podsumowanie</h2><p>Mam nadzieję, że ten artykuł pomógł Wam w poprawnym skonfigurowaniu środowiska pod generowanie raportów HTML z cypressa. Tego typu raporty bardzo pomagają nam, w Egnyte, w szybkim sprawdzeniu gdzie leży źródło problemu - wierzę, że pomogą i Wam :)</p><p>Podsumujmy nasze 3 proste kroki:</p><ol><li>Instalacja i konfiguracja reporterów</li><li>Zebranie wyników i wygenerowanie raportu HTML</li><li>Dekoracja raportu kontekstem - zrzutem ekranu - za pomocą metody <code>addContext</code>.</li></ol><p>Wszystkie niezbędne zmiany, możecie zobaczyć w przystępnej formie pull requesta: <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>. Oczywiście po wygenerowaniu raportu musicie jeszcze go podpiąć pod narzędzie CI. Ale to już historia na osobnego posta. :)</p><p>To jak? Jesteście gotowi na stworzenie swojego raportu HTML z cypress.io?</p></content:encoded>
</item>
</channel>
</rss>