bxo 0.0.5-dev.65 → 0.0.5-dev.67
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/README.md +83 -675
- package/example/cors-example.ts +49 -0
- package/example/index.html +5 -0
- package/example/index.ts +57 -0
- package/package.json +9 -15
- package/plugins/cors.ts +124 -98
- package/plugins/index.ts +2 -9
- package/plugins/openapi.ts +130 -0
- package/src/index.ts +646 -59
- package/tsconfig.json +3 -5
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
- package/examples/serve-react/README.md +0 -15
- package/examples/serve-react/app.tsx +0 -8
- package/examples/serve-react/bun.lock +0 -42
- package/examples/serve-react/index.html +0 -9
- package/examples/serve-react/index.ts +0 -27
- package/examples/serve-react/package.json +0 -17
- package/examples/serve-react/tsconfig.json +0 -29
- package/index.ts +0 -5
- package/plugins/README.md +0 -160
- package/plugins/ratelimit.ts +0 -136
- package/src/core/bxo.ts +0 -458
- package/src/handlers/request-handler.ts +0 -230
- package/src/types/index.ts +0 -167
- package/src/utils/context-factory.ts +0 -158
- package/src/utils/helpers.ts +0 -40
- package/src/utils/index.ts +0 -448
- package/src/utils/response-handler.ts +0 -293
- package/src/utils/route-matcher.ts +0 -191
- package/tests/README.md +0 -359
- package/tests/integration/bxo.test.ts +0 -616
- package/tests/run-tests.ts +0 -44
- package/tests/unit/context-factory.test.ts +0 -386
- package/tests/unit/helpers.test.ts +0 -253
- package/tests/unit/response-handler.test.ts +0 -327
- package/tests/unit/route-matcher.test.ts +0 -181
- package/tests/unit/utils.test.ts +0 -475
package/tests/unit/utils.test.ts
DELETED
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'bun:test';
|
|
2
|
-
import {
|
|
3
|
-
parseQuery,
|
|
4
|
-
parseHeaders,
|
|
5
|
-
parseCookies,
|
|
6
|
-
validateData,
|
|
7
|
-
validateResponse,
|
|
8
|
-
parseRequestBody,
|
|
9
|
-
cookiesToHeaders,
|
|
10
|
-
mergeHeadersWithCookies,
|
|
11
|
-
headersToPlainObject,
|
|
12
|
-
createRedirectResponse,
|
|
13
|
-
isFileUpload,
|
|
14
|
-
getFileFromUpload,
|
|
15
|
-
getFileInfo,
|
|
16
|
-
getFileUploads,
|
|
17
|
-
getFormFields
|
|
18
|
-
} from '../../src/utils';
|
|
19
|
-
import { z } from 'zod';
|
|
20
|
-
|
|
21
|
-
describe('Utility Functions', () => {
|
|
22
|
-
describe('parseQuery', () => {
|
|
23
|
-
it('should parse URLSearchParams correctly', () => {
|
|
24
|
-
const searchParams = new URLSearchParams('name=john&age=25&city=new%20york');
|
|
25
|
-
const result = parseQuery(searchParams);
|
|
26
|
-
|
|
27
|
-
expect(result).toEqual({
|
|
28
|
-
name: 'john',
|
|
29
|
-
age: '25',
|
|
30
|
-
city: 'new york'
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should handle empty search params', () => {
|
|
35
|
-
const searchParams = new URLSearchParams('');
|
|
36
|
-
const result = parseQuery(searchParams);
|
|
37
|
-
|
|
38
|
-
expect(result).toEqual({});
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('parseHeaders', () => {
|
|
43
|
-
it('should parse Headers object correctly', () => {
|
|
44
|
-
const headers = new Headers();
|
|
45
|
-
headers.set('content-type', 'application/json');
|
|
46
|
-
headers.set('authorization', 'Bearer token123');
|
|
47
|
-
|
|
48
|
-
const result = parseHeaders(headers);
|
|
49
|
-
|
|
50
|
-
expect(result).toEqual({
|
|
51
|
-
'content-type': 'application/json',
|
|
52
|
-
'authorization': 'Bearer token123'
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('parseCookies', () => {
|
|
58
|
-
it('should parse cookie header correctly', () => {
|
|
59
|
-
const cookieHeader = 'name=john; age=25; city=new%20york';
|
|
60
|
-
const result = parseCookies(cookieHeader);
|
|
61
|
-
|
|
62
|
-
expect(result).toEqual({
|
|
63
|
-
name: 'john',
|
|
64
|
-
age: '25',
|
|
65
|
-
city: 'new york'
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should handle null cookie header', () => {
|
|
70
|
-
const result = parseCookies(null);
|
|
71
|
-
expect(result).toEqual({});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should handle empty cookie header', () => {
|
|
75
|
-
const result = parseCookies('');
|
|
76
|
-
expect(result).toEqual({});
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
describe('validateData', () => {
|
|
81
|
-
it('should validate data with schema', () => {
|
|
82
|
-
const schema = z.object({
|
|
83
|
-
name: z.string(),
|
|
84
|
-
age: z.number()
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
const data = { name: 'john', age: 25 };
|
|
88
|
-
const result = validateData(schema, data);
|
|
89
|
-
|
|
90
|
-
expect(result).toEqual(data);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should return data without validation when no schema', () => {
|
|
94
|
-
const data = { name: 'john', age: 25 };
|
|
95
|
-
const result = validateData(undefined, data);
|
|
96
|
-
|
|
97
|
-
expect(result).toEqual(data);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should throw error for invalid data', () => {
|
|
101
|
-
const schema = z.object({
|
|
102
|
-
name: z.string(),
|
|
103
|
-
age: z.number()
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
const data = { name: 'john', age: 'invalid' };
|
|
107
|
-
|
|
108
|
-
expect(() => validateData(schema, data)).toThrow();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
describe('validateResponse', () => {
|
|
113
|
-
it('should validate response with simple schema', () => {
|
|
114
|
-
const schema = z.object({
|
|
115
|
-
message: z.string()
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const data = { message: 'success' };
|
|
119
|
-
const result = validateResponse({
|
|
120
|
-
200: schema
|
|
121
|
-
}, data, 200);
|
|
122
|
-
|
|
123
|
-
expect(result).toEqual(data);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should validate response with status-based schema', () => {
|
|
127
|
-
const schema = {
|
|
128
|
-
200: z.object({ message: z.string() }),
|
|
129
|
-
400: z.object({ error: z.string() })
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const data = { message: 'success' };
|
|
133
|
-
const result = validateResponse(schema, data, 200);
|
|
134
|
-
|
|
135
|
-
expect(result).toEqual(data);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should return data without validation when no schema', () => {
|
|
139
|
-
const data = { message: 'success' };
|
|
140
|
-
const result = validateResponse(undefined, data);
|
|
141
|
-
|
|
142
|
-
expect(result).toEqual(data);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('cookiesToHeaders', () => {
|
|
147
|
-
it('should convert internal cookies to header strings', () => {
|
|
148
|
-
const cookies = [
|
|
149
|
-
{
|
|
150
|
-
name: 'session',
|
|
151
|
-
value: 'abc123',
|
|
152
|
-
domain: 'example.com',
|
|
153
|
-
path: '/',
|
|
154
|
-
secure: true,
|
|
155
|
-
httpOnly: true
|
|
156
|
-
}
|
|
157
|
-
];
|
|
158
|
-
|
|
159
|
-
const result = cookiesToHeaders(cookies);
|
|
160
|
-
|
|
161
|
-
expect(result).toHaveLength(1);
|
|
162
|
-
expect(result[0]).toContain('session=abc123');
|
|
163
|
-
expect(result[0]).toContain('Domain=example.com');
|
|
164
|
-
expect(result[0]).toContain('Path=/');
|
|
165
|
-
expect(result[0]).toContain('Secure');
|
|
166
|
-
expect(result[0]).toContain('HttpOnly');
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('mergeHeadersWithCookies', () => {
|
|
171
|
-
it('should merge headers with cookies', () => {
|
|
172
|
-
const headers = { 'content-type': 'application/json' };
|
|
173
|
-
const cookies = [
|
|
174
|
-
{ name: 'session', value: 'abc123' }
|
|
175
|
-
];
|
|
176
|
-
|
|
177
|
-
const result = mergeHeadersWithCookies(headers, cookies);
|
|
178
|
-
|
|
179
|
-
expect(result.get('content-type')).toBe('application/json');
|
|
180
|
-
expect(result.get('set-cookie')).toBe('session=abc123');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should preserve special header casing', () => {
|
|
184
|
-
const headers = {
|
|
185
|
-
'WWW-Authenticate': 'Basic realm="example"',
|
|
186
|
-
'x-frame-options': 'DENY',
|
|
187
|
-
'content-type': 'application/json'
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const result = mergeHeadersWithCookies(headers, []);
|
|
191
|
-
|
|
192
|
-
// Check that special headers maintain their casing
|
|
193
|
-
expect(result.get('WWW-Authenticate')).toBe('Basic realm="example"');
|
|
194
|
-
expect(result.get('X-Frame-Options')).toBe('DENY');
|
|
195
|
-
// Regular headers should keep their original casing
|
|
196
|
-
expect(result.get('content-type')).toBe('application/json');
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('headersToPlainObject', () => {
|
|
201
|
-
it('should convert Headers object to plain object', () => {
|
|
202
|
-
const headers = new Headers();
|
|
203
|
-
headers.set('www-authenticate', 'Basic realm="example"');
|
|
204
|
-
headers.set('x-frame-options', 'DENY');
|
|
205
|
-
headers.set('content-type', 'application/json');
|
|
206
|
-
|
|
207
|
-
const result = headersToPlainObject(headers);
|
|
208
|
-
|
|
209
|
-
// Note: Headers object normalizes keys to lowercase
|
|
210
|
-
expect(result).toEqual({
|
|
211
|
-
'www-authenticate': 'Basic realm="example"',
|
|
212
|
-
'x-frame-options': 'DENY',
|
|
213
|
-
'content-type': 'application/json'
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('should handle empty Headers object', () => {
|
|
218
|
-
const headers = new Headers();
|
|
219
|
-
const result = headersToPlainObject(headers);
|
|
220
|
-
|
|
221
|
-
expect(result).toEqual({});
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('createRedirectResponse', () => {
|
|
226
|
-
it('should create redirect response with default status', () => {
|
|
227
|
-
const result = createRedirectResponse('/new-location');
|
|
228
|
-
|
|
229
|
-
expect(result.status).toBe(302);
|
|
230
|
-
expect(result.headers.get('location')).toBe('/new-location');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('should create redirect response with custom status', () => {
|
|
234
|
-
const result = createRedirectResponse('/new-location', 301);
|
|
235
|
-
|
|
236
|
-
expect(result.status).toBe(301);
|
|
237
|
-
expect(result.headers.get('location')).toBe('/new-location');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('should include additional headers', () => {
|
|
241
|
-
const headers = { 'x-custom': 'value' };
|
|
242
|
-
const result = createRedirectResponse('/new-location', 302, headers);
|
|
243
|
-
|
|
244
|
-
expect(result.headers.get('x-custom')).toBe('value');
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('parseRequestBody', () => {
|
|
249
|
-
it('should parse JSON body correctly', async () => {
|
|
250
|
-
const jsonData = { name: 'john', age: 25 };
|
|
251
|
-
const request = new Request('http://localhost/test', {
|
|
252
|
-
method: 'POST',
|
|
253
|
-
headers: { 'Content-Type': 'application/json' },
|
|
254
|
-
body: JSON.stringify(jsonData)
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
const result = await parseRequestBody(request);
|
|
258
|
-
expect(result).toEqual(jsonData);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('should parse form data with arrays correctly', async () => {
|
|
262
|
-
const formData = new FormData();
|
|
263
|
-
formData.append('app', 'zodula');
|
|
264
|
-
formData.append('model', 'zodula_User');
|
|
265
|
-
formData.append('recordIds[]', 'asdfasdfdsa');
|
|
266
|
-
formData.append('recordIds[]', 'jarupak.sri@gmail.com');
|
|
267
|
-
formData.append('fields', '*');
|
|
268
|
-
|
|
269
|
-
const request = new Request('http://localhost/test', {
|
|
270
|
-
method: 'POST',
|
|
271
|
-
body: formData
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const result = await parseRequestBody(request);
|
|
275
|
-
|
|
276
|
-
expect(result.app).toBe('zodula');
|
|
277
|
-
expect(result.model).toBe('zodula_User');
|
|
278
|
-
expect(Array.isArray(result.recordIds)).toBe(true);
|
|
279
|
-
expect(result.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
|
|
280
|
-
expect(result.fields).toBe('*');
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
it('should handle single form field correctly', async () => {
|
|
284
|
-
const formData = new FormData();
|
|
285
|
-
formData.append('name', 'john');
|
|
286
|
-
formData.append('email', 'john@example.com');
|
|
287
|
-
|
|
288
|
-
const request = new Request('http://localhost/test', {
|
|
289
|
-
method: 'POST',
|
|
290
|
-
body: formData
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
const result = await parseRequestBody(request);
|
|
294
|
-
|
|
295
|
-
expect(result.name).toBe('john');
|
|
296
|
-
expect(result.email).toBe('john@example.com');
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('should handle file uploads in form data', async () => {
|
|
300
|
-
const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
|
301
|
-
const formData = new FormData();
|
|
302
|
-
formData.append('name', 'john');
|
|
303
|
-
formData.append('file', file);
|
|
304
|
-
|
|
305
|
-
const request = new Request('http://localhost/test', {
|
|
306
|
-
method: 'POST',
|
|
307
|
-
body: formData
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
const result = await parseRequestBody(request);
|
|
311
|
-
|
|
312
|
-
expect(result.name).toBe('john');
|
|
313
|
-
expect(result.file).toBeInstanceOf(File);
|
|
314
|
-
expect(result.file.name).toBe('test.txt');
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('should parse nested object form data correctly', async () => {
|
|
318
|
-
const formData = new FormData();
|
|
319
|
-
formData.append('test[test]', 'test');
|
|
320
|
-
formData.append('test[new]', 'new');
|
|
321
|
-
formData.append('test[hi][hi]', 'hi');
|
|
322
|
-
|
|
323
|
-
const request = new Request('http://localhost/test', {
|
|
324
|
-
method: 'POST',
|
|
325
|
-
body: formData
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
const result = await parseRequestBody(request);
|
|
329
|
-
|
|
330
|
-
expect(result.test).toEqual({
|
|
331
|
-
test: 'test',
|
|
332
|
-
new: 'new',
|
|
333
|
-
hi: {
|
|
334
|
-
hi: 'hi'
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('should parse Axios-compatible multipart data with arrays and objects', async () => {
|
|
340
|
-
const formData = new FormData();
|
|
341
|
-
formData.append('x', '1');
|
|
342
|
-
formData.append('arr[]', '1');
|
|
343
|
-
formData.append('arr[]', '2');
|
|
344
|
-
formData.append('arr[]', '3');
|
|
345
|
-
formData.append('arr2[0]', '1');
|
|
346
|
-
formData.append('arr2[1][0]', '2');
|
|
347
|
-
formData.append('arr2[2]', '3');
|
|
348
|
-
formData.append('users[0][name]', 'Peter');
|
|
349
|
-
formData.append('users[0][surname]', 'Griffin');
|
|
350
|
-
formData.append('users[1][name]', 'Thomas');
|
|
351
|
-
formData.append('users[1][surname]', 'Anderson');
|
|
352
|
-
formData.append('obj2{}', '[{"x":1}]');
|
|
353
|
-
|
|
354
|
-
const request = new Request('http://localhost/test', {
|
|
355
|
-
method: 'POST',
|
|
356
|
-
body: formData
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
const result = await parseRequestBody(request);
|
|
360
|
-
|
|
361
|
-
expect(result.x).toBe('1');
|
|
362
|
-
expect(result.arr).toEqual(['1', '2', '3']);
|
|
363
|
-
expect(result.arr2).toEqual(['1', ['2'], '3']);
|
|
364
|
-
expect(result.users).toEqual([
|
|
365
|
-
{ name: 'Peter', surname: 'Griffin' },
|
|
366
|
-
{ name: 'Thomas', surname: 'Anderson' }
|
|
367
|
-
]);
|
|
368
|
-
expect(result.obj2).toEqual([{ x: 1 }]);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('should handle JSON serialization with special endings', async () => {
|
|
372
|
-
const formData = new FormData();
|
|
373
|
-
formData.append('myObj{}', '{"x": 1, "s": "foo"}');
|
|
374
|
-
formData.append('settings{}', '{"theme": "dark", "notifications": true}');
|
|
375
|
-
|
|
376
|
-
const request = new Request('http://localhost/test', {
|
|
377
|
-
method: 'POST',
|
|
378
|
-
body: formData
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
const result = await parseRequestBody(request);
|
|
382
|
-
|
|
383
|
-
expect(result.myObj).toEqual({ x: 1, s: 'foo' });
|
|
384
|
-
expect(result.settings).toEqual({ theme: 'dark', notifications: true });
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('should handle mixed array indexing styles', async () => {
|
|
388
|
-
const formData = new FormData();
|
|
389
|
-
formData.append('tags[]', 'javascript');
|
|
390
|
-
formData.append('tags[]', 'typescript');
|
|
391
|
-
formData.append('scores[0]', '100');
|
|
392
|
-
formData.append('scores[1]', '95');
|
|
393
|
-
formData.append('scores[2]', '88');
|
|
394
|
-
|
|
395
|
-
const request = new Request('http://localhost/test', {
|
|
396
|
-
method: 'POST',
|
|
397
|
-
body: formData
|
|
398
|
-
});
|
|
399
|
-
|
|
400
|
-
const result = await parseRequestBody(request);
|
|
401
|
-
|
|
402
|
-
expect(result.tags).toEqual(['javascript', 'typescript']);
|
|
403
|
-
expect(result.scores).toEqual(['100', '95', '88']);
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
describe('File Upload Utilities', () => {
|
|
408
|
-
it('should identify file uploads correctly', () => {
|
|
409
|
-
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
410
|
-
|
|
411
|
-
expect(isFileUpload(file)).toBe(true);
|
|
412
|
-
expect(isFileUpload('not a file')).toBe(false);
|
|
413
|
-
expect(isFileUpload({ type: 'text' })).toBe(false);
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
it('should extract file from upload', () => {
|
|
417
|
-
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
418
|
-
|
|
419
|
-
const result = getFileFromUpload(file);
|
|
420
|
-
expect(result).toBe(file);
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it('should return null for non-file uploads', () => {
|
|
424
|
-
const result = getFileFromUpload('not a file');
|
|
425
|
-
expect(result).toBeNull();
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('should get file info', () => {
|
|
429
|
-
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
430
|
-
|
|
431
|
-
const result = getFileInfo(file);
|
|
432
|
-
expect(result).toEqual({
|
|
433
|
-
name: 'test.jpg',
|
|
434
|
-
size: 4,
|
|
435
|
-
mimetype: 'image/jpeg',
|
|
436
|
-
lastModified: expect.any(Number)
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it('should return null for non-file uploads when getting info', () => {
|
|
441
|
-
const result = getFileInfo('not a file');
|
|
442
|
-
expect(result).toBeNull();
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
it('should get all file uploads from form data', () => {
|
|
446
|
-
const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
|
447
|
-
const formData = {
|
|
448
|
-
name: 'john',
|
|
449
|
-
avatar: file,
|
|
450
|
-
email: 'john@example.com'
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
const result = getFileUploads(formData);
|
|
454
|
-
|
|
455
|
-
expect(Object.keys(result)).toHaveLength(1);
|
|
456
|
-
expect(result.avatar).toBeInstanceOf(File);
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it('should get all non-file fields from form data', () => {
|
|
460
|
-
const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
|
|
461
|
-
const formData = {
|
|
462
|
-
name: 'john',
|
|
463
|
-
avatar: file,
|
|
464
|
-
email: 'john@example.com'
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
const result = getFormFields(formData);
|
|
468
|
-
|
|
469
|
-
expect(result).toEqual({
|
|
470
|
-
name: 'john',
|
|
471
|
-
email: 'john@example.com'
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
});
|
|
475
|
-
});
|