From e709f48c37e8e5a287bc6c90dd6a3f5c828a1d99 Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Thu, 10 Nov 2016 13:04:36 +0900 Subject: [PATCH 1/4] Split out phantomjs logic into separate protocol. This will allow us to have a clean split between phantom and electron logic. --- ghostjs-core/README.md | 4 + ghostjs-core/src/ghostjs.js | 193 ++---------- ghostjs-core/src/protocol/electron.js | 396 +++++++++++++++++++++++ ghostjs-core/src/protocol/phantom.js | 431 ++++++++++++++++++++++++++ ghostjs-examples/package.json | 3 +- 5 files changed, 851 insertions(+), 176 deletions(-) create mode 100644 ghostjs-core/src/protocol/electron.js create mode 100644 ghostjs-core/src/protocol/phantom.js diff --git a/ghostjs-core/README.md b/ghostjs-core/README.md index 615835e..a0c140a 100644 --- a/ghostjs-core/README.md +++ b/ghostjs-core/README.md @@ -149,6 +149,10 @@ GHOST_CONSOLE=1 ./node_modules/.bin/ghostjs onResourceRequested - Pass a custom function to execute whenever a resource is requested. +### Running on electron. + +GhostJS can also be used to run your integration tests on electron. ```--ghost-protocol=electron``` (defaults to phantom). + ## Contributors Please see: [CONTRIBUTING.md](https://github.com/KevinGrandon/ghostjs/blob/master/CONTRIBUTING.md) diff --git a/ghostjs-core/src/ghostjs.js b/ghostjs-core/src/ghostjs.js index 46c9b06..f9f2174 100644 --- a/ghostjs-core/src/ghostjs.js +++ b/ghostjs-core/src/ghostjs.js @@ -1,46 +1,21 @@ var debug = require('debug')('ghost') -var driver = require('node-phantom-simple') var argv = require('yargs').argv -import Element from './element' + +import PhantomProtocol from './protocol/phantom' +import ElectronProtocol from './protocol/electron' class Ghost { constructor () { // Default timeout per wait. this.waitTimeout = 30000 - this.testRunner = argv['ghost-runner'] || 'phantomjs-prebuilt' - this.driverOpts = null - this.setDriverOpts({}) - this.browser = null - this.currentContext = null - this.page = null - this.childPages = [] - this.clientScripts = [] + let protocolType = argv['ghost-protocol'] || 'phantom' - // Open the console if we're running slimer, and the GHOST_CONSOLE env var is set. - if (this.testRunner.match(/slimerjs/) && process.env.GHOST_CONSOLE) { - this.setDriverOpts({parameters: ['-jsconsole']}) - } - } - - /** - * Sets options object that is used in driver creation. - */ - setDriverOpts (opts) { - debug('set driver opts', opts) - this.driverOpts = this.testRunner.match(/phantom/) - ? opts - : {} - - if (opts.parameters) { - this.driverOpts.parameters = opts.parameters + if (protocolType === 'phantom') { + this.protocol = new PhantomProtocol(); + } else { + this.protocol = new ElectronProtocol(); } - - this.driverOpts.path = require(this.testRunner).path - - // The dnode `weak` dependency is failing to install on travis. - // Disable this for now until someone needs it. - this.driverOpts.dnodeOpts = { weak: false } } /** @@ -237,22 +212,7 @@ class Ghost { */ async findElement (selector) { debug('findElement called with selector', selector) - return new Promise(resolve => { - this.pageContext.evaluate((selector) => { - return !!document.querySelector(selector) - }, - selector, - (err, result) => { - if (err) { - console.warn('findElement error', err) - } - - if (!result) { - return resolve(null) - } - resolve(new Element(this.pageContext, selector)) - }) - }) + return await this.protocol.findElement(selector) } /** @@ -261,37 +221,7 @@ class Ghost { */ async findElements (selector) { debug('findElements called with selector', selector) - return new Promise(resolve => { - this.pageContext.evaluate((selector) => { - return document.querySelectorAll(selector).length - }, - selector, - (err, numElements) => { - if (err) { - console.warn('findElements error', err) - } - - if (!numElements) { - return resolve(null) - } - - var elementCollection = []; - for (var i = 0; i < numElements; i++) { - elementCollection.push(new Element(this.pageContext, selector, i)) - } - resolve(elementCollection) - }) - }) - } - - /** - * Returns all elements that match the current selector in the page. - * @Deprecated - */ - async countElements (selector) { - console.log('countElements is deprecated, use findElements().length instead.') - var collection = await this.findElements(selector); - return collection.length; + return await this.protocol.findElements(selector) } /** @@ -299,7 +229,7 @@ class Ghost { */ async resize (width, height) { debug('resizing to', width, height) - this.pageContext.set('viewportSize', {width, height}) + return await this.protocol.resize(width, height) } /** @@ -307,23 +237,7 @@ class Ghost { */ async script (func, args) { debug('scripting page', func) - if (!Array.isArray(args)) { - args = [args] - } - - return new Promise(resolve => { - this.pageContext.evaluate((stringyFunc, args) => { - var invoke = new Function( - "return " + stringyFunc - )(); - return invoke.apply(null, args) - }, - func.toString(), - args, - (err, result) => { - resolve(result) - }) - }) + return await this.protocol.scripting(func, args) } /** @@ -333,27 +247,7 @@ class Ghost { async wait (waitFor=1000, pollMs=100) { debug('waiting for', waitFor) debug('waiting (pollMs)', pollMs) - if (!(waitFor instanceof Function)) { - return new Promise((resolve) => { - setTimeout(resolve, waitFor) - }) - } else { - let timeWaited = 0 - return new Promise((resolve) => { - var poll = async () => { - var result = await waitFor() - if (result) { - resolve(result) - } else if (timeWaited > this.waitTimeout) { - this.onTimeout('Timeout while waiting.') - } else { - timeWaited += pollMs - setTimeout(poll, pollMs) - } - } - poll() - }) - } + return await this.protocol.wait(waitFor, pollMs) } /** @@ -362,8 +256,7 @@ class Ghost { */ onTimeout (errMessage) { console.log('ghostjs timeout', errMessage) - this.screenshot('timeout-' + Date.now()) - throw new Error(errMessage) + return await this.protocol.onTimeout(errMessage) } /** @@ -371,19 +264,7 @@ class Ghost { */ async waitForElement (selector) { debug('waitForElement', selector) - // Scoping gets broken within async promises, so bind these locally. - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var element = await waitFor(async () => { - var el = await findElement(selector) - if (el) { - return el - } - return false - }) - resolve(element) - }) + return await this.protocol.waitForElement(selector) } /** @@ -391,15 +272,7 @@ class Ghost { */ async waitForElementNotVisible (selector) { debug('waitForElementNotVisible', selector) - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var isHidden = await waitFor(async () => { - var el = await findElement(selector) - return !el || !await el.isVisible() - }) - resolve(isHidden) - }) + return await this.protocol.waitForElementNotVisible(selector) } /** @@ -407,19 +280,7 @@ class Ghost { */ async waitForElementVisible (selector) { debug('waitForElementVisible', selector) - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var visibleEl = await waitFor(async () => { - var el = await findElement(selector) - if (el && await el.isVisible()) { - return el - } else { - return false - } - }) - resolve(visibleEl) - }) + return await this.protocol.waitForElementVisible(selector) } /** @@ -427,25 +288,7 @@ class Ghost { */ waitForPage (url) { debug('waitForPage', url) - var waitFor = this.wait.bind(this) - var childPages = this.childPages - return new Promise(async resolve => { - var page = await waitFor(async () => { - return childPages.filter((val) => { - return val.url.includes(url) - }) - }) - resolve(page[0]) - }) - } - - /** - * Waits for a condition to be met - * @deprecated. - */ - async waitFor (func, pollMs = 100) { - console.log('waitFor is deprecated, use wait(fn) instead.') - return this.wait(func, pollMs) + return await this.protocol.waitForPage(selector) } } diff --git a/ghostjs-core/src/protocol/electron.js b/ghostjs-core/src/protocol/electron.js new file mode 100644 index 0000000..9cc5b55 --- /dev/null +++ b/ghostjs-core/src/protocol/electron.js @@ -0,0 +1,396 @@ +var debug = require('debug')('ghost:electron') + +export default class ElectronProtocol { + constructor () { + + } + + /** + * Adds scripts to be injected to for each page load. + * Should be called before ghost#open. + */ + injectScripts () { + debug('inject scripts', arguments) + Array.slice(arguments).forEach(script => { + this.clientScripts.push(script) + }) + } + + /** + * Callback when a page loads. + * Injects javascript and other things we need. + */ + onOpen () { + // Inject any client scripts + this.clientScripts.forEach(script => { + this.page.injectJs(script) + }) + } + + /** + * Opens a page. + * @param {String} url Url of the page to open. + * @param {Object} options Keys supported: + * settings - Key: Value map of all settings to set. + * headers - Key: Value map of custom headers. + * viewportSize - E.g., {height: 600, width: 800} + */ + async open (url, options={}) { + debug('open url', url, 'options', options) + // If we already have a page object, just navigate it. + if (this.page) { + return new Promise(resolve => { + this.page.open(url, (err, status) => { + this.onOpen() + resolve(status) + }) + }) + } + + return new Promise(resolve => { + driver.create(this.driverOpts, (err, browser) => { + this.browser = browser + browser.createPage((err, page) => { + this.page = page; + + options.settings = options.settings || {} + for (var i in options.settings) { + page.set('settings.' + i, options.settings[i]) + } + + if (options.headers) { + page.set('customHeaders', options.headers) + } + + if (options.viewportSize) { + page.set('viewportSize', options.viewportSize) + } + + /** + * Allow content to pass a custom function into onResourceRequested. + */ + if (options.onResourceRequested) { + page.setFn('onResourceRequested', options.onResourceRequested) + } + + page.onResourceTimeout = (url) => { + console.log('page timeout when trying to load ', url) + } + + page.onPageCreated = (page) => { + var pageObj = { + page: page, + url: null + } + + this.childPages.push(pageObj) + + page.onUrlChanged = (url) => { + pageObj.url = url; + } + + page.onClosing = (closingPage) => { + this.childPages = this.childPages.filter(eachPage => eachPage === closingPage) + } + } + + page.onConsoleMessage = (msg) => { + if (argv['verbose']) { + console.log('[Console]', msg) + } + } + + page.open(url, (err, status) => { + this.onOpen() + resolve(status) + }) + }) + }) + }) + } + + close () { + debug('close') + if (this.page) { + this.page.close() + } + this.page = null + this.currentContext = null + } + + async exit () { + this.close() + this.browser.exit() + this.browser = null + } + + /** + * Sets the current page context to run test methods on. + * This is useful for running tests in popups for example. + * To use the root page, pass an empty value. + */ + async usePage (pagePattern) { + debug('use page', pagePattern) + if (!pagePattern) { + this.currentContext = null; + } else { + this.currentContext = await this.waitForPage(pagePattern) + } + } + + /** + * Gets the current page context that we're using. + */ + get pageContext() { + return (this.currentContext && this.currentContext.page) || this.page; + } + + goBack () { + debug('goBack') + this.pageContext.goBack() + } + + goForward () { + debug('goForward') + this.pageContext.goForward() + } + + screenshot (filename, folder='screenshots') { + filename = filename || 'screenshot-' + Date.now() + this.pageContext.render(`${folder}/${filename}.png`) + } + + /** + * Returns the title of the current page. + */ + async pageTitle () { + debug('getting pageTitle') + return new Promise(resolve => { + this.pageContext.evaluate(() => { return document.title }, + (err, result) => { + resolve(result) + }) + }) + } + + /** + * Waits for the page title to match a given state. + */ + async waitForPageTitle (expected) { + debug('waitForPageTitle') + var waitFor = this.wait.bind(this) + var pageTitle = this.pageTitle.bind(this) + return new Promise(async resolve => { + var result = await waitFor(async () => { + var title = await pageTitle() + if (expected instanceof RegExp) { + return expected.test(title) + } else { + return title === expected + } + }) + resolve(result) + }) + } + + /** + * Returns an element if it finds it in the page, otherwise returns null. + * @param {string} selector + */ + async findElement (selector) { + debug('findElement called with selector', selector) + return new Promise(resolve => { + this.pageContext.evaluate((selector) => { + return !!document.querySelector(selector) + }, + selector, + (err, result) => { + if (err) { + console.warn('findElement error', err) + } + + if (!result) { + return resolve(null) + } + resolve(new Element(this.pageContext, selector)) + }) + }) + } + + /** + * Returns an array of {Element} instances that match a selector. + * @param {string} selector + */ + async findElements (selector) { + debug('findElements called with selector', selector) + return new Promise(resolve => { + this.pageContext.evaluate((selector) => { + return document.querySelectorAll(selector).length + }, + selector, + (err, numElements) => { + if (err) { + console.warn('findElements error', err) + } + + if (!numElements) { + return resolve(null) + } + + var elementCollection = []; + for (var i = 0; i < numElements; i++) { + elementCollection.push(new Element(this.pageContext, selector, i)) + } + resolve(elementCollection) + }) + }) + } + + /** + * Resizes the page to a desired width and height. + */ + async resize (width, height) { + debug('resizing to', width, height) + this.pageContext.set('viewportSize', {width, height}) + } + + /** + * Executes a script within the page. + */ + async script (func, args) { + debug('scripting page', func) + if (!Array.isArray(args)) { + args = [args] + } + + return new Promise(resolve => { + this.pageContext.evaluate((stringyFunc, args) => { + var invoke = new Function( + "return " + stringyFunc + )(); + return invoke.apply(null, args) + }, + func.toString(), + args, + (err, result) => { + resolve(result) + }) + }) + } + + /** + * Waits for an arbitrary amount of time, or an async function to resolve. + * @param (Number|Function) + */ + async wait (waitFor=1000, pollMs=100) { + debug('waiting for', waitFor) + debug('waiting (pollMs)', pollMs) + if (!(waitFor instanceof Function)) { + return new Promise((resolve) => { + setTimeout(resolve, waitFor) + }) + } else { + let timeWaited = 0 + return new Promise((resolve) => { + var poll = async () => { + var result = await waitFor() + if (result) { + resolve(result) + } else if (timeWaited > this.waitTimeout) { + this.onTimeout('Timeout while waiting.') + } else { + timeWaited += pollMs + setTimeout(poll, pollMs) + } + } + poll() + }) + } + } + + /** + * Called when wait or waitForElement times out. + * Can be used as a hook to take screenshots. + */ + onTimeout (errMessage) { + console.log('ghostjs timeout', errMessage) + this.screenshot('timeout-' + Date.now()) + throw new Error(errMessage) + } + + /** + * Waits for an element to exist in the page. + */ + async waitForElement (selector) { + debug('waitForElement', selector) + // Scoping gets broken within async promises, so bind these locally. + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var element = await waitFor(async () => { + var el = await findElement(selector) + if (el) { + return el + } + return false + }) + resolve(element) + }) + } + + /** + * Waits for an element to be hidden, or removed from the dom. + */ + async waitForElementNotVisible (selector) { + debug('waitForElementNotVisible', selector) + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var isHidden = await waitFor(async () => { + var el = await findElement(selector) + return !el || !await el.isVisible() + }) + resolve(isHidden) + }) + } + + /** + * Waits for an element to exist, and be visible. + */ + async waitForElementVisible (selector) { + debug('waitForElementVisible', selector) + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var visibleEl = await waitFor(async () => { + var el = await findElement(selector) + if (el && await el.isVisible()) { + return el + } else { + return false + } + }) + resolve(visibleEl) + }) + } + + /** + * Waits for a child page to be loaded. + */ + waitForPage (url) { + debug('waitForPage', url) + var waitFor = this.wait.bind(this) + var childPages = this.childPages + return new Promise(async resolve => { + var page = await waitFor(async () => { + return childPages.filter((val) => { + return val.url.includes(url) + }) + }) + resolve(page[0]) + }) + } +} + +var ghost = new Ghost() +export default ghost diff --git a/ghostjs-core/src/protocol/phantom.js b/ghostjs-core/src/protocol/phantom.js new file mode 100644 index 0000000..088a9aa --- /dev/null +++ b/ghostjs-core/src/protocol/phantom.js @@ -0,0 +1,431 @@ +var debug = require('debug')('ghost:phantom') +var argv = require('yargs').argv +var driver = require('node-phantom-simple') +import Element from './element' + +export default class PhantomProtocol { + constructor () { + this.testRunner = argv['ghost-runner'] || 'phantomjs-prebuilt' + this.driverOpts = null + this.setDriverOpts({}) + this.browser = null + this.currentContext = null + this.page = null + this.childPages = [] + this.clientScripts = [] + + // Open the console if we're running slimer, and the GHOST_CONSOLE env var is set. + if (this.testRunner.match(/slimerjs/) && process.env.GHOST_CONSOLE) { + this.setDriverOpts({parameters: ['-jsconsole']}) + } + } + + /** + * Sets options object that is used in driver creation. + */ + setDriverOpts (opts) { + debug('set driver opts', opts) + this.driverOpts = this.testRunner.match(/phantom/) + ? opts + : {} + + if (opts.parameters) { + this.driverOpts.parameters = opts.parameters + } + + this.driverOpts.path = require(this.testRunner).path + + // The dnode `weak` dependency is failing to install on travis. + // Disable this for now until someone needs it. + this.driverOpts.dnodeOpts = { weak: false } + } + + /** + * Adds scripts to be injected to for each page load. + * Should be called before ghost#open. + */ + injectScripts () { + debug('inject scripts', arguments) + Array.slice(arguments).forEach(script => { + this.clientScripts.push(script) + }) + } + + /** + * Callback when a page loads. + * Injects javascript and other things we need. + */ + onOpen () { + // Inject any client scripts + this.clientScripts.forEach(script => { + this.page.injectJs(script) + }) + } + + /** + * Opens a page. + * @param {String} url Url of the page to open. + * @param {Object} options Keys supported: + * settings - Key: Value map of all settings to set. + * headers - Key: Value map of custom headers. + * viewportSize - E.g., {height: 600, width: 800} + */ + async open (url, options={}) { + debug('open url', url, 'options', options) + // If we already have a page object, just navigate it. + if (this.page) { + return new Promise(resolve => { + this.page.open(url, (err, status) => { + this.onOpen() + resolve(status) + }) + }) + } + + return new Promise(resolve => { + driver.create(this.driverOpts, (err, browser) => { + this.browser = browser + browser.createPage((err, page) => { + this.page = page; + + options.settings = options.settings || {} + for (var i in options.settings) { + page.set('settings.' + i, options.settings[i]) + } + + if (options.headers) { + page.set('customHeaders', options.headers) + } + + if (options.viewportSize) { + page.set('viewportSize', options.viewportSize) + } + + /** + * Allow content to pass a custom function into onResourceRequested. + */ + if (options.onResourceRequested) { + page.setFn('onResourceRequested', options.onResourceRequested) + } + + page.onResourceTimeout = (url) => { + console.log('page timeout when trying to load ', url) + } + + page.onPageCreated = (page) => { + var pageObj = { + page: page, + url: null + } + + this.childPages.push(pageObj) + + page.onUrlChanged = (url) => { + pageObj.url = url; + } + + page.onClosing = (closingPage) => { + this.childPages = this.childPages.filter(eachPage => eachPage === closingPage) + } + } + + page.onConsoleMessage = (msg) => { + if (argv['verbose']) { + console.log('[Console]', msg) + } + } + + page.open(url, (err, status) => { + this.onOpen() + resolve(status) + }) + }) + }) + }) + } + + close () { + debug('close') + if (this.page) { + this.page.close() + } + this.page = null + this.currentContext = null + } + + async exit () { + this.close() + this.browser.exit() + this.browser = null + } + + /** + * Sets the current page context to run test methods on. + * This is useful for running tests in popups for example. + * To use the root page, pass an empty value. + */ + async usePage (pagePattern) { + debug('use page', pagePattern) + if (!pagePattern) { + this.currentContext = null; + } else { + this.currentContext = await this.waitForPage(pagePattern) + } + } + + /** + * Gets the current page context that we're using. + */ + get pageContext() { + return (this.currentContext && this.currentContext.page) || this.page; + } + + goBack () { + debug('goBack') + this.pageContext.goBack() + } + + goForward () { + debug('goForward') + this.pageContext.goForward() + } + + screenshot (filename, folder='screenshots') { + filename = filename || 'screenshot-' + Date.now() + this.pageContext.render(`${folder}/${filename}.png`) + } + + /** + * Returns the title of the current page. + */ + async pageTitle () { + debug('getting pageTitle') + return new Promise(resolve => { + this.pageContext.evaluate(() => { return document.title }, + (err, result) => { + resolve(result) + }) + }) + } + + /** + * Waits for the page title to match a given state. + */ + async waitForPageTitle (expected) { + debug('waitForPageTitle') + var waitFor = this.wait.bind(this) + var pageTitle = this.pageTitle.bind(this) + return new Promise(async resolve => { + var result = await waitFor(async () => { + var title = await pageTitle() + if (expected instanceof RegExp) { + return expected.test(title) + } else { + return title === expected + } + }) + resolve(result) + }) + } + + /** + * Returns an element if it finds it in the page, otherwise returns null. + * @param {string} selector + */ + async findElement (selector) { + debug('findElement called with selector', selector) + return new Promise(resolve => { + this.pageContext.evaluate((selector) => { + return !!document.querySelector(selector) + }, + selector, + (err, result) => { + if (err) { + console.warn('findElement error', err) + } + + if (!result) { + return resolve(null) + } + resolve(new Element(this.pageContext, selector)) + }) + }) + } + + /** + * Returns an array of {Element} instances that match a selector. + * @param {string} selector + */ + async findElements (selector) { + debug('findElements called with selector', selector) + return new Promise(resolve => { + this.pageContext.evaluate((selector) => { + return document.querySelectorAll(selector).length + }, + selector, + (err, numElements) => { + if (err) { + console.warn('findElements error', err) + } + + if (!numElements) { + return resolve(null) + } + + var elementCollection = []; + for (var i = 0; i < numElements; i++) { + elementCollection.push(new Element(this.pageContext, selector, i)) + } + resolve(elementCollection) + }) + }) + } + + /** + * Resizes the page to a desired width and height. + */ + async resize (width, height) { + debug('resizing to', width, height) + this.pageContext.set('viewportSize', {width, height}) + } + + /** + * Executes a script within the page. + */ + async script (func, args) { + debug('scripting page', func) + if (!Array.isArray(args)) { + args = [args] + } + + return new Promise(resolve => { + this.pageContext.evaluate((stringyFunc, args) => { + var invoke = new Function( + "return " + stringyFunc + )(); + return invoke.apply(null, args) + }, + func.toString(), + args, + (err, result) => { + resolve(result) + }) + }) + } + + /** + * Waits for an arbitrary amount of time, or an async function to resolve. + * @param (Number|Function) + */ + async wait (waitFor=1000, pollMs=100) { + debug('waiting for', waitFor) + debug('waiting (pollMs)', pollMs) + if (!(waitFor instanceof Function)) { + return new Promise((resolve) => { + setTimeout(resolve, waitFor) + }) + } else { + let timeWaited = 0 + return new Promise((resolve) => { + var poll = async () => { + var result = await waitFor() + if (result) { + resolve(result) + } else if (timeWaited > this.waitTimeout) { + this.onTimeout('Timeout while waiting.') + } else { + timeWaited += pollMs + setTimeout(poll, pollMs) + } + } + poll() + }) + } + } + + /** + * Called when wait or waitForElement times out. + * Can be used as a hook to take screenshots. + */ + onTimeout (errMessage) { + console.log('ghostjs timeout', errMessage) + this.screenshot('timeout-' + Date.now()) + throw new Error(errMessage) + } + + /** + * Waits for an element to exist in the page. + */ + async waitForElement (selector) { + debug('waitForElement', selector) + // Scoping gets broken within async promises, so bind these locally. + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var element = await waitFor(async () => { + var el = await findElement(selector) + if (el) { + return el + } + return false + }) + resolve(element) + }) + } + + /** + * Waits for an element to be hidden, or removed from the dom. + */ + async waitForElementNotVisible (selector) { + debug('waitForElementNotVisible', selector) + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var isHidden = await waitFor(async () => { + var el = await findElement(selector) + return !el || !await el.isVisible() + }) + resolve(isHidden) + }) + } + + /** + * Waits for an element to exist, and be visible. + */ + async waitForElementVisible (selector) { + debug('waitForElementVisible', selector) + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var visibleEl = await waitFor(async () => { + var el = await findElement(selector) + if (el && await el.isVisible()) { + return el + } else { + return false + } + }) + resolve(visibleEl) + }) + } + + /** + * Waits for a child page to be loaded. + */ + waitForPage (url) { + debug('waitForPage', url) + var waitFor = this.wait.bind(this) + var childPages = this.childPages + return new Promise(async resolve => { + var page = await waitFor(async () => { + return childPages.filter((val) => { + return val.url.includes(url) + }) + }) + resolve(page[0]) + }) + } +} + +var ghost = new Ghost() +export default ghost diff --git a/ghostjs-examples/package.json b/ghostjs-examples/package.json index 2acd4b2..f3e7f42 100644 --- a/ghostjs-examples/package.json +++ b/ghostjs-examples/package.json @@ -6,7 +6,8 @@ "scripts": { "test": "npm run test:phantom && npm run test:slimerjs", "test:phantom": "ghostjs test/*.js", - "test:slimerjs": "ghostjs --ghost-runner slimerjs-firefox test/*.js" + "test:slimerjs": "ghostjs --ghost-runner slimerjs-firefox test/*.js", + "test:electron": "ghostjs --ghost-protocol electron test/*.js" }, "author": "Kevin Grandon ", "license": "MPL-2.0", From b3f2d7dd8db28bbf8746bd7c6de7458d0ee8adfd Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Thu, 10 Nov 2016 14:07:58 +0900 Subject: [PATCH 2/4] Initial simple electron logic. --- ghostjs-core/package.json | 1 + ghostjs-core/src/element.js | 52 ++-- ghostjs-core/src/ghostjs.js | 60 +++- ghostjs-core/src/protocol/electron.js | 378 ++++++++++---------------- ghostjs-core/src/protocol/phantom.js | 101 +------ 5 files changed, 234 insertions(+), 358 deletions(-) diff --git a/ghostjs-core/package.json b/ghostjs-core/package.json index 21d69b0..090883c 100644 --- a/ghostjs-core/package.json +++ b/ghostjs-core/package.json @@ -36,6 +36,7 @@ ], "devDependencies": { "babel-eslint": "^7.1.0", + "electron": "^1.4.6", "standard": "^8.5.0" }, "standard": { diff --git a/ghostjs-core/src/element.js b/ghostjs-core/src/element.js index 34d9bae..7546006 100644 --- a/ghostjs-core/src/element.js +++ b/ghostjs-core/src/element.js @@ -2,12 +2,14 @@ export default class Element { /** * Creates a proxy to an element on the page. - * @param {object} page The current phantom/slimer page. + * @param {function} executeScript A method to script with the page. + * @param {function} uploadFiles A method to upload files to the page. * @param {string} selector The selector to locate the element. * @param {integer} lookupOffset The offset of the element. Used to lookup a single element in the case of a findElements() */ - constructor (page, selector, lookupOffset = 0) { - this.page = page + constructor (executeScript, uploadFile, selector, lookupOffset = 0) { + this.executeScript = executeScript + this.uploadFiles = uploadFiles this.selector = selector this.lookupOffset = lookupOffset } @@ -18,7 +20,7 @@ export default class Element { async mouse (method, xPos, yPos) { return new Promise(resolve => { - this.page.evaluate((selector, lookupOffset, mouseType, xPos, yPos) => { + resolve(this.executeScript((selector, lookupOffset, mouseType, xPos, yPos) => { try { var el = document.querySelectorAll(selector)[lookupOffset] var evt = document.createEvent('MouseEvents') @@ -41,10 +43,7 @@ export default class Element { return false } }, - this.selector, this.lookupOffset, method, xPos, yPos, - (err, result) => { - resolve(result) - }) + [this.selector, this.lookupOffset, method, xPos, yPos])) }) } @@ -54,7 +53,7 @@ export default class Element { async file (filePath) { return new Promise(resolve => { // TODO: This won't work for element collections (when this instance has an offset) - this.page.uploadFile(this.selector, filePath) + this.uploadFile(this.selector, filePath) resolve() }) } @@ -65,7 +64,7 @@ export default class Element { */ async fill (setFill) { return new Promise(resolve => { - this.page.evaluate((selector, lookupOffset, value) => { + resolve(this.executeScript((selector, lookupOffset, value) => { var el = document.querySelectorAll(selector)[lookupOffset] if (!el) { return null @@ -144,22 +143,16 @@ export default class Element { console.log('Unable to blur element ' + el.outerHTML + ': ' + e) } }, - this.selector, this.lookupOffset, setFill, - (err, result) => { - resolve(result) - }) + [this.selector, this.lookupOffset, setFill])) }) } async getAttribute (attribute) { return new Promise(resolve => { - this.page.evaluate((selector, lookupOffset, attribute) => { + resolve(this.executeScript((selector, lookupOffset, attribute) => { return document.querySelectorAll(selector)[lookupOffset][attribute] }, - this.selector, this.lookupOffset, attribute, - (err, result) => { - resolve(result) - }) + [this.selector, this.lookupOffset, attribute])) }) } @@ -173,7 +166,7 @@ export default class Element { async isVisible () { return new Promise(resolve => { - this.page.evaluate((selector, lookupOffset) => { + resolve(this.executeScript((selector, lookupOffset) => { var el = document.querySelectorAll(selector)[lookupOffset] var style try { @@ -193,16 +186,13 @@ export default class Element { } return el.clientHeight > 0 && el.clientWidth > 0 }, - this.selector, this.lookupOffset, - (err, result) => { - resolve(result) - }) + [this.selector, this.lookupOffset])) }) } async rect (func) { return new Promise(resolve => { - this.page.evaluate((selector, lookupOffset) => { + resolve(this.executeScript((selector, lookupOffset) => { var el = document.querySelectorAll(selector)[lookupOffset] if (!el) { return null @@ -218,10 +208,7 @@ export default class Element { width: rect.width } }, - this.selector, this.lookupOffset, - (err, result) => { - resolve(result) - }) + [this.selector, this.lookupOffset])) }) } @@ -231,7 +218,7 @@ export default class Element { } return new Promise(resolve => { - this.page.evaluate((func, selector, lookupOffset, args) => { + resolve(this.executeScript((func, selector, lookupOffset, args) => { var el = document.querySelectorAll(selector)[lookupOffset] args.unshift(el) var invoke = new Function( @@ -239,10 +226,7 @@ export default class Element { )(); return invoke.apply(null, args) }, - func.toString(), this.selector, this.lookupOffset, args, - (err, result) => { - resolve(result) - }) + [func.toString(), this.selector, this.lookupOffset, args])) }) } } diff --git a/ghostjs-core/src/ghostjs.js b/ghostjs-core/src/ghostjs.js index f9f2174..65e0503 100644 --- a/ghostjs-core/src/ghostjs.js +++ b/ghostjs-core/src/ghostjs.js @@ -247,7 +247,27 @@ class Ghost { async wait (waitFor=1000, pollMs=100) { debug('waiting for', waitFor) debug('waiting (pollMs)', pollMs) - return await this.protocol.wait(waitFor, pollMs) + if (!(waitFor instanceof Function)) { + return new Promise((resolve) => { + setTimeout(resolve, waitFor) + }) + } else { + let timeWaited = 0 + return new Promise((resolve) => { + var poll = async () => { + var result = await waitFor() + if (result) { + resolve(result) + } else if (timeWaited > this.waitTimeout) { + this.onTimeout('Timeout while waiting.') + } else { + timeWaited += pollMs + setTimeout(poll, pollMs) + } + } + poll() + }) + } } /** @@ -264,7 +284,19 @@ class Ghost { */ async waitForElement (selector) { debug('waitForElement', selector) - return await this.protocol.waitForElement(selector) + // Scoping gets broken within async promises, so bind these locally. + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var element = await waitFor(async () => { + var el = await findElement(selector) + if (el) { + return el + } + return false + }) + resolve(element) + }) } /** @@ -272,7 +304,15 @@ class Ghost { */ async waitForElementNotVisible (selector) { debug('waitForElementNotVisible', selector) - return await this.protocol.waitForElementNotVisible(selector) + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var isHidden = await waitFor(async () => { + var el = await findElement(selector) + return !el || !await el.isVisible() + }) + resolve(isHidden) + }) } /** @@ -280,7 +320,19 @@ class Ghost { */ async waitForElementVisible (selector) { debug('waitForElementVisible', selector) - return await this.protocol.waitForElementVisible(selector) + var waitFor = this.wait.bind(this) + var findElement = this.findElement.bind(this) + return new Promise(async resolve => { + var visibleEl = await waitFor(async () => { + var el = await findElement(selector) + if (el && await el.isVisible()) { + return el + } else { + return false + } + }) + resolve(visibleEl) + }) } /** diff --git a/ghostjs-core/src/protocol/electron.js b/ghostjs-core/src/protocol/electron.js index 9cc5b55..7d1ac2a 100644 --- a/ghostjs-core/src/protocol/electron.js +++ b/ghostjs-core/src/protocol/electron.js @@ -1,8 +1,10 @@ var debug = require('debug')('ghost:electron') +import {app, BrowserWindow} from 'electron'; export default class ElectronProtocol { constructor () { - + this.currentWin = null + this.domLoaded = false } /** @@ -23,10 +25,50 @@ export default class ElectronProtocol { onOpen () { // Inject any client scripts this.clientScripts.forEach(script => { - this.page.injectJs(script) + // this.page.injectJs(script) }) } + handleFailure = (event, code, details, failedUrl, isMainFrame) => { + if (isMainFrame) { + this.cleanup({ + message: 'navigation error', + code, + details, + url: failedUrl || this.url + }); + } + } + + handleDetails = (event, status, url, oldUrl, statusCode, method, referrer, headers, resourceType) => { + if (resourceType === 'mainFrame') { + this.responseDetails = { + url, + code, + method, + referrer, + headers + } + } + } + + handleDomReady = () => { + this.domLoaded = true + } + + handleFinish = (event) => { + this.cleanup(null, this.responseDetails) + } + + cleanup () { + this.currentWin.webContents.removeListener('did-fail-load', this.handleFailure); + this.currentWin.webContents.removeListener('did-fail-provisional-load', this.handleFailure); + this.currentWin.webContents.removeListener('did-get-response-details', this.handleDetails); + this.currentWin.webContents.removeListener('dom-ready', this.handleDomReady); + this.currentWin.webContents.removeListener('did-finish-load', this.handleFinish); + } + + /** * Opens a page. * @param {String} url Url of the page to open. @@ -37,91 +79,43 @@ export default class ElectronProtocol { */ async open (url, options={}) { debug('open url', url, 'options', options) - // If we already have a page object, just navigate it. - if (this.page) { - return new Promise(resolve => { - this.page.open(url, (err, status) => { - this.onOpen() - resolve(status) - }) - }) - } - return new Promise(resolve => { - driver.create(this.driverOpts, (err, browser) => { - this.browser = browser - browser.createPage((err, page) => { - this.page = page; - - options.settings = options.settings || {} - for (var i in options.settings) { - page.set('settings.' + i, options.settings[i]) - } - - if (options.headers) { - page.set('customHeaders', options.headers) - } - - if (options.viewportSize) { - page.set('viewportSize', options.viewportSize) - } - - /** - * Allow content to pass a custom function into onResourceRequested. - */ - if (options.onResourceRequested) { - page.setFn('onResourceRequested', options.onResourceRequested) - } - - page.onResourceTimeout = (url) => { - console.log('page timeout when trying to load ', url) - } - - page.onPageCreated = (page) => { - var pageObj = { - page: page, - url: null - } - - this.childPages.push(pageObj) - - page.onUrlChanged = (url) => { - pageObj.url = url; - } - - page.onClosing = (closingPage) => { - this.childPages = this.childPages.filter(eachPage => eachPage === closingPage) - } - } - - page.onConsoleMessage = (msg) => { - if (argv['verbose']) { - console.log('[Console]', msg) - } - } - - page.open(url, (err, status) => { - this.onOpen() - resolve(status) - }) - }) - }) + app.on('ready', async () => { + await this.createWindow(url, options)) + resolve() + } }) } + async createWindow (url, options) { + this.url = url; + + this.currentWin = new BrowserWindow(); + this.currentWin.webContents.on('did-fail-load', this.handleFailure); + this.currentWin.webContents.on('did-fail-provisional-load', this.handleFailure); + this.currentWin.webContents.on('did-get-response-details', this.handleDetails); + this.currentWin.webContents.on('dom-ready', this.handleDomReady); + this.currentWin.webContents.on('did-finish-load', this.handleFinish); + this.currentWin.webContents.loadURL(url, loadUrlOptions); + } + close () { debug('close') + /* if (this.page) { this.page.close() } this.page = null this.currentContext = null + */ } async exit () { + /* this.close() this.browser.exit() this.browser = null + */ } /** @@ -132,32 +126,27 @@ export default class ElectronProtocol { async usePage (pagePattern) { debug('use page', pagePattern) if (!pagePattern) { - this.currentContext = null; + this.currentWin = null; } else { this.currentContext = await this.waitForPage(pagePattern) } } - /** - * Gets the current page context that we're using. - */ - get pageContext() { - return (this.currentContext && this.currentContext.page) || this.page; - } - goBack () { debug('goBack') - this.pageContext.goBack() + this.currentWin.webContents.goBack(); } goForward () { debug('goForward') - this.pageContext.goForward() + this.currentWin.webContents.goForward(); } screenshot (filename, folder='screenshots') { - filename = filename || 'screenshot-' + Date.now() - this.pageContext.render(`${folder}/${filename}.png`) + var done = (img) => { + img.toPng() + } + this.currentWin.capturePage(done) } /** @@ -165,12 +154,9 @@ export default class ElectronProtocol { */ async pageTitle () { debug('getting pageTitle') - return new Promise(resolve => { - this.pageContext.evaluate(() => { return document.title }, - (err, result) => { - resolve(result) - }) - }) + return await this.script(() => { + return document.title + }); } /** @@ -178,19 +164,28 @@ export default class ElectronProtocol { */ async waitForPageTitle (expected) { debug('waitForPageTitle') - var waitFor = this.wait.bind(this) - var pageTitle = this.pageTitle.bind(this) - return new Promise(async resolve => { - var result = await waitFor(async () => { - var title = await pageTitle() - if (expected instanceof RegExp) { - return expected.test(title) - } else { - return title === expected - } - }) - resolve(result) - }) + // var waitFor = this.wait.bind(this) + // var pageTitle = this.pageTitle.bind(this) + // return new Promise(async resolve => { + // var result = await waitFor(async () => { + // var title = await pageTitle() + // if (expected instanceof RegExp) { + // return expected.test(title) + // } else { + // return title === expected + // } + // }) + // resolve(result) + // }) + } + + makeElement (selector, offset) { + return new Element( + this.script, + (selector, filePath) => console.log('not implemented yet'), + selector, + offset + ) } /** @@ -212,7 +207,7 @@ export default class ElectronProtocol { if (!result) { return resolve(null) } - resolve(new Element(this.pageContext, selector)) + resolve(makeElement(selector)) }) }) } @@ -223,26 +218,20 @@ export default class ElectronProtocol { */ async findElements (selector) { debug('findElements called with selector', selector) - return new Promise(resolve => { - this.pageContext.evaluate((selector) => { + return new Promise(async resolve => { + const numElements = await this.script((selector) => { return document.querySelectorAll(selector).length - }, - selector, - (err, numElements) => { - if (err) { - console.warn('findElements error', err) - } + } - if (!numElements) { - return resolve(null) - } + if (!numElements) { + return resolve(null) + } - var elementCollection = []; - for (var i = 0; i < numElements; i++) { - elementCollection.push(new Element(this.pageContext, selector, i)) - } - resolve(elementCollection) - }) + var elementCollection = [] + for (var i = 0; i < numElements; i++) { + elementCollection.push(makeElement(selector, i)) + } + resolve(elementCollection) }) } @@ -251,61 +240,44 @@ export default class ElectronProtocol { */ async resize (width, height) { debug('resizing to', width, height) - this.pageContext.set('viewportSize', {width, height}) + this.currentWin.setSize(width, height) } /** * Executes a script within the page. */ - async script (func, args) { + async script = (func, args) => { debug('scripting page', func) - if (!Array.isArray(args)) { - args = [args] - } - - return new Promise(resolve => { - this.pageContext.evaluate((stringyFunc, args) => { + return new Promise((resolve, reject) => { + if (!Array.isArray(args)) { + args = [args] + } + + const response = (event, response) => { + renderer.removeListener('error', error) + renderer.removeListener('log', log) + resolve(response) + } + + const error = (event, error) => { + renderer.removeListener('log', log) + renderer.removeListener('response', response) + reject(error) + } + + const log = (event, args) => console.log.bind(console) + + renderer.once('response', response) + renderer.once('error', error) + renderer.on('log', log) + + this.currentWin.webContents.executeJavaScript((stringyFunc, args) => { var invoke = new Function( "return " + stringyFunc )(); return invoke.apply(null, args) - }, - func.toString(), - args, - (err, result) => { - resolve(result) - }) - }) - } - - /** - * Waits for an arbitrary amount of time, or an async function to resolve. - * @param (Number|Function) - */ - async wait (waitFor=1000, pollMs=100) { - debug('waiting for', waitFor) - debug('waiting (pollMs)', pollMs) - if (!(waitFor instanceof Function)) { - return new Promise((resolve) => { - setTimeout(resolve, waitFor) - }) - } else { - let timeWaited = 0 - return new Promise((resolve) => { - var poll = async () => { - var result = await waitFor() - if (result) { - resolve(result) - } else if (timeWaited > this.waitTimeout) { - this.onTimeout('Timeout while waiting.') - } else { - timeWaited += pollMs - setTimeout(poll, pollMs) - } - } - poll() }) - } + }) } /** @@ -314,64 +286,8 @@ export default class ElectronProtocol { */ onTimeout (errMessage) { console.log('ghostjs timeout', errMessage) - this.screenshot('timeout-' + Date.now()) - throw new Error(errMessage) - } - - /** - * Waits for an element to exist in the page. - */ - async waitForElement (selector) { - debug('waitForElement', selector) - // Scoping gets broken within async promises, so bind these locally. - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var element = await waitFor(async () => { - var el = await findElement(selector) - if (el) { - return el - } - return false - }) - resolve(element) - }) - } - - /** - * Waits for an element to be hidden, or removed from the dom. - */ - async waitForElementNotVisible (selector) { - debug('waitForElementNotVisible', selector) - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var isHidden = await waitFor(async () => { - var el = await findElement(selector) - return !el || !await el.isVisible() - }) - resolve(isHidden) - }) - } - - /** - * Waits for an element to exist, and be visible. - */ - async waitForElementVisible (selector) { - debug('waitForElementVisible', selector) - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var visibleEl = await waitFor(async () => { - var el = await findElement(selector) - if (el && await el.isVisible()) { - return el - } else { - return false - } - }) - resolve(visibleEl) - }) + // this.screenshot('timeout-' + Date.now()) + // throw new Error(errMessage) } /** @@ -379,16 +295,16 @@ export default class ElectronProtocol { */ waitForPage (url) { debug('waitForPage', url) - var waitFor = this.wait.bind(this) - var childPages = this.childPages - return new Promise(async resolve => { - var page = await waitFor(async () => { - return childPages.filter((val) => { - return val.url.includes(url) - }) - }) - resolve(page[0]) - }) + // var waitFor = this.wait.bind(this) + // var childPages = this.childPages + // return new Promise(async resolve => { + // var page = await waitFor(async () => { + // return childPages.filter((val) => { + // return val.url.includes(url) + // }) + // }) + // resolve(page[0]) + // }) } } diff --git a/ghostjs-core/src/protocol/phantom.js b/ghostjs-core/src/protocol/phantom.js index 088a9aa..b42c255 100644 --- a/ghostjs-core/src/protocol/phantom.js +++ b/ghostjs-core/src/protocol/phantom.js @@ -228,6 +228,15 @@ export default class PhantomProtocol { }) } + makeElement (selector, offset) { + return new Element( + this.script, + (selector, filePath) => this.pageContextuploadFile(selector, filePath), + selector, + offset + ) + } + /** * Returns an element if it finds it in the page, otherwise returns null. * @param {string} selector @@ -247,7 +256,7 @@ export default class PhantomProtocol { if (!result) { return resolve(null) } - resolve(new Element(this.pageContext, selector)) + resolve(makeElement(selector)) }) }) } @@ -274,7 +283,7 @@ export default class PhantomProtocol { var elementCollection = []; for (var i = 0; i < numElements; i++) { - elementCollection.push(new Element(this.pageContext, selector, i)) + elementCollection.push(makeElement(selector, i)) } resolve(elementCollection) }) @@ -292,7 +301,7 @@ export default class PhantomProtocol { /** * Executes a script within the page. */ - async script (func, args) { + async script = (func, args) => { debug('scripting page', func) if (!Array.isArray(args)) { args = [args] @@ -313,36 +322,6 @@ export default class PhantomProtocol { }) } - /** - * Waits for an arbitrary amount of time, or an async function to resolve. - * @param (Number|Function) - */ - async wait (waitFor=1000, pollMs=100) { - debug('waiting for', waitFor) - debug('waiting (pollMs)', pollMs) - if (!(waitFor instanceof Function)) { - return new Promise((resolve) => { - setTimeout(resolve, waitFor) - }) - } else { - let timeWaited = 0 - return new Promise((resolve) => { - var poll = async () => { - var result = await waitFor() - if (result) { - resolve(result) - } else if (timeWaited > this.waitTimeout) { - this.onTimeout('Timeout while waiting.') - } else { - timeWaited += pollMs - setTimeout(poll, pollMs) - } - } - poll() - }) - } - } - /** * Called when wait or waitForElement times out. * Can be used as a hook to take screenshots. @@ -353,62 +332,6 @@ export default class PhantomProtocol { throw new Error(errMessage) } - /** - * Waits for an element to exist in the page. - */ - async waitForElement (selector) { - debug('waitForElement', selector) - // Scoping gets broken within async promises, so bind these locally. - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var element = await waitFor(async () => { - var el = await findElement(selector) - if (el) { - return el - } - return false - }) - resolve(element) - }) - } - - /** - * Waits for an element to be hidden, or removed from the dom. - */ - async waitForElementNotVisible (selector) { - debug('waitForElementNotVisible', selector) - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var isHidden = await waitFor(async () => { - var el = await findElement(selector) - return !el || !await el.isVisible() - }) - resolve(isHidden) - }) - } - - /** - * Waits for an element to exist, and be visible. - */ - async waitForElementVisible (selector) { - debug('waitForElementVisible', selector) - var waitFor = this.wait.bind(this) - var findElement = this.findElement.bind(this) - return new Promise(async resolve => { - var visibleEl = await waitFor(async () => { - var el = await findElement(selector) - if (el && await el.isVisible()) { - return el - } else { - return false - } - }) - resolve(visibleEl) - }) - } - /** * Waits for a child page to be loaded. */ From 0e91316ca3ef936acbfb7d1510bdd7ffd1d60cbd Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Thu, 10 Nov 2016 15:03:28 +0900 Subject: [PATCH 3/4] Get phantom tests passing. --- ghostjs-core/src/element.js | 2 +- ghostjs-core/src/ghostjs.js | 189 +++++---------------- ghostjs-core/src/protocol/electron.js | 27 +-- ghostjs-core/src/protocol/phantom.js | 22 +-- ghostjs-examples/test/find_element_test.js | 4 +- ghostjs-examples/test/https_test.js | 2 +- ghostjs-examples/test/navigation_test.js | 4 +- ghostjs-examples/test/wait_for_test.js | 2 +- ghostjs-examples/test/wait_test.js | 2 +- 9 files changed, 77 insertions(+), 177 deletions(-) diff --git a/ghostjs-core/src/element.js b/ghostjs-core/src/element.js index 7546006..753f609 100644 --- a/ghostjs-core/src/element.js +++ b/ghostjs-core/src/element.js @@ -9,7 +9,7 @@ export default class Element { */ constructor (executeScript, uploadFile, selector, lookupOffset = 0) { this.executeScript = executeScript - this.uploadFiles = uploadFiles + this.uploadFile = uploadFile this.selector = selector this.lookupOffset = lookupOffset } diff --git a/ghostjs-core/src/ghostjs.js b/ghostjs-core/src/ghostjs.js index 65e0503..c035c3d 100644 --- a/ghostjs-core/src/ghostjs.js +++ b/ghostjs-core/src/ghostjs.js @@ -12,9 +12,9 @@ class Ghost { let protocolType = argv['ghost-protocol'] || 'phantom' if (protocolType === 'phantom') { - this.protocol = new PhantomProtocol(); + this.protocol = new PhantomProtocol(this); } else { - this.protocol = new ElectronProtocol(); + this.protocol = new ElectronProtocol(this); } } @@ -24,20 +24,8 @@ class Ghost { */ injectScripts () { debug('inject scripts', arguments) - Array.slice(arguments).forEach(script => { - this.clientScripts.push(script) - }) - } - - /** - * Callback when a page loads. - * Injects javascript and other things we need. - */ - onOpen () { - // Inject any client scripts - this.clientScripts.forEach(script => { - this.page.injectJs(script) - }) + const args = Array.slice(arguments) + this.protocol.injectScripts.apply(this.protocol, args) } /** @@ -50,140 +38,22 @@ class Ghost { */ async open (url, options={}) { debug('open url', url, 'options', options) - // If we already have a page object, just navigate it. - if (this.page) { - return new Promise(resolve => { - this.page.open(url, (err, status) => { - this.onOpen() - resolve(status) - }) - }) - } - - return new Promise(resolve => { - driver.create(this.driverOpts, (err, browser) => { - this.browser = browser - browser.createPage((err, page) => { - this.page = page; - - options.settings = options.settings || {} - for (var i in options.settings) { - page.set('settings.' + i, options.settings[i]) - } - - if (options.headers) { - page.set('customHeaders', options.headers) - } - - if (options.viewportSize) { - page.set('viewportSize', options.viewportSize) - } - - /** - * Allow content to pass a custom function into onResourceRequested. - */ - if (options.onResourceRequested) { - page.setFn('onResourceRequested', options.onResourceRequested) - } - - page.onResourceTimeout = (url) => { - console.log('page timeout when trying to load ', url) - } - - page.onPageCreated = (page) => { - var pageObj = { - page: page, - url: null - } - - this.childPages.push(pageObj) - - page.onUrlChanged = (url) => { - pageObj.url = url; - } - - page.onClosing = (closingPage) => { - this.childPages = this.childPages.filter(eachPage => eachPage === closingPage) - } - } - - page.onConsoleMessage = (msg) => { - if (argv['verbose']) { - console.log('[Console]', msg) - } - } - - page.open(url, (err, status) => { - this.onOpen() - resolve(status) - }) - }) - }) - }) + return this.protocol.open(url, options) } close () { debug('close') - if (this.page) { - this.page.close() - } - this.page = null - this.currentContext = null - } - - async exit () { - this.close() - this.browser.exit() - this.browser = null - } - - /** - * Sets the current page context to run test methods on. - * This is useful for running tests in popups for example. - * To use the root page, pass an empty value. - */ - async usePage (pagePattern) { - debug('use page', pagePattern) - if (!pagePattern) { - this.currentContext = null; - } else { - this.currentContext = await this.waitForPage(pagePattern) - } - } - - /** - * Gets the current page context that we're using. - */ - get pageContext() { - return (this.currentContext && this.currentContext.page) || this.page; + return this.protocol.close() } goBack () { debug('goBack') - this.pageContext.goBack() + this.protocol.goBack() } goForward () { debug('goForward') - this.pageContext.goForward() - } - - screenshot (filename, folder='screenshots') { - filename = filename || 'screenshot-' + Date.now() - this.pageContext.render(`${folder}/${filename}.png`) - } - - /** - * Returns the title of the current page. - */ - async pageTitle () { - debug('getting pageTitle') - return new Promise(resolve => { - this.pageContext.evaluate(() => { return document.title }, - (err, result) => { - resolve(result) - }) - }) + this.protocol.goForward() } /** @@ -212,7 +82,7 @@ class Ghost { */ async findElement (selector) { debug('findElement called with selector', selector) - return await this.protocol.findElement(selector) + return this.protocol.findElement(selector) } /** @@ -221,7 +91,7 @@ class Ghost { */ async findElements (selector) { debug('findElements called with selector', selector) - return await this.protocol.findElements(selector) + return this.protocol.findElements(selector) } /** @@ -229,7 +99,7 @@ class Ghost { */ async resize (width, height) { debug('resizing to', width, height) - return await this.protocol.resize(width, height) + return this.protocol.resize(width, height) } /** @@ -237,7 +107,7 @@ class Ghost { */ async script (func, args) { debug('scripting page', func) - return await this.protocol.scripting(func, args) + return this.protocol.script(func, args) } /** @@ -276,7 +146,12 @@ class Ghost { */ onTimeout (errMessage) { console.log('ghostjs timeout', errMessage) - return await this.protocol.onTimeout(errMessage) + return this.protocol.onTimeout(errMessage) + } + + async exit () { + debug('exit') + return await this.protocol.exit() } /** @@ -338,9 +213,33 @@ class Ghost { /** * Waits for a child page to be loaded. */ - waitForPage (url) { + async waitForPage (url) { debug('waitForPage', url) - return await this.protocol.waitForPage(selector) + return await this.protocol.waitForPage(url) + } + + async usePage (pagePattern) { + debug('usePage', pagePattern) + return await this.protocol.usePage(pagePattern) + } + + async pageTitle () { + debug('pageTitle') + return await this.protocol.pageTitle() + } + + async waitForPageTitle (expected) { + debug('waitForPageTitle', expected) + return await this.protocol.waitForPageTitle(expected) + } + + async setDriverOpts (options) { + debug('setDriverOpts', options) + if (!this.protocol.setDriverOpts) { + console.log('The test protocol does not support setDriverOpts.') + return + } + return await this.protocol.setDriverOpts(options) } } diff --git a/ghostjs-core/src/protocol/electron.js b/ghostjs-core/src/protocol/electron.js index 7d1ac2a..27d8bcd 100644 --- a/ghostjs-core/src/protocol/electron.js +++ b/ghostjs-core/src/protocol/electron.js @@ -1,8 +1,12 @@ var debug = require('debug')('ghost:electron') import {app, BrowserWindow} from 'electron'; +import Element from '../element' export default class ElectronProtocol { - constructor () { + constructor (ghost) { + // TEMP: Pull in utility functions from ghost. + this.wait = ghost.wait + this.currentWin = null this.domLoaded = false } @@ -81,9 +85,9 @@ export default class ElectronProtocol { debug('open url', url, 'options', options) return new Promise(resolve => { app.on('ready', async () => { - await this.createWindow(url, options)) + await this.createWindow(url, options) resolve() - } + }) }) } @@ -164,7 +168,7 @@ export default class ElectronProtocol { */ async waitForPageTitle (expected) { debug('waitForPageTitle') - // var waitFor = this.wait.bind(this) + // var waitFor = this.wait // var pageTitle = this.pageTitle.bind(this) // return new Promise(async resolve => { // var result = await waitFor(async () => { @@ -181,7 +185,7 @@ export default class ElectronProtocol { makeElement (selector, offset) { return new Element( - this.script, + this.script.bind(this), (selector, filePath) => console.log('not implemented yet'), selector, offset @@ -207,7 +211,7 @@ export default class ElectronProtocol { if (!result) { return resolve(null) } - resolve(makeElement(selector)) + resolve(this.makeElement(selector)) }) }) } @@ -221,7 +225,7 @@ export default class ElectronProtocol { return new Promise(async resolve => { const numElements = await this.script((selector) => { return document.querySelectorAll(selector).length - } + }) if (!numElements) { return resolve(null) @@ -229,7 +233,7 @@ export default class ElectronProtocol { var elementCollection = [] for (var i = 0; i < numElements; i++) { - elementCollection.push(makeElement(selector, i)) + elementCollection.push(this.makeElement(selector, i)) } resolve(elementCollection) }) @@ -246,7 +250,7 @@ export default class ElectronProtocol { /** * Executes a script within the page. */ - async script = (func, args) => { + async script (func, args) { debug('scripting page', func) return new Promise((resolve, reject) => { if (!Array.isArray(args)) { @@ -295,7 +299,7 @@ export default class ElectronProtocol { */ waitForPage (url) { debug('waitForPage', url) - // var waitFor = this.wait.bind(this) + // var waitFor = this.wait // var childPages = this.childPages // return new Promise(async resolve => { // var page = await waitFor(async () => { @@ -307,6 +311,3 @@ export default class ElectronProtocol { // }) } } - -var ghost = new Ghost() -export default ghost diff --git a/ghostjs-core/src/protocol/phantom.js b/ghostjs-core/src/protocol/phantom.js index b42c255..8edff47 100644 --- a/ghostjs-core/src/protocol/phantom.js +++ b/ghostjs-core/src/protocol/phantom.js @@ -1,10 +1,13 @@ var debug = require('debug')('ghost:phantom') var argv = require('yargs').argv var driver = require('node-phantom-simple') -import Element from './element' +import Element from '../element' export default class PhantomProtocol { - constructor () { + constructor (ghost) { + // TEMP: Pull in utility functions from ghost. + this.wait = ghost.wait + this.testRunner = argv['ghost-runner'] || 'phantomjs-prebuilt' this.driverOpts = null this.setDriverOpts({}) @@ -213,7 +216,7 @@ export default class PhantomProtocol { */ async waitForPageTitle (expected) { debug('waitForPageTitle') - var waitFor = this.wait.bind(this) + var waitFor = this.wait var pageTitle = this.pageTitle.bind(this) return new Promise(async resolve => { var result = await waitFor(async () => { @@ -230,7 +233,7 @@ export default class PhantomProtocol { makeElement (selector, offset) { return new Element( - this.script, + this.script.bind(this), (selector, filePath) => this.pageContextuploadFile(selector, filePath), selector, offset @@ -256,7 +259,7 @@ export default class PhantomProtocol { if (!result) { return resolve(null) } - resolve(makeElement(selector)) + resolve(this.makeElement(selector)) }) }) } @@ -283,7 +286,7 @@ export default class PhantomProtocol { var elementCollection = []; for (var i = 0; i < numElements; i++) { - elementCollection.push(makeElement(selector, i)) + elementCollection.push(this.makeElement(selector, i)) } resolve(elementCollection) }) @@ -301,7 +304,7 @@ export default class PhantomProtocol { /** * Executes a script within the page. */ - async script = (func, args) => { + async script (func, args) { debug('scripting page', func) if (!Array.isArray(args)) { args = [args] @@ -337,7 +340,7 @@ export default class PhantomProtocol { */ waitForPage (url) { debug('waitForPage', url) - var waitFor = this.wait.bind(this) + var waitFor = this.wait var childPages = this.childPages return new Promise(async resolve => { var page = await waitFor(async () => { @@ -349,6 +352,3 @@ export default class PhantomProtocol { }) } } - -var ghost = new Ghost() -export default ghost diff --git a/ghostjs-examples/test/find_element_test.js b/ghostjs-examples/test/find_element_test.js index d91e7a5..23d7216 100644 --- a/ghostjs-examples/test/find_element_test.js +++ b/ghostjs-examples/test/find_element_test.js @@ -16,12 +16,12 @@ describe('ghost#findElement', () => { assert.equal(await myElement.html(), 'myElement Content') }) - it('waitFor element state', async () => { + it('wait for element state', async () => { await ghost.open('http://localhost:8888/basic_content.html') let trigger = await ghost.findElement('#moreContentTrigger') await trigger.click() - var isVisible = await ghost.waitFor(async () => { + var isVisible = await ghost.wait(async () => { var findEl = await ghost.findElement('.moreContent') return findEl && await findEl.isVisible() }) diff --git a/ghostjs-examples/test/https_test.js b/ghostjs-examples/test/https_test.js index baa9eac..255dfce 100644 --- a/ghostjs-examples/test/https_test.js +++ b/ghostjs-examples/test/https_test.js @@ -23,7 +23,7 @@ describe('HTTPS server', () => { assert.equal(result, 'fail') ghost.exit() }) - if (ghost.testRunner.match(/phantom/)) { + if (ghost.protocol.testRunner.match(/phantom/)) { it('has a title', async () => { // Only works with PhantomJS, at present ghost.setDriverOpts({parameters: {'ignore-ssl-errors': 'yes'}}) diff --git a/ghostjs-examples/test/navigation_test.js b/ghostjs-examples/test/navigation_test.js index 6371ad5..a51bc28 100644 --- a/ghostjs-examples/test/navigation_test.js +++ b/ghostjs-examples/test/navigation_test.js @@ -15,12 +15,12 @@ describe('ghost#goBack/goForward', () => { assert.equal(await ghost.pageTitle(), 'Form') ghost.goBack() - await ghost.waitFor(async () => { + await ghost.wait(async () => { return await ghost.pageTitle() === 'Basic Content' }) ghost.goForward() - await ghost.waitFor(async () => { + await ghost.wait(async () => { return await ghost.pageTitle() === 'Form' }) }) diff --git a/ghostjs-examples/test/wait_for_test.js b/ghostjs-examples/test/wait_for_test.js index 1a284cc..5460fc2 100644 --- a/ghostjs-examples/test/wait_for_test.js +++ b/ghostjs-examples/test/wait_for_test.js @@ -4,7 +4,7 @@ import assert from 'assert' describe('ghost#waitFor', () => { it('we can wait', async () => { var curr = 0 - await ghost.waitFor(() => { + await ghost.wait(() => { curr++ return curr === 10 }, 10) diff --git a/ghostjs-examples/test/wait_test.js b/ghostjs-examples/test/wait_test.js index eea0b25..046a471 100644 --- a/ghostjs-examples/test/wait_test.js +++ b/ghostjs-examples/test/wait_test.js @@ -15,7 +15,7 @@ describe('ghost#wait', () => { it('waits for a function', async () => { var curr = 0 - await ghost.waitFor(() => { + await ghost.wait(() => { curr++ return curr === 10 }, 10) From 5daa4f134a7708d65c5e2e8d1ebebf34d56c2b6f Mon Sep 17 00:00:00 2001 From: Kevin Grandon Date: Thu, 10 Nov 2016 15:21:47 +0900 Subject: [PATCH 4/4] Add todo. --- ghostjs-core/src/protocol/electron.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ghostjs-core/src/protocol/electron.js b/ghostjs-core/src/protocol/electron.js index 27d8bcd..fb9010e 100644 --- a/ghostjs-core/src/protocol/electron.js +++ b/ghostjs-core/src/protocol/electron.js @@ -9,6 +9,8 @@ export default class ElectronProtocol { this.currentWin = null this.domLoaded = false + + this.testRunner = 'ghost-electron' } /** @@ -94,6 +96,8 @@ export default class ElectronProtocol { async createWindow (url, options) { this.url = url; + // TODO: We can spawn an electron window here, or ideally communicate with an already opened window. + this.currentWin = new BrowserWindow(); this.currentWin.webContents.on('did-fail-load', this.handleFailure); this.currentWin.webContents.on('did-fail-provisional-load', this.handleFailure);