ui5-test-runner
Version:
Standalone test runner for UI5
316 lines (301 loc) • 9.76 kB
JavaScript
const { fork } = require('child_process')
const { join } = require('path')
const { writeFile, readFile, open, stat, unlink } = require('fs/promises')
const { recreateDir, filename, allocPromise } = require('./tools')
const { getPageTimeout, pageTimedOut } = require('./timeout')
const { getOutput, newProgress } = require('./output')
const { resolvePackage } = require('./npm')
const { UTRError } = require('./error')
const { $browsers } = require('./symbols')
let lastScreenshotId = 0
const screenshots = {}
async function instantiate (job, config) {
if (job.browserArgs.some((arg) => !arg.trim())) {
getOutput(job).emptyBrowserArg()
job.browserArgs = job.browserArgs.filter((arg) => arg.trim())
}
const { dir, url } = config
await recreateDir(dir)
const browserConfig = {
capabilities: job.browserCapabilities,
modules: job.browserModules,
...config,
args: job.browserArgs
}
const browserConfigPath = join(dir, 'browser.json')
await writeFile(browserConfigPath, JSON.stringify(browserConfig, undefined, 2))
const stdoutFilename = join(dir, 'stdout.txt')
const stderrFilename = join(dir, 'stderr.txt')
const stdout = await open(stdoutFilename, 'w')
const stderr = await open(stderrFilename, 'w')
const childProcess = fork(job.browser, [browserConfigPath], {
stdio: [0, stdout, stderr, 'ipc'],
env: {
...process.env,
...job.env
}
})
const { promise, resolve } = allocPromise()
childProcess.on('close', async code => {
await stdout.close()
await stderr.close()
if (code !== 0) {
getOutput(job).browserFailed(url, code, dir)
}
resolve(code)
})
childProcess.closed = promise
childProcess.stdoutFilename = stdoutFilename
childProcess.stderrFilename = stderrFilename
return childProcess
}
async function probe (job) {
if (job.browserCapabilities) {
return
}
const output = getOutput(job)
job.status = 'Probing browser instantiation command'
async function execute (folder) {
const dir = join(job.reportDir, folder)
const capabilities = join(dir, 'capabilities.json')
const childProcess = await instantiate(job, {
url: 'about:blank',
capabilities,
dir
})
const code = await childProcess.closed
if (code !== 0) {
throw UTRError.BROWSER_PROBE_FAILED(code.toString())
}
let browserCapabilities
try {
browserCapabilities = Object.assign({
modules: [],
screenshot: null,
scripts: false,
parallel: true,
traces: []
}, JSON.parse((await readFile(capabilities)).toString()))
} catch (e) {
throw UTRError.MISSING_OR_INVALID_BROWSER_CAPABILITIES(e.message)
}
return browserCapabilities
}
output.debug('browsers/probe', 'initial probing')
const browserCapabilities = await execute('probe')
output.debug('browsers/probe', 'browser capabilities', browserCapabilities)
job.browserCapabilities = browserCapabilities
const { modules } = browserCapabilities
const resolvedModules = {}
if (modules.length) {
for await (const name of browserCapabilities.modules) {
output.debug('browsers/probe', `resolving package ${name}...`)
resolvedModules[name] = await resolvePackage(job, name)
output.debug('browsers/probe', `package ${name} resolved`)
}
}
job.browserModules = resolvedModules
if (browserCapabilities['probe-with-modules']) {
output.debug('browsers/probe', 'probing with modules')
job.browserCapabilities = await execute('probe/with-modules')
output.debug('browsers/probe', 'browser capabilities', browserCapabilities)
}
if (job.debugCapabilitiesNoScript) {
job.browserCapabilities.scripts = false
}
output.browserCapabilities(job.browserCapabilities)
}
async function start (job, url, scripts = []) {
const output = getOutput(job)
if (!job[$browsers]) {
job[$browsers] = {}
}
output.browserStart(url)
const reportDir = join(job.reportDir, filename(url))
const resolvedScripts = []
for await (const script of scripts) {
if (script.endsWith('.js')) {
const scriptFilename = join(__dirname, 'inject', script)
const scriptContent = (await readFile(scriptFilename)).toString()
resolvedScripts.push(scriptContent)
} else {
resolvedScripts.push(script)
}
}
if (resolvedScripts.length) {
resolvedScripts.unshift(`(function () {
window['ui5-test-runner/base-host'] = 'http://${job.localhost}:${job.port}'
}())`)
}
const progress = newProgress(job, url)
const pageBrowser = {
url,
reportDir,
scripts: resolvedScripts,
retry: 0,
progress
}
const { promise, resolve, reject } = allocPromise()
pageBrowser.done = value => {
delete job[$browsers][url]
resolve(value)
}
pageBrowser.failed = reason => {
delete job[$browsers][url]
reject(reason)
}
job[$browsers][url] = pageBrowser
await run(job, pageBrowser)
try {
await promise
} finally {
progress.done()
output.browserStopped(url)
}
}
async function run (job, pageBrowser) {
const output = getOutput(job)
const { url, retry, reportDir, scripts } = pageBrowser
let dir = reportDir
if (retry) {
output.browserRetry(url, retry)
dir = join(dir, retry.toString())
if (pageBrowser.console.count) {
try {
await pageBrowser.console.flush
.then(() => unlink(join(reportDir, 'console.jsonl')))
} catch (e) {
// ignore
}
}
}
pageBrowser.console = {
count: 0,
byApi: {},
flush: Promise.resolve()
}
await recreateDir(dir)
delete pageBrowser.stopped
const childProcess = await instantiate(job, {
url,
retry,
scripts,
dir
})
pageBrowser.childProcess = childProcess
const timeout = getPageTimeout(job)
if (timeout) {
pageBrowser.timeoutId = setTimeout(() => {
output.browserTimeout(url, dir)
pageTimedOut(job, url)
stop(job, url)
}, timeout)
}
childProcess.on('message', message => {
if (message.command === 'screenshot') {
const { id } = message
screenshots[id]()
delete screenshots[id]
} else if (message.command === 'console') {
++pageBrowser.console.count
if (!pageBrowser.console.byApi[message.api]) {
pageBrowser.console.byApi[message.api] = 1
} else {
++pageBrowser.console.byApi[message.api]
}
pageBrowser.console.flush = pageBrowser.console.flush
.then(() => writeFile(join(reportDir, 'console.jsonl'), JSON.stringify({
t: message.t,
api: message.api,
args: message.args
}) + '\n', {
flag: 'a+'
}))
}
})
childProcess.on('close', async code => {
if (!pageBrowser.stopped) {
if (code === 0) {
output.browserClosed(url, code, dir)
}
childProcess.closed.then(() => stop(job, url, true))
}
})
}
async function screenshot (job, url, filename) {
if (!job.browserCapabilities.screenshot) {
throw UTRError.BROWSER_SCREENSHOT_NOT_SUPPORTED()
}
const output = getOutput(job)
const id = ++lastScreenshotId
try {
const { childProcess, reportDir } = job[$browsers][url]
const absoluteFilename = join(reportDir, filename + job.browserCapabilities.screenshot)
if (childProcess.connected) {
output.debug('screenshot', id, url, absoluteFilename)
const { promise, resolve, reject } = allocPromise()
screenshots[id] = resolve
output.debug('screenshot', id, 'sending command')
childProcess.send({
id,
command: 'screenshot',
filename: absoluteFilename
})
const timeoutId = setTimeout(() => {
reject(UTRError.BROWSER_SCREENSHOT_TIMEOUT())
}, job.screenshotTimeout)
output.debug('screenshot', id, 'command sent, waiting for answer')
await promise
output.debug('screenshot', id, 'answer received')
clearTimeout(timeoutId)
const result = await stat(absoluteFilename)
output.debug('screenshot', id, 'file size :', result.size)
if (!result.isFile() || result.size === 0) {
throw new Error('File expected')
}
output.debug('screenshot', id, 'done')
return absoluteFilename
}
} catch (e) {
output.debug('screenshot', id, e.message)
if (e.code === UTRError.BROWSER_SCREENSHOT_TIMEOUT_CODE) {
throw e
}
throw UTRError.BROWSER_SCREENSHOT_FAILED(e.toString())
}
}
async function stop (job, url, retry = false) {
const pageBrowser = job[$browsers][url]
if (pageBrowser) {
pageBrowser.stopped = true
const { childProcess, done, failed, timeoutId } = pageBrowser
if (timeoutId) {
clearTimeout(timeoutId)
}
if (childProcess.connected) {
/* istanbul ignore else */
if (!job.debugKeepBrowserOpen) {
childProcess.send({ command: 'stop' })
}
const { promise: closeTimeout, resolve } = allocPromise()
const timeoutId = setTimeout(resolve, job.browserCloseTimeout)
await Promise.race([
childProcess.closed,
closeTimeout
])
clearTimeout(timeoutId)
}
await pageBrowser.console.flush
if (retry) {
if (++pageBrowser.retry <= job.browserRetry) {
run(job, pageBrowser)
} else {
failed(UTRError.BROWSER_FAILED())
}
} else {
done()
}
}
}
module.exports = { probe, start, screenshot, stop }