1
0
Fork 0
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:
Arunoda Susiripala 2018-02-26 21:48:46 +05:30 committed by Tim Neutkens
parent 1bcd2e0575
commit 1c36b5b9ab
7 changed files with 279 additions and 107 deletions

View file

@ -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>
) )
}

View 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>)
}
}

View 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>
)

View 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()
})
})
}

View file

@ -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')

View file

@ -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)
}) })

View file

@ -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)
}
}