fix(vue-app): prevent looping on error during render of error page (#6217)

This commit is contained in:
Pim 2019-09-01 16:58:56 +02:00 committed by Pooya Parsa
parent 7c90310166
commit 93a0924754
9 changed files with 184 additions and 23 deletions

View File

@ -34,6 +34,15 @@ export default {
default: 'default' default: 'default'
} }
}, },
errorCaptured (error) {
// if we receive and error while showing the NuxtError component
// capture the error and force an immediate update so we re-render
// without the NuxtError component
if (this.displayingNuxtError) {
this.errorFromNuxtError = error
this.$forceUpdate()
}
},
computed: { computed: {
routerViewKey() { routerViewKey() {
// If nuxtChildKey prop is given or current route has children // If nuxtChildKey prop is given or current route has children
@ -65,18 +74,36 @@ export default {
Vue.util.defineReactive(this, 'nuxt', this.$root.$options.nuxt) Vue.util.defineReactive(this, 'nuxt', this.$root.$options.nuxt)
}, },
render(h) { render(h) {
// If there is some error // if there is no error
if (this.nuxt.err) { if (!this.nuxt.err) {
return h('NuxtError', { // Directly return nuxt child
props: { return h('NuxtChild', {
error: this.nuxt.err key: this.routerViewKey,
} props: this.$props
}) })
} }
// Directly return nuxt child
return h('NuxtChild', { // if an error occured within NuxtError show a simple
key: this.routerViewKey, // error message instead to prevent looping
props: this.$props if (this.errorFromNuxtError) {
this.$nextTick(() => (this.errorFromNuxtError = false))
return h('div', {}, [
h('h2', 'An error occured while showing the error page'),
h('p', 'Unfortunately an error occured and while showing the error page another error occured'),
h('p', `Error details: ${this.errorFromNuxtError.toString()}`),
h('nuxt-link', { props: { to: '/' } }, 'Go back to home')
])
}
// track if we are showing the NuxtError component
this.displayingNuxtError = true
this.$nextTick(() => (this.displayingNuxtError = false))
return h(NuxtError, {
props: {
error: this.nuxt.err
}
}) })
} }
} }

60
test/e2e/error.test.js Normal file
View File

@ -0,0 +1,60 @@
import Browser from '../utils/browser'
import { loadFixture, getPort, Nuxt } from '../utils'
let port
const browser = new Browser()
const url = route => 'http://localhost:' + port + route
let nuxt = null
let page = null
describe('basic browser', () => {
beforeAll(async () => {
const config = await loadFixture('error')
nuxt = new Nuxt(config)
await nuxt.ready()
port = await getPort()
await nuxt.server.listen(port, 'localhost')
await browser.start({
// slowMo: 50,
// headless: false
})
})
test('Open /', async () => {
page = await browser.page(url('/'))
expect(await page.$text('h1')).toBe('Error Loop incoming page')
})
test('/squared doesnt loop due to error on error page', async () => {
await page.nuxt.navigate('/squared')
expect(await page.$text('h2')).toBe('An error occured while showing the error page')
})
test('/about loads normally', async () => {
await page.nuxt.navigate('/about')
expect(await page.$text('h1')).toBe('About')
})
test('/info prints empty page', async () => {
await page.nuxt.navigate('/info')
expect(await page.$text('#__layout')).toBe('')
})
// Close server and ask nuxt to stop listening to file changes
afterAll(async () => {
await nuxt.close()
})
// Stop browser
afterAll(async () => {
await page.close()
await browser.close()
})
})

17
test/fixtures/error/layouts/error.vue vendored Normal file
View File

@ -0,0 +1,17 @@
<template>
<div>
<h1>Error</h1>
<p>Details: {{ this.$route.path === '/squared' ? error.response.data : error.message }}</p>
<nuxt-link to="/">
back
</nuxt-link>
</div>
</template>
<script>
export default {
// eslint-disable-next-line vue/require-prop-types
props: ['error']
}
</script>

View File

@ -1,2 +0,0 @@
export default {
}

8
test/fixtures/error/pages/about.vue vendored Normal file
View File

@ -0,0 +1,8 @@
<template>
<div>
<h1>About</h1>
<nuxt-link to="/">
back
</nuxt-link>
</div>
</template>

10
test/fixtures/error/pages/error.vue vendored Normal file
View File

@ -0,0 +1,10 @@
<template>
<h1>Error page</h1>
</template>
<script>
/* eslint no-undef: 0 */
export default {
not_defined
}
</script>

View File

@ -1,10 +1,14 @@
<template> <template>
<h1>Error page</h1> <div>
<h1>Error Loop incoming page</h1>
<nuxt-link id="squared" to="/squared">
Error during error
</nuxt-link>
<nuxt-link id="about" to="/about">
About
</nuxt-link>
<nuxt-link id="info" to="/info">
Info (with error)
</nuxt-link>
</div>
</template> </template>
<script>
/* eslint no-undef: 0 */
export default {
not_defined
}
</script>

19
test/fixtures/error/pages/info.vue vendored Normal file
View File

@ -0,0 +1,19 @@
<template>
<div>
<h1>Info</h1>
<p>Details: {{ info.message.title }}</p>
<nuxt-link to="/">
back
</nuxt-link>
</div>
</template>
<script>
export default {
data () {
return {
info: {}
}
}
}
</script>

View File

@ -19,7 +19,7 @@ describe('error', () => {
}) })
test('/ should display an error', async () => { test('/ should display an error', async () => {
await expect(nuxt.server.renderRoute('/')).rejects.toMatchObject({ await expect(nuxt.server.renderRoute('/error')).rejects.toMatchObject({
message: expect.stringContaining('not_defined is not defined') message: expect.stringContaining('not_defined is not defined')
}) })
}) })
@ -30,14 +30,14 @@ describe('error', () => {
}) })
test('/ with renderAndGetWindow()', async () => { test('/ with renderAndGetWindow()', async () => {
await expect(nuxt.server.renderAndGetWindow(url('/'))).rejects.toMatchObject({ await expect(nuxt.server.renderAndGetWindow(url('/error'))).rejects.toMatchObject({
statusCode: 500 statusCode: 500
}) })
}) })
test('Error: resolvePath()', () => { test('Error: resolvePath()', () => {
expect(() => nuxt.resolver.resolvePath()).toThrowError() expect(() => nuxt.resolver.resolvePath()).toThrowError()
expect(() => nuxt.resolver.resolvePath('@/pages/about.vue')).toThrowError('Cannot resolve "@/pages/about.vue"') expect(() => nuxt.resolver.resolvePath('@/pages/not-found.vue')).toThrowError('Cannot resolve "@/pages/not-found.vue"')
}) })
test('Error: callHook()', async () => { test('Error: callHook()', async () => {
@ -56,6 +56,24 @@ describe('error', () => {
expect(consola.fatal).toHaveBeenCalledWith(error) expect(consola.fatal).toHaveBeenCalledWith(error)
}) })
test('/info should display an error', async () => {
await expect(nuxt.server.renderRoute('/info')).rejects.toMatchObject({
message: expect.stringContaining(`Cannot read property 'title' of undefined`)
})
})
test('/about should work', async () => {
await expect(nuxt.server.renderRoute('/about')).resolves.toMatchObject({
html: expect.stringContaining('About')
})
})
test('/error-square should display an error', async () => {
await expect(nuxt.server.renderRoute('/squared')).rejects.toMatchObject({
message: expect.stringContaining(`Cannot read property 'data' of undefined`)
})
})
// Close server and ask nuxt to stop listening to file changes // Close server and ask nuxt to stop listening to file changes
afterAll(async () => { afterAll(async () => {
await nuxt.close() await nuxt.close()