From fe767165797db0ca6b1e80cf41b13fcac04f7ee6 Mon Sep 17 00:00:00 2001 From: Cole <cole@colevscode.io> Date: Fri, 6 May 2022 15:02:00 -0700 Subject: [PATCH 1/3] Exporting form error codes --- src/forms.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/forms.ts b/src/forms.ts index 25f7c7d..611b4f1 100644 --- a/src/forms.ts +++ b/src/forms.ts @@ -6,14 +6,32 @@ export interface SubmissionOptions { fetchImpl?: typeof fetch; } +export type FormErrorCode = + | 'INACTIVE' + | 'BLOCKED' + | 'EMPTY' + | 'PROJECT_NOT_FOUND' + | 'FORM_NOT_FOUND' + | 'NO_FILE_UPLOADS' + | 'TOO_MANY_FILES' + | 'FILES_TOO_BIG'; + +export type FieldErrorCode = + | 'REQUIRED_FIELD_MISSING' + | 'REQUIRED_FIELD_EMPTY' + | 'TYPE_EMAIL' + | 'TYPE_NUMERIC' + | 'TYPE_TEXT'; + export interface FormError { field?: string; - code: string | null; + code: FormErrorCode | FieldErrorCode | null; message: string; } export interface FieldError extends FormError { field: string; + code: FieldErrorCode | null; } export function isFieldError(error: FormError): error is FieldError { From dabe0c9ede463b51b2dfb228725ee5805eb3d210 Mon Sep 17 00:00:00 2001 From: Cole <cole@colevscode.io> Date: Tue, 10 May 2022 01:11:35 -0700 Subject: [PATCH 2/3] adding isKnownError type guard --- src/forms.ts | 56 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 37 insertions(+), 19 deletions(-) diff --git a/src/forms.ts b/src/forms.ts index 611b4f1..e8e3100 100644 --- a/src/forms.ts +++ b/src/forms.ts @@ -6,36 +6,54 @@ export interface SubmissionOptions { fetchImpl?: typeof fetch; } -export type FormErrorCode = - | 'INACTIVE' - | 'BLOCKED' - | 'EMPTY' - | 'PROJECT_NOT_FOUND' - | 'FORM_NOT_FOUND' - | 'NO_FILE_UPLOADS' - | 'TOO_MANY_FILES' - | 'FILES_TOO_BIG'; - -export type FieldErrorCode = - | 'REQUIRED_FIELD_MISSING' - | 'REQUIRED_FIELD_EMPTY' - | 'TYPE_EMAIL' - | 'TYPE_NUMERIC' - | 'TYPE_TEXT'; +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: FormErrorCode | FieldErrorCode | null; + code?: FormErrorCode | FieldErrorCode; message: string; } export interface FieldError extends FormError { field: string; - code: FieldErrorCode | null; + code: FieldErrorCode; } export function isFieldError(error: FormError): error is FieldError { - return (error as FieldError).field !== undefined; + return (error as FieldError).code in FieldErrorCodeEnum; +} + +type KnownError<T> = T extends + | { code: FormErrorCode } + | { code: FieldErrorCode } + ? T + : never; + +export function isKnownError(error: FormError): error is KnownError<FormError> { + return ( + !!error.code && + (error.code in FormErrorCodeEnum || error.code in FieldErrorCodeEnum) + ); } export interface SuccessBody { From 1c5b9aa4568f3ad1bd63f149b46ebb330f6925ed Mon Sep 17 00:00:00 2001 From: Cole <cole@colevscode.io> Date: Tue, 10 May 2022 15:58:13 -0700 Subject: [PATCH 3/3] adding tests --- src/forms.ts | 5 ++++- test/form.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ test/submitForm.test.js | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 test/form.test.js diff --git a/src/forms.ts b/src/forms.ts index e8e3100..460b8e3 100644 --- a/src/forms.ts +++ b/src/forms.ts @@ -40,7 +40,10 @@ export interface FieldError extends FormError { } export function isFieldError(error: FormError): error is FieldError { - return (error as FieldError).code in FieldErrorCodeEnum; + return ( + (error as FieldError).code in FieldErrorCodeEnum && + (error as FieldError).field !== undefined + ); } type KnownError<T> = T extends 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(