ghostjs
Version:
Modern web integration test runner
288 lines (254 loc) • 7.75 kB
JavaScript
/* eslint-disable no-new-func */
const CDP = require('chrome-remote-interface')
const fs = require('fs')
const events = require('events')
const path = require('path')
class ChromePageObject {
constructor ({ targetId, viewportSize } = {}) {
this.targetId = targetId
this.viewportSize = viewportSize || {
height: 300,
width: 400
}
events.EventEmitter.defaultMaxListeners = Infinity
}
getCDP () {
if (this.initialPromise) {
return this.initialPromise
}
this.initialPromise = new Promise((resolve) => {
if (this._client) {
resolve(this._client)
return
}
const startRequest = Date.now()
let backoffStartupTime = 25
const maxStartupTime = 60000
const initCDP = () => {
CDP(async (client) => {
this._client = client
// If we have a targetId, connect to that.
if (this.targetId) {
const { Target } = client
try {
await Target.attachToTarget({ targetId: this.targetId })
} catch (e) {
console.log('Could not attach to target', this.targetId, e)
}
}
resolve(client)
}).on('error', (err) => {
if (Date.now() - startRequest > maxStartupTime) {
console.error('CDP Error: ' + err.stack)
} else {
backoffStartupTime *= 2
setTimeout(initCDP, backoffStartupTime)
}
})
}
setTimeout(initCDP, backoffStartupTime)
})
return this.initialPromise
}
open (url, cb) {
this.getCDP().then(async (client) => {
const { Page, Emulation, Security, Target, Network } = client
const deviceMetrics = {
width: this.viewportSize.width,
height: this.viewportSize.height,
deviceScaleFactor: 0,
mobile: false,
fitWindow: true
}
try {
Security.certificateError((res) => {
cb(null, 'fail')
})
await Target.setDiscoverTargets({ discover: true })
Target.targetCreated(async ({ targetInfo }) => {
if (targetInfo.type === 'page') {
const { targetId } = targetInfo
// TODO: Handle the rest of the phantom childPage contract used in ghost.
const subPageObj = new ChromePageObject({ targetId })
subPageObj.evaluate(() => window.location.toString(), (err, targetLocation) => {
if (err) {
console.log(err)
}
this.onPageCreated(subPageObj)
subPageObj.onUrlChanged(targetLocation)
})
}
})
await Page.enable()
await Security.enable()
await Network.enable()
if (this.networkOptions) {
const {
offline = false,
latency = 0,
downloadThroughput,
uploadThroughput
} = this.networkOptions
const canEmulateNetworkConditions = await Network.canEmulateNetworkConditions()
if (canEmulateNetworkConditions) {
await Network.emulateNetworkConditions({
offline: offline,
latency: latency,
downloadThroughput: downloadThroughput,
uploadThroughput: uploadThroughput
})
} else {
console.warn('Unable to emulate network conditions in Chrome')
}
}
await Emulation.setDeviceMetricsOverride(deviceMetrics)
await Page.navigate({url: url})
await Page.loadEventFired(() => {
cb(null, url)
})
} catch (err) {
console.error(err.stack)
const failMessage = 'fail'
cb(failMessage, null)
}
})
}
async render (filePath) {
this.getCDP().then(async (client) => {
const { Page } = client
try {
const { data } = await Page.captureScreenshot()
fs.writeFileSync(filePath, Buffer.from(data, 'base64'))
} catch (err) {
console.error(err)
}
})
}
evaluate (fn, ...args) {
this.getCDP().then(async (client) => {
const { Runtime } = client
try {
const cb = args.pop()
let executor = (stringyFunc, args) => {
let invoke = new Function(
'return ' + stringyFunc
)()
return invoke.apply(null, args)
}
let executorString = `(${executor})(${fn.toString()}, ${JSON.stringify(args)})`
await Runtime.enable()
const { result } = await Runtime.evaluate({expression: executorString, returnByValue: true})
const value = result.value
cb(null, value)
} catch (err) {
console.log(err.stack)
const cb = args.pop()
cb(err, null)
console.error(err)
}
})
}
async goForward () {
this.getCDP().then(async (client) => {
const { Page } = client
try {
await Page.enable()
let {currentIndex, entries} = await Page.getNavigationHistory()
if (currentIndex === entries.length - 1) {
return
} else {
await Page.navigateToHistoryEntry({ entryId: entries[currentIndex + 1].id })
}
} catch (err) {
console.error(err)
}
})
}
async goBack () {
this.getCDP().then(async (client) => {
const { Page } = client
try {
await Page.enable()
let {currentIndex, entries} = await Page.getNavigationHistory()
if (currentIndex === 0) {
return
} else {
await Page.navigateToHistoryEntry({ entryId: entries[currentIndex - 1].id })
}
} catch (err) {
console.error(err)
}
})
}
async injectJs (scriptPath) {
const js = fs.readFileSync(scriptPath, {encoding: 'utf8'}).trim()
const { Runtime } = this._client
try {
let expression = `(()=>{${js}})()`
await Runtime.evaluate({ expression: expression })
} catch (err) {
console.error(err)
}
}
close () {
// Close is currently broken in chrome. Use await ghost.exit instead.
/*
this.getCDP().then(async (client) => {
if (this.targetId) {
const { Target } = client
Target.detachFromTarget({ targetId: this.targetId })
}
await client.close()
})
*/
}
async closeAll () {
this.getCDP().then(async (client) => {
const { Target } = client
const targets = await Target.getTargets()
console.log('targets.targetInfos', targets.targetInfos)
targets.targetInfos.forEach(target => {
if (target.type === 'page') {
Target.closeTarget({ targetId: target.targetId })
}
})
})
}
set (param, options) {
this.getCDP().then(async (client) => {
if (param === 'viewportSize') {
const { width, height } = options
const { Emulation } = client
this.viewportSize = {
width,
height
}
const deviceMetrics = {
width: width,
height: height,
deviceScaleFactor: 0,
mobile: false,
fitWindow: true
}
Emulation.setDeviceMetricsOverride(deviceMetrics)
} else if (param === 'networkOption') {
this.networkOptions = options
} else {
console.warn(`${param} currently not supported for Chrome.`)
}
})
}
}
ChromePageObject.create = (options, callback) => {
const pageObj = new ChromePageObject()
callback(null, {
createPage: (pageCb) => {
pageCb(null, pageObj)
},
exit: async () => {
await pageObj.closeAll()
}
})
}
ChromePageObject.path = path.join(__dirname, '/binary')
module.exports = ChromePageObject