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 () => ( +
+ Bad Page +
+) 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) + } +}