c5cc6072d0
Webpack 5 supports a single runtimechunk for the Node.js compilation, this solves sharing modules between entrypoints.
182 lines
5.7 KiB
JavaScript
182 lines
5.7 KiB
JavaScript
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(), initialFiles = new Map()) {
|
|
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';`
|
|
)
|
|
for (const [k, v] of initialFiles.entries()) {
|
|
await fs.writeFile(path.join(sandboxDirectory, k), v)
|
|
}
|
|
|
|
const appPort = await findPort()
|
|
const app = await launchApp(sandboxDirectory, appPort, {
|
|
env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
|
|
})
|
|
const browser = await webdriver(appPort, '/')
|
|
|
|
return [
|
|
{
|
|
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)
|
|
},
|
|
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'
|
|
}, 30 * 1000)
|
|
window.__NEXT_HMR_CB = function () {
|
|
clearTimeout(timeout)
|
|
window.__HMR_STATE = 'success'
|
|
}
|
|
})
|
|
|
|
await this.write(fileName, content)
|
|
|
|
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, 30 * 1000)
|
|
window.__NEXT_HYDRATED_CB = function () {
|
|
clearTimeout(timeout)
|
|
callback()
|
|
}
|
|
}
|
|
})
|
|
|
|
console.log('Application re-loaded.')
|
|
// Slow down tests a bit:
|
|
await new Promise((resolve) => setTimeout(resolve, 750))
|
|
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))
|
|
}
|
|
|
|
// Slow down tests a bit (we don't know how long re-rendering takes):
|
|
await new Promise((resolve) => setTimeout(resolve, 750))
|
|
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.`
|
|
)
|
|
}
|
|
},
|
|
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, #nextjs__container_build_error_label'
|
|
)
|
|
)
|
|
)
|
|
})
|
|
if (has) {
|
|
return true
|
|
}
|
|
if (--attempts < 0) {
|
|
break
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
} while (expected)
|
|
return false
|
|
},
|
|
async getRedboxSource(includeHeader = false) {
|
|
const header = includeHeader
|
|
? await this.evaluate(() => {
|
|
const portal = [].slice
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
.find((p) =>
|
|
p.shadowRoot.querySelector('[data-nextjs-dialog-header')
|
|
)
|
|
const root = portal.shadowRoot
|
|
return root
|
|
.querySelector('[data-nextjs-dialog-header]')
|
|
.innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown')
|
|
})
|
|
: ''
|
|
|
|
const source = await this.evaluate(() => {
|
|
const portal = [].slice
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
.find((p) =>
|
|
p.shadowRoot.querySelector(
|
|
'#nextjs__container_errors_label, #nextjs__container_build_error_label'
|
|
)
|
|
)
|
|
const root = portal.shadowRoot
|
|
return root.querySelector(
|
|
'[data-nextjs-codeframe], [data-nextjs-terminal]'
|
|
).innerText
|
|
})
|
|
|
|
if (includeHeader) {
|
|
return `${header}\n\n${source}`
|
|
}
|
|
return source
|
|
},
|
|
},
|
|
function cleanup() {
|
|
async function _cleanup() {
|
|
await browser.close()
|
|
await killApp(app)
|
|
}
|
|
_cleanup().catch(() => {})
|
|
},
|
|
]
|
|
}
|