Skip to content

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:

AmbienteComoServe para
Demo (recomendado para o roteiro)https://app.neemias.app (Worker em https://api.neemias.app) — HTTPS, seed de demonstraçãorodar 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 cloudflaredvalidar 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_ORIGINS em workers/.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 roda app/e2e/scripts/free-e2e-ports.sh antes; 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:5173 como origem segura em chrome://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 checkin na turma escolhida (o seed do demo cria os usuários; ver workers/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).

ContaPapelUse quando
chamadorCHAMADORa conta do roteiro — operador do dispositivo
adminADMINprecisa de admin (telão, exclusão de aluno)
cadastroCADASTROmatrícula na recepção
relatoriosRELATORIOSrelató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 mesmo clientHash que o navegador deriva da senha, e esse valor depende de VITE_PBKDF2_ITERATIONS (padrão 600k; playwright.online.config.ts roda 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:

  1. pnpm dev:device -- --fresh — apaga o D1 local e reseeda: todas as contas voltam a ter hash de servidor.
  2. 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 (curl sem clientHash): 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 ​

  1. Turma ativa selecionada (o Modo Fácil herda a turma do app normal).
  2. Aluno ativo na turma, com carteirinha emitida: Perfil do aluno → Gerar Carteirinha. Anote o Identificador (credentialValue, no formato STU-…) — é ele que o QR carrega.
  3. 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:
    1. copie o credentialValue inteiro (sem espaços antes/depois);
    2. 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);
    3. 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.
  4. 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:

MensagemCausaComo registrar
Pede a câmera e mostra o previewcaminho normalitem 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âmeranã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 restritoabra 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 getUserMedia de 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:

DataAmbienteAparelho / modeloSO + versãoNavegadorResultadoEvidência / notas
app.neemias.app | localSafari | Chrome | PWApassou | 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.

Distribuído sob licença MIT.