Even more reliable error-recovery tests (#5284)

This commit is contained in:
Tim Neutkens 2018-09-26 01:04:15 +02:00 committed by GitHub
parent 139bc40fb5
commit db216e0086
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 290 additions and 158 deletions

View file

@ -128,7 +128,7 @@
"babel-jest": "23.4.2", "babel-jest": "23.4.2",
"benchmark": "2.1.4", "benchmark": "2.1.4",
"cheerio": "0.22.0", "cheerio": "0.22.0",
"chromedriver": "2.32.3", "chromedriver": "2.42.0",
"clone": "2.1.1", "clone": "2.1.1",
"coveralls": "2.13.1", "coveralls": "2.13.1",
"cross-env": "5.2.0", "cross-env": "5.2.0",

View file

@ -1,10 +1,42 @@
/* global describe, it, expect */ /* global describe, it, expect */
import webdriver from 'next-webdriver' import webdriver from 'next-webdriver'
import { join } from 'path' import { join } from 'path'
import { check, File, getReactErrorOverlayContent } from 'next-test-utils' import { check, File, waitFor, getReactErrorOverlayContent, getBrowserBodyText } from 'next-test-utils'
export default (context, render) => { export default (context, render) => {
describe('Error Recovery', () => { describe('Error Recovery', () => {
it('should recover from 404 after a page has been added', async () => {
let browser
const newPage = new File(join(__dirname, '../', 'pages', 'hmr', 'new-page.js'))
try {
browser = await webdriver(context.appPort, '/hmr/new-page')
expect(await browser.elementByCss('body').text()).toMatch(/This page could not be found/)
// Add the page
newPage.write('export default () => (<div id="new-page">the-new-page</div>)')
await check(
() => getBrowserBodyText(browser),
/the-new-page/
)
newPage.delete()
await check(
() => getBrowserBodyText(browser),
/This page could not be found/
)
} catch (err) {
newPage.delete()
throw err
} finally {
if (browser) {
browser.close()
}
}
})
it('should have installed the react-overlay-editor editor handler', async () => { it('should have installed the react-overlay-editor editor handler', async () => {
let browser let browser
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js')) const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
@ -19,12 +51,20 @@ export default (context, render) => {
aboutPage.restore() aboutPage.restore()
await check( await check(
() => browser.elementByCss('body').text(), () => getBrowserBodyText(browser),
/This is the about page/ /This is the about page/
) )
} finally { } catch (err) {
aboutPage.restore() aboutPage.restore()
if (browser) {
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
}
throw err
} finally {
if (browser) { if (browser) {
browser.close() browser.close()
} }
@ -36,8 +76,7 @@ export default (context, render) => {
const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js')) const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
try { try {
browser = await webdriver(context.appPort, '/hmr/about') browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser const text = await browser.elementByCss('p').text()
.elementByCss('p').text()
expect(text).toBe('This is the about page.') expect(text).toBe('This is the about page.')
aboutPage.replace('</div>', 'div') aboutPage.replace('</div>', 'div')
@ -47,12 +86,20 @@ export default (context, render) => {
aboutPage.restore() aboutPage.restore()
await check( await check(
() => browser.elementByCss('body').text(), () => getBrowserBodyText(browser),
/This is the about page/ /This is the about page/
) )
} finally { } catch (err) {
aboutPage.restore() aboutPage.restore()
if (browser) {
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
}
throw err
} finally {
if (browser) { if (browser) {
browser.close() browser.close()
} }
@ -72,9 +119,19 @@ export default (context, render) => {
aboutPage.restore() aboutPage.restore()
await check( await check(
() => browser.elementByCss('body').text(), () => getBrowserBodyText(browser),
/This is the contact page/ /This is the contact page/
) )
} catch (err) {
aboutPage.restore()
if (browser) {
await check(
() => getBrowserBodyText(browser),
/This is the contact page/
)
}
throw err
} finally { } finally {
aboutPage.restore() aboutPage.restore()
if (browser) { if (browser) {
@ -99,7 +156,7 @@ export default (context, render) => {
aboutPage.restore() aboutPage.restore()
await check( await check(
() => browser.elementByCss('body').text(), () => getBrowserBodyText(browser),
/This is the about page/ /This is the about page/
) )
} finally { } finally {
@ -111,137 +168,195 @@ export default (context, render) => {
}) })
it('should recover from errors in the render function', async () => { it('should recover from errors in the render function', async () => {
const browser = await webdriver(context.appPort, '/hmr/about') let browser
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')) const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
aboutPage.replace('return', 'throw new Error("an-expected-error");\nreturn') try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error/) expect(text).toBe('This is the about page.')
aboutPage.restore() aboutPage.replace('return', 'throw new Error("an-expected-error");\nreturn')
await check( expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error/)
() => browser.elementByCss('body').text(),
/This is the about page/
)
browser.close() aboutPage.restore()
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
} catch (err) {
aboutPage.restore()
if (browser) {
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
}
throw err
} finally {
if (browser) {
browser.close()
}
}
}) })
it('should recover after exporting an invalid page', async () => { it('should recover after exporting an invalid page', async () => {
const browser = await webdriver(context.appPort, '/hmr/about') let browser
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')) const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
aboutPage.replace('export default', 'export default "not-a-page"\nexport const fn = ') try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check( aboutPage.replace('export default', 'export default "not-a-page"\nexport const fn = ')
() => browser.elementByCss('body').text(),
/The default export is not a React Component/
)
aboutPage.restore() await check(
() => getBrowserBodyText(browser),
/The default export is not a React Component/
)
await check( aboutPage.restore()
() => browser.elementByCss('body').text(),
/This is the about page/
)
browser.close() await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
} catch (err) {
aboutPage.restore()
if (browser) {
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
}
throw err
} finally {
if (browser) {
browser.close()
}
}
}) })
it('should recover after a bad return from the render function', async () => { it('should recover after a bad return from the render function', async () => {
const browser = await webdriver(context.appPort, '/hmr/about') let browser
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')) const aboutPage = new File(join(__dirname, '../', 'pages', 'hmr', 'about.js'))
aboutPage.replace('export default', 'export default () => /search/ \nexport const fn = ') try {
browser = await webdriver(context.appPort, '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
await check( aboutPage.replace('export default', 'export default () => /search/ \nexport const fn = ')
() => browser.elementByCss('body').text(),
/Objects are not valid as a React child/
)
aboutPage.restore() await check(
() => getBrowserBodyText(browser),
/Objects are not valid as a React child/
)
await check( aboutPage.restore()
() => browser.elementByCss('body').text(),
/This is the about page/
)
browser.close() await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
} catch (err) {
aboutPage.restore()
if (browser) {
await check(
() => getBrowserBodyText(browser),
/This is the about page/
)
}
throw err
} finally {
if (browser) {
browser.close()
}
}
}) })
it('should recover from errors in getInitialProps in client', async () => { it('should recover from errors in getInitialProps in client', async () => {
const browser = await webdriver(context.appPort, '/hmr') let browser
await browser.elementByCss('#error-in-gip-link').click()
expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error-in-gip/)
const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js')) const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js'))
erroredPage.replace('throw error', 'return {}') try {
browser = await webdriver(context.appPort, '/hmr')
await browser.elementByCss('#error-in-gip-link').click()
await check( expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error-in-gip/)
() => browser.elementByCss('body').text(),
/Hello/
)
erroredPage.restore() erroredPage.replace('throw error', 'return {}')
browser.close()
await check(
() => getBrowserBodyText(browser),
/Hello/
)
erroredPage.restore()
await check(
async () => {
await browser.refresh()
const text = await browser.elementByCss('body').text()
if (text.includes('Hello')) {
await waitFor(2000)
throw new Error('waiting')
}
return getReactErrorOverlayContent(browser)
},
/an-expected-error-in-gip/
)
} catch (err) {
erroredPage.restore()
throw err
} finally {
if (browser) {
browser.close()
}
}
}) })
it('should recover after an error reported via SSR', async () => { it('should recover after an error reported via SSR', async () => {
const browser = await webdriver(context.appPort, '/hmr/error-in-gip')
expect(await getReactErrorOverlayContent(browser)).toMatch(/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 () => {
let browser let browser
let newPage const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js'))
try { try {
browser = await webdriver(context.appPort, '/hmr/new-page') browser = await webdriver(context.appPort, '/hmr/error-in-gip')
expect(await browser.elementByCss('body').text()).toMatch(/This page could not be found/) expect(await getReactErrorOverlayContent(browser)).toMatch(/an-expected-error-in-gip/)
// Add the page const erroredPage = new File(join(__dirname, '../', 'pages', 'hmr', 'error-in-gip.js'))
newPage = new File(join(__dirname, '../', 'pages', 'hmr', 'new-page.js')) erroredPage.replace('throw error', 'return {}')
newPage.write('export default () => (<div id="new-page">the-new-page</div>)')
await check( await check(
() => { () => getBrowserBodyText(browser),
if (!browser.hasElementById('new-page')) { /Hello/
throw new Error('waiting')
}
return browser.elementByCss('body').text()
},
/the-new-page/
) )
// expect(await browser.elementByCss('body').text()).toMatch(/the-new-page/) erroredPage.restore()
await check(
async () => {
await browser.refresh()
const text = await getBrowserBodyText(browser)
if (text.includes('Hello')) {
await waitFor(2000)
throw new Error('waiting')
}
return getReactErrorOverlayContent(browser)
},
/an-expected-error-in-gip/
)
} catch (err) {
erroredPage.restore()
throw err
} finally { } finally {
if (newPage) {
newPage.delete()
}
if (browser) { if (browser) {
browser.close() browser.close()
} }

View file

@ -1,7 +1,7 @@
/* global describe, it, expect */ /* global describe, it, expect */
import webdriver from 'next-webdriver' import webdriver from 'next-webdriver'
import { waitFor } from 'next-test-utils' import { waitFor, check, File } from 'next-test-utils'
import { readFileSync, writeFileSync } from 'fs' import { readFileSync, writeFileSync } from 'fs'
import { join } from 'path' import { join } from 'path'
@ -10,7 +10,7 @@ export default (context, render) => {
it('should have config available on the client', async () => { it('should have config available on the client', async () => {
const browser = await webdriver(context.appPort, '/next-config') const browser = await webdriver(context.appPort, '/next-config')
// Wait for client side to load // Wait for client side to load
await waitFor(5000) await waitFor(10000)
const serverText = await browser.elementByCss('#server-only').text() const serverText = await browser.elementByCss('#server-only').text()
const serverClientText = await browser.elementByCss('#server-and-client').text() const serverClientText = await browser.elementByCss('#server-and-client').text()
@ -37,8 +37,7 @@ export default (context, render) => {
// Change the page // Change the page
writeFileSync(pagePath, editedContent, 'utf8') writeFileSync(pagePath, editedContent, 'utf8')
// wait for 5 seconds await waitFor(10000)
await waitFor(5000)
try { try {
// Check whether the this page has reloaded or not. // Check whether the this page has reloaded or not.
@ -59,36 +58,47 @@ export default (context, render) => {
}) })
it('should update sass styles using hmr', async () => { it('should update sass styles using hmr', async () => {
const file = new File(join(__dirname, '../', 'components', 'hello-webpack-sass.scss'))
let browser let browser
try { try {
browser = await webdriver(context.appPort, '/webpack-css') browser = await webdriver(context.appPort, '/webpack-css')
const pTag = await browser.elementByCss('.hello-world')
const initialFontSize = await pTag.getComputedCss('color')
expect(initialFontSize).toBe('rgba(255, 255, 0, 1)') expect(
await browser.elementByCss('.hello-world').getComputedCss('color')
).toBe('rgba(255, 255, 0, 1)')
const pagePath = join(__dirname, '../', 'components', 'hello-webpack-sass.scss') file.replace('yellow', 'red')
const originalContent = readFileSync(pagePath, 'utf8') await waitFor(10000)
const editedContent = originalContent.replace('yellow', 'red')
// Change the page await check(
writeFileSync(pagePath, editedContent, 'utf8') async () => {
const tag = await browser.elementByCss('.hello-world')
const prop = await tag.getComputedCss('color')
// wait for 5 seconds expect(prop).toBe('rgba(255, 0, 0, 1)')
await waitFor(5000) return 'works'
},
/works/
)
try { file.restore()
// Check whether the this page has reloaded or not.
const editedPTag = await browser.elementByCss('.hello-world')
const editedFontSize = await editedPTag.getComputedCss('color')
expect(editedFontSize).toBe('rgba(255, 0, 0, 1)') await waitFor(10000)
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome await check(
// restore the about page content. async () => {
writeFileSync(pagePath, originalContent, 'utf8') const tag = await browser.elementByCss('.hello-world')
} const prop = await tag.getComputedCss('color')
expect(prop).toBe('rgba(255, 255, 0, 1)')
return 'works'
},
/works/
)
} catch (err) {
file.restore()
throw err
} finally { } finally {
if (browser) { if (browser) {
browser.close() browser.close()

View file

@ -1,6 +1,6 @@
/* global describe, it, expect */ /* global describe, it, expect */
import webdriver from 'next-webdriver' import webdriver from 'next-webdriver'
import { waitFor } from 'next-test-utils' import { check } from 'next-test-utils'
export default function (context) { export default function (context) {
describe('Render via browser', () => { describe('Render via browser', () => {
@ -100,39 +100,36 @@ export default function (context) {
.elementByCss('#dynamic-imports-page').click() .elementByCss('#dynamic-imports-page').click()
.waitForElementByCss('#dynamic-imports-page') .waitForElementByCss('#dynamic-imports-page')
// Wait until browser loads the dynamic import chunk await check(
// TODO: We may need to find a better way to do this () => browser.elementByCss('#dynamic-imports-page p').text(),
await waitFor(5000) /Welcome to dynamic imports/
)
const text = await browser
.elementByCss('#dynamic-imports-page p').text()
expect(text).toBe('Welcome to dynamic imports.')
browser.close() browser.close()
}) })
it('should render pages with url hash correctly', async () => { it('should render pages with url hash correctly', async () => {
const browser = await webdriver(context.port, '/') let browser
try {
browser = await webdriver(context.port, '/')
// Check for the query string content // Check for the query string content
const text = await browser const text = await browser
.elementByCss('#with-hash').click() .elementByCss('#with-hash').click()
.waitForElementByCss('#dynamic-page') .waitForElementByCss('#dynamic-page')
.elementByCss('#dynamic-page p').text() .elementByCss('#dynamic-page p').text()
expect(text).toBe('zeit is awesome') expect(text).toBe('zeit is awesome')
// Check for the hash await check(
while (true) { () => browser.elementByCss('#hash').text(),
const hashText = await browser /cool/
.elementByCss('#hash').text() )
} finally {
if (/cool/.test(hashText)) { if (browser) {
break browser.close()
} }
} }
browser.close()
}) })
it('should navigate even if used a button inside <Link />', async () => { it('should navigate even if used a button inside <Link />', async () => {

View file

@ -1,5 +1,7 @@
import Link from 'next/link' import Link from 'next/link'
export default () => <div> export default () => <div>
<Link href='/nav/dynamic'><a id='to-dynamic'>To dynamic import</a></Link> <Link href='/nav/dynamic'>
<a id='to-dynamic'>To dynamic import</a>
</Link>
</div> </div>

View file

@ -7,7 +7,9 @@ import {
findPort, findPort,
launchApp, launchApp,
killApp, killApp,
waitFor waitFor,
check,
getBrowserBodyText
} from 'next-test-utils' } from 'next-test-utils'
const context = {} const context = {}
@ -63,12 +65,13 @@ describe('On Demand Entries', () => {
let browser let browser
try { try {
browser = await webdriver(context.appPort, '/nav') browser = await webdriver(context.appPort, '/nav')
const text = await browser
.elementByCss('#to-dynamic').click()
.waitForElementByCss('.dynamic-page')
.elementByCss('p').text()
expect(text).toBe('Hello') await browser.eval('document.getElementById("to-dynamic").click()')
await check(async () => {
const text = await getBrowserBodyText(browser)
return text
}, /Hello/)
} finally { } finally {
if (browser) { if (browser) {
browser.close() browser.close()

View file

@ -152,7 +152,7 @@ export async function startStaticServer (dir) {
export async function check (contentFn, regex) { export async function check (contentFn, regex) {
let found = false let found = false
setTimeout(async () => { const timeout = setTimeout(async () => {
if (found) { if (found) {
return return
} }
@ -170,6 +170,7 @@ export async function check (contentFn, regex) {
const newContent = await contentFn() const newContent = await contentFn()
if (regex.test(newContent)) { if (regex.test(newContent)) {
found = true found = true
clearTimeout(timeout)
break break
} }
await waitFor(1000) await waitFor(1000)
@ -231,3 +232,7 @@ export async function getReactErrorOverlayContent (browser) {
} }
return browser.eval(`document.querySelector('iframe').contentWindow.document.body.innerHTML`) return browser.eval(`document.querySelector('iframe').contentWindow.document.body.innerHTML`)
} }
export function getBrowserBodyText (browser) {
return browser.eval('document.getElementsByTagName("body")[0].innerText')
}