frontend: cover the error-translation helpers
The pt-BR message map, the pattern rules and their interpolated replacements, the field translation and the unknown-message fallthrough had no test, so a change to any of them could only be found by hand. Unit tests now pin the whole map, each pattern rule and the pass-through, and the stray console.log that printed on every lookup is gone
Pedro Lucas Porcellis porcellis@eletrotupi.com · 10 days ago
04c33553a625c767ca0aad85e69300d161216dda Browse filesParents
a11279fChanged files
2
Additions
+121
Deletions
-2
frontend/lib/errors/translations.ts
1 1
const MESSAGE_MAP: Record<string, string> = {
2 2
'Required': 'Campo obrigatório',
3 3
'Invalid email': 'Email inválido',
4 4
'Invalid email address': 'Email inválido',
5 5
'Invalid credentials': 'Credenciais inválidas',
26 26
export function translateError(message: unknown): string {
27 27
const text = message instanceof Error ? message.message : String(message);
28 28
29 29
if (MESSAGE_MAP[text]) return MESSAGE_MAP[text];
30 30
31 -
console.log("Erro foi: ", text)
32 -
33 31
for (const [pattern, replacement] of PATTERN_MAP) {
34 32
const match = text.match(pattern);
35 33
if (match) {
36 34
return typeof replacement === 'function' ? replacement(match) : replacement;
37 35
}
45 43
): Record<string, string> {
46 44
return Object.fromEntries(
47 45
Object.entries(fields).map(([k, v]) => [k, translateError(v)]),
48 46
);
49 47
}
/dev/null → frontend/tests/errors/translations.unit.test.ts
1 +
import {
2 +
translateError,
3 +
translateFields,
4 +
} from '@/lib/errors/translations';
5 +
6 +
describe('translateError', () => {
7 +
describe('message map', () => {
8 +
it.each([
9 +
['Required', 'Campo obrigatório'],
10 +
['Invalid email', 'Email inválido'],
11 +
['Invalid email address', 'Email inválido'],
12 +
['Invalid credentials', 'Credenciais inválidas'],
13 +
['Record not found', 'Registro não encontrado'],
14 +
['Unique constraint violation', 'Email já cadastrado'],
15 +
['Network error', 'Erro de conexão'],
16 +
['Internal server error', 'Erro interno. Tente novamente.'],
17 +
['Invalid or expired token', 'Link inválido ou expirado'],
18 +
['No token provided', 'Sessão inválida'],
19 +
[
20 +
'periodEnd must be after periodStart and range ≤ 90 days',
21 +
'O período deve ter entre 1 e 90 dias',
22 +
],
23 +
])('translates %p to %p', (message, expected) => {
24 +
expect(translateError(message)).toBe(expected);
25 +
});
26 +
});
27 +
28 +
describe('pattern rules', () => {
29 +
it('interpolates the minimum length', () => {
30 +
expect(
31 +
translateError('String must contain at least 8 character(s)'),
32 +
).toBe('Deve ter pelo menos 8 caractere(s)');
33 +
});
34 +
35 +
it('interpolates the maximum length', () => {
36 +
expect(
37 +
translateError('String must contain at most 64 character(s)'),
38 +
).toBe('Deve ter no máximo 64 caractere(s)');
39 +
});
40 +
41 +
it('interpolates the lower bound', () => {
42 +
expect(
43 +
translateError('Number must be greater than or equal to 1'),
44 +
).toBe('Deve ser maior ou igual a 1');
45 +
});
46 +
47 +
it('interpolates a decimal upper bound', () => {
48 +
expect(translateError('Number must be less than or equal to 10.5')).toBe(
49 +
'Deve ser menor ou igual a 10.5',
50 +
);
51 +
});
52 +
53 +
it('collapses a type mismatch to a single message', () => {
54 +
expect(translateError('Expected string, received number')).toBe(
55 +
'Tipo de valor inválido',
56 +
);
57 +
});
58 +
59 +
it('collapses an invalid enum value', () => {
60 +
expect(translateError("Invalid enum value. Expected 'a' | 'b'")).toBe(
61 +
'Opção inválida',
62 +
);
63 +
});
64 +
65 +
it('collapses a validation error', () => {
66 +
expect(translateError('Validation error: too many items')).toBe(
67 +
'Erro de validação',
68 +
);
69 +
});
70 +
71 +
it('collapses a bare HTTP status', () => {
72 +
expect(translateError('HTTP 500')).toBe(
73 +
'Algo deu errado. Tente novamente.',
74 +
);
75 +
});
76 +
77 +
it('does not collapse a status with a trailing phrase', () => {
78 +
expect(translateError('HTTP 500 Internal Server Error')).toBe(
79 +
'HTTP 500 Internal Server Error',
80 +
);
81 +
});
82 +
});
83 +
84 +
describe('input handling', () => {
85 +
it('reads the message out of an Error instance', () => {
86 +
expect(translateError(new Error('Invalid credentials'))).toBe(
87 +
'Credenciais inválidas',
88 +
);
89 +
});
90 +
91 +
it('stringifies a non-string, non-Error value', () => {
92 +
expect(translateError(42)).toBe('42');
93 +
});
94 +
95 +
it('passes an unknown message through unchanged', () => {
96 +
expect(translateError('Something nobody mapped')).toBe(
97 +
'Something nobody mapped',
98 +
);
99 +
});
100 +
});
101 +
});
102 +
103 +
describe('translateFields', () => {
104 +
it('translates each field value and keeps the keys', () => {
105 +
expect(
106 +
translateFields({
107 +
email: 'Invalid email',
108 +
password: 'String must contain at least 8 character(s)',
109 +
nickname: 'Untouched value',
110 +
}),
111 +
).toEqual({
112 +
email: 'Email inválido',
113 +
password: 'Deve ter pelo menos 8 caractere(s)',
114 +
nickname: 'Untouched value',
115 +
});
116 +
});
117 +
118 +
it('returns an empty object for no fields', () => {
119 +
expect(translateFields({})).toEqual({});
120 +
});
121 +
});