eletrotupi / tcc/ commit / 04c3355

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 files
Parents a11279f
Changed 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 +
});