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 { 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<any> {
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)

View File

@ -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', () => {