diff --git a/CHANGELOG.md b/CHANGELOG.md index 12a5765..119295d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,13 @@ - **`README.md` reescrito como vitrine.** Era um índice de pastas. Agora abre pelo problema (a rotina real do fisioterapeuta domiciliar e a responsabilidade LGPD que um SaaS tradicional transfere ao profissional), explica o BYODB como resposta, e traz badges de CI/versão/testes/licença, diagrama Mermaid da arquitetura (o app fala direto com Drive e Sheets; o Firebase Hosting só serve estático), tabela de stack, instruções de execução, testes e CI/CD. A pendência do `google-services.json` do `com.rodrigo.alta` foi preservada. Seção de screenshots criada com os arquivos ainda pendentes. - **Ruído removido:** `package-lock.json` (lockfile npm órfão, sem `package.json`, que ainda carregava `"name": "app-fisio2"`) apagado, e `.playwright-mcp/` removido do índice e adicionado ao `.gitignore` — artefato de sessão de ferramenta, não pertence ao repositório. +### Alterado +- **CPF deixou de ser obrigatório e de ser validado no cadastro de paciente.** Pedido explícito do cliente: nem todo paciente chega ao primeiro atendimento com o CPF em mãos, e o app não deve travar o cadastro por isso. `TelaCadastroPaciente` não checa mais o campo em `_listarCamposFaltando()` (campo vazio passa) nem chama `ValidadorCpf.validar()` (dígitos verificadores não são mais exigidos — qualquer sequência de até 11 dígitos é aceita, válida ou não). O rótulo do campo perdeu o `*` de obrigatório. + - **Duplicidade continua bloqueada**, mas só quando o campo não está vazio — a comparação de CPF (`p.cpf` vs. o texto digitado, sem máscara) é puramente textual, então dois pacientes sem CPF não colidem entre si, mas dois pacientes com o **mesmo** CPF "inválido" (ex.: `11111111111` repetido) continuam sendo barrados igual a qualquer duplicata real. + - `Validadores.validarCPF`/`ValidadorCpf.validar` (o algoritmo em si) não foram tocados — continuam disponíveis e testados como utilitário; só pararam de ser chamados nesta tela. + - `test/widgets/telas/tela_cadastro_paciente_test.dart`: CT-F1/CT-F2/CT-F4 ajustados (CPF não conta mais como pendência), CT-F3 reescrito para testar exatamente isso, e 3 novos testes cobrindo CPF com sequência sem checksum válido, CPF em branco não gerando falso positivo de duplicata, e CPF duplicado sendo barrado mesmo quando "inválido". + - Total de testes: **365 → 368** (`tela_cadastro_paciente_test.dart` 23 → 26). + ### Correções - **Login no web não completava no iPhone, e no resto sempre pedia duas tentativas.** O fluxo antigo abria dois popups do Google: identidade pelo botão do GIS sobreposto ao desenho, e autorização dos escopos Drive/Sheets num segundo popup disparado automaticamente pelo stream `authenticationEvents` — sem clique nenhum nesta página, já que o clique original tinha sido consumido pela interação inteira com o primeiro popup. O Chrome tolerava isso na maioria das vezes (daí precisar tentar duas vezes); o WebKit do iOS — usado por todo navegador no iOS, Chrome incluso, por exigência da Apple — bloqueava sempre, impedindo o login por completo. Um cliente relatou não conseguir entrar de jeito nenhum pelo iPhone. - **Login interativo no web virou um popup só.** `AutorizadorGoogleWeb.entrarInterativo` (`lib/servicos/autorizador_google_web.dart`) pede conta e consentimento dos escopos juntos, chamado sem nenhum `await` real entre o toque do usuário e `requestAccessToken()` — preserva a ativação do usuário que o navegador exige para não bloquear o popup. Identidade (nome/e-mail) passa a vir do próprio access token via `userinfo` do Google. diff --git a/CLAUDE.md b/CLAUDE.md index 53da08c..a957ca4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,7 +132,7 @@ fisio-home-care/ │ │ ├── gerador_id_test.dart (8 testes — 100% cobertura) │ │ └── mensagens_erro_google_test.dart (8 testes — diagnóstico de login) │ │ -│ └── widgets/ # 191 testes — UI + componentes +│ └── widgets/ # 194 testes — UI + componentes │ ├── componentes/ │ │ ├── modal_detalhes_evolucao_test.dart (6 testes) │ ├── modal_detalhes_paciente_test.dart (12 testes) @@ -143,7 +143,7 @@ fisio-home-care/ │ ├── tela_splash_test.dart (7 testes — abertura animada + handoff para o login) │ ├── tela_login_test.dart (13 testes — navegação única + login não-programático) │ ├── tela_dashboard_test.dart (19 testes) -│ ├── tela_cadastro_paciente_test.dart (23 testes) +│ ├── tela_cadastro_paciente_test.dart (26 testes) │ ├── tela_editar_paciente_test.dart (6 testes — campos travados + atualização) │ ├── tela_editar_sessao_test.dart (7 testes — editar/reagendar sessão) │ ├── tela_financeiro_test.dart (8 testes — resumo financeiro mensal) @@ -166,9 +166,9 @@ fisio-home-care/ │ ├── chaves.md # (no .gitignore) — credenciais │ └── testes/ │ ├── README.md # Índice de testes -│ ├── VISAO_GERAL.md # Overview 365 testes +│ ├── VISAO_GERAL.md # Overview 368 testes │ ├── UNITARIOS.md # Detalhe dos 174 unitários -│ └── WIDGETS.md # Detalhe dos 191 widgets +│ └── WIDGETS.md # Detalhe dos 194 widgets │ ├── QA/ │ └── qa.md # Script QA manual (NOT E2E automatizado) @@ -309,7 +309,7 @@ Paciente.calcularIdade() // ✓ delega para UtilitariosData --- -## Testes (365 testes automatizados) +## Testes (368 testes automatizados) ### Estrutura @@ -321,7 +321,7 @@ test/ │ ├── servicos/ — 50 testes (preferencias, Drive, Sheets, repositório) │ └── utilitarios/ — 94 testes (validadores, data, CPF, gerador_id, erros do Google) │ -└── widgets/ (191 testes) +└── widgets/ (194 testes) ├── telas/ — 13 telas principais (UI, interação) ├── componentes/ — modal de detalhes do paciente + rodapé versão └── utilitarios/ — ações de agendamento @@ -354,7 +354,7 @@ flutter test --coverage ✅ **Validação de entrada** — 55 testes (CPF, telefone, nome, data) ✅ **Modelos** — 25 testes (serialização, cópia, status) ✅ **Utilitários** — 31 testes (idade, formatação, geração de ID) -✅ **UI + Interação** — 191 testes (13 telas principais + componentes/utilitários) +✅ **UI + Interação** — 194 testes (13 telas principais + componentes/utilitários) ❌ **Não coberto:** - Google Sheets API real (usaria quota, seria lento) @@ -574,7 +574,7 @@ make release-prod # mescla develop → main → dispara deploy de produção (p | `documentacao/ESPECIFICACOES_TELAS.md` | Requisitos funcionais das telas | ✅ | | `documentacao/SEGURANCA_E_DADOS.md` | LGPD, OAuth, modelo BYODB | ✅ | | `documentacao/IMPLEMENTAR.md` | Roadmap priorizado | ✅ | -| `documentacao/testes/` | 365 testes automatizados | ✅ | +| `documentacao/testes/` | 368 testes automatizados | ✅ | | `documentacao/CI_CD.md` | Pipeline GitHub Actions: fluxo, secrets, uso e troubleshooting | ✅ | | `QA/qa.md` | Script QA manual (não é E2E) | ✅ | diff --git a/QA/qa.md b/QA/qa.md index f5e7039..df11633 100644 --- a/QA/qa.md +++ b/QA/qa.md @@ -24,7 +24,7 @@ Você é um Analista de Qualidade Sênior. Sua tarefa é criar scripts de teste - **Agenda do dia:** Exibe somente sessões com `Situacao = "Agendado"` na data atual; sessões antigas sem desfecho aparecem em **Pendências**. - **Desfechos de sessão:** `Realizado`, `Cancelado`, `Cancelado pelo paciente`, `Cancelado pelo profissional`, `Faltou com aviso`, `Faltou sem aviso`. - **Nova sessão:** Não permite agendar horários retroativos (anteriores ao momento atual). -- **Pacientes:** Cadastro com validação de campos obrigatórios (nome, CPF, telefone, endereço, dados clínicos iniciais); filtros `Todos` e `Ativos`. +- **Pacientes:** Cadastro com validação de campos obrigatórios (nome, telefone, endereço, dados clínicos iniciais); CPF é opcional e aceito sem validação de dígitos verificadores; filtros `Todos` e `Ativos`. - **Evolução clínica:** Registro estruturado obrigatório após atendimento realizado. - **Rotas:** Integração com Google Maps e Waze a partir do endereço do paciente. - **LGPD:** O profissional atua como Controlador dos dados; o app exige consentimento na entrada e oferece ferramentas de privacidade em Configurações. @@ -143,10 +143,10 @@ Use este helper para interagir com qualquer elemento na tela, seja do app ou do ### 7. Validação de Campos Obrigatórios via AlertDialog -A validação de campos obrigatórios (Nome, CPF, Telefone, Data de Nascimento, Endereço) no cadastro de paciente usa `AlertDialog` em vez de validadores inline do `TextFormField`, pois os erros inline do Flutter **não são expostos na árvore de acessibilidade do Android**. +A validação de campos obrigatórios (Nome, Telefone, Data de Nascimento, Endereço) no cadastro de paciente usa `AlertDialog` em vez de validadores inline do `TextFormField`, pois os erros inline do Flutter **não são expostos na árvore de acessibilidade do Android**. CPF **não** é obrigatório e não passa por nenhuma validação de dígitos verificadores — aceita qualquer sequência de até 11 dígitos, inclusive vazia (pedido explícito do cliente: nem todo paciente tem CPF em mãos no primeiro atendimento). Duplicidade de CPF continua bloqueada, mas só quando o campo não está vazio — dois pacientes sem CPF não colidem entre si. **Comportamento:** -- Ao clicar "Salvar Paciente" com campos vazios → `AlertDialog` com título "Campos obrigatórios" e lista dos itens faltando +- Ao clicar "Salvar Paciente" com campos vazios → `AlertDialog` com título "Campos obrigatórios" e lista dos itens faltando (CPF nunca aparece nessa lista) - Cada item da lista aparece como texto acessível → `getByLabel('Nome Completo')` funciona - Botão "OK" fecha o dialog → `getByLabel('OK').tap()` - `barrierDismissible: false` — só fecha clicando no botão OK @@ -163,13 +163,16 @@ class MockListaPacientesNotifier extends ListaPacientesNotifier { } ``` -**Testes unitários (6):** -- CT-F1: 5 campos vazios → dialog com 5 itens -- CT-F2: só Nome preenchido → dialog com 4 itens -- CT-F3: só CPF preenchido → dialog com 4 itens -- CT-F4: Nome+CPF+Telefone → dialog com 2 itens (Data, Endereço) +**Testes unitários** (`test/widgets/telas/tela_cadastro_paciente_test.dart`): +- CT-F1: todos os campos obrigatórios vazios → dialog com 4 itens (Nome, Telefone, Data, Endereço) +- CT-F2: só Nome preenchido → dialog com 3 itens +- CT-F3: CPF em branco não aparece nos campos obrigatórios e o cadastro segue normalmente +- CT-F4: Nome+Telefone → dialog com 2 itens (Data, Endereço) - CT-F5: todos preenchidos → salva sem dialog - CT-F6: dialog fecha ao clicar OK +- CPF sem dígito verificador válido (ex.: `11111111111`) é aceito, mas duplicado é bloqueado +- CPF com sequência qualquer (sem checksum válido) é salvo como digitado +- CPF em branco não gera falso positivo de duplicidade entre dois pacientes ### 8. `getByLabel` em AlertDialog @@ -180,7 +183,7 @@ O Flutter expõe `AlertDialog.title`, `AlertDialog.content` Text widgets e `Text await expect(screen.getByLabel('Campos obrigatórios')).toBeVisible(); // Verificar itens da lista await expect(screen.getByLabel('Nome Completo')).toBeVisible(); -await expect(screen.getByLabel('CPF')).toBeVisible(); +// CPF não é mais obrigatório — não aparece nesta lista. // Fechar o dialog await screen.getByLabel('OK').tap(); // Confirmar que fechou diff --git a/README.md b/README.md index e8c2ccb..30e4d96 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Deploy de Produção](https://github.com/lacerdaRodrigo/alta/actions/workflows/deploy-prod.yml/badge.svg)](https://github.com/lacerdaRodrigo/alta/actions/workflows/deploy-prod.yml) [![Deploy de Testes](https://github.com/lacerdaRodrigo/alta/actions/workflows/deploy-preview.yml/badge.svg)](https://github.com/lacerdaRodrigo/alta/actions/workflows/deploy-preview.yml) [![Flutter](https://img.shields.io/badge/Flutter-3.44.1-02569B?logo=flutter&logoColor=white)](https://flutter.dev) -[![Testes](https://img.shields.io/badge/testes-365%20passando-success)](documentacao/testes/) +[![Testes](https://img.shields.io/badge/testes-368%20passando-success)](documentacao/testes/) [![Licença](https://img.shields.io/badge/licen%C3%A7a-propriet%C3%A1ria-lightgrey)](LICENSE) [**Acessar o aplicativo**](https://app-fisio-care-2.web.app) · [Documentação](documentacao/) · [Modelo de dados](documentacao/MODELO_DADOS.md) · [Segurança e LGPD](documentacao/SEGURANCA_E_DADOS.md) @@ -165,7 +165,7 @@ make dev-web # http://localhost:5000 ## Testes -**365 testes automatizados** — 174 unitários + 191 de widget, cobrindo as 14 +**368 testes automatizados** — 174 unitários + 194 de widget, cobrindo as 14 telas, os 3 modelos, os serviços de Drive/Sheets/repositório e os validadores. ```bash @@ -234,7 +234,7 @@ infraestrutura do mantenedor. | [`documentacao/SEGURANCA_E_DADOS.md`](documentacao/SEGURANCA_E_DADOS.md) | LGPD, OAuth e o modelo BYODB | | [`documentacao/CI_CD.md`](documentacao/CI_CD.md) | Pipeline, secrets e troubleshooting | | [`documentacao/IMPLEMENTAR.md`](documentacao/IMPLEMENTAR.md) | Roadmap priorizado | -| [`documentacao/testes/`](documentacao/testes/) | Os 365 testes, um a um | +| [`documentacao/testes/`](documentacao/testes/) | Os 368 testes, um a um | | [`QA/qa.md`](QA/qa.md) | Roteiro de QA manual | | [`CHANGELOG.md`](CHANGELOG.md) | Histórico de versões | diff --git a/documentacao/IMPLEMENTAR.md b/documentacao/IMPLEMENTAR.md index d6ee8c2..4fd4cae 100644 --- a/documentacao/IMPLEMENTAR.md +++ b/documentacao/IMPLEMENTAR.md @@ -4,7 +4,7 @@ > registrado no [`CHANGELOG.md`](../CHANGELOG.md); o estado atual da arquitetura > está no [`CLAUDE.md`](../CLAUDE.md). -**Status:** MVP funcional completo em produção (web) · 365 testes automatizados +**Status:** MVP funcional completo em produção (web) · 368 testes automatizados **Atualizado em:** 2026-07-29 --- diff --git a/documentacao/testes/COBERTURA_ANALISE.md b/documentacao/testes/COBERTURA_ANALISE.md index b58e156..30c5474 100644 --- a/documentacao/testes/COBERTURA_ANALISE.md +++ b/documentacao/testes/COBERTURA_ANALISE.md @@ -1,7 +1,7 @@ # 📊 Análise de Cobertura de Testes > ⚠️ **ARQUIVADO — snapshot histórico de 2026-06-16.** Todas as 12 telas -> principais já têm testes de widget hoje (365 testes no total). Para os +> principais já têm testes de widget hoje (368 testes no total). Para os > números atuais, veja [`VISAO_GERAL.md`](./VISAO_GERAL.md). Este documento > fica só como registro de como a cobertura evoluiu (era 60% de telas > testadas nessa data) — não reflete o estado atual do projeto. diff --git a/documentacao/testes/README.md b/documentacao/testes/README.md index c58552a..a0cfa14 100644 --- a/documentacao/testes/README.md +++ b/documentacao/testes/README.md @@ -1,6 +1,6 @@ # 📚 Documentação de Testes -Guia completo dos 365 testes automatizados do Alta. +Guia completo dos 368 testes automatizados do Alta. --- @@ -8,7 +8,7 @@ Guia completo dos 365 testes automatizados do Alta. 1. **[VISAO_GERAL.md](./VISAO_GERAL.md)** — Overview, estrutura, como rodar 2. **[UNITARIOS.md](./UNITARIOS.md)** — 174 testes unitários (validadores, modelos, serviços) -3. **[WIDGETS.md](./WIDGETS.md)** — 191 testes de widget (telas, componentes, utilitários) +3. **[WIDGETS.md](./WIDGETS.md)** — 194 testes de widget (telas, componentes, utilitários) --- @@ -39,7 +39,7 @@ flutter test test/unitarios/utilitarios/validadores_test.dart | Unit — Serviços | 50 | 14% | | Widget — Telas | 164 | 45% | | Widget — Componentes/Utilitários | 27 | 8% | -| **TOTAL** | **365** | **100%** | +| **TOTAL** | **368** | **100%** | --- diff --git a/documentacao/testes/VISAO_GERAL.md b/documentacao/testes/VISAO_GERAL.md index 3fe4c1a..9d36534 100644 --- a/documentacao/testes/VISAO_GERAL.md +++ b/documentacao/testes/VISAO_GERAL.md @@ -1,6 +1,6 @@ # 📋 Testes Automatizados — Visão Geral -**Status:** ✅ 365 testes passando +**Status:** ✅ 368 testes passando --- @@ -10,12 +10,12 @@ O projeto utiliza **apenas testes unitários e de widget** (sem E2E automatizado | Categoria | Quantidade | % | Focos | |---|---|---|---| -| **Unit — Utilitários** | 93 | 26% | Validadores, formatadores, gerador de IDs | +| **Unit — Utilitários** | 94 | 26% | Validadores, formatadores, gerador de IDs | | **Unit — Modelos** | 30 | 8% | Serialização, transformação, cópia | | **Unit — Serviços** | 50 | 14% | Preferências, Drive, Sheets e repositório de dados (via `ServidorGoogleFake`) | -| **Widget — Telas** | 164 | 45% | UI, interação, estados visuais | +| **Widget — Telas** | 167 | 45% | UI, interação, estados visuais | | **Widget — Componentes/Utilitários** | 27 | 8% | Modal de detalhes, rodapé versão, ações de agendamento | -| **TOTAL** | **365** | **100%** | — | +| **TOTAL** | **368** | **100%** | — | --- @@ -41,7 +41,7 @@ test/ │ ├── validador_cpf_test.dart (9 testes) │ ├── validadores_test.dart (46 testes) │ └── gerador_id_test.dart (8 testes) -└── widgets/ (191 testes — UI + componentes) +└── widgets/ (194 testes — UI + componentes) ├── componentes/ │ ├── modal_detalhes_evolucao_test.dart (6 testes) │ ├── modal_detalhes_paciente_test.dart (12 testes) @@ -52,7 +52,7 @@ test/ ├── tela_splash_test.dart (7 testes — abertura animada) ├── tela_login_test.dart (13 testes) ├── tela_dashboard_test.dart (19 testes) - ├── tela_cadastro_paciente_test.dart (23 testes) + ├── tela_cadastro_paciente_test.dart (26 testes) ├── tela_editar_paciente_test.dart (6 testes — campos travados + atualização) ├── tela_editar_sessao_test.dart (7 testes — editar/reagendar sessão) ├── tela_financeiro_test.dart (8 testes — resumo financeiro mensal) @@ -69,7 +69,7 @@ test/ ## Como Rodar ```bash -# Todos os 365 testes +# Todos os 368 testes flutter test # Apenas unitários diff --git a/documentacao/testes/WIDGETS.md b/documentacao/testes/WIDGETS.md index 972fe01..188e443 100644 --- a/documentacao/testes/WIDGETS.md +++ b/documentacao/testes/WIDGETS.md @@ -1,4 +1,4 @@ -# 🎨 Testes de Widget (191 testes) +# 🎨 Testes de Widget (194 testes) Testes de componentes visuais: telas, componentes reutilizáveis e utilitários de UI, interação do usuário e estados visuais. @@ -168,7 +168,7 @@ valor e observações. --- -### tela_cadastro_paciente_test.dart (23 testes) +### tela_cadastro_paciente_test.dart (26 testes) **Tela:** Cadastro de novo paciente — formulário com validação de campos obrigatórios. @@ -194,31 +194,44 @@ valor e observações. ``` #### Validação de Campos Obrigatórios — CT-F (6 testes) + +CPF deixou de ser obrigatório e de ser validado por dígito verificador — pedido +explícito do cliente, já que nem todo paciente chega ao primeiro atendimento +com o CPF em mãos. Aceita qualquer sequência de até 11 dígitos, inclusive vazia. + ```dart -✓ CT-F1: 5 campos vazios → dialog com 5 itens - (Nome, CPF, Telefone, Data de Nascimento, Endereço) -✓ CT-F2: Só Nome preenchido → dialog com 4 itens -✓ CT-F3: Só CPF preenchido → dialog com 4 itens -✓ CT-F4: Nome+CPF+Telefone → dialog com 2 itens +✓ CT-F1: todos os campos obrigatórios vazios → dialog com 4 itens + (Nome, Telefone, Data de Nascimento, Endereço — CPF nunca aparece) +✓ CT-F2: Só Nome preenchido → dialog com 3 itens +✓ CT-F3: CPF em branco não aparece nos campos obrigatórios e o + cadastro segue normalmente +✓ CT-F4: Nome+Telefone → dialog com 2 itens (Faltam Data e Endereço) ✓ CT-F5: Todos preenchidos → salva sem dialog ✓ CT-F6: Dialog fecha ao clicar OK ``` -#### Cobertura adicional (8 testes) +#### Cobertura adicional (11 testes) ```dart ✓ Botão fechar (X) aciona o retorno ✓ Selecionar gênero no dropdown atualiza a seleção ✓ Validação do formulário aceita o gênero selecionado ✓ Telefone com menos de 10 dígitos é sinalizado ✓ CPF já cadastrado exibe snackbar de erro +✓ CPF sem dígito verificador válido (ex.: 11111111111) é aceito, + mas duplicado é bloqueado — duplicidade continua sendo comparação + de texto, não depende do CPF ser "válido" +✓ CPF com sequência qualquer (sem checksum válido) é salvo como digitado +✓ CPF em branco não gera falso positivo de duplicidade entre dois pacientes ✓ Salvar com todos os campos gera próximo ID (P005 → P006) e persiste a anamnese ✓ Falha ao salvar exibe snackbar de erro ✓ Popup de campos definitivos: "Revisar" cancela sem salvar ``` > Obs.: os testes que persistem o paciente confirmam o popup de campos -> definitivos (Nome/CPF/Data/Gênero) via `btn_confirmar_cadastro` antes de gravar. +> definitivos (Nome/CPF/Data/Gênero) via `btn_confirmar_cadastro` antes de +> gravar — CPF continua **imutável após o cadastro** (se preenchido), mesmo +> não sendo mais obrigatório nem validado na criação. ### tela_editar_paciente_test.dart (6 testes) diff --git a/lib/telas/tela_cadastro_paciente.dart b/lib/telas/tela_cadastro_paciente.dart index efc4403..41b697f 100644 --- a/lib/telas/tela_cadastro_paciente.dart +++ b/lib/telas/tela_cadastro_paciente.dart @@ -6,7 +6,6 @@ import '../componentes/design_system.dart'; import '../modelos/paciente.dart'; import '../provedores/provedores_dados.dart'; import '../utilitarios/formatters.dart'; -import '../utilitarios/validador_cpf.dart'; class TelaCadastroPaciente extends ConsumerStatefulWidget { const TelaCadastroPaciente({super.key}); @@ -122,7 +121,7 @@ class _TelaCadastroPacienteState extends ConsumerState { key: const Key('campo_cpf'), controller: _cpfController, decoration: const InputDecoration( - labelText: 'CPF *', + labelText: 'CPF', prefixIcon: Icon(Icons.fingerprint_rounded), hintText: '000.000.000-00', ), @@ -467,14 +466,6 @@ class _TelaCadastroPacienteState extends ConsumerState { if (_nomeController.text.trim().isEmpty) faltando.add('Nome Completo'); - final cpfTexto = _cpfController.text.trim(); - if (cpfTexto.isEmpty) { - faltando.add('CPF'); - } else { - final cpfLimpo = cpfTexto.replaceAll(RegExp(r'[^\d]'), ''); - if (!ValidadorCpf.validar(cpfLimpo)) faltando.add('CPF inválido'); - } - final digitosTel = _telefoneController.text.trim().replaceAll(RegExp(r'[^\d]'), ''); if (digitosTel.isEmpty) { faltando.add('Telefone'); @@ -598,9 +589,14 @@ class _TelaCadastroPacienteState extends ConsumerState { final pacientes = ref.read(provedorListaPacientes); final cpfLimpo = _cpfController.text.trim().replaceAll(RegExp(r'[^\d]'), ''); - final cpfJaCadastrado = pacientes.any( - (p) => p.cpf.replaceAll(RegExp(r'[^\d]'), '') == cpfLimpo, - ); + // CPF não é mais obrigatório: com o campo em branco, `cpfLimpo` fica vazio + // e não pode contar como duplicata — senão o segundo paciente sem CPF + // já seria barrado pelo primeiro. + final cpfJaCadastrado = + cpfLimpo.isNotEmpty && + pacientes.any( + (p) => p.cpf.replaceAll(RegExp(r'[^\d]'), '') == cpfLimpo, + ); if (cpfJaCadastrado) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/test/widgets/telas/tela_cadastro_paciente_test.dart b/test/widgets/telas/tela_cadastro_paciente_test.dart index 2684b6e..cff4b01 100644 --- a/test/widgets/telas/tela_cadastro_paciente_test.dart +++ b/test/widgets/telas/tela_cadastro_paciente_test.dart @@ -74,7 +74,9 @@ Future _montarTela( await tester.pumpAndSettle(); } -/// Preenche os campos obrigatórios (nome, CPF, telefone, data, endereço, dor). +/// Preenche os campos obrigatórios (nome, telefone, data, endereço, dor) e, +/// por conveniência, também o CPF — que não é obrigatório, mas a maioria dos +/// testes quer um paciente realista. Future _preencherObrigatorios( WidgetTester tester, { String cpf = '52998224725', @@ -301,7 +303,7 @@ testWidgets('deve permitir salvar paciente sem preencher campos de anamnese', ( // Preencher apenas dados pessoais obrigatórios await tester.enterText(find.widgetWithText(TextFormField, 'Nome Completo *'), 'João Souza'); - await tester.enterText(find.widgetWithText(TextFormField, 'CPF *'), '52998224725'); + await tester.enterText(find.widgetWithText(TextFormField, 'CPF'), '52998224725'); await tester.enterText(find.widgetWithText(TextFormField, 'Telefone *'), '31987654321'); // Preencher data de nascimento @@ -397,7 +399,7 @@ testWidgets('deve bloquear digitação de valor acima de 10 na escala de dor', ( }); group('TelaCadastroPaciente - Campos obrigatorios', () { - testWidgets('CT-F1: todos os 5 campos vazios mostra dialog com 5 itens', ( + testWidgets('CT-F1: todos os campos obrigatórios vazios mostra dialog com 4 itens', ( tester, ) async { await tester.pumpWidget(criarAppTeste()); @@ -408,7 +410,9 @@ group('TelaCadastroPaciente - Campos obrigatorios', () { expect(find.text('Campos obrigatórios'), findsOneWidget); expect(find.text('Nome Completo'), findsOneWidget); - expect(find.text('CPF'), findsOneWidget); + // CPF não é mais obrigatório: nunca aparece como pendência na lista do + // dialog. Não dá pra checar com `find.text('CPF')` aqui — esse texto + // também é o rótulo do próprio campo, sempre presente na tela. expect(find.text('Telefone'), findsOneWidget); expect(find.text('Data de Nascimento'), findsWidgets); expect(find.text('Endereço'), findsWidgets); @@ -419,7 +423,7 @@ group('TelaCadastroPaciente - Campos obrigatorios', () { expect(find.text('Campos obrigatórios'), findsNothing); }); - testWidgets('CT-F2: apenas Nome preenchido mostra dialog com 4 itens', ( + testWidgets('CT-F2: apenas Nome preenchido mostra dialog com 3 itens', ( tester, ) async { await tester.pumpWidget(criarAppTeste()); @@ -432,7 +436,6 @@ group('TelaCadastroPaciente - Campos obrigatorios', () { expect(find.text('Campos obrigatórios'), findsOneWidget); expect(find.text('Nome Completo'), findsNothing); - expect(find.text('CPF'), findsOneWidget); expect(find.text('Telefone'), findsOneWidget); expect(find.text('Data de Nascimento'), findsWidgets); expect(find.text('Endereço'), findsWidgets); @@ -441,36 +444,108 @@ group('TelaCadastroPaciente - Campos obrigatorios', () { await tester.pumpAndSettle(); }); - testWidgets('CT-F3: apenas CPF preenchido mostra dialog com 4 itens', ( - tester, - ) async { - await tester.pumpWidget(criarAppTeste()); - await tester.pumpAndSettle(); + testWidgets( + 'CT-F3: CPF em branco não aparece nos campos obrigatórios e o cadastro segue normalmente', + (tester) async { + final container = ProviderContainer( + overrides: [ + provedorListaPacientes.overrideWith(() => ListaPacientesNotifier()), + provedorRepositorioDados.overrideWith( + (ref) => FakeRepositorioDadosGoogle(), + ), + ], + ); + addTearDown(container.dispose); - await tester.enterText(find.widgetWithText(TextFormField, 'CPF *'), '529.982.247-25'); + await tester.pumpWidget( + UncontrolledProviderScope( + container: container, + child: const MaterialApp( + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + home: TelaCadastroPaciente(), + supportedLocales: [Locale('en', 'US')], + ), + ), + ); + await tester.pumpAndSettle(); - await tester.tap(find.text('Salvar Paciente')); - await tester.pumpAndSettle(); + // Preenche tudo, menos o CPF (fica em branco de propósito). + await tester.enterText( + find.widgetWithText(TextFormField, 'Nome Completo *'), + 'Sem CPF', + ); + await tester.enterText( + find.widgetWithText(TextFormField, 'Telefone *'), + '31987654321', + ); - expect(find.text('Campos obrigatórios'), findsOneWidget); - expect(find.text('CPF'), findsNothing); - expect(find.text('Nome Completo'), findsOneWidget); - expect(find.text('Telefone'), findsOneWidget); - expect(find.text('Data de Nascimento'), findsWidgets); - expect(find.text('Endereço'), findsWidgets); + await tester.tap(find.text('Selecionar data')); + await tester.pumpAndSettle(); + final okButton = find.widgetWithText(TextButton, 'OK'); + if (okButton.evaluate().isNotEmpty) { + await tester.tap(okButton); + } + await tester.pumpAndSettle(); - await tester.tap(find.text('OK')); - await tester.pumpAndSettle(); - }); + await tester.tap(find.text('Toque para preencher endereço')); + await tester.pumpAndSettle(); + await tester.enterText( + find.widgetWithText(TextFormField, 'Rua/Avenida *'), + 'Rua A', + ); + await tester.enterText( + find.widgetWithText(TextFormField, 'Bairro *'), + 'Bairro B', + ); + await tester.enterText( + find.widgetWithText(TextFormField, 'Cidade *'), + 'Cidade C', + ); + await tester.tap(find.text('Confirmar')); + await tester.pumpAndSettle(); + + await tester.scrollUntilVisible( + find.widgetWithText(TextFormField, 'Escala de dor (0-10)'), + 200, + scrollable: find.byType(Scrollable).first, + ); + await tester.pumpAndSettle(); + await tester.enterText( + find.widgetWithText(TextFormField, 'Escala de dor (0-10)'), + '5', + ); + await tester.pumpAndSettle(); + + await tester.scrollUntilVisible( + find.text('Salvar Paciente'), + 200, + scrollable: find.byType(Scrollable).first, + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Salvar Paciente')); + await tester.pumpAndSettle(); + + // Sem CPF, o dialog de campos obrigatórios não aparece. + expect(find.text('Campos obrigatórios'), findsNothing); + expect(find.text('Atenção: campos definitivos'), findsOneWidget); + await _confirmarCamposDefinitivos(tester); + + final pacientes = container.read(provedorListaPacientes); + expect(pacientes, isNotEmpty); + expect(pacientes.first.cpf, isEmpty); + }, + ); - testWidgets('CT-F4: Nome+CPF+Telefone preenchidos mostra dialog com 2 itens', ( + testWidgets('CT-F4: Nome+Telefone preenchidos mostra dialog com 2 itens', ( tester, ) async { await tester.pumpWidget(criarAppTeste()); await tester.pumpAndSettle(); await tester.enterText(find.widgetWithText(TextFormField, 'Nome Completo *'), 'Maria'); - await tester.enterText(find.widgetWithText(TextFormField, 'CPF *'), '529.982.247-25'); await tester.enterText(find.widgetWithText(TextFormField, 'Telefone *'), '(11) 91234-5678'); await tester.tap(find.text('Salvar Paciente')); @@ -478,7 +553,6 @@ group('TelaCadastroPaciente - Campos obrigatorios', () { expect(find.text('Campos obrigatórios'), findsOneWidget); expect(find.text('Nome Completo'), findsNothing); - expect(find.text('CPF'), findsNothing); expect(find.text('Telefone'), findsNothing); expect(find.text('Data de Nascimento'), findsWidgets); expect(find.text('Endereço'), findsWidgets); @@ -513,7 +587,7 @@ group('TelaCadastroPaciente - Campos obrigatorios', () { // Preencher todos os campos obrigatórios await tester.enterText(find.widgetWithText(TextFormField, 'Nome Completo *'), 'João Teste'); - await tester.enterText(find.widgetWithText(TextFormField, 'CPF *'), '52998224725'); + await tester.enterText(find.widgetWithText(TextFormField, 'CPF'), '52998224725'); await tester.enterText(find.widgetWithText(TextFormField, 'Telefone *'), '31987654321'); // Data de nascimento @@ -683,6 +757,102 @@ group('TelaCadastroPaciente - Cobertura adicional', () { expect(find.text('Este CPF já está cadastrado.'), findsOneWidget); }); + testWidgets( + 'CPF sem dígito verificador válido (ex.: 11111111111) é aceito, mas duplicado é bloqueado', + (tester) async { + // Não valida mais o algoritmo do CPF, mas duplicidade continua sendo + // comparação de texto — então mesmo uma sequência "inválida" repetida + // precisa ser barrada igual a qualquer outro CPF. + await _montarTela( + tester, + pacientes: [_pacienteExistente(cpf: '11111111111')], + repositorio: FakeRepositorioDadosGoogle(), + ); + + await _preencherObrigatorios(tester, cpf: '11111111111'); + + await tester.tap(find.byKey(const Key('btn_salvar_paciente'))); + await tester.pumpAndSettle(); + + expect(find.text('Campos obrigatórios'), findsNothing); + expect(find.text('Este CPF já está cadastrado.'), findsOneWidget); + }, + ); + + testWidgets( + 'CPF com sequência qualquer (sem checksum válido) é salvo como digitado', + (tester) async { + final container = ProviderContainer( + overrides: [ + provedorListaPacientes.overrideWith(() => ListaPacientesNotifier()), + provedorRepositorioDados.overrideWith( + (ref) => FakeRepositorioDadosGoogle(), + ), + ], + ); + addTearDown(container.dispose); + // `_preencherObrigatorios` toca em `campo_escala_dor` sem rolar até + // ele — precisa da superfície alta para o campo já estar renderizado, + // senão o finder não encontra nada ("Bad state: No element"). + await tester.binding.setSurfaceSize(const Size(1000, 4000)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + + await tester.pumpWidget( + UncontrolledProviderScope( + container: container, + child: const MaterialApp( + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: [Locale('pt', 'BR'), Locale('en', 'US')], + home: TelaCadastroPaciente(), + ), + ), + ); + await tester.pumpAndSettle(); + + // "123" não passa em nenhum algoritmo de CPF real — nem em dígitos + // suficientes. Precisa ser aceito do mesmo jeito. + await _preencherObrigatorios(tester, cpf: '123'); + + await tester.tap(find.byKey(const Key('btn_salvar_paciente'))); + await tester.pumpAndSettle(); + + expect(find.text('Campos obrigatórios'), findsNothing); + await _confirmarCamposDefinitivos(tester); + + final pacientes = container.read(provedorListaPacientes); + expect(pacientes, isNotEmpty); + // Compara só os dígitos: a máscara pode ou não anexar pontuação a um + // trecho parcial, o que não é o que este teste quer verificar. + expect( + pacientes.first.cpf.replaceAll(RegExp(r'[^\d]'), ''), + '123', + ); + }, + ); + + testWidgets( + 'CPF em branco não gera falso positivo de duplicidade entre dois pacientes', + (tester) async { + await _montarTela( + tester, + pacientes: [_pacienteExistente(cpf: '')], + repositorio: FakeRepositorioDadosGoogle(), + ); + + await _preencherObrigatorios(tester, cpf: ''); + + await tester.tap(find.byKey(const Key('btn_salvar_paciente'))); + await tester.pumpAndSettle(); + + expect(find.text('Este CPF já está cadastrado.'), findsNothing); + expect(find.text('Atenção: campos definitivos'), findsOneWidget); + }, + ); + testWidgets( 'salvar com todos os campos gera próximo ID e persiste a anamnese', (tester) async {