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) 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 }) : '' 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(() => {}) }, ] }