Pular para conteúdo

Testes E2E

Testes end-to-end com Playwright.

Visão Geral

Testes E2E simulam usuários reais navegando pelo site.

def test_user_can_search(page, base_url):
    page.goto(base_url)
    page.fill('input[name="q"]', "lua")
    page.click('button[type="submit"]')

    expect(page.locator(".card")).to_be_visible()

Setup

Instalação

# Dependência (já no pyproject.toml)
poetry add playwright --group dev

# Instalar browsers
poetry run playwright install chromium

Configuração

# tests/e2e/conftest.py
import pytest
from playwright.sync_api import sync_playwright, Page, Browser


@pytest.fixture(scope="session")
def browser():
    """Browser instance compartilhada."""
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        yield browser
        browser.close()


@pytest.fixture
def page(browser: Browser):
    """Nova página para cada teste."""
    context = browser.new_context(
        viewport={"width": 1280, "height": 720}
    )
    page = context.new_page()
    yield page
    page.close()
    context.close()


@pytest.fixture
def base_url():
    """URL do servidor de testes."""
    return "http://localhost:9000"


@pytest.fixture
def authenticated_page(page: Page, base_url: str):
    """Página com usuário logado."""
    page.goto(f"{base_url}/accounts/login/")
    page.fill('input[name="login"]', "teste2e@example.com")
    page.fill('input[name="password"]', "testpass123")
    page.click('button[type="submit"]')
    page.wait_for_url(f"{base_url}/**")
    return page

Estrutura

tests/e2e/
├── conftest.py           # Fixtures E2E
├── test_navigation.py    # Navegação pública
├── test_auth.py          # Autenticação
├── test_upload.py        # Upload de hinários
└── test_social.py        # Features sociais

Exemplos

# tests/e2e/test_navigation.py
import pytest
from playwright.sync_api import Page, expect


@pytest.mark.e2e
class TestPublicNavigation:
    def test_home_page_loads(self, page: Page, base_url: str):
        page.goto(base_url)

        expect(page.locator("h1")).to_be_visible()
        title = page.title()
        assert "Portal" in title or "Hinário" in title

    def test_hymnbook_list(self, page: Page, base_url: str):
        page.goto(f"{base_url}/hinarios/")

        expect(page.get_by_role("heading", name="Hinários")).to_be_visible()
        expect(page.locator(".card").first).to_be_visible()

    def test_search_works(self, page: Page, base_url: str):
        page.goto(f"{base_url}/busca/")
        page.fill('input[name="q"]', "lua")
        page.click('button[type="submit"]')

        page.wait_for_load_state("networkidle")
        # Verifica resultados ou mensagem de não encontrado

Autenticação

# tests/e2e/test_auth.py
@pytest.mark.e2e
class TestAuthentication:
    def test_login_page_loads(self, page: Page, base_url: str):
        page.goto(f"{base_url}/accounts/login/")

        expect(page.locator('input[name="login"]')).to_be_visible()
        expect(page.locator('input[name="password"]')).to_be_visible()

    def test_invalid_login_shows_error(self, page: Page, base_url: str):
        page.goto(f"{base_url}/accounts/login/")
        page.fill('input[name="login"]', "wrong@example.com")
        page.fill('input[name="password"]', "wrongpass")
        page.click('button[type="submit"]')

        page.wait_for_load_state("networkidle")
        # Deve mostrar erro ou permanecer na página
        assert "/accounts/login/" in page.url

    def test_protected_page_redirects(self, page: Page, base_url: str):
        page.goto(f"{base_url}/contribuir/")

        page.wait_for_load_state("networkidle")
        assert "/accounts/login/" in page.url

Upload

# tests/e2e/test_upload.py
@pytest.mark.e2e
class TestUpload:
    def test_upload_requires_auth(self, page: Page, base_url: str):
        page.goto(f"{base_url}/contribuir/")

        assert "/accounts/login/" in page.url

    def test_upload_page_loads(self, authenticated_page: Page, base_url: str):
        authenticated_page.goto(f"{base_url}/contribuir/")

        expect(authenticated_page.locator('input[name="yaml_file"]')).to_be_visible()

    def test_invalid_yaml_shows_error(self, authenticated_page: Page, base_url: str, tmp_path):
        # Criar YAML inválido
        yaml_file = tmp_path / "invalid.yaml"
        yaml_file.write_text("invalid: true\nno_name: here")

        authenticated_page.goto(f"{base_url}/contribuir/")
        authenticated_page.set_input_files('input[name="yaml_file"]', str(yaml_file))
        authenticated_page.click('button[type="submit"]')

        page.wait_for_load_state("networkidle")
        expect(authenticated_page.locator(".errorlist, .error")).to_be_visible()

Rodando

Pré-requisitos

# 1. Servidor rodando
poetry run python manage.py runserver 9000

# 2. Usuário de teste criado
poetry run python manage.py shell -c "
from apps.users.models import User
if not User.objects.filter(email='teste2e@example.com').exists():
    User.objects.create_user('teste2e', 'teste2e@example.com', 'testpass123')
"

# 3. Dados de exemplo
poetry run python manage.py import_yaml tests/fixtures/test_hymnbook.yaml

Comandos

# Rodar E2E
poetry run pytest tests/e2e/ -v

# Com browser visível
poetry run pytest tests/e2e/ -v --headed

# Teste específico
poetry run pytest tests/e2e/test_navigation.py::TestPublicNavigation::test_home_page_loads -v

# Slow motion (debug)
poetry run pytest tests/e2e/ --slowmo=500

Boas Práticas

Esperar por Estado

# Bom: espera explícito
page.wait_for_load_state("networkidle")
page.wait_for_selector(".card")

# Evitar: sleep fixo
import time
time.sleep(2)  # Não faça isso

Seletores Robustos

# Bom: seletores específicos
page.locator('[data-testid="search-button"]')
page.get_by_role("button", name="Buscar")
page.locator('button[type="submit"]')

# Evitar: seletores frágeis
page.locator("div > div > button")
page.locator(".btn-primary")  # Pode mudar

Screenshots em Falha

@pytest.fixture
def page(browser):
    context = browser.new_context()
    page = context.new_page()
    yield page

    # Screenshot se falhou
    if hasattr(page, '_test_failed'):
        page.screenshot(path=f"tests/e2e/screenshots/{test_name}.png")

    page.close()

CI

# .github/workflows/ci.yml
e2e-tests:
  runs-on: ubuntu-latest
  services:
    postgres:
      image: postgres:16-alpine
    typesense:
      image: typesense/typesense:27.1
    redis:
      image: redis:7-alpine

  steps:
    - name: Install Playwright
      run: poetry run playwright install chromium --with-deps

    - name: Start server
      run: |
        poetry run python manage.py migrate
        poetry run python manage.py runserver 9000 &
        sleep 5

    - name: Run E2E tests
      run: poetry run pytest tests/e2e/ -v