2017-01-12 05:14:49 +01:00
|
|
|
import fetch from 'node-fetch'
|
|
|
|
import qs from 'querystring'
|
2017-02-09 14:40:09 +01:00
|
|
|
import http from 'http'
|
2017-05-10 01:24:34 +02:00
|
|
|
import express from 'express'
|
2017-07-20 18:00:45 +02:00
|
|
|
import path from 'path'
|
2018-04-12 09:47:42 +02:00
|
|
|
import getPort from 'get-port'
|
2018-12-15 22:55:59 +01:00
|
|
|
import spawn from 'cross-spawn'
|
2018-02-26 17:18:46 +01:00
|
|
|
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs'
|
2019-05-09 04:51:23 +02:00
|
|
|
import treeKill from 'tree-kill'
|
2017-01-12 05:14:49 +01:00
|
|
|
|
2018-07-24 11:24:40 +02:00
|
|
|
// `next` here is the symlink in `test/node_modules/next` which points to the root directory.
|
|
|
|
// This is done so that requiring from `next` works.
|
|
|
|
// The reason we don't import the relative path `../../dist/<etc>` is that it would lead to inconsistent module singletons
|
|
|
|
import server from 'next/dist/server/next'
|
|
|
|
import _pkg from 'next/package.json'
|
2017-01-12 05:14:49 +01:00
|
|
|
|
|
|
|
export const nextServer = server
|
|
|
|
export const pkg = _pkg
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function initNextServerScript(
|
2019-05-30 03:19:32 +02:00
|
|
|
scriptPath,
|
|
|
|
successRegexp,
|
|
|
|
env,
|
|
|
|
failRegexp
|
|
|
|
) {
|
2018-02-02 15:43:36 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const instance = spawn('node', [scriptPath], { env })
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStdout(data) {
|
2018-02-02 15:43:36 +01:00
|
|
|
const message = data.toString()
|
|
|
|
if (successRegexp.test(message)) {
|
|
|
|
resolve(instance)
|
|
|
|
}
|
|
|
|
process.stdout.write(message)
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStderr(data) {
|
2019-03-23 17:20:54 +01:00
|
|
|
const message = data.toString()
|
|
|
|
if (failRegexp && failRegexp.test(message)) {
|
|
|
|
instance.kill()
|
|
|
|
return reject(new Error('received failRegexp'))
|
|
|
|
}
|
|
|
|
process.stderr.write(message)
|
2018-02-02 15:43:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
instance.stdout.on('data', handleStdout)
|
|
|
|
instance.stderr.on('data', handleStderr)
|
|
|
|
|
|
|
|
instance.on('close', () => {
|
|
|
|
instance.stdout.removeListener('data', handleStdout)
|
|
|
|
instance.stderr.removeListener('data', handleStderr)
|
|
|
|
})
|
|
|
|
|
2019-05-30 03:19:32 +02:00
|
|
|
instance.on('error', err => {
|
2018-02-02 15:43:36 +01:00
|
|
|
reject(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function renderViaAPI(app, pathname, query) {
|
2017-10-22 21:00:31 +02:00
|
|
|
const url = `${pathname}${query ? `?${qs.stringify(query)}` : ''}`
|
2017-05-03 18:40:09 +02:00
|
|
|
return app.renderToHTML({ url }, {}, pathname, query)
|
2017-01-12 05:14:49 +01:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function renderViaHTTP(appPort, pathname, query) {
|
2019-05-30 03:19:32 +02:00
|
|
|
return fetchViaHTTP(appPort, pathname, query).then(res => res.text())
|
2017-11-05 20:17:03 +01:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function fetchViaHTTP(appPort, pathname, query, opts) {
|
2019-05-30 03:19:32 +02:00
|
|
|
const url = `http://localhost:${appPort}${pathname}${
|
|
|
|
query ? `?${qs.stringify(query)}` : ''
|
|
|
|
}`
|
2019-02-19 21:58:47 +01:00
|
|
|
return fetch(url, opts)
|
2017-01-12 05:14:49 +01:00
|
|
|
}
|
2017-02-09 14:40:09 +01:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function findPort() {
|
2018-04-12 09:47:42 +02:00
|
|
|
return getPort()
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function runNextCommand(argv, options = {}) {
|
2019-03-28 20:13:27 +01:00
|
|
|
const nextDir = path.dirname(require.resolve('next/package'))
|
|
|
|
const nextBin = path.join(nextDir, 'dist/bin/next')
|
|
|
|
const cwd = options.cwd || nextDir
|
2019-03-28 23:05:18 +01:00
|
|
|
// Let Next.js decide the environment
|
2020-01-21 21:33:58 +01:00
|
|
|
const env = {
|
|
|
|
...process.env,
|
|
|
|
...options.env,
|
|
|
|
NODE_ENV: '',
|
|
|
|
__NEXT_TEST_MODE: 'true',
|
|
|
|
}
|
2019-03-28 23:05:18 +01:00
|
|
|
|
2018-12-15 22:55:59 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
console.log(`Running command "next ${argv.join(' ')}"`)
|
2019-05-30 03:19:32 +02:00
|
|
|
const instance = spawn('node', [nextBin, ...argv], {
|
|
|
|
...options.spawnOptions,
|
|
|
|
cwd,
|
|
|
|
env,
|
2019-11-11 04:24:53 +01:00
|
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
2019-05-30 03:19:32 +02:00
|
|
|
})
|
2019-02-15 17:49:40 +01:00
|
|
|
|
2019-03-03 19:36:32 +01:00
|
|
|
if (typeof options.instance === 'function') {
|
|
|
|
options.instance(instance)
|
|
|
|
}
|
|
|
|
|
2019-02-15 17:49:40 +01:00
|
|
|
let stderrOutput = ''
|
|
|
|
if (options.stderr) {
|
2019-11-11 04:24:53 +01:00
|
|
|
instance.stderr.on('data', function(chunk) {
|
2019-02-15 17:49:40 +01:00
|
|
|
stderrOutput += chunk
|
|
|
|
})
|
|
|
|
}
|
2018-12-15 22:55:59 +01:00
|
|
|
|
|
|
|
let stdoutOutput = ''
|
|
|
|
if (options.stdout) {
|
2019-11-11 04:24:53 +01:00
|
|
|
instance.stdout.on('data', function(chunk) {
|
2018-12-15 22:55:59 +01:00
|
|
|
stdoutOutput += chunk
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-01-17 03:39:00 +01:00
|
|
|
instance.on('close', code => {
|
2018-12-15 22:55:59 +01:00
|
|
|
resolve({
|
2020-01-17 03:39:00 +01:00
|
|
|
code,
|
2019-02-15 17:49:40 +01:00
|
|
|
stdout: stdoutOutput,
|
2019-11-11 04:24:53 +01:00
|
|
|
stderr: stderrOutput,
|
2018-12-15 22:55:59 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2019-05-30 03:19:32 +02:00
|
|
|
instance.on('error', err => {
|
2019-02-15 17:49:40 +01:00
|
|
|
err.stdout = stdoutOutput
|
|
|
|
err.stderr = stderrOutput
|
2018-12-15 22:55:59 +01:00
|
|
|
reject(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function runNextCommandDev(argv, stdOut, opts = {}) {
|
2018-10-01 01:02:10 +02:00
|
|
|
const cwd = path.dirname(require.resolve('next/package'))
|
2019-08-22 22:34:24 +02:00
|
|
|
const env = {
|
|
|
|
...process.env,
|
|
|
|
NODE_ENV: undefined,
|
|
|
|
__NEXT_TEST_MODE: 'true',
|
2019-11-11 04:24:53 +01:00
|
|
|
...opts.env,
|
2019-08-22 22:34:24 +02:00
|
|
|
}
|
2019-03-30 01:50:24 +01:00
|
|
|
|
2017-07-20 18:00:45 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
2019-03-30 01:50:24 +01:00
|
|
|
const instance = spawn('node', ['dist/bin/next', ...argv], { cwd, env })
|
2019-12-10 15:54:56 +01:00
|
|
|
let didResolve = false
|
2017-07-20 18:00:45 +02:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStdout(data) {
|
2017-07-20 18:00:45 +02:00
|
|
|
const message = data.toString()
|
2019-04-09 17:52:03 +02:00
|
|
|
if (/ready on/i.test(message)) {
|
2019-12-10 15:54:56 +01:00
|
|
|
if (!didResolve) {
|
|
|
|
didResolve = true
|
|
|
|
resolve(stdOut ? message : instance)
|
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
2019-07-27 10:34:29 +02:00
|
|
|
if (typeof opts.onStdout === 'function') {
|
|
|
|
opts.onStdout(message)
|
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
process.stdout.write(message)
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStderr(data) {
|
2019-07-27 10:34:29 +02:00
|
|
|
const message = data.toString()
|
|
|
|
if (typeof opts.onStderr === 'function') {
|
|
|
|
opts.onStderr(message)
|
|
|
|
}
|
|
|
|
process.stderr.write(message)
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
instance.stdout.on('data', handleStdout)
|
|
|
|
instance.stderr.on('data', handleStderr)
|
|
|
|
|
|
|
|
instance.on('close', () => {
|
|
|
|
instance.stdout.removeListener('data', handleStdout)
|
|
|
|
instance.stderr.removeListener('data', handleStderr)
|
2019-12-10 15:54:56 +01:00
|
|
|
if (!didResolve) {
|
|
|
|
didResolve = true
|
|
|
|
resolve()
|
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
})
|
|
|
|
|
2019-05-30 03:19:32 +02:00
|
|
|
instance.on('error', err => {
|
2017-07-20 18:00:45 +02:00
|
|
|
reject(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-12-15 22:55:59 +01:00
|
|
|
// Launch the app in dev mode.
|
2019-11-11 04:24:53 +01:00
|
|
|
export function launchApp(dir, port, opts) {
|
2019-07-27 10:34:29 +02:00
|
|
|
return runNextCommandDev([dir, '-p', port], undefined, opts)
|
2018-12-15 22:55:59 +01:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function nextBuild(dir, args = [], opts = {}) {
|
2019-06-06 15:57:42 +02:00
|
|
|
return runNextCommand(['build', dir, ...args], opts)
|
2018-12-15 22:55:59 +01:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function nextExport(dir, { outdir }, opts = {}) {
|
2019-10-06 13:44:03 +02:00
|
|
|
return runNextCommand(['export', dir, '--outdir', outdir], opts)
|
2018-12-15 22:55:59 +01:00
|
|
|
}
|
|
|
|
|
2019-12-13 20:30:22 +01:00
|
|
|
export function nextExportDefault(dir, opts = {}) {
|
|
|
|
return runNextCommand(['export', dir], opts)
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function nextStart(dir, port, opts = {}) {
|
2019-06-06 15:57:42 +02:00
|
|
|
return runNextCommandDev(['start', '-p', port, dir], undefined, opts)
|
2019-05-22 18:36:53 +02:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function buildTS(args = [], cwd, env = {}) {
|
2019-07-04 04:38:58 +02:00
|
|
|
cwd = cwd || path.dirname(require.resolve('next/package'))
|
|
|
|
env = { ...process.env, NODE_ENV: undefined, ...env }
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const instance = spawn(
|
|
|
|
'node',
|
|
|
|
[require.resolve('typescript/lib/tsc'), ...args],
|
|
|
|
{ cwd, env }
|
|
|
|
)
|
|
|
|
let output = ''
|
|
|
|
|
|
|
|
const handleData = chunk => {
|
|
|
|
output += chunk.toString()
|
|
|
|
}
|
|
|
|
|
|
|
|
instance.stdout.on('data', handleData)
|
|
|
|
instance.stderr.on('data', handleData)
|
|
|
|
|
|
|
|
instance.on('exit', code => {
|
2019-07-10 23:13:44 +02:00
|
|
|
if (code) {
|
|
|
|
return reject(new Error('exited with code: ' + code + '\n' + output))
|
|
|
|
}
|
2019-07-04 04:38:58 +02:00
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2017-07-20 18:00:45 +02:00
|
|
|
// Kill a launched app
|
2019-11-11 04:24:53 +01:00
|
|
|
export async function killApp(instance) {
|
2019-05-09 04:51:23 +02:00
|
|
|
await new Promise((resolve, reject) => {
|
2019-05-30 03:19:32 +02:00
|
|
|
treeKill(instance.pid, err => {
|
2019-08-06 18:51:05 +02:00
|
|
|
if (err) {
|
|
|
|
if (
|
|
|
|
process.platform === 'win32' &&
|
|
|
|
typeof err.message === 'string' &&
|
2019-08-23 04:01:12 +02:00
|
|
|
(err.message.includes(`no running instance of the task`) ||
|
|
|
|
err.message.includes(`not found`))
|
2019-08-06 18:51:05 +02:00
|
|
|
) {
|
|
|
|
// Windows throws an error if the process is already dead
|
|
|
|
//
|
|
|
|
// Command failed: taskkill /pid 6924 /T /F
|
|
|
|
// ERROR: The process with PID 6924 (child process of PID 6736) could not be terminated.
|
|
|
|
// Reason: There is no running instance of the task.
|
|
|
|
return resolve()
|
|
|
|
}
|
|
|
|
return reject(err)
|
|
|
|
}
|
|
|
|
|
2019-05-09 04:51:23 +02:00
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export async function startApp(app) {
|
2017-02-09 14:40:09 +01:00
|
|
|
await app.prepare()
|
|
|
|
const handler = app.getRequestHandler()
|
|
|
|
const server = http.createServer(handler)
|
|
|
|
server.__app = app
|
|
|
|
|
|
|
|
await promiseCall(server, 'listen')
|
|
|
|
return server
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export async function stopApp(server) {
|
2017-05-10 01:24:34 +02:00
|
|
|
if (server.__app) {
|
|
|
|
await server.__app.close()
|
|
|
|
}
|
2017-02-09 14:40:09 +01:00
|
|
|
await promiseCall(server, 'close')
|
|
|
|
}
|
|
|
|
|
2020-01-06 16:55:39 +01:00
|
|
|
export function promiseCall(obj, method, ...args) {
|
2017-02-09 14:40:09 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const newArgs = [
|
|
|
|
...args,
|
2019-11-11 04:24:53 +01:00
|
|
|
function(err, res) {
|
2017-02-09 14:40:09 +01:00
|
|
|
if (err) return reject(err)
|
|
|
|
resolve(res)
|
2019-11-11 04:24:53 +01:00
|
|
|
},
|
2017-02-09 14:40:09 +01:00
|
|
|
]
|
|
|
|
|
|
|
|
obj[method](...newArgs)
|
|
|
|
})
|
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function waitFor(millis) {
|
2019-05-30 03:19:32 +02:00
|
|
|
return new Promise(resolve => setTimeout(resolve, millis))
|
2017-02-26 20:45:16 +01:00
|
|
|
}
|
2017-05-10 01:24:34 +02:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export async function startStaticServer(dir) {
|
2017-05-10 01:24:34 +02:00
|
|
|
const app = express()
|
|
|
|
const server = http.createServer(app)
|
|
|
|
app.use(express.static(dir))
|
|
|
|
|
|
|
|
await promiseCall(server, 'listen')
|
|
|
|
return server
|
|
|
|
}
|
2018-02-26 17:18:46 +01:00
|
|
|
|
2019-11-13 03:34:12 +01:00
|
|
|
export async function startCleanStaticServer(dir) {
|
|
|
|
const app = express()
|
|
|
|
const server = http.createServer(app)
|
|
|
|
app.use(express.static(dir, { extensions: ['html'] }))
|
|
|
|
|
|
|
|
await promiseCall(server, 'listen')
|
|
|
|
return server
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export async function check(contentFn, regex) {
|
2018-07-24 11:24:40 +02:00
|
|
|
let found = false
|
2018-09-26 01:04:15 +02:00
|
|
|
const timeout = setTimeout(async () => {
|
2018-07-24 11:24:40 +02:00
|
|
|
if (found) {
|
|
|
|
return
|
|
|
|
}
|
2018-09-02 17:22:29 +02:00
|
|
|
let content
|
|
|
|
try {
|
|
|
|
content = await contentFn()
|
|
|
|
} catch (err) {
|
2019-02-19 22:45:07 +01:00
|
|
|
console.error('Error while getting content', { regex })
|
2018-09-02 17:22:29 +02:00
|
|
|
}
|
2019-02-19 22:45:07 +01:00
|
|
|
console.error('TIMED OUT CHECK: ', { regex, content })
|
2018-09-02 17:22:29 +02:00
|
|
|
throw new Error('TIMED OUT: ' + regex + '\n\n' + content)
|
2018-07-24 11:24:40 +02:00
|
|
|
}, 1000 * 30)
|
|
|
|
while (!found) {
|
2018-02-26 17:18:46 +01:00
|
|
|
try {
|
|
|
|
const newContent = await contentFn()
|
2018-07-24 11:24:40 +02:00
|
|
|
if (regex.test(newContent)) {
|
|
|
|
found = true
|
2018-09-26 01:04:15 +02:00
|
|
|
clearTimeout(timeout)
|
2018-07-24 11:24:40 +02:00
|
|
|
break
|
|
|
|
}
|
2018-02-26 17:18:46 +01:00
|
|
|
await waitFor(1000)
|
|
|
|
} catch (ex) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class File {
|
2019-11-11 04:24:53 +01:00
|
|
|
constructor(path) {
|
2018-02-26 17:18:46 +01:00
|
|
|
this.path = path
|
2019-05-30 03:19:32 +02:00
|
|
|
this.originalContent = existsSync(this.path)
|
|
|
|
? readFileSync(this.path, 'utf8')
|
|
|
|
: null
|
2018-02-26 17:18:46 +01:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
write(content) {
|
2018-02-26 17:18:46 +01:00
|
|
|
if (!this.originalContent) {
|
|
|
|
this.originalContent = content
|
|
|
|
}
|
|
|
|
writeFileSync(this.path, content, 'utf8')
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
replace(pattern, newValue) {
|
2018-02-26 17:18:46 +01:00
|
|
|
const newContent = this.originalContent.replace(pattern, newValue)
|
|
|
|
this.write(newContent)
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
delete() {
|
2018-02-26 17:18:46 +01:00
|
|
|
unlinkSync(this.path)
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
restore() {
|
2018-02-26 17:18:46 +01:00
|
|
|
this.write(this.originalContent)
|
|
|
|
}
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
|
|
|
|
// react-error-overlay uses an iframe so we have to read the contents from the frame
|
2019-11-11 04:24:53 +01:00
|
|
|
export async function getReactErrorOverlayContent(browser) {
|
2018-07-24 11:24:40 +02:00
|
|
|
let found = false
|
|
|
|
setTimeout(() => {
|
|
|
|
if (found) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
console.error('TIMED OUT CHECK FOR IFRAME')
|
|
|
|
throw new Error('TIMED OUT CHECK FOR IFRAME')
|
|
|
|
}, 1000 * 30)
|
|
|
|
while (!found) {
|
|
|
|
try {
|
2018-09-25 16:54:03 +02:00
|
|
|
await browser.waitForElementByCss('iframe', 10000)
|
|
|
|
|
2018-07-24 11:24:40 +02:00
|
|
|
const hasIframe = await browser.hasElementByCssSelector('iframe')
|
|
|
|
if (!hasIframe) {
|
|
|
|
throw new Error('Waiting for iframe')
|
|
|
|
}
|
|
|
|
|
|
|
|
found = true
|
2019-05-30 03:19:32 +02:00
|
|
|
return browser.eval(
|
|
|
|
`document.querySelector('iframe').contentWindow.document.body.innerHTML`
|
|
|
|
)
|
2018-07-24 11:24:40 +02:00
|
|
|
} catch (ex) {
|
|
|
|
await waitFor(1000)
|
|
|
|
}
|
|
|
|
}
|
2019-05-30 03:19:32 +02:00
|
|
|
return browser.eval(
|
|
|
|
`document.querySelector('iframe').contentWindow.document.body.innerHTML`
|
|
|
|
)
|
2018-07-24 11:24:40 +02:00
|
|
|
}
|
2018-09-26 01:04:15 +02:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function getBrowserBodyText(browser) {
|
2018-09-26 01:04:15 +02:00
|
|
|
return browser.eval('document.getElementsByTagName("body")[0].innerText')
|
|
|
|
}
|
2019-12-14 18:25:55 +01:00
|
|
|
|
|
|
|
export function normalizeRegEx(src) {
|
2019-12-20 21:15:55 +01:00
|
|
|
return new RegExp(src).source.replace(/\^\//g, '^\\/')
|
2019-12-14 18:25:55 +01:00
|
|
|
}
|