# 07 — Estratégia de Testes

> A v6 tem pasta de testes (`v6/tests/Feature` e `v6/tests/Unit`), mas **nenhum** deles referencia o módulo: a suíte cobre apenas Dashboard, Revisões e Configuration (`v6/tests/Feature/Dashboard*Test.php`, `v6/tests/Feature/RevisoesWidgetTest.php`). A verificação do módulo hoje é manual. Esta é a especificação de testes a criar na v7.

## 1. Estratégia

| Tipo | Escopo | Ferramenta sugerida (v7) |
|---|---|---|
| **Feature (API/CMS)** | Rotas do CMS: CRUD de questionário, categorias, status, exclusão em lote, painel de respostas, exportação. | PHPUnit + `Laravel\Testing\RefreshDatabase` |
| **Feature (Portal)** | Fluxo público: ver questionário, enviar respostas, upload de arquivo, ver resultado. | Pest ou PHPUnit |
| **Unit** | `AskType`/`AskStyle` (enums), `ResponsesExport` (transformação de valor → célula), máscaras (CPF/CNPJ/data), geração de slug único, limpeza de órfãos. | Pest (unit) |
| **Caracterização** | Gerar um questionário no formato v6 e comparar o XLSX exportado célula a célula com o da v7. | Comparador de planilha (PHPExcel/PhpSpreadsheet) |

## 2. Fixtures e factories necessárias

```php
// App\Models\DynamicForms\QuestionnaireFactory
Questionnaire::factory()
    ->published()          // published_at = now()
    ->withSlug('avaliacao-de-servicos')
    ->withStatus(true)
    ->withResult(true);

// App\Models\DynamicForms\AskCategoryFactory
AskCategory::factory()->for($questionnaire)->withOrder(1);

// App\Models\DynamicForms\AskFactory
Ask::factory()
    ->for($questionnaire)
    ->for($askCategory)
    ->ofType(AskType::TEXT)
    ->required()
    ->withConfig(['min' => 0, 'max' => 10]);   // conforme o tipo

// App\Models\DynamicForms\FilledQuestionnaireFactory
FilledQuestionnaire::factory()->for($questionnaire)->withAnswers([
    ['ask_id' => 1, 'value' => 'texto'],
    ['ask_id' => 2, 'value' => ['a', 'b']],
]);
```

Catálogos semeados: rodar `AskTypeSeeder` e `AskStyleSeeder` no `setUp` — o `AskTypeSeeder` faz `DB::table('ask_types')->updateOrInsert(['id' => AskTypesEnum::TEXT], ['name' => 'TEXT', 'description' => 'Texto', 'icon' => 'fa-font'])` para os tipos 1..9 (o `AskTypesEnum::SATISFACTION_LEVEL` = 10 **não** é inserido pelo seeder) e o `AskStyleSeeder` usa a mesma técnica para os estilos 1..6.

## 3. Casos de teste — CMS

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| C1 | Salvar questionário novo | payload com 1 categoria e 1 pergunta | `POST /cms/formularios-dinamicos/questionarios` | 201; `questionnaires` criada com `identifier` preenchido; `asks` e `ask_categories` criadas; `config` gravado como JSON |
| C2 | Slug duplicado | 2 questionários com o mesmo título | salvar o 2º | slug do 2º = `titulo-1`; `questionnaires.slug` diferente |
| C3 | Slug a partir do request | payload com `slug` explícito | salvar | `slug` = `Str::slug($slug)` |
| C4 | Categoria sem pergunta | payload com `categories[0].asks = []` | salvar | 422 `errors.asks` = "Não foram encontradas as perguntas do questionário." |
| C5 | Pergunta duplicada (nome de categoria) | 2 categorias com o mesmo `name` | salvar | 422 (regra `distinct`) |
| C6 | Remover pergunta **sem** resposta | questionário salvo, 2 perguntas; payload remove 1 | salvar | pergunta removida do banco |
| C7 | Remover pergunta **com** resposta | questionário com `filled_questionnaires` contendo o `ask_id` | salvar removendo a pergunta | 422 com a mensagem "Existe resposta associada ao campo de título: {título}." e nada é gravado (transação revertida) |
| C8 | Remover categoria | categoria com perguntas | `DELETE /cms/formularios-dinamicos/questionarios/categoria` | 422 "Não é possível deletar a categoria pois existem perguntas associadas a ela." |
| C9 | Alternar status | questionário `status = false` | `POST /cms/formularios-dinamicos/questionarios/atualiza-status` | `status = true`; resposta 201 |
| C10 | Alternar resultado | questionário `has_result = false` | `POST /cms/formularios-dinamicos/questionarios/atualiza-resultado` | `has_result = true` |
| C11 | Exclusão em lote registra atividade | 2 questionários | `POST /cms/formularios-dinamicos/questionarios/delete-por-lote` | 2 soft-deletes; 1 registro em `activity_log` com `Sentinel::getUser()` como causador |
| C12 | Exclusão individual **não** registra atividade | questionário | `DELETE /cms/formularios-dinamicos/questionarios` | soft delete ok; nenhum registro novo em `activity_log` |
| C13 | Formulário inexistente | — | `GET /cms/formularios-dinamicos/questionarios/999999` | 404 com `errors.questionnaire` |
| C14 | Listagem de questionários | questionários cadastrados | `GET /cms/listagem_questionarios` | JSON DataTable com `asks_count` correto e apenas questionários não deletados |
| C15 | Listagem de preenchidos | respostas em 2 questionários | `GET /cms/listagem_questionarios_preenchidos` | uma linha por questionário, `responses` = nº de respostas |
| C16 | Resposta individual | 2 respostas | `GET /cms/formularios-dinamicos/questionarios-preenchidos/{id}/respostas/individual/1` | `data.categories` agrupado, `data.answers` com `ask_title`/`ask_order`/`ask_type`/`ask_config`; arquivo com `ask_file_path` |
| C17 | Exportar planilha | 1 questionário com respostas | `GET /cms/formularios-dinamicos/questionarios-preenchidos/{id}/respostas/exportar-planilha` | 200; arquivo `content/formularios-dinamicos/exports/{id}/exported-data.xlsx` existe; cabeçalho = títulos das perguntas (exceto MULTI_FILE e RADIO) |

## 4. Casos de teste — Portal

| # | Cenário | Arrange | Act | Assert |
|---|---|---|---|---|
| P1 | Ver questionário ativo | questionário `status = true`, slug `avaliacao` | `GET /formularios/avaliacao` | 200; HTML com `<input name="{identifier}">` e categorias em `fieldset` |
| P2 | Ver questionário inativo | `status = false` | `GET /formularios/avaliacao` | 404 |
| P3 | Slug inexistente | — | `GET /formularios/nao-existe` | 404 |
| P4 | Enviar respostas | questionário com 2 perguntas | `POST /formularios/fill-questionnaire/save` | `filled_questionnaires` criada; `answers` = lista de pares `{"ask_id": 1, "value": "Maria Silva"}`; `ip` preenchido |
| P5 | Enviar com arquivo | pergunta tipo FILE; arquivo já enviado | `POST /formularios/fill-questionnaire/save` com o `ask_file.id` | `ask_files.filled_questionnaire_id` = id da resposta |
| P6 | Upload de arquivo | — | `POST /formularios/ask-file` (multipart, 1 MB) | 200; `ask_files` criada com `filled_questionnaire_id = null`; arquivo em `content/formularios-dinamicos/{q}/arquivos/{ask}/` |
| P7 | Upload acima de 20 MB | arquivo de 21 MB | `POST /formularios/ask-file` | 422 com erro em `file` |
| P8 | Upload com `identifier` inválido | — | `POST /formularios/ask-file` | 422 (`exists:asks,identifier`) |
| P9 | Múltipla escolha vazia | pergunta MULTI_SELECT | `POST /formularios/fill-questionnaire/save` sem valor | 422 `errors.message` = "Erro ao preencher questionário!" |
| P10 | reCAPTCHA obrigatório | `Configuration` com as duas chaves | `POST /formularios/fill-questionnaire/save` sem `g-recaptcha-response` | 422 com mensagem "O campo recaptcha é obrigatório." |
| P11 | Resultado sem `has_result` | `has_result = false` | `GET /formularios/resultado/{slug}` | 404 |
| P12 | Resultado com `has_result` | `has_result = true` | `GET /formularios/resultado/{slug}` | 200; HTML com `#idQuestionnaire` e `#divResultado` |
| P13 | Lista de resultados (JSON) | 1 resposta | `GET /formularios/resultado/{id}/list` | 200; `data.categories`, `data.answers`, `data.allResponses`, `data.currentResponse` |
| P14 | Layout do portal | `Configuration.layout = modelo3` | `GET /formularios/avaliacao` | usa `portal.layouts.modelo3.dynamics-forms.questionnaire.edit` |

## 5. Casos de teste — regras de negócio / motor

| # | Cenário | Assert |
|---|---|---|
| M1 | `config` de SELECT é `{options:[{value,text}]}` | salvar e ler o `config` gravado |
| M2 | `config` de FILE é `{isImg:bool}` | idem |
| M3 | `config` de RANGE é `{min,max}` | idem |
| M4 | `config` de TEXT/TEXTAREA/CHECKBOX/SATISFACTION_LEVEL | `config` vazio/`{}` |
| M5 | `required` é booleano no banco | `asks.required = true/false` (cast do model) |
| M6 | Estilo CPF gera a classe `cpf` no input | HTML do portal contém `class="form-control  cpf"` |
| M7 | Estilo CEP gera `cep`, data gera `date`, telefone `phone`, dinheiro `cash` | idem |
| M8 | Satisfação grava 0..4 | ao clicar, o hidden recebe o `data-value` |
| M9 | Exportação converte SELECT no texto da opção | célula = `text` da opção, não o `value` |
| M10 | Exportação converte MULTI_SELECT em lista separada por `, ` | célula = "A, B" |
| M11 | Exportação converte CHECKBOX em "Sim"/"Não" | célula |
| M12 | Exportação converte SATISFACTION_LEVEL nos rótulos | célula = "Muito Satisfeito" etc. |
| M13 | Exportação de FILE gera URL com hiperlink "Clique aqui" | célula com hiperlink |

## 6. Testes de caracterização (v6 → v7)

1. Criar no banco da v6 um questionário com 1 pergunta de cada tipo, 2 respostas e exportar a planilha.
2. Reproduzir a mesma massa no banco da v7 e exportar.
3. Comparar o XLSX célula a célula (cabeçalhos, ordem das colunas, ordem das linhas, tipos de célula).

## 7. Fixtures de view/seed para ambiente de teste

- Seed mínimo: `AskTypeSeeder`, `AskStyleSeeder` e `AddSatisfationLevelOptionOnAskType` (ids 1..10 e 1..6).
- `Configuration` com `layout = modelo1` e (opcionalmente) `recaptcha_public_key`/`recaptcha_private_key` para alternar o comportamento do reCAPTCHA.
- Usuário Sentinel de teste com o papel que enxerga `questionarios.*` (para o middleware `check.route.permission`).
- `Storage::fake('public')` para os testes de upload.

## 8. Como rodar

```bash
# v7 (Laravel 12)
php artisan test --filter=Questionnaire
php artisan test --filter=FilledQuestionnaire
php artisan test --filter=ResponsesExport

# com cobertura
php artisan test --coverage --min=80

# suíte completa do módulo
php artisan test tests/Feature/DynamicForms tests/Unit/DynamicForms
```

Para a v6 (sem suíte), a verificação é manual: subir o container, criar o questionário pelo CMS, conferir `select * from asks` e `select answers from filled_questionnaires`, e abrir o XLSX exportado.
