diff --git a/src/forms.ts b/src/forms.ts index 25f7c7d..460b8e3 100644 --- a/src/forms.ts +++ b/src/forms.ts @@ -6,18 +6,57 @@ export interface SubmissionOptions { fetchImpl?: typeof fetch; } +enum FormErrorCodeEnum { + INACTIVE = 'INACTIVE', + BLOCKED = 'BLOCKED', + EMPTY = 'EMPTY', + PROJECT_NOT_FOUND = 'PROJECT_NOT_FOUND', + FORM_NOT_FOUND = 'FORM_NOT_FOUND', + NO_FILE_UPLOADS = 'NO_FILE_UPLOADS', + TOO_MANY_FILES = 'TOO_MANY_FILES', + FILES_TOO_BIG = 'FILES_TOO_BIG' +} + +enum FieldErrorCodeEnum { + REQUIRED_FIELD_MISSING = 'REQUIRED_FIELD_MISSING', + REQUIRED_FIELD_EMPTY = 'REQUIRED_FIELD_EMPTY', + TYPE_EMAIL = 'TYPE_EMAIL', + TYPE_NUMERIC = 'TYPE_NUMERIC', + TYPE_TEXT = 'TYPE_TEXT' +} + +export type FormErrorCode = keyof typeof FormErrorCodeEnum; +export type FieldErrorCode = keyof typeof FieldErrorCodeEnum; + export interface FormError { field?: string; - code: string | null; + code?: FormErrorCode | FieldErrorCode; message: string; } export interface FieldError extends FormError { field: string; + code: FieldErrorCode; } export function isFieldError(error: FormError): error is FieldError { - return (error as FieldError).field !== undefined; + return ( + (error as FieldError).code in FieldErrorCodeEnum && + (error as FieldError).field !== undefined + ); +} + +type KnownError = T extends + | { code: FormErrorCode } + | { code: FieldErrorCode } + ? T + : never; + +export function isKnownError(error: FormError): error is KnownError { + return ( + !!error.code && + (error.code in FormErrorCodeEnum || error.code in FieldErrorCodeEnum) + ); } export interface SuccessBody { diff --git a/test/form.test.js b/test/form.test.js new file mode 100644 index 0000000..b0e7b36 --- /dev/null +++ b/test/form.test.js @@ -0,0 +1,41 @@ +import { hasErrors, isFieldError, isKnownError } from '../src/forms'; + +describe('handleErrors', () => { + it('recognizes errors', () => { + expect(hasErrors({ errors: [{ message: 'doh!' }] })).toBe(true); + }); + + it('recognizes Field errors', () => { + expect( + isFieldError({ + field: 'email', + code: 'TYPE_EMAIL', + message: 'should be an email' + }) + ).toBe(true); + expect( + isFieldError({ + code: 'INACTIVE', + message: 'form is inactive' + }) + ).toBe(false); + expect( + isFieldError({ + code: 'TYPE_EMAIL', + message: 'something should be an email' + }) + ).toBe(false); + }); + + it('recognizes known and unknown errors', () => { + for (const code of [ + 'INACTIVE', + 'FORM_NOT_FOUND', + 'REQUIRED_FIELD_EMPTY', + 'TYPE_EMAIL' + ]) { + expect(isKnownError({ code, message: 'doh!' })).toBe(true); + } + expect(isKnownError({ code: 'UNKNOWN', message: 'doh!' })).toBe(false); + }); +}); diff --git a/test/submitForm.test.js b/test/submitForm.test.js index 893e82a..06853b2 100644 --- a/test/submitForm.test.js +++ b/test/submitForm.test.js @@ -1,4 +1,5 @@ import { createClient } from '../src'; +import { hasErrors, isKnownError } from '../src/forms'; import { version } from '../package.json'; // A fake success result for a mocked `fetch` call. @@ -21,6 +22,20 @@ const success = new Promise((resolve, _reject) => { resolve(response); }); +const failure = new Promise((resolve, _reject) => { + const response = { + status: 400, + json: () => { + return new Promise(resolve => { + resolve({ + errors: [{ code: 'UNKNOWN', message: 'doh!' }] + }); + }); + } + }; + resolve(response); +}); + it('resolves with body and response when successful', () => { const mockFetch = (url, props) => { expect(props.method).toEqual('POST'); @@ -71,6 +86,29 @@ it('uses the form URL when no project key is provided', () => { }); }); +it('handles errors returned from the server', () => { + const mockFetch = () => { + return failure; + }; + + return createClient() + .submitForm( + 'xxyyhashid', + {}, + { + fetchImpl: mockFetch + } + ) + .then(({ body, response }) => { + expect(response.status).toEqual(400); + expect(hasErrors(body)).toEqual(true); + expect(isKnownError(body.errors[0])).toEqual(false); + }) + .catch(e => { + throw e; + }); +}); + it('uses a default client header if none is given', () => { const mockFetch = (_url, props) => { expect(props.headers['Formspree-Client']).toEqual(