@12-apps/forms-core 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/forms-core",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Pure, framework-agnostic form primitives — field validators, the Result type, and createServerAction. Zero dependencies, consumed from source by @12-apps/ui and apps.",
@@ -15,7 +15,7 @@
15
15
  "typecheck": "tsc --noEmit"
16
16
  },
17
17
  "devDependencies": {
18
- "@12-apps/typescript-config": "^1.5.0",
18
+ "@12-apps/typescript-config": "^1.5.2",
19
19
  "typescript": "^5.9.2",
20
20
  "vitest": "^3.2.4"
21
21
  },
@@ -34,6 +34,14 @@
34
34
  "prisma",
35
35
  "*.js",
36
36
  "*.mjs",
37
- "*.md"
37
+ "*.md",
38
+ "!eslint.config.js",
39
+ "!**/__tests__/**",
40
+ "!**/tests/**",
41
+ "!**/*.test.*",
42
+ "!**/*.spec.*",
43
+ "!**/*.stories.*",
44
+ "!**/*.test-story.*",
45
+ "!**/test-helpers.*"
38
46
  ]
39
47
  }
package/src/br.spec.ts DELETED
@@ -1,173 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import {
4
- cep,
5
- CEP_PATTERN,
6
- cepDigits,
7
- cnpj,
8
- cnpjChars,
9
- formatCep,
10
- formatCnpj,
11
- isUf,
12
- isValidCep,
13
- isValidCnpj,
14
- UF_LIST,
15
- } from './br';
16
-
17
- /**
18
- * `11.222.333/0001-81` is the canonical all-numeric fixture; `12.ABC.345/01DE-35`
19
- * is Receita Federal's own alphanumeric example (IN 2.229/2024).
20
- */
21
- const VALID_NUMERIC = '11.222.333/0001-81';
22
- const VALID_ALPHANUMERIC = '12.ABC.345/01DE-35';
23
-
24
- describe('forms-core/br', () => {
25
- describe('UF_LIST / isUf', () => {
26
- it('holds the 27 federative units', () => {
27
- expect(UF_LIST).toHaveLength(27);
28
- expect(new Set(UF_LIST).size).toBe(27);
29
- });
30
-
31
- it('accepts a real UF and rejects anything else', () => {
32
- expect(isUf('SP')).toBe(true);
33
- expect(isUf('sp')).toBe(false);
34
- expect(isUf('XX')).toBe(false);
35
- });
36
- });
37
-
38
- describe('cepDigits', () => {
39
- it('keeps only digits, capped at eight', () => {
40
- expect(cepDigits('01310-100')).toBe('01310100');
41
- expect(cepDigits('01310100999')).toBe('01310100');
42
- expect(cepDigits('abc')).toBe('');
43
- });
44
- });
45
-
46
- describe('formatCep', () => {
47
- it('inserts the hyphen once the sixth digit arrives', () => {
48
- expect(formatCep('01310')).toBe('01310');
49
- expect(formatCep('013101')).toBe('01310-1');
50
- expect(formatCep('01310100')).toBe('01310-100');
51
- });
52
-
53
- it('is idempotent and forgiving of messy input', () => {
54
- expect(formatCep(formatCep('01310100'))).toBe('01310-100');
55
- expect(formatCep('01.310-100')).toBe('01310-100');
56
- });
57
-
58
- it('produces a value matching CEP_PATTERN once complete', () => {
59
- expect(CEP_PATTERN.test(formatCep('01310100'))).toBe(true);
60
- });
61
- });
62
-
63
- describe('isValidCep', () => {
64
- it('accepts a complete CEP in either shape', () => {
65
- expect(isValidCep('01310100')).toBe(true);
66
- expect(isValidCep('01310-100')).toBe(true);
67
- });
68
-
69
- it('rejects an incomplete or oversized CEP', () => {
70
- expect(isValidCep('0131010')).toBe(false);
71
- expect(isValidCep('')).toBe(false);
72
- });
73
- });
74
-
75
- describe('cep validator', () => {
76
- it('treats empty as valid (the field is optional)', () => {
77
- expect(cep()('')).toBeUndefined();
78
- expect(cep()(' ')).toBeUndefined();
79
- });
80
-
81
- it('returns a message for a malformed CEP', () => {
82
- expect(cep()('123')).toBeTruthy();
83
- expect(cep('Informe o CEP')('123')).toBe('Informe o CEP');
84
- });
85
-
86
- it('returns undefined for a well-formed CEP', () => {
87
- expect(cep()('01310-100')).toBeUndefined();
88
- });
89
- });
90
-
91
- describe('cnpjChars', () => {
92
- it('strips punctuation, uppercases and keeps letters', () => {
93
- expect(cnpjChars(VALID_NUMERIC)).toBe('11222333000181');
94
- expect(cnpjChars('12.abc.345/01de-35')).toBe('12ABC34501DE35');
95
- });
96
-
97
- it('caps at fourteen characters', () => {
98
- expect(cnpjChars('11222333000181999')).toHaveLength(14);
99
- });
100
- });
101
-
102
- describe('formatCnpj', () => {
103
- it('masks progressively as characters arrive', () => {
104
- expect(formatCnpj('11')).toBe('11');
105
- expect(formatCnpj('11222')).toBe('11.222');
106
- expect(formatCnpj('11222333')).toBe('11.222.333');
107
- expect(formatCnpj('112223330001')).toBe('11.222.333/0001');
108
- expect(formatCnpj('11222333000181')).toBe(VALID_NUMERIC);
109
- });
110
-
111
- it('masks an alphanumeric CNPJ', () => {
112
- expect(formatCnpj('12ABC34501DE35')).toBe(VALID_ALPHANUMERIC);
113
- expect(formatCnpj('12abc34501de35')).toBe(VALID_ALPHANUMERIC);
114
- });
115
-
116
- it('is idempotent', () => {
117
- expect(formatCnpj(VALID_NUMERIC)).toBe(VALID_NUMERIC);
118
- expect(formatCnpj(VALID_ALPHANUMERIC)).toBe(VALID_ALPHANUMERIC);
119
- });
120
-
121
- it('never lets a letter into the check-digit positions', () => {
122
- expect(formatCnpj('12ABC34501DEXY')).toBe('12.ABC.345/01DE-');
123
- });
124
- });
125
-
126
- describe('isValidCnpj', () => {
127
- it('accepts an all-numeric CNPJ, masked or bare', () => {
128
- expect(isValidCnpj(VALID_NUMERIC)).toBe(true);
129
- expect(isValidCnpj('11222333000181')).toBe(true);
130
- });
131
-
132
- it("accepts Receita Federal's alphanumeric example", () => {
133
- expect(isValidCnpj(VALID_ALPHANUMERIC)).toBe(true);
134
- expect(isValidCnpj('12abc34501de35')).toBe(true);
135
- });
136
-
137
- it('rejects a wrong check digit', () => {
138
- expect(isValidCnpj('11222333000182')).toBe(false);
139
- expect(isValidCnpj('12ABC34501DE36')).toBe(false);
140
- });
141
-
142
- it('rejects a repeated-character sequence', () => {
143
- expect(isValidCnpj('00000000000000')).toBe(false);
144
- expect(isValidCnpj('11111111111111')).toBe(false);
145
- });
146
-
147
- it('rejects the wrong length', () => {
148
- expect(isValidCnpj('')).toBe(false);
149
- expect(isValidCnpj('1122233300018')).toBe(false);
150
- });
151
-
152
- it('rejects letters in the check-digit positions', () => {
153
- expect(isValidCnpj('12ABC34501DEAB')).toBe(false);
154
- });
155
- });
156
-
157
- describe('cnpj validator', () => {
158
- it('treats empty as valid (the field is optional)', () => {
159
- expect(cnpj()('')).toBeUndefined();
160
- expect(cnpj()(' ')).toBeUndefined();
161
- });
162
-
163
- it('returns a message for an invalid CNPJ', () => {
164
- expect(cnpj()('11222333000182')).toBeTruthy();
165
- expect(cnpj('Confira o CNPJ')('123')).toBe('Confira o CNPJ');
166
- });
167
-
168
- it('returns undefined for a valid CNPJ', () => {
169
- expect(cnpj()(VALID_NUMERIC)).toBeUndefined();
170
- expect(cnpj()(VALID_ALPHANUMERIC)).toBeUndefined();
171
- });
172
- });
173
- });
@@ -1,136 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- import {
4
- ActionError,
5
- createServerAction,
6
- email,
7
- err,
8
- ok,
9
- required,
10
- } from './index';
11
-
12
- describe('forms-core/result', () => {
13
- describe('ok', () => {
14
- it('returns the success shape wrapping the data', () => {
15
- expect(ok(42)).toEqual({ ok: true, data: 42 });
16
- });
17
- });
18
-
19
- describe('err', () => {
20
- it('returns the error shape wrapping the message', () => {
21
- expect(err('bad')).toEqual({ ok: false, error: 'bad' });
22
- });
23
-
24
- it('includes fieldErrors when provided', () => {
25
- expect(err('bad name', { name: 'bad name' })).toEqual({
26
- ok: false,
27
- error: 'bad name',
28
- fieldErrors: { name: 'bad name' },
29
- });
30
- });
31
- });
32
- });
33
-
34
- describe('forms-core/createServerAction', () => {
35
- const schema = { email: [required(), email()] };
36
-
37
- it('returns ok and calls the handler exactly once on valid input', async () => {
38
- const handler = vi.fn(async (input: { email: string }) => input.email);
39
- const action = createServerAction(schema, handler);
40
-
41
- const result = await action({ email: 'a@b.com' });
42
-
43
- expect(result).toEqual({ ok: true, data: 'a@b.com' });
44
- expect(handler).toHaveBeenCalledTimes(1);
45
- });
46
-
47
- it('returns err and never calls the handler on invalid input', async () => {
48
- const handler = vi.fn(async (input: { email: string }) => input.email);
49
- const action = createServerAction(schema, handler);
50
-
51
- const result = await action({ email: '' });
52
-
53
- expect(result.ok).toBe(false);
54
- if (!result.ok) {
55
- expect(typeof result.error).toBe('string');
56
- expect(result.error).toBeTruthy();
57
- }
58
- expect(handler).not.toHaveBeenCalled();
59
- });
60
-
61
- it('maps a throwing handler to a generic err with no stack trace leak', async () => {
62
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
63
- const secret = 'INTERNAL_STACK_DETAIL';
64
- const handler = vi.fn(async () => {
65
- throw new Error(secret);
66
- });
67
- const action = createServerAction(schema, handler);
68
-
69
- const result = await action({ email: 'a@b.com' });
70
-
71
- expect(result.ok).toBe(false);
72
- if (!result.ok) {
73
- expect(typeof result.error).toBe('string');
74
- expect(result.error).toBeTruthy();
75
- expect(result.error).not.toContain(secret);
76
- expect(result.error).not.toContain('at ');
77
- }
78
- // The real cause is logged server-side for diagnosis, never returned.
79
- expect(errorSpy).toHaveBeenCalled();
80
- errorSpy.mockRestore();
81
- });
82
-
83
- it('passes through the message of a thrown ActionError verbatim', async () => {
84
- const handler = vi.fn(async () => {
85
- throw new ActionError('A product with this name already exists.');
86
- });
87
- const action = createServerAction(schema, handler);
88
-
89
- const result = await action({ email: 'a@b.com' });
90
-
91
- expect(result).toEqual({
92
- ok: false,
93
- error: 'A product with this name already exists.',
94
- });
95
- });
96
-
97
- it('attributes a validation failure to the offending field', async () => {
98
- const handler = vi.fn(async (input: { email: string }) => input.email);
99
- const action = createServerAction(schema, handler);
100
-
101
- const result = await action({ email: '' });
102
-
103
- expect(result.ok).toBe(false);
104
- if (!result.ok) {
105
- expect(result.fieldErrors).toEqual({ email: result.error });
106
- }
107
- expect(handler).not.toHaveBeenCalled();
108
- });
109
-
110
- it('surfaces an ActionError field as fieldErrors so the form can highlight it', async () => {
111
- const handler = vi.fn(async () => {
112
- throw new ActionError('Já existe um produto com esse nome.', 'name');
113
- });
114
- const action = createServerAction(schema, handler);
115
-
116
- const result = await action({ email: 'a@b.com' });
117
-
118
- expect(result).toEqual({
119
- ok: false,
120
- error: 'Já existe um produto com esse nome.',
121
- fieldErrors: { name: 'Já existe um produto com esse nome.' },
122
- });
123
- });
124
-
125
- it('passes an ActionError fieldErrors map straight through (multi-field)', async () => {
126
- const fieldErrors = { 'sku:ABC': 'ABC já usado', 'sku:XYZ': 'XYZ já usado' };
127
- const handler = vi.fn(async () => {
128
- throw new ActionError('SKUs duplicados.', undefined, fieldErrors);
129
- });
130
- const action = createServerAction(schema, handler);
131
-
132
- const result = await action({ email: 'a@b.com' });
133
-
134
- expect(result).toEqual({ ok: false, error: 'SKUs duplicados.', fieldErrors });
135
- });
136
- });
@@ -1,78 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
- import { email, min, minLength, required, validateFields } from './validators';
4
-
5
- describe('forms-core/validators', () => {
6
- describe('required', () => {
7
- it('returns an error message for an empty string', () => {
8
- const result = required()('');
9
- expect(typeof result).toBe('string');
10
- expect(result).toBeTruthy();
11
- });
12
-
13
- it('returns undefined for a non-empty value', () => {
14
- expect(required()('hello')).toBeUndefined();
15
- });
16
-
17
- it('supports a custom message', () => {
18
- expect(required('Campo obrigatório')('')).toBe('Campo obrigatório');
19
- });
20
- });
21
-
22
- describe('email', () => {
23
- it('returns an error message for a malformed address', () => {
24
- const result = email()('not-an-email');
25
- expect(typeof result).toBe('string');
26
- expect(result).toBeTruthy();
27
- });
28
-
29
- it('returns undefined for a valid address', () => {
30
- expect(email()('a@b.com')).toBeUndefined();
31
- });
32
-
33
- it('supports a custom message', () => {
34
- expect(email('E-mail inválido')('bad')).toBe('E-mail inválido');
35
- });
36
- });
37
-
38
- describe('minLength / min', () => {
39
- it('returns an error message when the value is shorter than the minimum', () => {
40
- const result = minLength(3)('ab');
41
- expect(typeof result).toBe('string');
42
- expect(result).toBeTruthy();
43
- });
44
-
45
- it('returns undefined when the value meets the minimum length', () => {
46
- expect(minLength(3)('abc')).toBeUndefined();
47
- });
48
-
49
- it('exposes min as an alias of minLength', () => {
50
- expect(min).toBe(minLength);
51
- expect(min(3)('ab')).toBeTruthy();
52
- expect(min(3)('abc')).toBeUndefined();
53
- });
54
-
55
- it('supports a custom message', () => {
56
- expect(minLength(3, 'Muito curto')('ab')).toBe('Muito curto');
57
- });
58
- });
59
-
60
- describe('validateFields', () => {
61
- it('returns an entry per failing field using the first failing validator', () => {
62
- const requiredMsg = required()('') as string;
63
- const errors = validateFields(
64
- { email: '' },
65
- { email: [required(), email()] },
66
- );
67
- expect(errors).toEqual({ email: requiredMsg });
68
- });
69
-
70
- it('returns an empty object when all fields are valid', () => {
71
- const errors = validateFields(
72
- { email: 'a@b.com' },
73
- { email: [required(), email()] },
74
- );
75
- expect(errors).toEqual({});
76
- });
77
- });
78
- });