Lidando com regressão visual: enfrentando desafios com Django, Selenium e Pillow
Tags: braziliandevs, ui, django, opensource
Published: 2023-09-11 01:15:58

O **django-image-uploader-widget** é um projeto de componente de upload de imagem para o **django**. Por motivos de retrocompatibilidade entre as versões 3.x e 4.x do **django**, foi decidido implementar testes de regressão visual utilizando **Selenium** e **Pillow**. Nesse texto vamos trabalhar esse tema e seus desafios.

## Índice

- [Introdução](#intro)
- [O suporte do django a suas versões](#django-versions)
- [O que é um teste de regressão visual?](#teste-regressao-visual)
- [O Browser e o Selenium](#selenium)
- [Pillow](#pillow)
- [Juntando as Peças](#juntando-as-pecas)
- [Desafios](#desafios)
 - [O problema inicial](#problema-inicial)
 - [Alteração nas fontes](#fontes)
 - [Animações, transições e threshold](#animacoes)
 - [Theme Toggle](#theme)
- [Referências](#refs)

## Introdução <a name="intro"></a>

O [django-image-uploader-widget](https://github.com/inventare/django-image-uploader-widget) é um projeto de componente de upload de imagem para o **django**, criado para melhorar a estética dos *uploaders* originais. É mantido como open-source desde o início, inclusive recebendo colaborações de outros desenvolvedores.

![Visualização do Componente](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t0pzhx6vm8kwio4xf5i9.png)

Com o lançamento das versões 4.x do django, o componente passou a ter diferenças significativas no visual entre algumas de suas versões, conforme registrado na issue [#96](https://github.com/inventare/django-image-uploader-widget/issues/96#issuecomment-1690740705).

A partir dessa diferença visual, em diferentes versões do **django**, entra no radar do projeto a possibilidade de implementação de testes de regressão visual. Nesse artigo vamos explorar os desafios enfrentados durante essa implementação.

## O suporte do django a suas versões <a name="django-versions"></a>

Um ponto que vale ser levado em consideração, para justificar a decisão de implementar testes de regressão visual, é o fato de que o **django** mantem, por muito tempo, suporte a diversas de suas versões ([Ver Referência](https://www.djangoproject.com/download/)), como na imagem:

![django supported versions roadmap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/03mlns5lyp7k2s16drdr.png)

## O que é um teste de regressão visual? <a name="teste-regressao-visual"></a>

Como o Guilherme Ananias comenta no seu texto ([Ver Referência](https://dev.to/woovi/avoiding-ui-regressions-with-jest-49f3)), um teste de regressão visual é um tipo de teste que irá garantir que qualquer alteração no código não irá alterar a forma como o usuário final irá verá esse componente ou página.

Dado o contexto de suporte a múltiplas versões do **django** e, principalmente, do **django-admin**, com alterações de estilização entre as versões, esse tipo de teste pode, também, ser utilizado para garantir retrocompatibilidade entre o componente e as versões do **django**.

## O Browser e o Selenium <a name="selenium"></a>

O código desse componente já possuía uma suite de testes, funcionais, rodando com o **Selenium** e o **Chrome Driver**. A justificativa para o uso de testes funcionais pode ser lida nas [documentações do projeto](https://inventare.github.io/django-image-uploader-widget/docs/development/tests/).

Tendo o browser e, mais especificamente, o **selenium** disponíveis, é possível utilizar o método `screenshot(filename)` para salvar um *screenshot* de um elemento da página, conforme [documentação](https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webelement.WebElement.screenshot).

## Pillow <a name="pillow"></a>

A utilização dos campos de imagem do **django** só é possível com a biblioteca **Pillow** instalada no projeto. Essa biblioteca traz, com a sua instalação, métodos para lidar com a diferença entre imagens ([Ver Referência](https://pillow.readthedocs.io/en/stable/reference/ImageChops.html#PIL.ImageChops.difference)).

## Juntando as Peças <a name="juntando-as-pecas"></a>

Ao utilizar o **selenium** e a lib **Pillow** é possível, de forma bem simples, tirar *screenshot* dos elementos e comparar com outro *screenshot* já armazenado ([ver implementação](https://github.com/inventare/django-image-uploader-widget/blob/main/utils/tests/snapshot.py#L188)).

## Desafios <a name="desafios"></a>

Foram enfrentados diversos desafios durante a implementação dos testes de regressão visual. Alguns desses desafios foram causados pela natureza das escolhas para a implementação desses testes e outros pela natureza do **django** e **django-admin**.

### O problema inicial <a name="problema-inicial"></a>

Toda a implementação dos testes de regressão visual nasceram devido a um problema entre versões do django, vide [#96](https://github.com/inventare/django-image-uploader-widget/issues/96#issuecomment-1690740705), onde, devido a alteração de estilos do **django-admin**, adicionando `flexbox`, causou a quebra da estilização.

Assim, é necessário dizer que um dos primeiros desafios é da natureza do **django** e, principalmente, do suporte a múltiplas versões do mesmo.

A solução, pra esse caso, em específico, foi simples e corriqueira fazendo o tratamento tanto pro **css** das versões 3.x do **django-admin** quanto para as novas, que utilizam `flexbox`.

### Alteração nas fontes <a name="fontes"></a>

Complementando o problema inicial, logo nos primeiros testes, foi identificado uma alteração no padrão visual do componente (ou *widget*), em uma das versões mais recentes do **django**. As fontes do *widget* estavam diferentes. Ao buscar pelas *releases notes* do **django**, é observado a seguinte descrição ([referência](https://docs.djangoproject.com/en/4.2/releases/4.2/)):

> The admin’s font stack now prefers system UI fonts and no longer requires downloading fonts. Additionally, CSS variables are available to more easily override the default font families.

Uma solução, não necessariamente a melhor, porém a mais simples foi a de adiantar essa alteração das fontes, para o componente de upload de imagens, independente da versão do **django**.

### Animações, transições e threshold <a name="animacoes"></a>

Da natureza da comparação das imagens, junto ao fato de ser uma aplicação *web* executando, tem-se o problema de ter alguns pixeis de diferença em algumas situações, provavelmente causado por animações e transições.

Para aguardar o fim das transições e animações, o método `time.sleep()` do Python foi utilizado. Por conta da utilização do `time.sleep()` é, natural, que o tempo de execução dos testes aumente significativamente.

> Nesse ponto, é sempre importante deixar pontuado que, não existe certo ou errado, ou solução pra todos os problemas, ou, ainda, solução sem contrapontos. Toda técnica, ou escolha, geralmente, tem suas vantagens e desvantagens. Cabe ponderar se as vantagens superam as desvantagens no contexto ao qual se quer utilizar a ferramenta.

Por fim, após a implementação do `time.sleep()` raras se tornaram as vezes que as imagens divergiam em locais que não deveriam, porém, por garantia, ainda foi implementado um sistema de comparação com *threshold* (limiar) para esses casos.

### Theme Toggle <a name="theme"></a>

Ainda nas *release notes* da versão **4.2** do **django** é possível verificar a adição de um *theme toggle* no admin. Quando o componente foi, inicialmente, desenvolvido, as versões 4.x do *django* ainda não tinham assumido o status de **LTS**. Além disso, por um tempo, apenas as manutenções básicas foram dadas ao projeto. Dados esses contextos, o componente não tinha suporte a mudança de tema pela interface de usuário.

Esse problema só foi percebido, de fato, com a implementação dos testes de regressão visual e, para a solução, foi aberta uma nova issue no GitHub ([#103](https://github.com/inventare/django-image-uploader-widget/issues/103)).

Esse foi o único caso em que foi implementada uma condicional da versão do **django** em que os testes estavam sendo executados:

```python
import django
# ...

class InlineEditorUIRegressionTestCase(IUWTestCase):
    # ...
    def test_ui_initialized_toggle_dark_theme_inverted(self):
        self.dark_mode()

        major, minor, _, _, _ = django.VERSION
        if major < 4 or minor < 2:
            return
```

---

## Referências <a name="refs"></a>

As referências citadas no texto, listadas:

- [django-image-uploader-widget](https://github.com/inventare/django-image-uploader-widget)
- [django-image-uploader-widget#96](https://github.com/inventare/django-image-uploader-widget/issues/96#issuecomment-1690740705)
- [Download - Django](https://www.djangoproject.com/download/)
- [Avoiding UI Regressions With Jest](https://dev.to/woovi/avoiding-ui-regressions-with-jest-49f3)
- [Tests - django-image-uploader-widget docs](https://inventare.github.io/django-image-uploader-widget/docs/development/tests/)
- [Selenium Docs](https://selenium-python.readthedocs.io/api.html#selenium.webdriver.remote.webelement.WebElement.screenshot)
- [Pillow Docs](https://pillow.readthedocs.io/en/stable/reference/ImageChops.html#PIL.ImageChops.difference)
- [django-image-uploader-widget/snapshot.py](https://github.com/inventare/django-image-uploader-widget/blob/main/utils/tests/snapshot.py#L188)
- [Django - Django 4.2 Release](https://docs.djangoproject.com/en/4.2/releases/4.2/)
- [django-image-uploader-widget#103](https://github.com/inventare/django-image-uploader-widget/issues/103)

---

Foto de <a href="https://unsplash.com/pt-br/@buudkaanaa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Budka Damdinsuren</a> na <a href="https://unsplash.com/pt-br/fotografias/xihqiK6rD9k?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>