From 906bda80ab3a28ac7b1d06dcd1d5bfaa167bd801 Mon Sep 17 00:00:00 2001 From: Ross Date: Mon, 30 May 2016 17:20:02 -0400 Subject: [PATCH] adding docs for `.use()` and `.action()`, fixes #3 --- README.md | 11 +- docs/beginner/action.md | 121 ++++++++++++++++++++ docs/beginner/use.md | 63 ++++++++++ examples/beginner/action/clear-cache.js | 36 ++++++ examples/beginner/action/heading-extract.js | 35 ++++++ examples/beginner/use/yahooTopResult.js | 46 ++++++++ 6 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 docs/beginner/action.md create mode 100644 docs/beginner/use.md create mode 100644 examples/beginner/action/clear-cache.js create mode 100644 examples/beginner/action/heading-extract.js create mode 100644 examples/beginner/use/yahooTopResult.js diff --git a/README.md b/README.md index 5e6874a..feaa54c 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,20 @@ New to Nightmare? The most basic way to use it is with promises or callbacks. He - [Native promises](https://github.com/rosshinkley/nightmare-examples/blob/master/docs/beginner/promises.md) - [Callbacks](https://github.com/rosshinkley/nightmare-examples/blob/master/docs/beginner/callbacks.md) -**You can also use Nightmare with generators.** Several JavaScript libraries use generators to give you a simpler way to deal with asynchronous events. Here’s how to use Nightmare with some of them: +#### Nightmare and Generators + +Several JavaScript libraries use generators to give you a simpler way to deal with asynchronous events. Here’s how to use Nightmare with some of them: - [`vo`](https://github.com/rosshinkley/nightmare-examples/blob/master/docs/beginner/vo.md) - [`co`](https://github.com/rosshinkley/nightmare-examples/blob/master/docs/beginner/co.md) +#### Extending Nightmare + +Nightmare may not do everything out of the box that you need it to do. + +- [`.action()`](https://github.com/rosshinkley/nightmare-examples/blob/master/docs/beginner/action.md) - Look here if adding a method that does a specific activity in the browser. +- [`.use()`](https://github.com/rosshinkley/nightmare-examples/blob/master/docs/beginner/use.md) - Wrap several actions up for reusability in one tidy place. + ### Common Pitfalls Working with an automated web browser can be complex. Here are a few common gotchas and issues you might run into while working with Nightmare: diff --git a/docs/beginner/action.md b/docs/beginner/action.md new file mode 100644 index 0000000..40ae1a5 --- /dev/null +++ b/docs/beginner/action.md @@ -0,0 +1,121 @@ +# Using `.action()` + +The `.action()` method allows the Nightmare prototype to be extended to include custom actions. There are two main ways to use `.action()` - one, creating a method that calls `nightmare.evaluate_now` that acts like a named `.evaluate()` function; and two, creating a method that calls a custom Electron action. + +## Defining a custom action with `.evaluate_now()` + +Let's say you found yourself commonly executing an `.evaluate()` to perform an action. For example, say we wanted to extract text from elements returned for a given selector: + +```js +var Nightmare = require('nightmare'); + +//define a new Nightmare method named "textExtract" +//note that it takes a selector as a parameter +Nightmare.action('textExtract', function(selector, done) { + //`this` is the Nightmare instance + this.evaluate_now((selector) => { + //query the document for all elements that match `selector` + //note that `document.querySelectorAll` returns a DOM list, not an array + //as such, convert the result to an Array with `Array.from` + //return the array result + return Array.from(document.querySelectorAll(selector)) + //extract and return the text for each element matched + .map((element) => element.innerText); + //pass done as the first argument, other arguments following + }, done, selector) +}); + +//create a nightmare instance +var nightmare = Nightmare(); +nightmare + //go to a url + .goto('http://example.com') + //extract text, in this case for all headings + .textExtract('h1, h2, h3, h4, h5, h6') + //execute the command chain, extracting the headings + .then((headings) => { + //log the result + console.log(headings); + }) + //... do other actions... + //end the nightmare instance + .then(nightmare.end()) + .catch((e) => console.dir(e)); +``` + +Please note: + +- In the context of a method defined with `.action()`, `this` is bound to the Nightmare instance. +- The DOM list from `document.querySelectorAll` is an array-like, and because of that needs to be converted to an `Array` before being able to leverage array methods (`.map()` in this case). +- Parameters for `evaluate_now` are passed _after_ the `done` callback. + +## Defining a custom action with an Electron method + +Not all actions can be done with a simple evaluate. Sometimes, it is necessary to use Electron's internals to perform an action that is not predefined in Nightmare. For example, say we wanted to clear the cache between navigation: + +```js +var Nightmare = require('nightmare'); + +//define a new Nightmare action named "clearCache" +Nightmare.action('clearCache', + //define the action to run inside Electron + function(name, options, parent, win, renderer, done) { + //call the IPC parent's `respondTo` method for clearCache... + parent.respondTo('clearCache', function(done) { + //clear the session cache and call the action's `done` + win.webContents.session.clearCache(done); + }); + //call the action creation `done` + done(); + }, + function(done) { + //use the IPC child's `call` to call the action added to the Electron instance + this.child.call('clearCache', done); + }); + +//create a nightmare instance +var nightmare = Nightmare(); + +nightmare + //go to a url + .goto('http://example.com') + //clear the cache + .clearCache() + //go to another url + .goto('http://google.com') + //perform other actions + .then(() => { + //... + }) + //... other actions... + .then(nightmare.end()) + .catch((e) => console.dir(e)); +``` + +#### Electron method parameters + +- **name** - the action name. +- **options** - the options hash that Electron was started with. +- **parent** - the IPC process used to communicate back to the Nightmare instance. +- **win** - a reference to the `browserWindow` instance from Electron. +- **renderer** - a reference to the Electron renderer. +- **done** - the callback method to call when the action setup is complete. + +There is also an ambient reference to `require` so you can pull in libraries as you see fit. Note that `require` will require from the Electron install location, so it is very likely you'll need to supply a relative or absolute path for the library you need. + +#### IPC methods + +- `nightmare.child.call(name, [args,] done)` - This method calls the `method` through the IPC process to the Electron process. There is also some internal sugar to prevent multiple calls to the same method from getting results from a different call. +- `IPC.respondTo(name, done)` - This responds to the `child.call()` name, calling `done` when complete as a callback. Note that data can be passed through `done` as a normal callback would. + +## `.action()` must be called before instantiation + +Be sure that the action is added to the Nightmare prototype before you create your instance of Nightmare. Otherwise, the action will not be added to the instance. + +## References + +- [`Array.from`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) +- [`document.querySelectorAll`](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll) +- [`.action()` documentation](https://github.com/segmentio/nightmare#nightmareactionname-electronactionelectronnamespace-actionnamespace) +- [runnable `.action()` examples](https://github.com/rosshinkley/nightmare-examples/tree/master/examples/beginner/action) +- [v3 plugin update proposal](https://github.com/segmentio/nightmare/issues/593#issuecomment-217209512) diff --git a/docs/beginner/use.md b/docs/beginner/use.md new file mode 100644 index 0000000..e7d2ccd --- /dev/null +++ b/docs/beginner/use.md @@ -0,0 +1,63 @@ +# Defining reusable parts with `.use()` + +`.use()` is intended to provide a handy way to add groups of actions commonly executed together to the queue without calling them individually. This also allows for complex, composable actions. + +## Grouping Actions + +As an example, say we wanted to group the navigation, typing in a search bar, and the clicking of the form submission into one single repeatable action. Consider: + +```js +var Nightmare = require('nightmare'); +var nightmare = Nightmare() + +//define a function to be used +var yahooTopResult = function(search) { + //return a closure that takes the nightmare instance as the only argument + return function(nightmare) { + //using the given nightmare instance, + nightmare + //goto the yahoo homepage + .goto('http://yahoo.com') + //search for the given search term + .type('form[action*="/search"] [name=p]', search) + //search + .click('form[action*="/search"] [type=submit]') + //wait for the results + .wait('#web') + //get the text and href of the first result + .evaluate(function() { + var element = document.querySelector('#web a'); + return { + text: element.innerText, + href: element.href + }; + }); + }; +}; + +nightmare + //use the `yahooTopResult` function to add actions to search for "github nightmare" + .use(yahooTopResult('github nightmare')) + //print the result + .then(function(result) { + console.log(result) + }) + //use the `yahooTopResult` function again to search for "electron" + .then(() => nightmare.use(yahooTopResult('electron'))) + //print the result and end + .then(function(result){ + console.log(result); + return nightmare.end(); + }) + .then(() => console.log('done')) + .catch(function(error) { + console.error('Search failed:', error); + }); +``` + +This works by a Nightmare instance passing a reference to itself to `.use()` to have actions (in this case, `goto`, `type`, `click`, `wait`, and `evaluate`) added to the queue prior to execution. + +## References + +- [`.use()` documentation](https://github.com/segmentio/nightmare#useplugin) +- [runnable `.use()` examples](https://github.com/rosshinkley/nightmare-examples/tree/master/examples/beginner/use) diff --git a/examples/beginner/action/clear-cache.js b/examples/beginner/action/clear-cache.js new file mode 100644 index 0000000..1861b84 --- /dev/null +++ b/examples/beginner/action/clear-cache.js @@ -0,0 +1,36 @@ +var Nightmare = require('nightmare'); + +//define a new Nightmare action named "clearCache" +Nightmare.action('clearCache', + //define the action to run inside Electron + function(name, options, parent, win, renderer, done) { + //call the IPC parent's `respondTo` method for clearCache... + parent.respondTo('clearCache', function(done) { + //clear the session cache and call the action's `done` + win.webContents.session.clearCache(done); + }); + //call the action creation `done` + done(); + }, + function(done) { + //use the IPC child's `call` to call the action added to the Electron instance + this.child.call('clearCache', done); + }); + +//create a nightmare instance +var nightmare = Nightmare(); + +nightmare + //go to a url + .goto('http://example.com') + //clear the cache + .clearCache() + //go to another url + .goto('http://google.com') + //perform other actions + .then(() => { + //... + }) + //... other actions... + .then(nightmare.end()) + .catch((e) => console.dir(e)); diff --git a/examples/beginner/action/heading-extract.js b/examples/beginner/action/heading-extract.js new file mode 100644 index 0000000..123b984 --- /dev/null +++ b/examples/beginner/action/heading-extract.js @@ -0,0 +1,35 @@ +var Nightmare = require('nightmare'); + +//define a new Nightmare method named "textExtract" +//note that it takes a selector as a parameter +Nightmare.action('textExtract', function(selector, done) { + //`this` is the Nightmare instance + this.evaluate_now((selector) => { + //query the document for all elements that match `selector` + //note that `document.querySelectorAll` returns a DOM list, not an array + //as such, convert the result to an Array with `Array.from` + //return the array result + return Array.from(document.querySelectorAll(selector)) + //extract and return the text for each element matched + .map((element) => element.innerText); + //pass done as the first argument, other arguments following + }, done, selector) +}); + +//create a nightmare instance +var nightmare = Nightmare(); +nightmare + //go to a url + .goto('http://example.com') + //extract text, in this case for all headings + .textExtract('h1, h2, h3, h4, h5, h6') + //execute the command chain, extracting the headings + .then((headings) => { + //log the result + console.log(headings); + }) + //... do other actions... + //end the nightmare instance + .then(nightmare.end()) + .catch((e) => console.dir(e)); + diff --git a/examples/beginner/use/yahooTopResult.js b/examples/beginner/use/yahooTopResult.js new file mode 100644 index 0000000..d57560e --- /dev/null +++ b/examples/beginner/use/yahooTopResult.js @@ -0,0 +1,46 @@ +var Nightmare = require('nightmare'); +var nightmare = Nightmare() + +//define a function to be used +var yahooTopResult = function(search) { + //return a closure that takes the nightmare instance as the only argument + return function(nightmare) { + //using the given nightmare instance, + nightmare + //goto the yahoo homepage + .goto('http://yahoo.com') + //search for the given search term + .type('form[action*="/search"] [name=p]', search) + //search + .click('form[action*="/search"] [type=submit]') + //wait for the results + .wait('#web') + //get the text and href of the first result + .evaluate(function() { + var element = document.querySelector('#web a'); + return { + text: element.innerText, + href: element.href + }; + }); + }; +}; + +nightmare + //use the `yahooTopResult` function to add actions to search for "github nightmare" + .use(yahooTopResult('github nightmare')) + //print the result + .then(function(result) { + console.log(result) + }) + //use the `yahooTopResult` function again to search for "electron" + .then(() => nightmare.use(yahooTopResult('electron'))) + //print the result and end + .then(function(result){ + console.log(result); + return nightmare.end(); + }) + .then(() => console.log('done')) + .catch(function(error) { + console.error('Search failed:', error); + });