# História 20 — Perfis e matriz de permissões

- **Spec:** `tests/20-cms-papeis.spec.ts`
- **Camadas:** E2E (Playwright + axe + teclado), PHPUnit feature (`AcessoRolesCmsTest`)
- **Dono:** Infraestrutura / Acesso (`Modules\Infraestrutura\Acesso`)
- **Status:** ativa

## Jornada
Um administrador com `acesso.editar` abre **Perfis**, cria um perfil novo marcando uma ação
**declarada** pelo manifesto com o teclado (foco + Espaço, sem clique), confere a matriz com as
quatro colunas canônicas e a ação não declarada **desabilitada**, encontra a linha na lista
(contagem de usuários) e exclui o perfil criado pela confirmação nativa. Nenhum dado de teste
permanece: o perfil não tem usuários vinculados.

## Critérios de aceite
1. `GET /papeis` exige `acesso.ver`; sem a permissão responde **403** e o menu omite o item.
2. A lista tem `caption`, `scope="col"`, busca por nome, paginação nativa e perfil protegido
   (`suporte`) sem ações de edição/exclusão.
3. A matriz é uma tabela semântica (`caption`, `scope="col|row|rowgroup"`), agrupada pelo grupo do
   manifesto, com as quatro ações; ação não declarada fica **desabilitada**, nunca omitida.
4. Resumo “N módulos com edição” e contagem de usuários vinculados (inclui soft-deleted).
5. A matriz funciona **sem JavaScript** (checkboxes nativos + `<form>`); teclado percorre e ativa.
6. Criar/editar exigem `acesso.editar`; `permissoes[]` só aceita token declarado; duplicatas são
   normalizadas; token órfão é avisado, preservável/removível e nunca concedido de novo.
7. Perfil `suporte` é recusado na rota e no model; exclusão com usuários vinculados é bloqueada no
   servidor (**422** i18n, nunca 500).
8. Mutação só por `POST`/`PUT`/`DELETE` com CSRF; nunca `GET` que muta.
9. **Axe sem violações A/AA** na lista e no formulário; alvos ≥ 2.75rem.

## Evidência
`make check` (PHPUnit: matriz do manifesto, órfãos, auditoria before/after, sem N+1, i18n, layout)
+ `make check E2E=1` (projeto `cms-desktop`, spec `20-cms-papeis`).

## Observação de dados
Cada execução usa um nome único (`Perfil E2E <timestamp>`) e **exclui** o perfil ao final (sem
usuários vinculados), então a base de dev não acumula perfis de teste.
