Better UX for backend errors (fix #64)

This commit is contained in:
Pig Fang 2019-05-22 23:55:37 +08:00
parent ef50c635c3
commit 50dbd4ee52
2 changed files with 44 additions and 19 deletions

View File

@ -1,7 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import { emit } from './event' import { emit } from './event'
import { queryStringify } from './utils' import { queryStringify } from './utils'
import { showAjaxError } from './notify' import { showAjaxError, showModal } from './notify'
class HTTPError extends Error { class HTTPError extends Error {
response: Response response: Response
@ -30,26 +30,27 @@ export async function walkFetch(request: Request): Promise<any> {
try { try {
const response = await fetch(request) const response = await fetch(request)
const body = response.headers.get('Content-Type') === 'application/json'
? await response.json()
: await response.text()
if (response.ok) { if (response.ok) {
return response.headers.get('Content-Type') === 'application/json' return body
? response.json()
: response.text()
} }
// Process validation errors from Laravel. // Process validation errors from Laravel.
if (response.status === 422) { if (response.status === 422) {
const { errors }: { const { errors }: { message: string, errors: { [field: string]: string[] } } = body
message: string,
errors: { [field: string]: string[] }
} = await response.json()
return { return {
code: 1, code: 1,
message: Object.keys(errors).map(field => errors[field][0])[0], 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() const res = response.clone()
throw new HTTPError(await response.text(), res) throw new HTTPError(body.message || body, res)
} catch (error) { } catch (error) {
emit('fetchError', error) emit('fetchError', error)
showAjaxError(error) showAjaxError(error)

View File

@ -1,7 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import * as net from '@/scripts/net' import * as net from '@/scripts/net'
import { on } from '@/scripts/event' import { on } from '@/scripts/event'
import { showAjaxError } from '@/scripts/notify' import { showAjaxError, showModal } from '@/scripts/notify'
jest.mock('@/scripts/notify') jest.mock('@/scripts/notify')
@ -77,9 +77,16 @@ test('low level fetch', async () => {
.mockRejectedValueOnce(new Error('network')) .mockRejectedValueOnce(new Error('network'))
.mockResolvedValueOnce({ .mockResolvedValueOnce({
ok: false, ok: false,
headers: new Map(),
text: () => Promise.resolve('404'), text: () => Promise.resolve('404'),
clone: () => ({}), clone: () => ({}),
}) })
.mockResolvedValueOnce({
ok: false,
json: () => Promise.resolve({ message: 'error' }),
headers: new Map([['Content-Type', 'application/json']]),
clone: () => ({}),
})
.mockResolvedValueOnce({ .mockResolvedValueOnce({
ok: true, ok: true,
json, 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('message', '404')
expect(stub.mock.calls[1][0]).toHaveProperty('response') 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) await net.walkFetch(request as Request)
expect(json).toBeCalled() expect(json).toBeCalled()
expect(await net.walkFetch(request as Request)).toBe('text') expect(await net.walkFetch(request as Request)).toBe('text')
}) })
test('process Laravel validation errors', async () => { test('process backend errors', async () => {
window.fetch = jest.fn().mockResolvedValue({ window.fetch = jest.fn()
status: 422, .mockResolvedValueOnce({
json() { status: 422,
return Promise.resolve({ headers: new Map([['Content-Type', 'application/json']]),
errors: { name: ['required'] }, 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: { const result: {
code: number, code: number,
@ -128,6 +149,9 @@ test('process Laravel validation errors', async () => {
} = await net.walkFetch({ headers: new Headers() } as Request) } = await net.walkFetch({ headers: new Headers() } as Request)
expect(result.code).toBe(1) expect(result.code).toBe(1)
expect(result.message).toBe('required') expect(result.message).toBe('required')
await net.walkFetch({ headers: new Headers() } as Request)
expect(showModal).toBeCalledWith('forbidden', undefined, 'warning')
}) })
test('inject to Vue instance', () => { test('inject to Vue instance', () => {