# Menus do Site — Estrategia de Testes

> A suite existente na v6 (`v6/tests/`) cobre apenas o Dashboard (arquivos
> `Dashboard*Test.php` em `v6/tests/Feature/`). **Nao ha nenhum teste do modulo
> de menus** em `v6` nem em `v5` — este documento e a especificacao do que deve
> existir na v7.

---

## 1. Ferramentas

| Item | Escolha na v7 |
|---|---|
| Framework | PHPUnit 11 + Laravel TestCase (`Tests\TestCase`) |
| Banco de teste | PostgreSQL de teste (mesmo engine do legado) ou SQLite com cuidado (as views SQL e o `row_number()` exigem PostgreSQL) |
| Autenticacao | `actingAs()` com um usuario Sentinel criado por factory (o `check.route.permission` exige `Sentinel::check()`) |
| Front-end | Pest ou Playwright apenas para o drag-and-drop (opcional) |

Comandos:

```bash
# suite completa
php artisan test

# somente o modulo
php artisan test --filter=MenuTest

# feature do modulo
php artisan test tests/Feature/Menu/MenuManagementTest.php
```

> Nao ha `php artisan` disponivel no container de analise (PHP ausente); os
> comandos acima sao a convencao padrao Laravel 11/12.

---

## 2. Fixtures / factories necessarias

| Factory | Campos | Observacao |
|---|---|---|
| `MenuFactory` | `name`, `slug` (gerado), `status_create = true` | `slug` unico |
| `MenuItemFactory` | `menus_id`, `menus_items_id` (null), `name`, `order`, `type`, `links_id`, `static_pages_id`, `category_contents_id`, `contents_id`, `is_active = true`, `is_mega_menu = false` | Estados: `root()`, `childOf($item)`, `inactive()`, `megaMenu()`, `typeLink()`, `typeStaticPage()`, `typeContent()` |
| `MenuOrderFactory` | `slug`, `clicks = 0`, `modulo_id` | 1 linha por modulo ativo |
| `ModuloFactory` | `name`, `slug`, `status` | id 31 = `menus-do-site` |
| `LinkFactory` | `title`, `link`, `alt`, `name`, `isBlank` | dependencia de `menu_items` tipo 0 |
| `StaticPageFactory` | `title`, `slug`, `status_create` | id 4 tem semantica especial no presenter |
| `CategoryContentFactory` | `name`, `slug`, `category_contents_id` (null) | ids 2, 3, 7, 12, 32 tem semantica especial |
| `ContentFactory` | `title`, `slug`, `category_contents_id`, `status` | |
| `UserFactory` (Sentinel) | `email`, `password` + `roles` | papeis: `super-admin`, `editor` (com `menu.*`), `sem-permissao` |

### States Deterministicos

```php
// factories ilustrativos
MenuItem::factory()->root()->create(['menus_id' => $menu->id, 'order' => 1]);
MenuItem::factory()->childOf($parent)->create(['menus_id' => $menu->id]);
MenuItem::factory()->inactive()->create();
MenuItem::factory()->megaMenu()->create();
```

---

## 3. Testes unitarios

| # | Alvo | Arrange / Act / Assert |
|---|---|---|
| U1 | `MenuLinkResolver::resolve` com `type = 0` | Item com `link->link = '/contato'` -> resolve `'/contato'` (sem `url()`, igual ao legado) |
| U2 | `MenuLinkResolver::resolve` com `type = 1` e `static_pages_id = 4` | resolve `url($slug)` (sem prefixo `acesso-rapido/`) |
| U3 | `MenuLinkResolver::resolve` com `type = 1` e `static_pages_id != 4` | resolve `url('acesso-rapido/' . $slug)` |
| U4 | `MenuLinkResolver::resolve` com `type = 2` e `category_contents_id = 3` | resolve `url('publicacoes/' . $slug . $contentSlug)` |
| U5 | `MenuLinkResolver::resolve` com `type = 2` e `category_contents_id = 2` | resolve `url('legislacao/' . $slug . $contentSlug)` |
| U6 | `MenuLinkResolver::resolve` com `type = 2` e `category_contents_id = 7` ou `32` | resolve `url('a-instituicao/' . $slug . $contentSlug)` |
| U7 | `MenuLinkResolver::resolve` com `type = 2` e `category_contents_id = 12` | resolve `url('acesso-rapido/' . $slug . $contentSlug)` |
| U8 | `MenuLinkResolver::resolve` com `type = 2` e categoria com pai de slug `conselhos` | resolve `url('portal/' . $slugPai . '/' . $slug)` |
| U9 | `MenuLinkResolver::resolve` com `type = 2` e categoria raiz | resolve `url('/' . $slug . $contentSlug)` |
| U10 | `MenuLinkResolver::resolve` com `type = 2` **sem** categoria | resolve `'#'` (fallback do legado) |
| U11 | `targetLink()` com `isBlank = 1` e `type != 0` | retorna `'target=_blank'` |
| U12 | `targetLink()` com `type = 0` e `link->isBlank = 1` | retorna `'target=_blank'` |
| U13 | `targetLink()` com `type = 0` e `link->isBlank = 0` | retorna `false` (valor booleano cru) |
| U14 | `MenuItem::isFather()` / `is_father` | item raiz -> `true`; item com pai -> `false` (preservar a semantica **invertida** do legado ou corrigir de forma documentada) |
| U15 | `MenuTreeBuilder::forMenu` | 3 niveis de arvore -> arvore aninhada correta, `children` preenchido e `order` respeitado |
| U16 | `MenuLinkResolver` com `content` sem `slug` | resolve sem sufixo (equivalente ao `else { $content_slug = ''; }` do legado) |

---

## 4. Testes de feature — CMS

| # | Cenario | Arrange / Act / Assert |
|---|---|---|
| F1 | Listar menus | Usuario com `menu.index`; GET `/cms/menu` -> 200, view `prefecture.menus.index`; GET `/cms/datatable/menus` -> JSON com `value`, `id`, `name`, `slug` |
| F2 | Listar menus sem permissao | Usuario sem `menu.index`; GET `/cms/menu` -> redirect com flash de autorizacao |
| F3 | Criar menu | POST `/cms/menu` com `name = 'Minha area'` -> 302 para `menu.index`; `menus.slug = 'minha-area'`; `status_create` gravado |
| F4 | Criar menu sem `name` | POST sem `name` -> nao deve criar registro (no legado, estoura `NOT NULL` e resulta em erro; na v7 deve retornar 422) |
| F5 | Editar menu | PUT `/cms/menu/{id}` -> `name` e `slug` atualizados; log de atividade criado |
| F6 | Excluir menu | DELETE `/cms/menu/{id}` -> soft delete; `menus_site` deixa de listar; `menu_items` orfaos permanecem |
| F7 | Criar item raiz tipo 0 | POST `/cms/menu-item` com `type = 0`, `link = '/x'` -> `menu_items.links_id` criado e **um** registro novo em `links` com `title = name` |
| F8 | Criar item raiz tipo 1 | `type = 1`, `static_pages_id` valido -> `menu_items.static_pages_id` gravado; nenhum registro em `links` criado |
| F9 | Criar item raiz tipo 2 | `type = 2`, `category_contents_id` -> gravado; `contents_id` opcional |
| F10 | Criar item com subcategoria | `type = 2`, `sub_category_contents_id = 12` -> `category_contents_id` gravado com **12** (sobrescreve) |
| F11 | Limite de 6 itens raiz | Menu com 6 itens raiz; POST de novo item **sem** `menus_items_id` -> 422 com a mensagem "Limite máximo de itens atingido, escolha um item pai para sub-itens." |
| F12 | Limite de 6 itens raiz (com pai) | Mesmo cenario, POST **com** `menus_items_id` -> 302 e item criado como filho |
| F13 | Limite de 6 itens raiz (na edicao) | Item existente editado quando ja ha 6+ raizes -> **deve passar** (o legado so aplica a regra na criacao) |
| F14 | Criar item sem `menus_id` | -> 422 com "Não foi possível recuperar o número de indentificação do menu." |
| F15 | Reordenar itens | POST `/cms/ajust-items-lvl-1` com `sortable_data = [3, 1, 2]` -> `order` = 1, 2, 3 na ordem recebida; resposta `1` |
| F16 | Reordenar preserva a arvore | POST com ids de niveis misturados -> gravacao sem erro e sem perder vinculos de pai |
| F17 | Excluir item sem filhos | DELETE `/cms/menu-item/{id}` -> 302; item removido; log "Exclusão de um item de menu" |
| F18 | Excluir item com filhos | DELETE de um item raiz com subitem -> item **nao** removido; flash "O item não pôde ser deletado, pois existem depêndencias."; log "Tentativa de exclusão de um item de menu" |
| F19 | Alternar ativacao | POST `/cms/menu-item/{id}/toggle` -> `is_active` invertido; resposta `{"success": true}` |
| F20 | Alternar **sem autenticacao** | POST anonimo -> **401/403** (na v7; no legado passa) |
| F21 | Pagina de itens | GET `/cms/menu-item/{menuId}` -> 200, view `prefecture.menus-itens.show`, contem os nomes dos itens raiz |
| F22 | Ajustar itens pela listagem | GET `/cms/datatable/menus` -> acao "Ajustar itens" aponta para `/cms/menu-item/{idDoMenu}` |

---

## 5. Testes de feature — portal

| # | Cenario | Arrange / Act / Assert |
|---|---|---|
| P1 | Menu principal no portal | Menu `menus_id = 1` com 1 item raiz ativo -> GET `/` -> HTML contem o nome do item e o `href` resolvido pelo presenter |
| P2 | Item inativo nao aparece | Item raiz com `is_active = false` -> o nome **nao** aparece no HTML |
| P3 | Pai ativo sem filhos ativos | Pai ativo + filho inativo -> pai aparece, filho nao |
| P4 | Limite de 3 niveis | Item raiz + filho + neto + bisneto -> o bisneto **nao** aparece no HTML do menu |
| P5 | `is_mega_menu` | Item raiz com `is_mega_menu = true` -> HTML contem `mega-menu-item` e `javascript:;` no `href` |
| P6 | Menu de acesso rapido | `menus_id = 3` -> aparece na home; item inativo nao aparece |
| P7 | Atalhos do topo | `menus_id = 4` com item "Telefones" -> o link do atalho usa `links.link`; sem o item -> fallback `/telefones-uteis` |
| P8 | Rodape | `menus_id = 2` -> secao de rodape renderizada |
| P9 | Grade "Servicos" (modelo5) | `layouts.secretarias-grid = 'menus:{id}'` -> a home do modelo5 lista os itens raiz do menu escolhido |
| P10 | Grade "Servicos" fallback | `secretarias-grid = 'menus'` (sem subId) e sem menu de slug `abas` -> grade vazia, sem erro |
| P11 | Sitemap | Item tipo 0 -> a URL aparece no sitemap |
| P12 | Recarga apos mudanca no CMS | Editar o nome de um item no CMS e recarregar o portal -> o novo nome aparece (garante que nao ha cache stale) |

---

## 6. Testes de integracao

| # | Cenario | Verificacao |
|---|---|---|
| I1 | View SQL `menus_site` | Retorna apenas menus com `deleted_at IS NULL`; ordena `status_create = true` primeiro; `value` e sequencial |
| I2 | Exclusao de categoria de conteudo | Ao excluir uma categoria, os `menu_items` que a referenciam sao tratados (verificar o comportamento legado em `v6/app/Http/Controllers/Prefecture/CategoryContentController.php:709`) |
| I3 | Exclusao de conteudo | idem para `contents_id` (`v6/app/Http/Controllers/Prefecture/ContentController.php:1334`) |
| I4 | Exclusao de pagina estatica | idem para `static_pages_id` (`v6/app/Http/Controllers/Prefecture/StaticPageController.php:368`) |
| I5 | Contador `clicks` | Acessar `/cms/menu` incrementa `menu_orders.clicks` da linha `slug = 'menu'`; acessar `/cms/menu-item/{id}` **nao** incrementa (slug `menu-items` inexistente) |
| I6 | Menu lateral do CMS | Com `modulos.status = false`, a entrada "Menus do site" nao aparece no menu lateral; com `status = true` e permissao `menu.index`, aparece |

---

## 7. Testes de regressao (o que nao pode quebrar)

| # | Regra | Verificacao |
|---|---|---|
| R1 | Limite de 6 itens raiz | Continua valendo na criacao e **nao** na edicao |
| R2 | `is_active` oculta sem excluir | Item desativado continua no banco e no log |
| R3 | Ordem global por menu | Arrastar um subitem nao reordena os irmaos de outro nivel de forma inesperada |
| R4 | `type` determina o destino | Os 3 tipos continuam produzindo as mesmas URLs do legado (ver U1-U10) |
| R5 | Item com filhos nao e excluido | Comportamento preservado (F18) |
| R6 | `is_mega_menu` so no item raiz | Itens com `is_mega_menu = true` e pai definido nao quebram a renderizacao |
| R7 | Atalho "Contato"/"Ouvidoria" | Continuam apontando para os `links` criados pelo `MenuLinkContatoSeeder` |
| R8 | Modulo "Atalhos" sem interruptor | O interruptor nao e renderizado para esse menu |

---

## 8. Fixtures de ambiente (seed de teste)

Para o ambiente de desenvolvimento/teste, a v7 deve ter seeds idempotentes:

| Seed | Conteudo minimo |
|---|---|
| `ModuloSeeder` | `menus-do-site` (id 31, `status = true`) + demais modulos |
| `MenuSeeder` | `principal`, `rodape`, `acesso-rapido`, `atalhos` (`status_create = true`) + `abas` (usada como fallback do modelo5) |
| `MenuOrderSeeder` | `menu` com `modulo_id = 31` |
| `MenuItemSeeder` | arvore minima: "A Cidade" (link) no menu principal; "Contato" e "Ouvidoria" no menu Atalhos |
| `MenuLayoutSeeder` | `layouts.secretarias-grid = 'menus:{id do menu principal}'` |

O seed da v5 `AddMenuTagsToProductionSeeder` **nao deve** ser portado: a v6
resolveu o caso com o seletor de modulo do editor de layout (ver
`09-TAGS-DE-MENU.md`).

---

## 9. Pendencias de teste identificadas

1. **Nao existe cobertura** para o modulo hoje — a suite da v6 so cobre o
   Dashboard.
2. O `Repository::upInsert` chama `dd($th)` em caso de erro
   (`v6/app/Repositories/Repository.php:62`): em teste, isso mata o processo. A
   v7 deve lancar excecao.
3. O `MenuItemRequest` consulta o banco durante a validacao — precisa de banco
   populado em teste.
4. O `check.route.permission` depende de `Route::getCurrentRoute()->getName()`:
   os testes devem usar as rotas reais (e nao chamar o controller direto) para
   exercitar a autorizacao.
5. Os testes de portal exigem o *view composer* registrado — e preciso instanciar
   o `MenuServiceProvider` (o `boot()` retorna cedo em `runningInConsole()`, mas
   o *composer* e registrado no `boot`; verificar o comportamento em ambiente de
   teste).
