mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Add a set of test cases for error recovery in the dev mode (#3895)
* Add specific test cases for Error Recovery. * Update hmr/about.js * Add a test case: should recover after a bad return from the render function * Add test case: should recover from errors in getInitialProps in client * Add test case: should recover after an error reported via SSR * Add a test case: should recover from 404 after a page has been added * Refactor code base.
This commit is contained in:
parent
1bcd2e0575
commit
1c36b5b9ab
|
@ -1,7 +1,9 @@
|
||||||
export default () => (
|
export default () => {
|
||||||
|
return (
|
||||||
<div className='hmr-about-page'>
|
<div className='hmr-about-page'>
|
||||||
<p>
|
<p>
|
||||||
This is the about page.
|
This is the about page.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
11
test/integration/basic/pages/hmr/error-in-gip.js
Normal file
11
test/integration/basic/pages/hmr/error-in-gip.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react'
|
||||||
|
export default class extends React.Component {
|
||||||
|
static getInitialProps () {
|
||||||
|
const error = new Error('an-expected-error-in-gip')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (<div>Hello</div>)
|
||||||
|
}
|
||||||
|
}
|
7
test/integration/basic/pages/hmr/index.js
Normal file
7
test/integration/basic/pages/hmr/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default () => (
|
||||||
|
<div>
|
||||||
|
<Link href='/hmr/error-in-gip'><a id='error-in-gip-link'>Bad Page</a></Link>
|
||||||
|
</div>
|
||||||
|
)
|
211
test/integration/basic/test/error-recovery.js
Normal file
211
test/integration/basic/test/error-recovery.js
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/* global describe, it, expect */
|
||||||
|
import webdriver from 'next-webdriver'
|
||||||
|
import { join } from 'path'
|
||||||
|
import { check, File } from 'next-test-utils'
|
||||||
|
|
||||||
|
export default (context, render) => {
|
||||||
|
describe('Error Recovery', () => {
|
||||||
|
it('should detect syntax errors and recover', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||||
|
const text = await browser
|
||||||
|
.elementByCss('p').text()
|
||||||
|
expect(text).toBe('This is the about page.')
|
||||||
|
|
||||||
|
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||||
|
aboutPage.replace('</div>', 'div')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Unterminated JSX contents/
|
||||||
|
)
|
||||||
|
|
||||||
|
aboutPage.restore()
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/This is the about page/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show the error on all pages', async () => {
|
||||||
|
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||||
|
aboutPage.replace('</div>', 'div')
|
||||||
|
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/contact')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Unterminated JSX contents/
|
||||||
|
)
|
||||||
|
|
||||||
|
aboutPage.restore()
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/This is the contact page/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should detect runtime errors on the module scope', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||||
|
const text = await browser
|
||||||
|
.elementByCss('p').text()
|
||||||
|
expect(text).toBe('This is the about page.')
|
||||||
|
|
||||||
|
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||||
|
aboutPage.replace('export', 'aa=20;\nexport')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/aa is not defined/
|
||||||
|
)
|
||||||
|
|
||||||
|
aboutPage.restore()
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/This is the about page/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should recover from errors in the render function', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||||
|
const text = await browser
|
||||||
|
.elementByCss('p').text()
|
||||||
|
expect(text).toBe('This is the about page.')
|
||||||
|
|
||||||
|
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||||
|
aboutPage.replace('return', 'throw new Error("an-expected-error");\nreturn')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/an-expected-error/
|
||||||
|
)
|
||||||
|
|
||||||
|
aboutPage.restore()
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/This is the about page/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should recover after exporting an invalid page', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||||
|
const text = await browser
|
||||||
|
.elementByCss('p').text()
|
||||||
|
expect(text).toBe('This is the about page.')
|
||||||
|
|
||||||
|
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||||
|
aboutPage.replace('export default', 'export default "not-a-page"\nexport const fn = ')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/The default export is not a React Component/
|
||||||
|
)
|
||||||
|
|
||||||
|
aboutPage.restore()
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/This is the about page/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should recover after a bad return from the render function', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/about')
|
||||||
|
const text = await browser
|
||||||
|
.elementByCss('p').text()
|
||||||
|
expect(text).toBe('This is the about page.')
|
||||||
|
|
||||||
|
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
|
||||||
|
aboutPage.replace('export default', 'export default () => /search/ \nexport const fn = ')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Objects are not valid as a React child/
|
||||||
|
)
|
||||||
|
|
||||||
|
aboutPage.restore()
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/This is the about page/
|
||||||
|
)
|
||||||
|
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should recover from errors in getInitialProps in client', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr')
|
||||||
|
await browser.elementByCss('#error-in-gip-link').click()
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/an-expected-error-in-gip/
|
||||||
|
)
|
||||||
|
|
||||||
|
const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js'))
|
||||||
|
erroredPage.replace('throw error', 'return {}')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Hello/
|
||||||
|
)
|
||||||
|
|
||||||
|
erroredPage.restore()
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should recover after an error reported via SSR', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/error-in-gip')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/an-expected-error-in-gip/
|
||||||
|
)
|
||||||
|
|
||||||
|
const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js'))
|
||||||
|
erroredPage.replace('throw error', 'return {}')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/Hello/
|
||||||
|
)
|
||||||
|
|
||||||
|
erroredPage.restore()
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should recover from 404 after a page has been added', async () => {
|
||||||
|
const browser = await webdriver(context.appPort, '/hmr/new-page')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/This page could not be found/
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add the page
|
||||||
|
const newPage = new File(join(__dirname, '../', 'pages', 'hmr', 'new-page.js'))
|
||||||
|
newPage.write('export default () => (<div>the-new-page</div>)')
|
||||||
|
|
||||||
|
await check(
|
||||||
|
() => browser.elementByCss('body').text(),
|
||||||
|
/the-new-page/
|
||||||
|
)
|
||||||
|
|
||||||
|
newPage.delete()
|
||||||
|
browser.close()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,109 +2,10 @@
|
||||||
import webdriver from 'next-webdriver'
|
import webdriver from 'next-webdriver'
|
||||||
import { readFileSync, writeFileSync, renameSync } from 'fs'
|
import { readFileSync, writeFileSync, renameSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { waitFor } from 'next-test-utils'
|
import { waitFor, check } from 'next-test-utils'
|
||||||
|
|
||||||
async function check (contentFn, regex) {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const newContent = await contentFn()
|
|
||||||
if (regex.test(newContent)) break
|
|
||||||
await waitFor(1000)
|
|
||||||
} catch (ex) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (context, render) => {
|
export default (context, render) => {
|
||||||
describe('Hot Module Reloading', () => {
|
describe('Hot Module Reloading', () => {
|
||||||
describe('errors', () => {
|
|
||||||
it('should detect syntax errors and recover', async () => {
|
|
||||||
const browser = await webdriver(context.appPort, '/hmr/about')
|
|
||||||
const text = await browser
|
|
||||||
.elementByCss('p').text()
|
|
||||||
expect(text).toBe('This is the about page.')
|
|
||||||
|
|
||||||
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
|
|
||||||
|
|
||||||
const originalContent = readFileSync(aboutPagePath, 'utf8')
|
|
||||||
const erroredContent = originalContent.replace('</div>', 'div')
|
|
||||||
|
|
||||||
// change the content
|
|
||||||
writeFileSync(aboutPagePath, erroredContent, 'utf8')
|
|
||||||
|
|
||||||
await check(
|
|
||||||
() => browser.elementByCss('body').text(),
|
|
||||||
/Unterminated JSX contents/
|
|
||||||
)
|
|
||||||
|
|
||||||
// add the original content
|
|
||||||
writeFileSync(aboutPagePath, originalContent, 'utf8')
|
|
||||||
|
|
||||||
await check(
|
|
||||||
() => browser.elementByCss('body').text(),
|
|
||||||
/This is the about page/
|
|
||||||
)
|
|
||||||
|
|
||||||
browser.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should show the error on all pages', async () => {
|
|
||||||
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
|
|
||||||
|
|
||||||
const originalContent = readFileSync(aboutPagePath, 'utf8')
|
|
||||||
const erroredContent = originalContent.replace('</div>', 'div')
|
|
||||||
|
|
||||||
// change the content
|
|
||||||
writeFileSync(aboutPagePath, erroredContent, 'utf8')
|
|
||||||
|
|
||||||
const browser = await webdriver(context.appPort, '/hmr/contact')
|
|
||||||
|
|
||||||
await check(
|
|
||||||
() => browser.elementByCss('body').text(),
|
|
||||||
/Unterminated JSX contents/
|
|
||||||
)
|
|
||||||
|
|
||||||
// add the original content
|
|
||||||
writeFileSync(aboutPagePath, originalContent, 'utf8')
|
|
||||||
|
|
||||||
await check(
|
|
||||||
() => browser.elementByCss('body').text(),
|
|
||||||
/This is the contact page/
|
|
||||||
)
|
|
||||||
|
|
||||||
browser.close()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should detect runtime errors on the module scope', async () => {
|
|
||||||
const browser = await webdriver(context.appPort, '/hmr/about')
|
|
||||||
const text = await browser
|
|
||||||
.elementByCss('p').text()
|
|
||||||
expect(text).toBe('This is the about page.')
|
|
||||||
|
|
||||||
const aboutPagePath = join(__dirname, '../', 'pages', 'hmr', 'about.js')
|
|
||||||
|
|
||||||
const originalContent = readFileSync(aboutPagePath, 'utf8')
|
|
||||||
const erroredContent = originalContent.replace('export', 'aa=20;\nexport')
|
|
||||||
|
|
||||||
// change the content
|
|
||||||
writeFileSync(aboutPagePath, erroredContent, 'utf8')
|
|
||||||
|
|
||||||
await check(
|
|
||||||
() => browser.elementByCss('body').text(),
|
|
||||||
/aa is not defined/
|
|
||||||
)
|
|
||||||
|
|
||||||
// add the original content
|
|
||||||
writeFileSync(aboutPagePath, originalContent, 'utf8')
|
|
||||||
|
|
||||||
await check(
|
|
||||||
() => browser.elementByCss('body').text(),
|
|
||||||
/This is the about page/
|
|
||||||
)
|
|
||||||
|
|
||||||
browser.close()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('delete a page and add it back', () => {
|
describe('delete a page and add it back', () => {
|
||||||
it('should load the page properly', async () => {
|
it('should load the page properly', async () => {
|
||||||
const browser = await webdriver(context.appPort, '/hmr/contact')
|
const browser = await webdriver(context.appPort, '/hmr/contact')
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
import rendering from './rendering'
|
import rendering from './rendering'
|
||||||
import clientNavigation from './client-navigation'
|
import clientNavigation from './client-navigation'
|
||||||
import hmr from './hmr'
|
import hmr from './hmr'
|
||||||
|
import errorRecovery from './error-recovery'
|
||||||
import dynamic from './dynamic'
|
import dynamic from './dynamic'
|
||||||
import asset from './asset'
|
import asset from './asset'
|
||||||
|
|
||||||
|
@ -62,5 +63,6 @@ describe('Basic Features', () => {
|
||||||
clientNavigation(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
clientNavigation(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||||
dynamic(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
dynamic(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||||
hmr(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
hmr(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||||
|
errorRecovery(context, (p, q) => renderViaHTTP(context.appPort, p, q))
|
||||||
asset(context)
|
asset(context)
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,6 +5,7 @@ import express from 'express'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import portfinder from 'portfinder'
|
import portfinder from 'portfinder'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
|
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
|
||||||
import fkill from 'fkill'
|
import fkill from 'fkill'
|
||||||
|
|
||||||
import server from '../../dist/server/next'
|
import server from '../../dist/server/next'
|
||||||
|
@ -146,3 +147,40 @@ export async function startStaticServer (dir) {
|
||||||
await promiseCall(server, 'listen')
|
await promiseCall(server, 'listen')
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function check (contentFn, regex) {
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const newContent = await contentFn()
|
||||||
|
if (regex.test(newContent)) break
|
||||||
|
await waitFor(1000)
|
||||||
|
} catch (ex) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class File {
|
||||||
|
constructor (path) {
|
||||||
|
this.path = path
|
||||||
|
this.originalContent = existsSync(this.path) ? readFileSync(this.path, 'utf8') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
write (content) {
|
||||||
|
if (!this.originalContent) {
|
||||||
|
this.originalContent = content
|
||||||
|
}
|
||||||
|
writeFileSync(this.path, content, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
replace (pattern, newValue) {
|
||||||
|
const newContent = this.originalContent.replace(pattern, newValue)
|
||||||
|
this.write(newContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete () {
|
||||||
|
unlinkSync(this.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
restore () {
|
||||||
|
this.write(this.originalContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue