Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .claude/skills/verificar-local/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ Prefixe todo comando com `MSYS_NO_PATHCONV=1`. Sem isso o Git Bash converte `/ap
## Ciclo rápido (containers já de pé)

```bash
MSYS_NO_PATHCONV=1 docker cp . tk-ruby:/app && MSYS_NO_PATHCONV=1 docker exec -e RAILS_ENV=test -e DB_HOST=tk-pg -e DB_USERNAME=postgres -e DB_PASSWORD=postgres -e DB_NAME=task_keeper_api_test -e SECRET_KEY_BASE=dummy tk-ruby bash -c 'cd /app && bundle lock --add-platform x86_64-linux >/dev/null && bundle exec rubocop --format simple | tail -2 && bundle exec rspec 2>&1 | tail -4'
MSYS_NO_PATHCONV=1 docker cp . tk-ruby:/app && MSYS_NO_PATHCONV=1 docker exec -e RAILS_ENV=test -e DB_HOST=tk-pg -e DB_USERNAME=postgres -e DB_PASSWORD=postgres -e DB_NAME=task_keeper_api_test -e SECRET_KEY_BASE=dummy -e CHROME_BIN=/usr/bin/chromium tk-ruby bash -c 'cd /app && bundle lock --add-platform x86_64-linux >/dev/null && bundle exec rubocop --format simple | tail -2 && bundle exec rspec 2>&1 | tail -4'
```

Leva ~20s. O `add-platform` está ali porque o `docker cp` sobrescreve o `Gemfile.lock` do container com o do worktree, que não tem a plataforma Linux. As gems ficam em `/usr/local/bundle`, fora do `/app`, então recopiar o código não obriga a reinstalar nada.

`CHROME_BIN` aponta os system specs (`spec/system`) para o Chromium do Debian — no runner do GitHub o binário é o `google-chrome`, que o Selenium acha sozinho, mas aqui não existe com esse nome. Sem a variável **a suíte inteira falha**, não só os cinco system specs: eles fazem parte do `bundle exec rspec` normal, de propósito, para que "rodar os testes" signifique a mesma coisa aqui e no CI.

Rodou uma migration? Acrescente `bin/rails db:migrate` antes do RSpec, senão o `maintain_test_schema!` recarrega o schema antigo e o RSpec aborta com "Migrations are pending".

**`docker cp` acrescenta e sobrescreve, mas nunca apaga.** Um arquivo removido aqui continua existindo no container e segue sendo lintado e executado — dá para ver um RuboCop vermelho ou uma contagem de exemplos maior que a real, por causa de um arquivo que não existe mais. Se os números não baterem, compare a contagem (`bundle exec rubocop` diz quantos arquivos inspecionou) e remova o resíduo com `docker exec tk-ruby rm -f /app/<caminho>`.
Expand All @@ -32,12 +34,12 @@ MSYS_NO_PATHCONV=1 docker network create tk-test; MSYS_NO_PATHCONV=1 docker run
Depois, dentro do `tk-ruby`: copie o código, remova o `/app/.git` (é um arquivo de worktree apontando para um caminho Windows que não existe lá), instale `build-essential` e `libpq-dev`, fixe o Bundler na versão do `Gemfile.lock` (`BUNDLED WITH`) e rode `bundle install`.

```bash
MSYS_NO_PATHCONV=1 docker cp . tk-ruby:/app && MSYS_NO_PATHCONV=1 docker exec tk-ruby bash -c 'rm -f /app/.git && apt-get update -qq && apt-get install -y --no-install-recommends build-essential libpq-dev >/dev/null 2>&1 && gem install bundler -v 4.0.18 --no-document >/dev/null && cd /app && bundle lock --add-platform x86_64-linux >/dev/null && bundle install --jobs 4 --retry 3 2>&1 | tail -2'
MSYS_NO_PATHCONV=1 docker cp . tk-ruby:/app && MSYS_NO_PATHCONV=1 docker exec tk-ruby bash -c 'rm -f /app/.git && apt-get update -qq && apt-get install -y --no-install-recommends build-essential libpq-dev chromium chromium-driver >/dev/null 2>&1 && gem install bundler -v 4.0.18 --no-document >/dev/null && cd /app && bundle lock --add-platform x86_64-linux >/dev/null && bundle install --jobs 4 --retry 3 2>&1 | tail -2'
```

## Verificação no navegador

Obrigatória para mudança em JS, CSS, HAML ou CSP — o `Gemfile` não tem driver Capybara/JS, então nada disso tem spec.
Obrigatória para mudança em JS, CSS, HAML ou CSP. Os system specs cobrem CSP e o alto contraste sobrevivendo ao Turbo — mas tour guiado, dropdown de busca, busca por voz e o widget do VLibras continuam sem spec, e são justamente os que só o olho pega.

Congele a imagem com as gems e suba a app numa porta publicada:

Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ jobs:
- name: Zeitwerk check (eager load isolado, fora do processo do RSpec)
run: bin/rails zeitwerk:check

# Inclui os system specs (spec/system, Capybara + Chrome headless).
# Não há passo de instalação de navegador porque a imagem do runner
# já traz o Google Chrome, e o Selenium o encontra sozinho — o
# container de verificação local, que usa o Chromium do Debian,
# precisa de CHROME_BIN (ver a skill verificar-local).
- name: RSpec
run: bundle exec rspec

Expand Down
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,14 @@ Na ordem, todos dentro do container:

## O que os specs não cobrem

O `Gemfile` não tem driver Capybara/JS. Nada que dependa de JavaScript no navegador tem teste automatizado: barra de acessibilidade, tour guiado, dropdown de busca, widget do VLibras, e o CSP inteiro.
Existem cinco system specs (`spec/system`, Capybara + Chrome headless) e eles cobrem só o que falha em **silêncio**: violação de CSP em página pública, autenticada e depois de uma navegação do Turbo; o alto contraste sobrevivendo ao Turbo; e a confirmação do Turbo ao excluir.

Se você mexeu nessas áreas, suba a app e verifique de verdade — console sem erros, elemento aparecendo, clique funcionando, **e o mesmo depois de uma navegação do Turbo Drive**. O CSP deste projeto foi corrigido duas vezes por causa de coisas que só o navegador mostrou, e nenhuma delas quebraria um spec.
Continua sem teste: tour guiado, dropdown de busca, busca por voz e o widget do VLibras. Se você mexeu nisso — ou em qualquer JS, CSS, HAML ou CSP —, suba a app e verifique de verdade: console sem erros, elemento aparecendo, clique funcionando, **e o mesmo depois de uma navegação do Turbo Drive**.

Dois detalhes que mordem ao escrever system spec aqui:

- **o container de verificação precisa de Chromium** (`apt-get install -y chromium chromium-driver` + `CHROME_BIN=/usr/bin/chromium`). Sem ele `bundle exec rspec` falha inteiro, não só os system specs — ver a skill `verificar-local`;
- **o tour guiado dispara sozinho** no painel de quem tem `tour_completed_at` nulo, e o overlay intercepta todo clique. Use `create(:user, ..., tour_completed_at: 1.day.ago)` nos specs que precisam clicar.

## Armadilhas deste repositório

Expand Down
15 changes: 15 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,19 @@ end

group :test do
gem 'rails-controller-testing'

# Cobertura de navegador (ver spec/system e o README, seção "Testes").
# Até aqui nada que dependesse de JavaScript tinha teste — e o CSP deste
# projeto precisou ser corrigido DUAS vezes por comportamento que só
# aparece num navegador de verdade (o widget do VLibras e o nonce por
# requisição quebrando o Turbo Drive). Nenhuma das duas quebraria um
# spec de request.
#
# É a única dependência pesada admitida no projeto, e entra só no grupo
# de teste: não vai para a imagem de produção (BUNDLE_WITHOUT no
# Dockerfile). selenium-webdriver em vez de cuprite/ferrum por ser o
# driver padrão dos system tests do Rails — o Chrome já vem instalado no
# runner do GitHub, então não exige pacote de sistema no CI.
gem 'capybara'
gem 'selenium-webdriver'
end
33 changes: 33 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ GEM
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
ast (2.4.3)
base64 (0.3.0)
bcrypt (3.1.22)
Expand All @@ -84,6 +86,15 @@ GEM
msgpack (~> 1.5)
builder (3.3.0)
cancancan (3.6.1)
capybara (3.40.0)
addressable
matrix
mini_mime (>= 0.1.3)
nokogiri (~> 1.11)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
concurrent-ruby (1.3.8)
connection_pool (3.0.2)
crass (1.0.7)
Expand Down Expand Up @@ -190,6 +201,7 @@ GEM
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
rack
public_suffix (7.0.5)
puma (8.0.2)
nio4r (~> 2.0)
raabro (1.5.0)
Expand Down Expand Up @@ -257,6 +269,7 @@ GEM
responders (3.2.0)
actionpack (>= 7.0)
railties (>= 7.0)
rexml (3.4.4)
rspec-core (3.13.6)
rspec-support (~> 3.13.0)
rspec-expectations (3.13.5)
Expand Down Expand Up @@ -299,7 +312,14 @@ GEM
regexp_parser (>= 2.0)
rubocop (~> 1.86, >= 1.86.2)
ruby-progressbar (1.13.0)
rubyzip (3.6.0)
securerandom (0.4.1)
selenium-webdriver (4.48.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
shoulda-matchers (8.0.1)
activesupport (>= 7.2)
solid_queue (1.7.0)
Expand Down Expand Up @@ -335,10 +355,13 @@ GEM
actionview (>= 8.0.0)
bindex (>= 0.4.0)
railties (>= 8.0.0)
websocket (1.2.11)
websocket-driver (0.8.2)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
zeitwerk (2.8.3)

PLATFORMS
Expand All @@ -348,6 +371,7 @@ PLATFORMS
DEPENDENCIES
bootsnap
cancancan
capybara
debug
devise
factory_bot_rails
Expand All @@ -366,6 +390,7 @@ DEPENDENCIES
rubocop
rubocop-rails
rubocop-rspec
selenium-webdriver
shoulda-matchers
solid_queue
stimulus-rails
Expand All @@ -386,6 +411,7 @@ CHECKSUMS
activerecord (8.1.3.1) sha256=0a2fb6c28f4938f6b013a3a549bec0a7e37d535f3dc8990e804bcc3258c0403b
activestorage (8.1.3.1) sha256=f555254f387b1cffa499d2fd3115d12635eadc5b15206a8534316a67036163ef
activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376
addressable (2.9.0)
ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bcrypt (3.1.22) sha256=1f0072e88c2d705d94aff7f2c5cb02eb3f1ec4b8368671e19112527489f29032
Expand All @@ -395,6 +421,7 @@ CHECKSUMS
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
bundler (4.0.18) sha256=02d9a17429de1847b4e0c9f27a9ee4b20c0a74c0a641b4e77195d6019e3618ac
cancancan (3.6.1) sha256=975c1d5cbf58d5df48a9452a7f61ae3d254608cd87570402f5925a8864c56b62
capybara (3.40.0)
concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1
connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
Expand Down Expand Up @@ -446,6 +473,7 @@ CHECKSUMS
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
propshaft (1.3.2) sha256=1d56a3e56a92c21bfc29caf07406b5386b00d4c47ddf357cf989a5a234b1389e
public_suffix (7.0.5)
puma (8.0.2) sha256=c8ed871dfbbe66448ea9ffd46692342d9804d4071522b52b5331b7b6e7b686fb
raabro (1.5.0) sha256=3f998a7bc84f9c84df3ab580634d2e0a5bda4f0841168d56035f529c9877440a
racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
Expand All @@ -466,6 +494,7 @@ CHECKSUMS
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
responders (3.2.0) sha256=89c2d6ac0ae16f6458a11524cae4a8efdceba1a3baea164d28ee9046bd3df55a
rexml (3.4.4)
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
Expand All @@ -476,7 +505,9 @@ CHECKSUMS
rubocop-rails (2.37.0) sha256=6e1645add5060e0328f8ddda0d820f55697c591394398bf14bb9dccb62f14b7e
rubocop-rspec (3.10.2) sha256=0b3e2ecc592cd10ecbf0095bb58d1e357905276e069643523cc19eb7495f65e2
ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
rubyzip (3.6.0)
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-webdriver (4.48.0)
shoulda-matchers (8.0.1) sha256=5dbb46e5765b9da225111b085e0819e8c8a121ff94bba430a153eb1ea2c60288
solid_queue (1.7.0) sha256=6566b70b801d1c317c81bba7bcdd5677c019afac584a30374b4164002ca356d3
stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06
Expand All @@ -495,8 +526,10 @@ CHECKSUMS
useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844
warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0
web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4
websocket (1.2.11)
websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146
websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241
xpath (3.2.0)
zeitwerk (2.8.3) sha256=2c85125a8467ce069e20123d1e709a08955c9d29c118c25b46b7b7fafdbb92e5

RUBY VERSION
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ A configuração vive em três arquivos versionados:

O CI é a fonte de verdade — roda em Linux, com as versões travadas. Mas descobrir uma ofensa de estilo só depois do push custa um commit extra e um ciclo de dois minutos; foi o que aconteceu duas vezes antes deste fluxo existir. O container encurta o laço sem substituir o CI.

Ele também permite o que o CI não faz: **subir a aplicação e olhar no navegador**. O `Gemfile` não tem driver Capybara/JS, então nada que dependa de JavaScript tem teste — barra de acessibilidade, tour guiado, busca, widget do VLibras e o CSP inteiro. A política de CSP deste projeto foi corrigida **duas vezes** por causa de comportamento que só apareceu num navegador de verdade, e nenhuma das duas quebraria um spec.
Ele também permite o que o CI não faz: **subir a aplicação e olhar no navegador**. Existe hoje uma cobertura automatizada de navegador (`spec/system`, ver "Testes"), mas ela é pequena de propósito e cobre a classe de bug que já mordeu — CSP e o JS da aplicação sobrevivendo ao Turbo. Fora disso continua sem teste: tour guiado, dropdown de busca, busca por voz e o widget do VLibras. Olhar de verdade continua valendo para mudança em JS, CSS, HAML ou CSP.

### O hook

Expand Down Expand Up @@ -258,7 +258,23 @@ Cenários validados explicitamente:
- a API rejeita com `415` qualquer `POST`/`PATCH`/`DELETE` sem `Content-Type: application/json` (proteção contra CSRF — ver seção "Endpoints principais"), sem afetar `GET`;
- ao tentar acessar uma tela protegida sem login, ou ao errar e-mail/senha, a mensagem aparece traduzida em pt-BR (não `Translation missing` — ver seção "Mensagens em pt-BR").

O que **não** tem cobertura automatizada, e por quê: interações que são só JavaScript/CSS (tamanho de fonte, alto contraste, o tooltip de ajuda do Chat ID) não têm teste, porque o `Gemfile` não inclui um driver Capybara/JS — foram verificadas manualmente (incluindo screenshots) antes de cada merge.
### Navegador (`spec/system`)

Capybara + Chrome headless, e deliberadamente **poucos** exemplos: eles não replicam o que os request specs já cobrem, cobrem os lugares onde a falha é **silenciosa** — a página renderiza, o servidor responde `200`, e só o navegador sabe que algo foi bloqueado ou deixou de rodar.

| Exemplo | O que quebraria sem ele |
|---|---|
| página pública sem violação de CSP | uma diretiva nova bloqueando um recurso legítimo, sem sintoma no servidor |
| página autenticada sem violação de CSP | idem, nas telas com importmap e widget do VLibras |
| **sem violação depois de uma navegação do Turbo Drive** | o bug que já aconteceu: nonce por requisição contra um CSP que o Turbo mantém em vigor da primeira resposta |
| alto contraste sobrevive à navegação do Turbo | `application.js` não executando — nonce do importmap recusado deixa a página bonita e inerte |
| confirmação do Turbo ao excluir | `data-turbo-confirm` deixando de funcionar, e a exclusão passando a acontecer sem perguntar |

Violação de CSP não levanta erro nem quebra a renderização: o navegador recusa o recurso e escreve no console. Por isso o driver liga `goog:loggingPrefs` e os exemplos leem o log — sem isso veriam uma página aparentemente perfeita, que é exatamente como as duas quebras anteriores passaram despercebidas.

O filtro é pelo texto `Content Security Policy` do próprio navegador, e não "console limpo": o layout carrega Bootstrap, fontes do Google e o widget do VLibras de CDNs externos, e uma falha de rede do runner vira `Failed to load resource` — ruído que não é o que estes exemplos verificam.

O que **continua** sem cobertura automatizada: tour guiado, dropdown de busca, busca por voz e o widget do VLibras em si. São verificados à mão (incluindo screenshots) quando mudam.

## Endpoints principais

Expand Down
13 changes: 13 additions & 0 deletions app/assets/images/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions app/views/layouts/application.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
%meta{ charset: "utf-8" }
%meta{ name: "viewport", content: "width=device-width, initial-scale=1" }
%title= content_for?(:page_title) ? "#{yield(:page_title)} · Task Keeper API" : "Task Keeper API"
-# Sem um <link rel="icon"> declarado, todo navegador pede /favicon.ico
-# por conta própria — e como não existe rota para isso, cada primeira
-# visita virava um ActionController::RoutingError no log. Ninguém tinha
-# percebido porque nenhum teste usava um navegador de verdade; o
-# primeiro system spec (spec/system) falhou exatamente nisso.
-# Declarando o ícone, o navegador usa este e não pede o .ico.
%link{ rel: "icon", href: asset_path("icon.svg"), type: "image/svg+xml" }
%link{ rel: "preconnect", href: "https://fonts.googleapis.com" }
%link{ rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "anonymous" }
%link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Kanit:wght@300;400;500;600;700&family=Open+Sans:wght@300;400;600&display=swap" }
Expand Down
20 changes: 20 additions & 0 deletions spec/rails_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

require 'rspec/rails'
require 'cancan/matchers'
# Explícito, e não um Dir[] varrendo spec/support: só há dois arquivos
# ali, e um require nomeado diz de onde vem o driver dos system specs
# (ver o comentário em spec/support/factory_bot.rb sobre o glob que este
# projeto optou por não usar).
require_relative 'support/system'

begin
ActiveRecord::Migration.maintain_test_schema!
Expand All @@ -24,6 +29,21 @@
config.include FactoryBot::Syntax::Methods
config.include Devise::Test::IntegrationHelpers, type: :request

# Nos system specs o navegador é um cliente HTTP de verdade, então os
# helpers de request do Devise não valem — quem autentica é o Warden em
# modo de teste, que injeta a sessão no servidor que a Capybara sobe no
# mesmo processo. É o que evita ter que preencher o formulário de login
# em todo spec só para chegar na tela que interessa.
config.include Warden::Test::Helpers, type: :system
config.include ConsoleDoNavegador, type: :system

config.before(type: :system) do
driven_by :chrome_headless
Warden.test_mode!
end

config.after(type: :system) { Warden.test_reset! }

# O throttle das telas de autenticação (ver AuthThrottling) guarda o
# contador de tentativas no cache, que em teste é :memory_store e
# sobrevive entre exemplos. Sem limpar, um spec que faz vários POST em
Expand Down
Loading
Loading