From 50dbd4ee5257dae16a80c6fe52aefbcf54698d7e Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Wed, 22 May 2019 23:55:37 +0800 Subject: [PATCH] Better UX for backend errors (fix #64) --- resources/assets/src/scripts/net.ts | 19 +++++----- resources/assets/tests/scripts/net.test.ts | 44 +++++++++++++++++----- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/resources/assets/src/scripts/net.ts b/resources/assets/src/scripts/net.ts index d81e4d33..57ad8643 100644 --- a/resources/assets/src/scripts/net.ts +++ b/resources/assets/src/scripts/net.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import { emit } from './event' import { queryStringify } from './utils' -import { showAjaxError } from './notify' +import { showAjaxError, showModal } from './notify' class HTTPError extends Error { response: Response @@ -30,26 +30,27 @@ export async function walkFetch(request: Request): Promise { try { const response = await fetch(request) + const body = response.headers.get('Content-Type') === 'application/json' + ? await response.json() + : await response.text() if (response.ok) { - return response.headers.get('Content-Type') === 'application/json' - ? response.json() - : response.text() + return body } // Process validation errors from Laravel. if (response.status === 422) { - const { errors }: { - message: string, - errors: { [field: string]: string[] } - } = await response.json() + const { errors }: { message: string, errors: { [field: string]: string[] } } = body return { code: 1, message: Object.keys(errors).map(field => errors[field][0])[0], } + } else if (response.status === 403) { + showModal(body.message, undefined, 'warning') + return } const res = response.clone() - throw new HTTPError(await response.text(), res) + throw new HTTPError(body.message || body, res) } catch (error) { emit('fetchError', error) showAjaxError(error) diff --git a/resources/assets/tests/scripts/net.test.ts b/resources/assets/tests/scripts/net.test.ts index 6e7c51c4..68064ab5 100644 --- a/resources/assets/tests/scripts/net.test.ts +++ b/resources/assets/tests/scripts/net.test.ts @@ -1,7 +1,7 @@ import Vue from 'vue' import * as net from '@/scripts/net' import { on } from '@/scripts/event' -import { showAjaxError } from '@/scripts/notify' +import { showAjaxError, showModal } from '@/scripts/notify' jest.mock('@/scripts/notify') @@ -77,9 +77,16 @@ test('low level fetch', async () => { .mockRejectedValueOnce(new Error('network')) .mockResolvedValueOnce({ ok: false, + headers: new Map(), text: () => Promise.resolve('404'), clone: () => ({}), }) + .mockResolvedValueOnce({ + ok: false, + json: () => Promise.resolve({ message: 'error' }), + headers: new Map([['Content-Type', 'application/json']]), + clone: () => ({}), + }) .mockResolvedValueOnce({ ok: true, json, @@ -106,21 +113,35 @@ test('low level fetch', async () => { expect(stub.mock.calls[1][0]).toHaveProperty('message', '404') expect(stub.mock.calls[1][0]).toHaveProperty('response') + await net.walkFetch(request as Request) + expect(showAjaxError.mock.calls[2][0]).toBeInstanceOf(Error) + expect(stub.mock.calls[2][0]).toHaveProperty('message', 'error') + expect(stub.mock.calls[2][0]).toHaveProperty('response') + await net.walkFetch(request as Request) expect(json).toBeCalled() expect(await net.walkFetch(request as Request)).toBe('text') }) -test('process Laravel validation errors', async () => { - window.fetch = jest.fn().mockResolvedValue({ - status: 422, - json() { - return Promise.resolve({ - errors: { name: ['required'] }, - }) - }, - }) +test('process backend errors', async () => { + window.fetch = jest.fn() + .mockResolvedValueOnce({ + status: 422, + headers: new Map([['Content-Type', 'application/json']]), + json() { + return Promise.resolve({ + errors: { name: ['required'] }, + }) + }, + }) + .mockResolvedValueOnce({ + status: 403, + headers: new Map([['Content-Type', 'application/json']]), + json() { + return Promise.resolve({ message: 'forbidden' }) + }, + }) const result: { code: number, @@ -128,6 +149,9 @@ test('process Laravel validation errors', async () => { } = await net.walkFetch({ headers: new Headers() } as Request) expect(result.code).toBe(1) expect(result.message).toBe('required') + + await net.walkFetch({ headers: new Headers() } as Request) + expect(showModal).toBeCalledWith('forbidden', undefined, 'warning') }) test('inject to Vue instance', () => {