diff --git a/test/integration/basic/pages/hmr/about.js b/test/integration/basic/pages/hmr/about.js
index 86b0b765..3336029e 100644
--- a/test/integration/basic/pages/hmr/about.js
+++ b/test/integration/basic/pages/hmr/about.js
@@ -1,7 +1,9 @@
-export default () => (
-
-
- This is the about page.
-
-
-)
+export default () => {
+ return (
+
+
+ This is the about page.
+
+
+ )
+}
diff --git a/test/integration/basic/pages/hmr/error-in-gip.js b/test/integration/basic/pages/hmr/error-in-gip.js
new file mode 100644
index 00000000..2a367749
--- /dev/null
+++ b/test/integration/basic/pages/hmr/error-in-gip.js
@@ -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 (Hello
)
+ }
+}
diff --git a/test/integration/basic/pages/hmr/index.js b/test/integration/basic/pages/hmr/index.js
new file mode 100644
index 00000000..bb58f405
--- /dev/null
+++ b/test/integration/basic/pages/hmr/index.js
@@ -0,0 +1,7 @@
+import Link from 'next/link'
+
+export default () => (
+
+)
diff --git a/test/integration/basic/test/error-recovery.js b/test/integration/basic/test/error-recovery.js
new file mode 100644
index 00000000..20e082f4
--- /dev/null
+++ b/test/integration/basic/test/error-recovery.js
@@ -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')
+
+ 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')
+
+ 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 () => (the-new-page
)')
+
+ await check(
+ () => browser.elementByCss('body').text(),
+ /the-new-page/
+ )
+
+ newPage.delete()
+ browser.close()
+ })
+ })
+}
diff --git a/test/integration/basic/test/hmr.js b/test/integration/basic/test/hmr.js
index e3e3b465..36c46420 100644
--- a/test/integration/basic/test/hmr.js
+++ b/test/integration/basic/test/hmr.js
@@ -2,109 +2,10 @@
import webdriver from 'next-webdriver'
import { readFileSync, writeFileSync, renameSync } from 'fs'
import { join } from 'path'
-import { waitFor } 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) {}
- }
-}
+import { waitFor, check } from 'next-test-utils'
export default (context, render) => {
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')
-
- // 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')
-
- // 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', () => {
it('should load the page properly', async () => {
const browser = await webdriver(context.appPort, '/hmr/contact')
diff --git a/test/integration/basic/test/index.test.js b/test/integration/basic/test/index.test.js
index 94494f83..d2818ea5 100644
--- a/test/integration/basic/test/index.test.js
+++ b/test/integration/basic/test/index.test.js
@@ -13,6 +13,7 @@ import {
import rendering from './rendering'
import clientNavigation from './client-navigation'
import hmr from './hmr'
+import errorRecovery from './error-recovery'
import dynamic from './dynamic'
import asset from './asset'
@@ -62,5 +63,6 @@ describe('Basic Features', () => {
clientNavigation(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))
+ errorRecovery(context, (p, q) => renderViaHTTP(context.appPort, p, q))
asset(context)
})
diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js
index d327349b..d916e953 100644
--- a/test/lib/next-test-utils.js
+++ b/test/lib/next-test-utils.js
@@ -5,6 +5,7 @@ import express from 'express'
import path from 'path'
import portfinder from 'portfinder'
import { spawn } from 'child_process'
+import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
import fkill from 'fkill'
import server from '../../dist/server/next'
@@ -146,3 +147,40 @@ export async function startStaticServer (dir) {
await promiseCall(server, 'listen')
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)
+ }
+}