2020-04-19 19:58:31 +02:00
|
|
|
import * as fs from 'fs-extra'
|
|
|
|
import nanoid from 'nanoid'
|
|
|
|
import { findPort, killApp, launchApp } from 'next-test-utils'
|
|
|
|
import webdriver from 'next-webdriver'
|
|
|
|
import path from 'path'
|
|
|
|
|
|
|
|
const rootSandboxDirectory = path.join(__dirname, '__tmp__')
|
|
|
|
|
|
|
|
export async function sandbox(id = nanoid()) {
|
|
|
|
const sandboxDirectory = path.join(rootSandboxDirectory, id)
|
|
|
|
|
|
|
|
const pagesDirectory = path.join(sandboxDirectory, 'pages')
|
|
|
|
await fs.remove(sandboxDirectory)
|
|
|
|
await fs.mkdirp(pagesDirectory)
|
|
|
|
|
|
|
|
await fs.writeFile(
|
|
|
|
path.join(pagesDirectory, 'index.js'),
|
|
|
|
`export { default } from '../index';`
|
|
|
|
)
|
|
|
|
await fs.writeFile(
|
|
|
|
path.join(sandboxDirectory, 'index.js'),
|
|
|
|
`export default () => 'new sandbox';`
|
|
|
|
)
|
|
|
|
|
|
|
|
const appPort = await findPort()
|
|
|
|
const app = await launchApp(sandboxDirectory, appPort)
|
|
|
|
const browser = await webdriver(appPort, '/')
|
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
2020-04-20 21:48:01 +02:00
|
|
|
async write(fileName, content) {
|
|
|
|
// Update the file on filesystem
|
|
|
|
const fullFileName = path.join(sandboxDirectory, fileName)
|
|
|
|
const dir = path.dirname(fullFileName)
|
|
|
|
await fs.mkdirp(dir)
|
|
|
|
await fs.writeFile(fullFileName, content)
|
|
|
|
},
|
2020-04-19 19:58:31 +02:00
|
|
|
async patch(fileName, content) {
|
|
|
|
// Register an event for HMR completion
|
|
|
|
await browser.executeScript(function() {
|
|
|
|
window.__HMR_STATE = 'pending'
|
|
|
|
|
|
|
|
var timeout = setTimeout(() => {
|
|
|
|
window.__HMR_STATE = 'timeout'
|
2020-04-20 21:48:01 +02:00
|
|
|
}, 10000)
|
2020-04-19 19:58:31 +02:00
|
|
|
window.__NEXT_HMR_CB = function() {
|
|
|
|
clearTimeout(timeout)
|
|
|
|
window.__HMR_STATE = 'success'
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2020-04-20 21:48:01 +02:00
|
|
|
await this.write(fileName, content)
|
2020-04-19 19:58:31 +02:00
|
|
|
|
|
|
|
for (;;) {
|
|
|
|
const status = await browser.executeScript(() => window.__HMR_STATE)
|
|
|
|
if (!status) {
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 750))
|
|
|
|
|
|
|
|
// Wait for application to re-hydrate:
|
|
|
|
await browser.executeAsyncScript(function() {
|
|
|
|
var callback = arguments[arguments.length - 1]
|
|
|
|
if (window.__NEXT_HYDRATED) {
|
|
|
|
callback()
|
|
|
|
} else {
|
|
|
|
var timeout = setTimeout(callback, 10 * 1000)
|
|
|
|
window.__NEXT_HYDRATED_CB = function() {
|
|
|
|
clearTimeout(timeout)
|
|
|
|
callback()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
console.log('Application re-loaded.')
|
|
|
|
// Slow down tests a bit:
|
2020-04-19 21:58:55 +02:00
|
|
|
await new Promise(resolve => setTimeout(resolve, 750))
|
2020-04-19 19:58:31 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
if (status === 'success') {
|
|
|
|
console.log('Hot update complete.')
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if (status !== 'pending') {
|
|
|
|
throw new Error(`Application is in inconsistent state: ${status}.`)
|
|
|
|
}
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 30))
|
|
|
|
}
|
|
|
|
|
2020-04-19 21:58:55 +02:00
|
|
|
// Slow down tests a bit (we don't know how long re-rendering takes):
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 750))
|
2020-04-19 19:58:31 +02:00
|
|
|
return true
|
|
|
|
},
|
|
|
|
async remove(fileName) {
|
|
|
|
const fullFileName = path.join(sandboxDirectory, fileName)
|
|
|
|
await fs.remove(fullFileName)
|
|
|
|
},
|
|
|
|
async evaluate() {
|
|
|
|
const input = arguments[0]
|
|
|
|
if (typeof input === 'function') {
|
|
|
|
const result = await browser.executeScript(input)
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 30))
|
|
|
|
return result
|
|
|
|
} else {
|
|
|
|
throw new Error(
|
|
|
|
`You must pass a function to be evaluated in the browser.`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
},
|
2020-04-19 21:58:55 +02:00
|
|
|
async getOverlayContent() {
|
|
|
|
await browser.waitForElementByCss('iframe', 10000)
|
|
|
|
const hasIframe = await browser.hasElementByCssSelector('iframe')
|
|
|
|
if (!hasIframe) {
|
|
|
|
throw new Error('Unable to find overlay')
|
|
|
|
}
|
|
|
|
return browser.eval(
|
|
|
|
`document.querySelector('iframe').contentWindow.document.body.innerHTML`
|
|
|
|
)
|
|
|
|
},
|
2020-04-30 16:50:25 +02:00
|
|
|
async hasRedbox(expected = false) {
|
|
|
|
let attempts = 3
|
|
|
|
do {
|
|
|
|
const has = await this.evaluate(() => {
|
|
|
|
return Boolean(
|
|
|
|
[].slice
|
|
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
|
|
.find(p =>
|
|
|
|
p.shadowRoot.querySelector('#nextjs__container_errors_label')
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
if (has) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
if (--attempts < 0) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
|
|
|
} while (expected)
|
|
|
|
return false
|
|
|
|
},
|
|
|
|
async getRedboxSource() {
|
|
|
|
return this.evaluate(() => {
|
|
|
|
const portal = [].slice
|
|
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
|
|
.find(p =>
|
|
|
|
p.shadowRoot.querySelector('#nextjs__container_errors_label')
|
|
|
|
)
|
|
|
|
const root = portal.shadowRoot
|
|
|
|
return root.querySelector('[data-nextjs-codeframe]').innerText
|
|
|
|
})
|
|
|
|
},
|
2020-04-19 19:58:31 +02:00
|
|
|
},
|
|
|
|
function cleanup() {
|
|
|
|
async function _cleanup() {
|
|
|
|
await browser.close()
|
|
|
|
await killApp(app)
|
|
|
|
}
|
|
|
|
_cleanup().catch(() => {})
|
|
|
|
},
|
|
|
|
]
|
|
|
|
}
|