Smoke manual do Modo Fácil em device real (câmera / QR)
Critério de aceite humano do #733. A câmera física não é automatizável: a suíte E2E cobre o pipeline (jsQR, loop de frames em rAF, lookup do credencial, confirmação 1.1.1) com apenas o sensor substituído — o que sobra só um humano vê: o prompt de permissão do sistema, o sensor de verdade, a orientação física, o indicador de câmera do SO e os navegadores in-app.
Onde está a lista de itens. Os itens verificáveis vivem em
app/e2e/specs/14-modo-facil-qr-checkin.spec.ts(cabeçalho, seção "Smoke manual em device real"), junto do teste automatizado que cobre o resto — assim o teste e o roteiro manual não se separam. Este documento é a forma executável dela: ambiente, pré-requisitos, como obter o QR e como registrar o resultado.
Quando rodar
Antes de qualquer release do Modo Fácil que toque a leitura de QR: mudanças em app/src/modules/modoFacil/useQrScanner.ts ou na tela 1.1.4. Não é um gate de todo push — é o gate da câmera.
Ambientes
O celular precisa de contexto seguro: getUserMedia não funciona em http:// fora de localhost, e o celular nunca é o localhost do seu Mac. http://192.168.x.x:5173 não vai abrir a câmera.
Não existe certificado público para um IP privado — não é limitação de fornecedor: a CA/Browser Forum proíbe CAs de emitir para Reserved IP Addresses (RFC 1918: 10/8, 172.16/12, 192.168/16) desde 2015, porque o mesmo endereço existe em milhões de redes. Os certificados de IP do Let's Encrypt (GA desde 2026-01) são só para IPs públicos, com 6 dias de validade. Então "SSL no IP" só existe como CA própria (mkcert + instalar a raiz no aparelho) ou túnel:
| Ambiente | Como | Serve para |
|---|---|---|
| Demo (recomendado para o roteiro) | https://app.neemias.app (Worker em https://api.neemias.app) — HTTPS, seed de demonstração | rodar o roteiro sem montar infraestrutura |
| Local (um comando) | pnpm dev:device — scripts/dev-device.sh abre dois túneis cloudflared (app → :5173, API → :8788), sobe worker + Vite e imprime a URL do celular, com ALLOWED_ORIGINS e VITE_BACKEND_URL já ligados nos túneis. Requer brew install cloudflared | validar exatamente a sua árvore de trabalho |
Por que dois túneis: o app é carregado do primeiro e chama a API do segundo — o localhost:8788 do VITE_BACKEND_URL é o do Mac, não o do celular.
Armadilhas do caminho local:
- CORS: o Worker local valida origem (
ALLOWED_ORIGINSemworkers/.dev.vars). O script passa o origin do túnel via--var; se você montar o túnel na mão, a URL precisa entrar na lista, senão login e chamadas de API falham no celular mesmo com a câmera funcionando. - Portas ocupadas: um
wrangler dev/Vite órfão de uma execução anterior faz o teste servir código velho. O script rodaapp/e2e/scripts/free-e2e-ports.shantes; rode na mão se algo ficar preso. - Alternativa só para Android, sem instalar nada: o Chrome do Android aceita
http://192.168.x.x:5173como origem segura emchrome://flags/#unsafely-treat-insecure-origin-as-secure. O iOS Safari não tem equivalente — e é a fila principal — então isso não substitui o túnel para o AC do #733. - Login no demo: use uma conta com permissão
checkinna turma escolhida (o seed do demo cria os usuários; verworkers/src/routes/seed.ts). - Reseed do demo: o ambiente de demonstração tem reseed periódico — se o aluno sumir entre uma passada e outra, emita a carteirinha de novo (ver pré-requisitos).
Contas e o login local (a armadilha que se disfarça de senha errada)
pnpm dev:device imprime as contas, um QR da URL (aponte a câmera do celular em vez de digitar 40 caracteres aleatórios) e sonda um login de verdade antes de você pegar o telefone. Senha padrão senha123 (ou SEED_PASSWORD, se você exportar).
| Conta | Papel | Use quando |
|---|---|---|
chamador | CHAMADOR | a conta do roteiro — operador do dispositivo |
admin | ADMIN | precisa de admin (telão, exclusão de aluno) |
cadastro | CADASTRO | matrícula na recepção |
relatorios | RELATORIOS | relatórios |
"Usuário ou senha incorretos" com a senha certa não é a senha. O login local aceita dois formatos de hash, e só um deles funciona com a senha crua:
- hash de servidor (
pbkdf2:<salt>:<hash>) — verificado contra a senha crua; funciona sempre, de qualquer cliente. É o que o seed do Worker (POST /api/v1/_seed) grava. - hash de cliente (
pbkdf2:client:<salt>:<hash>) — só verifica contra o mesmoclientHashque o navegador deriva da senha, e esse valor depende deVITE_PBKDF2_ITERATIONS(padrão 600k;playwright.online.config.tsroda o Vite com 1000).
Como uma linha vira "de cliente": a primeira sessão bem-sucedida no app reescreve o hash (migração de dois passos, ADR-0031). Então uma conta que já logou num Vite de 1000 iterações (o E2E online) fica amarrada a esse valor e nunca autentica de um dev server normal — com a mensagem idêntica à de senha errada, e sem pista nenhuma de que a causa é o número de iterações. Medido em 2026-09-15: admin respondia 401 com o clientHash de 600k e 200 com o de 1000 iterações; chamador, multi e relatorios ainda tinham hash de servidor e funcionavam.
Correções, em ordem de preferência:
pnpm dev:device -- --fresh— apaga o D1 local e reseeda: todas as contas voltam a ter hash de servidor.- Use uma conta que ainda tenha hash de servidor (
chamador).
Duas consequências que valem saber:
- Não sonde o login com só a senha crua (
curlsemclientHash): numa conta já migrada ele devolve 401 mesmo saudável. É por isso que a sonda do script manda o corpo inteiro (password+clientHash), como o app. - Não misture sessões do E2E online (1000 iterações) com o dev normal (600k) na mesma conta, ou ela volta a ficar amarrada ao valor "errado".
Pré-requisitos
- Turma ativa selecionada (o Modo Fácil herda a turma do app normal).
- Aluno ativo na turma, com carteirinha emitida: Perfil do aluno → Gerar Carteirinha. Anote o Identificador (
credentialValue, no formatoSTU-…) — é ele que o QR carrega. - QR do passaporte. O app não renderiza o QR do passaporte na tela: o perfil mostra só o valor, e o QR completo vai na impressão da carteirinha. Para o roteiro, produza o QR fora do app:
- copie o
credentialValueinteiro (sem espaços antes/depois); - gere o QR com um gerador confiável que aceite texto puro (ex.: um app de QR offline no próprio celular, ou uma folha impressa);
- exiba o QR em papel (testa o sensor) e, se quiser, numa tela (testa brilho/reflexo). O mesmo valor serve para o fallback de digitação manual da 1.1.4 — útil para separar "a câmera falhou" de "o lookup falhou": se a digitação resolve e a câmera não, o problema é a câmera.
- copie o
- Escurecimento automático desligado e brilho alto na tela que exibe o QR; ambiente com luz razoável.
Roteiro
Itens canônicos em app/e2e/specs/14-modo-facil-qr-checkin.spec.ts (cabeçalho). Resumo do que cada um prova:
iOS / Safari (a fila principal — é o navegador do PWA no iPhone)
Android / Chrome
Ambos
O que cada mensagem da câmera significa (e o que fazer)
A câmera falhava de formas que o app escondia: "Iniciando câmera…" para sempre (sem prazo, sem motivo) e, quando o erro aparecia, "object Object". Desde o commit ca52d815 cada falha sai com uma frase própria — esta tabela separa item que passou, bug e ambiente errado, que é o que a passada precisa decidir:
| Mensagem | Causa | Como registrar |
|---|---|---|
| Pede a câmera e mostra o preview | caminho normal | item de permissão passou — siga o roteiro |
| "Permissão de câmera negada — use a digitação manual do código." | permissão negada (site ou sistema) | é o item de permissão negada: confirme que a digitação resolve o aluno |
| "A câmera está em uso por outro app ou aba. Feche-o e tente de novo" | outro app/aba segurando a câmera | não é bug do app — feche o outro app e repita |
| "Nenhuma câmera encontrada neste aparelho." | aparelho sem câmera acessível (emulador, política do SO) | limitação de ambiente, não falha do app |
| "A câmera exige uma página segura (HTTPS)" | origem não-HTTPS (o caso http://192.168.x.x) | ambiente errado: use o túnel ou a demo — a página precisa ser https |
| "Este navegador não oferece a câmera (navigator.mediaDevices ausente)" | navegador in-app (WhatsApp/Instagram) ou WebView restrito | abra no Chrome/Safari e registre como o cenário "navegador in-app" |
| "A câmera não respondeu em 12 s" | getUserMedia pendente (prompt dispensado, app segurando a câmera) | repita; se persistir, é bug — abra issue com a mensagem e o aparelho |
Uma mensagem fora desta tabela é informação nova: cole o texto exato, com SO e navegador, no comentário do #733. "Iniciando câmera…" eterno, hoje, é impossível — há prazo de 12 s.
Passadas extras que valem a pena (não estão na lista canônica)
- Navegador in-app (WhatsApp/Instagram): abrir o link de dentro do app costuma bloquear
getUserMediade forma diferente do Safari — se a recepção compartilha links, isso aparece em campo. Registrar o resultado mesmo que seja "não funciona": é informação. - PWA instalado (
Adicionar à Tela de Início): o contexto standalone tem regras próprias de permissão; teste separado do Safari (aba). - Segunda visita (permissão já concedida): não deve reaparecer prompt nem travar o preview.
Limites do que este roteiro pode concluir
- Desktop WebKit (o tier noturno
safari.yml) não é iOS Safari: ele pega divergência de engine, não o fluxo de instalação/permissão do iOS. São coisas diferentes, e é por isso que este roteiro existe. - Um QR pequeno ou de baixa resolução pode falhar legitimamente: o jsQR precisa de módulos razoavelmente grandes (o fixture do E2E usa 8 px/módulo com quiet zone de 4). Antes de abrir bug, aumente o QR.
- O roteiro cobre a câmera. A correção do lookup, do vínculo guardião↔aluno e do registro de presença já é coberta pela suíte E2E online.
Como registrar o resultado
Postar um comentário no #733 com esta tabela preenchida (uma linha por aparelho/navegador), mais screenshots do preview e da mensagem de permissão negada:
| Data | Ambiente | Aparelho / modelo | SO + versão | Navegador | Resultado | Evidência / notas |
|---|---|---|---|---|---|---|
| app.neemias.app | local | Safari | Chrome | PWA | passou | falhou (item nº) |
O issue só fecha com iOS e Android cobertos e sem item falhando sem issue de acompanhamento. Falha confirmada vira issue própria e é linkada daqui.
Referências
app/e2e/specs/14-modo-facil-qr-checkin.spec.ts— a lista canônica dos itens + o teste automatizado do pipeline.app/e2e/qrFixture.ts— como o E2E substitui só o sensor (jsQR real, QR real).- #733 — o issue e o registro histórico das passadas.
- Handbook de execução de testes §12 — os tiers de E2E e o que cada um cobre.