<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>rbardini.com</title>
        <link>https://rbardini.com</link>
        <description>Rafael Bardini's blog</description>
        <lastBuildDate>Mon, 16 Mar 2026 00:22:38 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>CC BY-NC-SA 4.0</copyright>
        <atom:link href="https://rbardini.com/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Web components initialization and lifecycle pitfalls]]></title>
            <link>https://rbardini.com/web-components-initialization-lifecycle-pitfalls/</link>
            <guid isPermaLink="true">https://rbardini.com/web-components-initialization-lifecycle-pitfalls/</guid>
            <pubDate>Tue, 06 Jan 2026 20:08:29 GMT</pubDate>
            <description><![CDATA[<p>I've been writing some vanilla web components lately, and it has made me greatly appreciate how <a href="https://lit.dev/">Lit</a> abstracts away a lot of the quirks in the web platform. Here are a few <em>aha!</em> moments I've had:</p>
<h2><code>isConnected</code> becomes <code>true</code> before <code>connectedCallback()</code> is called</h2>
<p>You can't rely on <code>isConnected</code> to determine if <code>connectedCallback()</code> has already been called:</p>
<pre><code class="language-js">connectedCallback() {
  this.foo = 'bar'
}

doSomething() {
  if (!this.isConnected) return
  console.log(this.foo) // 'bar' | undefined
}
</code></pre>
<p>If you have code that should only ever run after <code>connectedCallback()</code>, you probably want to set your own flag:</p>
<pre><code class="language-js">connectedCallback() {
  this.foo = 'bar'
  this.isInitialized = true
}

doSomething() {
  if (!this.isInitialized) return
  console.log(this.foo) // 'bar'
}
</code></pre>
<h2><code>attributeChangedCallback()</code> is also called on attribute initialization</h2>
<p><code>attributeChangedCallback()</code> is called after each observed attribute is <em>initialized</em> once the element is parsed, even if those attributes never change:</p>
<pre><code class="language-html">&lt;my-element foo=&quot;bar&quot;&gt;&lt;/my-element&gt;
</code></pre>
<pre><code class="language-js">attributeChangedCallback(name, oldValue, newValue) {
  console.log(name, oldValue, newValue) // 'foo', null, 'bar'
}
</code></pre>
<p>The good news is, all initialization calls happen <em>before</em> <code>connectedCallback()</code>. If you only want to respond to actual modifications, you can once again check if <code>connectedCallback()</code> has been called:</p>
<pre><code class="language-js">connectedCallback() {
  this.isInitialized = true
}

attributeChangedCallback(name, oldValue, newValue) {
  if (!this.isInitialized) return
  console.log(name, oldValue, newValue) // unreachable during initialization
}
</code></pre>
<h2><code>observedAttributes</code> can be used as a (property) initializer</h2>
<p>The <code>observedAttributes</code> static property is accessed before any <code>attributeChangedCallback()</code> calls. This means you can hook into it to, for example, dynamically create a property for each attribute before attributes are initialized:</p>
<pre><code class="language-js">static createProperties(attributes) {
  const store = {}

  // map attributes to properties
  attributes.forEach(attr =&gt;
    Object.defineProperty(this.prototype, camelCase(attr), {
      get() { return store[attr] },
      set(value) { store[attr] = value },
    })
  )

  // ensure properties are only created once
  this.createProperties = function noop() {}
}

static get observedAttributes() {
  const attributes = ['foo']
  this.createProperties(attributes)

  return attributes
}

attributeChangedCallback(name, oldValue, newValue) {
  this[camelCase(name)] = newValue
}

connectedCallback() {
  console.log(this.foo) // 'bar'
}
</code></pre>
<h2>Wrapping up</h2>
<p>I'm sure there are other edge cases I'm not taking into account, and maybe I <a href="https://hawkticehurst.com/2023/11/you-are-probably-using-connectedcallback-wrong/">shouldn't be using <code>connectedCallback()</code></a> in the first place. Hopefully <a href="https://github.com/WICG/webcomponents/issues/1081">this related WICG discussion</a> leads to improvements to the Custom Elements specification.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[How to build a TypeScript library with Vite]]></title>
            <link>https://rbardini.com/how-to-build-ts-library-with-vite/</link>
            <guid isPermaLink="true">https://rbardini.com/how-to-build-ts-library-with-vite/</guid>
            <pubDate>Sun, 28 May 2023 13:50:45 GMT</pubDate>
            <description><![CDATA[<p>I recently moved a few TypeScript libraries from <a href="https://github.com/developit/microbundle">Microbundle</a>, <a href="https://github.com/jaredpalmer/tsdx">TSDX</a> and <a href="https://github.com/weiran-zsd/dts-cli">dts-cli</a> to <a href="https://vitejs.dev/">Vite</a>. I wanted more control over the build setup, and to be able to develop, build and test<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup> them using the same configuration.</p>
<p>The secret is to use Vite's <a href="https://vitejs.dev/guide/build.html#library-mode">library mode</a>, and externalize all dependencies and built-in Node.js modules so that they are not bundled with your code:</p>
<pre><code class="language-ts">// vite.config.js
import { defineConfig } from 'vite'
import dts from 'vite-plugin-dts'
import pkg from './package.json' assert { type: 'json' }

export default defineConfig({
  build: {
    lib: {
      entry: './src/index.ts',
      formats: ['es'], // pure ESM package
    },
    rollupOptions: {
      external: [
        ...Object.keys(pkg.dependencies), // don't bundle dependencies
        /^node:.*/, // don't bundle built-in Node.js modules (use protocol imports!)
      ],
    },
    target: 'esnext', // transpile as little as possible
  },
  plugins: [dts()], // emit TS declaration files
})
</code></pre>
<p>You will also need a <code>tsconfig.json</code> specifying root files and compiler options. If you don't have one yet, you can generate one with the appropriate <a href="https://www.npmjs.com/package/create-vite"><code>create-vite</code></a> TypeScript template, e.g. <code>vanilla-ts</code>.</p>
<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p>I got tired of trying to make Jest work, especially after my migrations to ESM. Adopting <a href="https://vitest.dev/">Vitest</a>, with its compatible API and unified configuration with Vite, was a no-brainer. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></description>
        </item>
        <item>
            <title><![CDATA[Código-fonte do Carteiro liberado]]></title>
            <link>https://rbardini.com/codigo-fonte-carteiro-liberado/</link>
            <guid isPermaLink="true">https://rbardini.com/codigo-fonte-carteiro-liberado/</guid>
            <pubDate>Sun, 21 May 2023 13:43:33 GMT</pubDate>
            <description><![CDATA[<p>O Carteiro agora é código aberto!</p>
<p>Há um bom tempo considero liberar o código-fonte do aplicativo, e seu <a href="/aposentando-o-carteiro/">descontinuamento</a> foi o empurrão que faltava para a mudança. Tal abertura oferece à comunidade acesso ao conhecimento acumulado durante anos de desenvolvimento.</p>
<p>O código-fonte do Carteiro está disponível no <a href="https://github.com/rbardini/carteiro">GitHub</a>.</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Aposentando o Carteiro]]></title>
            <link>https://rbardini.com/aposentando-o-carteiro/</link>
            <guid isPermaLink="true">https://rbardini.com/aposentando-o-carteiro/</guid>
            <pubDate>Fri, 04 Nov 2022 01:06:25 GMT</pubDate>
            <description><![CDATA[<p>Há 10 anos, eu estava ainda na faculdade. Tinha acabado de comprar meu primeiro smartphone—um Motorola Milestone usado—e queria justificar o investimento aprendendo a desenvolver aplicativos Android. Escolhi criar um rastreador de encomendas, por necessidade pessoal, por não encontrar uma alternativa satisfatória, e pelos dados dos objetos serem publicamente acessíveis<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>.</p>
<p>Em abril de 2012, o <a href="/notas-de-lancamento/">Carteiro era lançado</a>.</p>
<figure>
  <img src="/img/carteiro/1-0-0-postal-list.png" alt="" />
  <figcaption>Lista de objetos no Carteiro 1.0.0</figcaption>
</figure>
<p>Desde então, o aplicativo (que era pago) se tornou gratuito, o número de usuários cresceu expressivamente, foi recomandado por <a href="https://www.techtudo.com.br/tudo-sobre/carteiro/">diversas</a> <a href="https://www.baixaki.com.br/android/download/carteiro.htm">publicações</a> de tecnologia, destacado na Play Store entre produções brasileiras, e até mesmo participou do <a href="https://www.jusbrasil.com.br/diarios/79871944/dou-secao-1-11-11-2014-pg-51">pacote de aplicativos nacionais da LG</a>.</p>
<p>Uma constante ameaça durante todos esses anos, porém, foram os próprios Correios. Como não tenho empresa e, portanto, contrato com a ECT, não tenho acesso à API oficial de rastreamento. Isso me forçou a extrair dados de páginas web, migrar para APIs não oficiais, e/ou usar credenciais de teste para validar requisições. A completa falta de comunicação resultou muitas vezes na <a href="/carteiro-removido-temporariamente-da-play-store">interrupção do serviço</a>, que felizmente sempre consegui contornar. Bem, até agora.</p>
<p>Os Correios fecharam a última porta na semana passada, quando puxaram da tomada a API de seu <a href="https://play.google.com/store/apps/details?id=br.com.correios.srocorreios">finado aplicativo móvel</a>. Dessa forma, não há mais como o Carteiro buscar informações sobre o andamento das entregas, pelo menos não sem investir uma quantidade de tempo e recursos que não tenho à disposição no momento.</p>
<p>Apesar do sucesso, o Carteiro sempre foi um <em>hobby</em>, sem qualquer retorno financeiro significativo. Resisti a colocar propagandas, porque meu principal objetivo era criar um produto agradável, focado na experiência do usuário e respeitando sua privacidade. Mas como a maioria dos meus hobbies, eventualmente me cansei dele. Não apenas isso, hoje tenho diferentes prioridades, e pouca motivação para contribuir conteúdo aos jardins murados de gigantes da tecnologia.</p>
<p>Chegou a hora do Carteiro pendurar o boné e ter um merecido descanso. A todos os destinatários e remetentes que confiaram no meu trabalho, meu sincero muito obrigado. E quem sabe, abrindo o código-fonte do projeto e com apoio da comunidade, ele não faça uma nova tentativa de entrega?</p>
<p>Um abraço!</p>
<p>Rafael Bardini</p>
<blockquote>
<p><strong>Atualização 21/05/2023:</strong> o código-fonte do Carteiro foi <a href="/codigo-fonte-carteiro-liberado">liberado</a>.</p>
</blockquote>
<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p>Lembra do <a href="https://web.archive.org/web/20120108230723/http://websro.correios.com.br:80/sro_bin/txect01$.QueryList?P_LINGUA=001&amp;P_TIPO=001&amp;P_COD_UNI=SI054542659BR">SRO</a>? <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Carteiro versão 5.1.0 lançado]]></title>
            <link>https://rbardini.com/carteiro-versao-5-1-0-lancado/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-versao-5-1-0-lancado/</guid>
            <pubDate>Thu, 21 Apr 2022 22:02:06 GMT</pubDate>
            <description><![CDATA[<p>O que há de novo:</p>
<ul>
<li>Inclusão de leitor de código de barras próprio</li>
<li>Atualização da lista de status</li>
</ul>
<p>A nova versão do Carteiro está <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">disponível gratuitamente no Google Play</a>.</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Carteiro versão 5.0.0 lançado]]></title>
            <link>https://rbardini.com/carteiro-versao-5-0-0-lancado/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-versao-5-0-0-lancado/</guid>
            <pubDate>Thu, 07 Apr 2022 11:34:39 GMT</pubDate>
            <description><![CDATA[<p>O que há de novo:</p>
<ul>
<li>Melhorias de design—novos esquemas de cores e interface mais limpa ✨</li>
<li>Uso do seletor de arquivos ao criar/restaurar backups</li>
<li>Atualização da URL da página do SRO</li>
<li>Atualização das listas de status e serviços postais</li>
<li>Correção de bugs</li>
<li>Remoção do suporte a Android 7.1.1 ou inferior</li>
</ul>
<p>O novo esquema de cores claro apresenta tons pastéis, enquanto a maioria dos elementos decorativos foram removidos:</p>
<figure class="image-comparison">
  <img src="/img/carteiro/5-0-0-light-theme-before.png" alt="" />
  <img src="/img/carteiro/5-0-0-light-theme-after.png" alt="" />
  <figcaption>Tema claro, antes e depois</figcaption>
</figure>
<p>A nova versão do Carteiro está <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">disponível gratuitamente no Google Play</a>.</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Carteiro versão 4.0.0 lançado]]></title>
            <link>https://rbardini.com/carteiro-versao-4-0-0-lancado/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-versao-4-0-0-lancado/</guid>
            <pubDate>Sat, 17 Oct 2020 11:02:44 GMT</pubDate>
            <description><![CDATA[<p>O que há de novo:</p>
<ul>
<li>Melhorias de design—maior contraste e tema escuro mais escuro 🌒</li>
<li>Adição de opção padrão do sistema à lista de temas</li>
<li>Cópia do código de rastreamento ao pressionar e segurar texto</li>
<li>Definida largura máxima da tela de adição de objeto</li>
<li>Exibição da data do último backup automático realizado</li>
<li>Correção de bugs durante adição e sincronização de objetos</li>
<li>Remoção do suporte a Android 5.1 ou inferior</li>
</ul>
<p>O redesenhado tema escuro traz um melhor contraste entre os elementos, facilitando a leitura enquanto consome menos energia:</p>
<figure class="image-comparison">
  <img src="/img/carteiro/4-0-0-dark-theme-before.png" alt="" />
  <img src="/img/carteiro/4-0-0-dark-theme-after.png" alt="" />
  <figcaption>Tema escuro, antes e depois</figcaption>
</figure>
<p>A nova versão do Carteiro já pode ser <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">instalada gratuitamente pelo Google Play</a>.</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Hiding fully-covered files from Jest coverage report]]></title>
            <link>https://rbardini.com/hiding-covered-files-jest-coverage-report/</link>
            <guid isPermaLink="true">https://rbardini.com/hiding-covered-files-jest-coverage-report/</guid>
            <pubDate>Wed, 06 May 2020 14:40:33 GMT</pubDate>
            <description><![CDATA[<p>While working on a large JavaScript codebase, one thing that bothered me was the coverage report output to the console: as most files had 100% coverage, it was difficult to spot the few exceptions in the table.</p>
<p>Luckily, additional options can be <a href="https://jestjs.io/docs/en/configuration#coveragereporters-arraystring--stringany">passed to Istanbul reporters</a>. I couldn't find documentation for the <code>text</code> reporter, so I digged into its code and found the <code>skipFull</code> option:</p>
<pre><code class="language-diff"> {
   &quot;coverageReporters&quot;: [
-    &quot;text&quot;
+    [&quot;text&quot;, { &quot;skipFull&quot;: true }]
   ]
 }
</code></pre>
<p>This <a href="https://github.com/istanbuljs/istanbuljs/pull/170">hides all rows with full coverage</a>, letting you focus on what matters most: partially or fully–uncovered files.</p>
<p>I also recommend <a href="https://github.com/rickhanlonii/jest-silent-reporter">jest-silent-reporter</a> for an even quieter output (especially in CI builds) and <a href="https://github.com/rbardini/jest-it-up">jest-it-up</a> to automatically bump up global Jest thresholds.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Making optional properties nullable in TypeScript]]></title>
            <link>https://rbardini.com/making-optional-properties-nullable-typescript/</link>
            <guid isPermaLink="true">https://rbardini.com/making-optional-properties-nullable-typescript/</guid>
            <pubDate>Fri, 20 Dec 2019 21:10:39 GMT</pubDate>
            <description><![CDATA[<p>Let's say you follow the TypeScript project coding guidelines and <a href="https://github.com/microsoft/TypeScript/wiki/Coding-guidelines#null-and-undefined">only use <code>undefined</code></a>. Your types are defined with non-nullable optional properties (e.g., <code>x?: number</code>), but the data coming from the API returns <code>null</code> instead.</p>
<p>You decide to write a function to strip all these <code>null</code> values from the response, so that they conform to your types:</p>
<pre><code class="language-ts">function stripNullableProperties(obj) {
  // Return a new object without null properties
}
</code></pre>
<p>How can you strongly type such helper without duplicating your input and output types? You could try:</p>
<pre><code class="language-ts">function stripNullableProperties&lt;T extends {}&gt;(obj: T): T
</code></pre>
<p>But it won't work in <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#--strictnullchecks">strict null checking mode</a>, since <code>obj</code> might have <code>null</code> values that are not assignable to the non-nullable optional properties in <code>T</code>:</p>
<pre><code class="language-ts">type A = {
  x: number
  y?: number
}

stripNullableProperties&lt;A&gt;({
  x: 1,
  y: null, // Error: Type 'null' is not assignable to type 'number | undefined'.
})
</code></pre>
<p>What you really need is something like:</p>
<pre><code class="language-ts">function stripNullableProperties&lt;T extends {}&gt;(obj: NullableOptional&lt;T&gt;): T
</code></pre>
<h2>The <code>NullableOptional&lt;T&gt;</code> type</h2>
<p>The <code>NullableOptional&lt;T&gt;</code> type constructs a type with all optional properties of <code>T</code> set to nullable:</p>
<pre><code class="language-ts">type A = {
  x: number
  y?: number
}

type B = NullableOptional&lt;A&gt;
// {
//   x: number
//   y?: number | null
// }
</code></pre>
<p>You won't find <code>NullableOptional</code> in the <a href="https://www.typescriptlang.org/docs/handbook/utility-types.html">TypeScript documentation</a>, and that's because it's a custom type. It actually looks like this:</p>
<pre><code class="language-ts">type RequiredKeys&lt;T&gt; = {
  [K in keyof T]-?: {} extends { [P in K]: T[K] } ? never : K
}[keyof T]

type OptionalKeys&lt;T&gt; = {
  [K in keyof T]-?: {} extends { [P in K]: T[K] } ? K : never
}[keyof T]

type PickRequired&lt;T&gt; = Pick&lt;T, RequiredKeys&lt;T&gt;&gt;

type PickOptional&lt;T&gt; = Pick&lt;T, OptionalKeys&lt;T&gt;&gt;

type Nullable&lt;T&gt; = { [P in keyof T]: T[P] | null }

type NullableOptional&lt;T&gt; = PickRequired&lt;T&gt; &amp; Nullable&lt;PickOptional&lt;T&gt;&gt;
</code></pre>
<p>In short:</p>
<ol>
<li>pick the required properties from <code>T</code>;</li>
<li>pick the optional properties from <code>T</code> and make them nullable;</li>
<li>intersect (1) with (2).</li>
</ol>
<p>With this, you can remove all nullable properties from an object whose interface should only have non-nullable optional properties, while still ensuring type safety:</p>
<pre><code class="language-ts">type A = {
  x: number
  y?: number
}

stripNullableProperties&lt;A&gt;({
  x: 1,
  y: null,
})
// {
//   x: 1,
// }: A
</code></pre>
<p>You could always do a type assertion and avoid all this trouble, but ensuring type safety—even in seemingly unharmful cases like this one—pays off in the long run.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Automating tests for the GTM data layer]]></title>
            <link>https://rbardini.com/automating-gtm-data-layer-tests/</link>
            <guid isPermaLink="true">https://rbardini.com/automating-gtm-data-layer-tests/</guid>
            <pubDate>Sat, 20 Jul 2019 18:50:06 GMT</pubDate>
            <description><![CDATA[<p>At <a href="https://www.travix.com/">Travix</a> we are constantly analyzing application and user behavior on our websites in order to offer the best experience to our customers. One of the tools employed for this purpose is <a href="https://tagmanager.google.com/">Google Tag Manager</a> (also called GTM), alongside a <em>data layer</em>. A data layer is a JavaScript object that is used to pass information from the website to the Tag Manager container<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="" aria-describedby="footnote-label">1</a></sup>, like product views and purchases.</p>
<p>While implementing new features and refactoring parts of the frontend application, we frequently faced an issue: some GTM events would go missing, be duplicated, dispatch at the wrong time, or just lack important dimensions. This can heavily impact our ability to analyze the data, and thus demanded a lot of manual testing to ensure that everything was working as intended.</p>
<p>We already have end-to-end tests in place, doing a lot of user interactions throughout the website, that in turn push events to the data layer. What if we could extend these tests to also check if the information in the data layer is consistent? Since the data layer is basically an array of JavaScript objects, we can do a sort of snapshot testing, comparing the current values with what we expect them to be.</p>
<p>In this article, I will cover how we automated GTM data layer testing using our end-to-end test framework of choice, <a href="https://devexpress.github.io/testcafe/">TestCafe</a>. The same principles can be easily applied to other test frameworks though.</p>
<h2>Retrieving the data layer</h2>
<p>The data layer is assigned to a global <code>dataLayer</code> variable. To retrieve the data layer items in end-to-end tests, we must execute code in the browser's context. In TestCafe, this can be done with a <a href="https://devexpress.github.io/testcafe/documentation/test-api/obtaining-data-from-the-client/"><code>ClientFunction</code></a>:</p>
<pre><code class="language-js">import { ClientFunction } from 'testcafe'

const getDataLayer = ClientFunction(() =&gt; window.dataLayer)
</code></pre>
<p>However, if you try to run this function in your tests, you may face the following error:</p>
<pre><code>ClientFunction cannot return DOM elements. Use Selector functions for this purpose.
</code></pre>
<p>This happens due to some events like <code>gtm.click</code> containing references to DOM nodes, which cannot be serialized. One way to fix this is to traverse over all items and remove any such references before returning the data. I will leave this as an exercise, mainly because the problem went away once we started filtering out default GTM events, as I will explain in the next section.</p>
<h2>Filtering out default GTM events</h2>
<p>One thing I noticed after printing the data layer a couple of times was that default GTM events (e.g., <code>gtm.load</code>, <code>gtm.click</code>) would fire at different points in time, thus their order would not be the same and our tests would often fail. To avoid this issue, I decided to simply filter out these default events, since they are not very relevant to us—we care more about the custom events we fire ourselves.</p>
<p>All default GTM events start with <code>gtm</code>, so we can just ignore them with a <code>filter</code> on the event name:</p>
<pre><code class="language-js">const getDataLayer = ClientFunction(() =&gt;
  window.dataLayer.filter(({ event }) =&gt; !event.startsWith('gtm')),
)
</code></pre>
<h2>Comparing the data layer</h2>
<p>Now that we have the data layer in hand, we can make assertions on it. Write your reference data layer snapshot and do a <a href="https://devexpress.github.io/testcafe/documentation/test-api/assertions/assertion-api.html#deep-equal">deep equality check</a> against it<sup><a href="#user-content-fn-2" id="user-content-fnref-2" data-footnote-ref="" aria-describedby="footnote-label">2</a></sup>:</p>
<pre><code class="language-js">import { t } from 'testcafe'

const dataLayerSnapshot = [
  { event: 'productClick' },
  { event: 'addToCart' },
  { event: 'removeFromCart' },
  { event: 'promotionClick' },
  { event: 'checkout' },
  { event: 'checkoutOption' },
]

await t.expect(getDataLayer()).eql(dataLayerSnapshot)
</code></pre>
<p>If the data layer does not match the snapshot, the test will fail:</p>
<pre><code>AssertionError: expected [ Array(5) ] to deeply equal [ Array(6) ]
</code></pre>
<p>Then it is a matter of fixing the code if it is a regression issue, or (manually) updating the snapshot.</p>
<h2>Bonus: improving test failure output</h2>
<p>You probably noticed that the error message is not very helpful—it does not tell you exactly what the difference is between the expected and the received values.</p>
<p>We can work around this by doing a string comparison instead, stringifying both the data layer and the snapshot before the assertion:</p>
<pre><code class="language-js">const getDataLayer = ClientFunction(() =&gt;
  JSON.stringify(
    window.dataLayer.filter(({ event }) =&gt; !event.startsWith('gtm')),
  ),
)
</code></pre>
<pre><code class="language-js">const dataLayerSnapshot = JSON.stringify([
  { event: 'productClick' },
  { event: 'addToCart' },
  { event: 'removeFromCart' },
  { event: 'promotionClick' },
  { event: 'checkout' },
  { event: 'checkoutOption' },
])
</code></pre>
<p>Not pretty, but it does the job—although it is still a bit difficult to spot what the actual problem is:</p>
<pre><code>AssertionError: expected

'[{&quot;event&quot;:&quot;productClick&quot;},{&quot;event&quot;:&quot;addToCart&quot;},{&quot;event&quot;:&quot;promotionClick&quot;},{&quot;event&quot;:&quot;checkout&quot;},{&quot;event&quot;:&quot;checkoutOption&quot;}]'
   to deeply equal

'[{&quot;event&quot;:&quot;productClick&quot;},{&quot;event&quot;:&quot;addToCart&quot;},{&quot;event&quot;:&quot;removeFromCart&quot;},{&quot;event&quot;:&quot;promotionClick&quot;},{&quot;event&quot;:&quot;checkout&quot;},{&quot;event&quot;:&quot;checkoutOption&quot;}]'
</code></pre>
<p>In a follow-up post I will explain how we managed to improve this even further by using the <a href="https://www.npmjs.com/package/expect"><code>expect</code></a> module inside TestCafe for a more Jest-like assertion output.</p>
<h2>Conclusion</h2>
<p>Manually testing the data layer after each frontend change is a very time consuming process. Inspection tools like <a href="https://dataslayer.org/">dataslayer</a> can help, but they are no match for proper automation. By leveraging the power of end-to-end tests, we can save valuable time from developers and data analysts, while being more confident that changes to the codebase will not negatively impact sales and performance tracking.</p>
<section data-footnotes="" class="footnotes"><h2 id="footnote-label" class="sr-only">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p>See <a href="https://support.google.com/tagmanager/answer/6164391">this GTM help center article</a> for more information. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
<li id="user-content-fn-2">
<p>For a single-page application (SPA), this could be the very last step of the test. <a href="#user-content-fnref-2" data-footnote-backref="" aria-label="Back to reference 2" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></description>
        </item>
        <item>
            <title><![CDATA[Carteiro versão 3.5.1 disponível]]></title>
            <link>https://rbardini.com/carteiro-versao-3-5-1-disponivel/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-versao-3-5-1-disponivel/</guid>
            <pubDate>Sun, 03 Mar 2019 16:21:14 GMT</pubDate>
            <description><![CDATA[<p>Uma nova versão do Carteiro <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">acaba de ser lançada no Google Play</a>! As principais mudanças são:</p>
<ul>
<li>Opção de desfazer ao arquivar ou excluir objetos</li>
<li>Correção da seleção do tema não sendo aplicada</li>
<li>Melhorias de design, interação e desempenho</li>
</ul>
<p>A interface está mais harmoniosa, com todos os elementos alinhados à grade, e a transição entre a lista de objetos e o histórico seguindo melhor as diretrizes do Material Design.</p>
<figure>
  <img src="/img/carteiro/postal-list-comparison.png" alt="" />
  <figcaption>Lista de objetos, antes e depois</figcaption>
</figure>
<p>Além disso, a maioria das janelas de confirmação foram substituídas por opções de desfazer, minimizando interrupções durante o uso.</p>
<p><a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">Instale já o novo do Carteiro no seu Android</a> e aproveite todas as novidades!</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[How to add a Netlify deploy status badge to your project]]></title>
            <link>https://rbardini.com/how-to-add-netlify-badge/</link>
            <guid isPermaLink="true">https://rbardini.com/how-to-add-netlify-badge/</guid>
            <pubDate>Mon, 12 Nov 2018 21:44:46 GMT</pubDate>
            <description><![CDATA[<p>Ever since I moved this blog to <a href="https://www.netlify.com/">Netlify</a> I wanted to add a badge to the <a href="https://github.com/rbardini/rbardini.com#readme">repository's README</a> displaying the deploy status. The <a href="https://shields.io/">Shields.io</a> service doesn't support Netlify badges yet, but luckily I found out that you can build dynamic badges by querying structured data from any public URL.</p>
<p>After digging into the <a href="https://www.netlify.com/docs/api/">Netlify REST API</a>, I managed to make a badge that fetches all deploys for my site and extracts the status of the last deploy:</p>
<pre><code class="language-md">[![Deploy status](https://img.shields.io/badge/dynamic/json.svg?url=https://api.netlify.com/api/v1/sites/rbardini.com/deploys&amp;label=deploy&amp;query=$[0].state&amp;colorB=blue)](https://app.netlify.com/sites/rbardini/deploys)
</code></pre>
<p>Which looks like this:</p>
<p><a href="https://app.netlify.com/sites/rbardini/deploys"><img src="https://img.shields.io/badge/dynamic/json.svg?url=https://api.netlify.com/api/v1/sites/rbardini.com/deploys&amp;label=deploy&amp;query=$%5B0%5D.state&amp;colorB=blue" alt="Deploy status" /></a></p>
<p>One shortcoming is that you cannot set a different color depending on the status, that's why I'm using a &quot;neutral&quot; blue background here. Also, I assume deploy logs must be public for the link (and possibly the badge itself) to work.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Carteiro versão 3.4.0 disponível]]></title>
            <link>https://rbardini.com/carteiro-versao-3-4-0-disponivel/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-versao-3-4-0-disponivel/</guid>
            <pubDate>Wed, 01 Aug 2018 22:56:17 GMT</pubDate>
            <description><![CDATA[<p>Uma nova versão do Carteiro <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">acaba de ser lançada no Google Play</a>. Entre as várias mudanças estão:</p>
<ul>
<li>Melhorias no processo de sincronização automática</li>
<li>Opção de sincronizar objetos ao iniciar</li>
<li>Opção de falar com os Correios no menu de navegação</li>
<li>Adição de nova categoria &quot;em trânsito&quot;</li>
<li>Opção de alterar a categoria exibida ao iniciar</li>
<li>Exibição da última sincronização no menu de navegação</li>
<li>Opção de marcar todos os objetos durante seleção</li>
<li>Adição da bandeira de Hong Kong</li>
<li>Correção dos botões de adição de objeto em telas menores</li>
<li>Correção do tema escuro após abrir objeto no SRO</li>
</ul>
<p>O foco dessa versão é aprimorar as sincronizações em segundo plano, reagendando-as sempre que necessário, como nas ocasiões em que o sistema termina o processo para liberar memória ou economizar bateria. Além disso, agora a lista de objetos é atualizada automaticamente ao abrir o aplicativo, e é possível checar o momento da última sincronização no cabeçalho do menu de navegação.</p>
<p>Confira essas e outras as novidades <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">instalando a nova versão do Carteiro no seu Android</a>!</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Carteiro versão 3.0.4 disponível]]></title>
            <link>https://rbardini.com/carteiro-versao-3-0-4-disponivel/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-versao-3-0-4-disponivel/</guid>
            <pubDate>Mon, 15 May 2017 05:00:13 GMT</pubDate>
            <description><![CDATA[<p>Após um longo tempo e uma semana difícil, com a inesperada <a href="http://www.correios.com.br/para-voce/avisos/atualizacao-do-sistema-de-rastreamento-de-objetos-sro/">desativação do WebSRO</a> por parte dos Correios, uma nova versão do Carteiro já <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">se encontra no Google Play</a>. E para compensar toda essa espera, as novidades são:</p>
<ul>
<li>Muitas melhorias de interface, novo ícone e tema escuro!</li>
<li>Utilização do web service oficial dos Correios</li>
<li>Opção de desabilitar teclado adaptativo</li>
<li>Adição de múltiplos objetos facilitada</li>
<li>Chance de pular a busca inicial por dados ao adicionar objeto</li>
<li>Seleção de objetos via ícone de status na lista de rastreamento</li>
<li>Alerta se serviço dos Correios está fora do ar durante sincronização</li>
<li>Lista de serviços postais atualizada</li>
<li>Removido suporte a Android 4.4 ou inferior</li>
</ul>
<p>A maior novidade, sem dúvida alguma, é o novo tema escuro, muito pedido pelos usuários e que pode ser ativado na categoria <em>Aparência</em> da tela de configurações do aplicativo. Mas uma outra melhoria que será bastante útil, principalmente para aqueles que adicionam vários objetos sequencialmente, como lojistas e comerciantes, é o novo fluxo de adição de objetos. Agora, é possível pular a busca inicial por dados do objeto, lembrar de não pedir confirmação por objetos não encontrados e adicionar um novo objeto em seguida, com apenas um toque:</p>
<p><img src="/img/carteiro/new-add-flow.gif" alt="Novo fluxo de adição de objetos" /></p>
<p><a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">Instale agora o novo Carteiro no seu Android</a>! Lembrando que o Carteiro passa a suportar apenas Android 5.0 ou superior a partir dessa versão, caso a atualização não seja oferecida ao seu aparelho. E se quiser ficar por dentro das últimas novidades, não deixe de participar do <a href="https://plus.google.com/+RafaelBardini/posts/Bv7LfxRMrLr">programa de testes</a> :)</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Carteiro removido temporariamente da Play Store]]></title>
            <link>https://rbardini.com/carteiro-removido-temporariamente-da-play-store/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-removido-temporariamente-da-play-store/</guid>
            <pubDate>Thu, 16 Jun 2016 14:50:33 GMT</pubDate>
            <description><![CDATA[<p>Devido à inesperada desativação do serviço WebSRO por parte dos Correios, o Carteiro foi removido temporariamente da Play Store até que uma solução alternativa seja implementada. A previsão é que o aplicativo retorne à loja até o começo da semana que vem (20/06).</p>
<p>Obrigado pela compreensão e apoio,</p>
<p>Rafael Bardini</p>
<blockquote>
<p><strong>Atualização 17/06/2016:</strong> o WebSRO está de volta, e o Carteiro também :)</p>
</blockquote>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Alain de Botton on Love ↗]]></title>
            <link>https://www.youtube.com/watch?v=jJ6K_f7oSdg</link>
            <guid isPermaLink="true">https://www.youtube.com/watch?v=jJ6K_f7oSdg</guid>
            <pubDate>Fri, 10 Jun 2016 01:43:33 GMT</pubDate>
            <description><![CDATA[<p>Amazing presentation by one of the greatest thinkers of our time on how Romanticism has made our relationships difficult.</p>
<figure class="video-container">
  <lite-youtube videoid="jJ6K_f7oSdg">
    <a href="https://www.youtube.com/watch?v=jJ6K_f7oSdg" class="lty-playbtn" title="Play video">
      <span class="lyt-visually-hidden">Alain de Botton on Love - YouTube</span>
    </a>
  </lite-youtube>
</figure>
<p>De Botton is one of the minds (and sometimes also the voice) behind <a href="https://www.youtube.com/theschooloflifetv">The School of Life channel</a>, which I highly recommend you to subscribe to.</p>

<p><a href="https://rbardini.com/alain-de-botton-on-love/" rel="bookmark">∞ Permalink</a></p>]]></description>
        </item>
        <item>
            <title><![CDATA[Is forgetting a child in the backseat of a car a crime? ↗]]></title>
            <link>https://www.washingtonpost.com/lifestyle/magazine/fatal-distraction-forgetting-a-child-in-thebackseat-of-a-car-is-a-horrifying-mistake-is-it-a-crime/2014/06/16/8ae0fe3a-f580-11e3-a3a5-42be35962a52_story.html</link>
            <guid isPermaLink="true">https://www.washingtonpost.com/lifestyle/magazine/fatal-distraction-forgetting-a-child-in-thebackseat-of-a-car-is-a-horrifying-mistake-is-it-a-crime/2014/06/16/8ae0fe3a-f580-11e3-a3a5-42be35962a52_story.html</guid>
            <pubDate>Sat, 14 May 2016 19:45:16 GMT</pubDate>
            <description><![CDATA[<p>Gene Weingarten:</p>
<blockquote>
<p>&quot;Death by hyperthermia&quot; is the official designation. When it happens to young children, the facts are often the same: An otherwise loving and attentive parent one day gets busy, or distracted, or upset, or confused by a change in his or her daily routine, and just... forgets a child is in the car.</p>
</blockquote>
<p>This is one of the most disturbing, eye-opening articles I've ever read, not because those parents are monsters, but because it could potentially happen to any of us.</p>
<p>Quoting David Diamond, a professor of molecular physiology at the University of South Florida:</p>
<blockquote>
<p>&quot;The quality of prior parental care seems to be irrelevant,&quot; he said. &quot;The important factors that keep showing up involve a combination of stress, emotion, lack of sleep and change in routine, where the basal ganglia is trying to do what it’s supposed to do, and the conscious mind is too weakened to resist. What happens is that the memory circuits in a vulnerable hippocampus literally get overwritten, like with a computer program. Unless the memory circuit is rebooted -- such as if the child cries, or, you know, if the wife mentions the child in the back -- it can entirely disappear.&quot;</p>
</blockquote>
<p>I think these cases also say a lot about how innocent people, like rape victims, are sometimes blamed by others in order for them to cope with the harsh reality that the world is not inherently fair:</p>
<blockquote>
<p>Humans, Hickling said, have a fundamental need to create and maintain a narrative for their lives in which the universe is not implacable and heartless, that terrible things do not happen at random, and that catastrophe can be avoided if you are vigilant and responsible.</p>
<p>In hyperthermia cases, he believes, the parents are demonized for much the same reasons. “We are vulnerable, but we don’t want to be reminded of that. We want to believe that the world is understandable and controllable and unthreatening, that if we follow the rules, we’ll be okay. So, when this kind of thing happens to other people, we need to put them in a different category from us. We don’t want to resemble them, and the fact that we might is too terrifying to deal with. So, they have to be monsters.”</p>
</blockquote>
<p>Jeff Atwood wrote a <a href="http://blog.codinghorror.com/they-have-to-be-monsters/">great piece</a> on this behavior in relation to internet harassment, so please check it out.</p>

<p><a href="https://rbardini.com/tragic-lapses-of-memory/" rel="bookmark">∞ Permalink</a></p>]]></description>
        </item>
        <item>
            <title><![CDATA[Migrating from Second Crack to Metalsmith]]></title>
            <link>https://rbardini.com/migrating-to-metalsmith/</link>
            <guid isPermaLink="true">https://rbardini.com/migrating-to-metalsmith/</guid>
            <pubDate>Mon, 09 May 2016 01:14:08 GMT</pubDate>
            <description><![CDATA[<p>I've just migrated this blog from <a href="https://github.com/marcoarment/secondcrack">Second Crack</a> to <a href="http://www.metalsmith.io/">Metalsmith</a>, mainly because I wanted to switch away from a PHP-based static site generator. I considered using <a href="https://gohugo.io/">Hugo</a>, specially because it is pretty fast---and it would be nice to learn Go---but I found its template syntax a little off-putting.</p>
<p>It's now being served by <a href="https://pages.github.com/">GitHub Pages</a> too, with the <a href="https://github.com/rbardini/rbardini.com">code available here</a>. I'll write some follow-up posts soon detailing the plugins and scripts I've used to build and deploy the site.</p>
<p>Stay tuned.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Carteiro versão 2.2.1 disponível]]></title>
            <link>https://rbardini.com/carteiro-versao-2-2-1-disponivel/</link>
            <guid isPermaLink="true">https://rbardini.com/carteiro-versao-2-2-1-disponivel/</guid>
            <pubDate>Thu, 24 Sep 2015 03:36:02 GMT</pubDate>
            <description><![CDATA[<p>Uma nova versão do Carteiro acaba de ser <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">publicada no Google Play</a>. Dessa vez as novidades são:</p>
<ul>
<li>Adicionada opção de sincronização apenas via Wi-Fi</li>
<li>Menu de navegação redesenhado</li>
<li>Suporte a permissões individuais e backup automático de dados no Android 6.0+</li>
<li>Reorganização das configurações do aplicativo</li>
<li>Lista de serviços postais atualizada</li>
<li>Correção de bugs e melhora do desempenho</li>
</ul>
<p>Com essa atualização, o menu de navegação passa a contar com ícones, facilitando a identificação de cada categoria, bem como um cabeçalho estilo Material Design:</p>
<p><img src="/img/carteiro/navigation-drawer.png" alt="Menu de navegação" /></p>
<p>Já a opção de sincronização apenas via Wi-Fi permite limitar o consumo do plano de dados móvel. Essa opção pode ser encontrada na seção <em>Sincronização</em> nas configurações do aplicativo, e requer uma nova permissão para acesso ao estado da rede.</p>
<p>Aproveite todas a novidades <a href="https://play.google.com/store/apps/details?id=com.rbardini.carteiro">instalando já o Carteiro no seu Android</a>!</p>
]]></description>
            <category>carteiro</category>
        </item>
        <item>
            <title><![CDATA[Exercendo a não-opinião como exercício de empatia ↗]]></title>
            <link>http://www.papodehomem.com.br/exercer-a-nao-opiniao-or-exercicio-de-empatia-6</link>
            <guid isPermaLink="true">http://www.papodehomem.com.br/exercer-a-nao-opiniao-or-exercicio-de-empatia-6</guid>
            <pubDate>Fri, 20 Feb 2015 00:34:03 GMT</pubDate>
            <description><![CDATA[<p>Alex Castro:</p>
<blockquote>
<p>Quando perguntamos a nossa amiga casada quando ela vai finalmente ter filhos ou filhas, a pergunta nos parece inócua e amigável. Afinal, só estamos perguntando porque temos intimidade, certo?</p>
<p>Entretanto, para a pessoa que está do outro lado, o comentário é opressivo. Porque não é a primeira, nem a vigésima, nem a centésima vez que é feito.</p>
<p>Como essa pessoa pode se sentir acolhida, feliz, aceita entre suas amigas e familiares se praticamente todo dia alguma delas a interpela sobre uma das escolhas mais importantes de sua vida?</p>
<p>A mensagem passada por essa constante enxurrada de comentários, uma mensagem ao mesmo tempo violenta e invasiva, é que a sua escolha de vida, que deveria ser íntima e indevassável, causa enorme desconforto às suas pessoas mais próximas. Se não, por que tanta insistência? Se não, por que tanta intrusão?</p>
</blockquote>
<p>Em situações como essa, é ainda mais apropriado seguir o conselho supostamente dado pelo guru indiano <a href="http://pt.wikipedia.org/wiki/Sathya_Sai_Baba">Sathya Sai Baba</a> (tradução minha):</p>
<blockquote>
<p>Antes de falar, pense: É necessário? É verdadeiro? É gentil? Machucará alguém? Acrescentará algo ao silêncio?</p>
</blockquote>
<p>Pense, sempre.</p>

<p><a href="https://rbardini.com/exercendo-a-nao-opiniao/" rel="bookmark">∞ Permalink</a></p>]]></description>
        </item>
    </channel>
</rss>