diff --git a/__tests__/unit/no-err-message.test.ts b/__tests__/unit/no-err-message.test.ts new file mode 100644 index 0000000..2df82ef --- /dev/null +++ b/__tests__/unit/no-err-message.test.ts @@ -0,0 +1,53 @@ +import { describe, it, expect } from 'vitest' +import { readFileSync, readdirSync, statSync } from 'node:fs' +import { join } from 'node:path' + +// Gate por string sobre o código-fonte (#122). `err.message`/`error.message` +// exibido via `toast.*` é sempre o parágrafo genérico do React em produção — +// erro que atravessa a fronteira de Server Action chega ao cliente como um +// `Error` reconstruído por `resolveErrorProd`, então `err instanceof Error` é +// sempre verdadeiro e `.message` nunca é a mensagem lançada pela action. +// Precedente: __tests__/unit/row-actions.test.ts (mesma técnica de +// readFileSync + asserção sobre o conteúdo do arquivo). + +const ROOT = process.cwd() +const SCAN_DIRS = ['app', 'components'] +const IGNORED_DIRS = new Set(['node_modules', '.next', '.git']) + +function collectTsxFiles(dir: string): string[] { + const entries = readdirSync(dir) + const files: string[] = [] + for (const entry of entries) { + if (IGNORED_DIRS.has(entry)) continue + const fullPath = join(dir, entry) + const stat = statSync(fullPath) + if (stat.isDirectory()) { + files.push(...collectTsxFiles(fullPath)) + } else if (entry.endsWith('.tsx')) { + files.push(fullPath) + } + } + return files +} + +// Âncora no sink (`toast.*(...)`), não numa distância de `catch`: o defeito +// da #122 não é ler `err.message`, é exibi-lo ao usuário, e `toast.*` é o +// sink em todos os sites que a #34 enumerou. Uma janela de distância a partir +// de `catch` fica cega assim que um `console.error(...)` (o próprio remédio +// que este arquivo institui) empurra o `toast.error(...)` para fora dela — +// e o falso-positivo que a janela existia para evitar (`errors.message`, +// `next.message` como chave de mapa de erros de formulário) já não ocorre: +// `\b` antes de `err`/`error`/`e` não casa no meio de "errors"/"next". +const TOAST_ERR_MESSAGE = /toast\.\w+\([^)]*\b(?:err|error|e)\s*\.\s*message\b/ + +describe('nenhum componente exibe err.message em toast (#122)', () => { + it('a mensagem de erro de Server Action é mascarada em produção — ler err.message nunca mostra a mensagem real', () => { + const files = SCAN_DIRS.flatMap((dir) => collectTsxFiles(join(ROOT, dir))) + + const ofensores = files + .filter((file) => TOAST_ERR_MESSAGE.test(readFileSync(file, 'utf-8'))) + .map((file) => file.replace(ROOT + '/', '')) + + expect(ofensores).toEqual([]) + }) +}) diff --git a/components/devedores/EditEntryDialog.tsx b/components/devedores/EditEntryDialog.tsx index 0d247e0..48a8ddb 100644 --- a/components/devedores/EditEntryDialog.tsx +++ b/components/devedores/EditEntryDialog.tsx @@ -71,7 +71,8 @@ export function EditEntryDialog({ entry, open, onOpenChange }: Props) { toast.success('Lançamento atualizado.') onOpenChange(false) } catch (err) { - toast.error(err instanceof Error ? err.message : 'Erro ao editar lançamento.') + console.error('[EditEntryDialog] updateDebtEntry falhou', err) + toast.error('Erro ao editar lançamento.') } }) }