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(