Showing preview only (227K chars total). Download the full file or copy to clipboard to get everything.
Repository: segment-boneyard/nightmare
Branch: master
Commit: 0f47de849912
Files: 50
Total size: 213.4 KB
Directory structure:
gitextract_y9toqmvg/
├── .circleci/
│ └── config.yml
├── .eslintrc.js
├── .gitignore
├── History.md
├── Makefile
├── Readme.md
├── example.js
├── lib/
│ ├── actions.js
│ ├── frame-manager.js
│ ├── ipc.js
│ ├── javascript.js
│ ├── nightmare.js
│ ├── preload.js
│ └── runner.js
├── package.json
└── test/
├── Preferences
├── bb-xvfb
├── files/
│ ├── globals.js
│ ├── nightmare-created.js
│ ├── nightmare-error.js
│ ├── nightmare-unended.js
│ ├── server.crt
│ ├── server.key
│ └── test.css
├── fixtures/
│ ├── cookies/
│ │ └── index.html
│ ├── evaluation/
│ │ └── index.html
│ ├── events/
│ │ └── index.html
│ ├── manipulation/
│ │ ├── index.html
│ │ ├── result.html
│ │ └── results.html
│ ├── navigation/
│ │ ├── a.html
│ │ ├── b.html
│ │ ├── c.html
│ │ ├── hanging-resources.html
│ │ ├── index.html
│ │ ├── invalid-frame.html
│ │ ├── invalid-image.html
│ │ └── valid-frame.html
│ ├── options/
│ │ └── index.html
│ ├── preload/
│ │ ├── index.html
│ │ └── index.js
│ ├── rendering/
│ │ └── index.html
│ ├── security/
│ │ └── index.html
│ ├── simple/
│ │ └── index.html
│ └── unload/
│ ├── add-event-listener.html
│ └── index.html
├── index.js
├── mocha.opts
├── server.js
└── waitForX
================================================
FILE CONTENTS
================================================
================================================
FILE: .circleci/config.yml
================================================
test: &test
working_directory: ~/repo
steps:
- checkout
- run:
name: Print system information
command: |
echo "node $(node -v)"
echo "npm v$(npm --version)"
- run:
name: Install dependencies
command: sudo apt-get update && sudo apt-get install dbus-x11 -y
- restore_cache:
keys:
- v1-npm-deps-{{ checksum "package.json" }}
- v1-npm-deps
- run:
name: npm install
command: npm install
- save_cache:
key: v1-npm-deps-{{ checksum "package.json" }}
paths:
- node_modules
- run:
name: npm test
command: npm test
publish: &publish
working_directory: ~/repo
steps:
- checkout
- run:
name: Print system information
command: |
echo "node $(node -v)"
echo "npm v$(npm --version)"
- run:
name: Install dependencies
command: sudo apt-get update && sudo apt-get install dbus-x11 -y
# Do not explicitly use caching when publishing
- run:
name: npm install
command: npm install
- run:
name: set npm auth token
command: npm config set "//registry.npmjs.org/:_authToken" $NPM_AUTH
- run:
name: npm publish
command: npm publish
version: 2
jobs:
test-node6:
<<: *test
docker:
- image: circleci/node:6-browsers
test-node7:
<<: *test
docker:
- image: circleci/node:7-browsers
test-node-latest:
<<: *test
docker:
- image: circleci/node:latest-browsers
publish:
<<: *publish
docker:
- image: circleci/node:6-browsers
workflows:
version: 2
build-test-and-publish:
jobs:
- test-node6
- test-node7
- test-node-latest
- publish:
filters:
tags:
only: '/v?[0-9]+(\.[0-9]+)*(-.+)?/'
branches:
ignore: /.*/
requires:
- test-node6
- test-node7
- test-node-latest
================================================
FILE: .eslintrc.js
================================================
module.exports =
{
extends: [ 'eslint:recommended', 'prettier' ],
plugins: ['prettier' ], // activating esling-plugin-prettier (--fix stuff)
env: {
browser: true,
node: true,
es6: true
},
rules: {
'prettier/prettier': [
// customizing prettier rules (unfortunately not many of them are customizable)
'error',
{
singleQuote: true,
semi: false
}
],
'semi': ['error', 'never' ],
'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
'no-inner-declarations': 'off'
}
}
================================================
FILE: .gitignore
================================================
node_modules
.DS_Store
test/tmp
test/DevTools Extensions
.idea
================================================
FILE: History.md
================================================
# 3.0.1 / 2018-03-29
* bump electron to fix #1424
# 3.0.0 / 2018-03-02
* BREAKING: remove window.\_\_nightmare.ipc to resolve the major security issues (#1390)
* BREAKING: properly serialize error values (#1391)
* added linting, formatting and a git pre-commit hook (#1386)
* Added ability to specify the client certificate selected by electron (#1339)
* adding selector to the timeout error in .wait() (#1381)
* Pin Electron version (#1270)
* Fix error on preload (#1247)
* Add mouseout action to complement mouseover (#1238)
* fix problems that are rejected when adding child actions with objects. (#1093)
* Set mouse position for mouse events (#1077)
* Repaired support for multiple timeouts in FrameManager (#945)
# 2.10.0 / 2017-02-23
* Remove redundant docs for 'log' event from README
* changed some `var` declarations to `const`
* replace the 404 link with valid link
* added Promise override tests
* added docs for new Promise override features
* added ability to override internal Promise library
# 2.9.1 / 2017-01-02
* Minor touchups to key press documentation
* Link to Electron documentation updated
* Updates speed information on the readme
* Swaps Yahoo example out for a faster DuckDuckGo example
* Fixes an issue where `nightmare` may be undefined in the browser at injection time
* Changes screenshot rendering to use debugger API instead of forcing a DOM change
# 2.9.0 / 2016-12-17
* Prevents unload dialogs, allowing Nightmare to end gracefully
* `.end(fn)` now uses `.then()` under the covers
* **Possibly breaking change:** Nightmare will now default to using a non-persistent partition. Data between executions of Nightmare will no longer be saved.
* Adds `.mouseup()` action
* Fixes several typos/copy-paste errors in the readme, as well as clarifying error-first callbacks
* Adds `.path()` to get the URL's route (as opposed to the fully-qualified URL)
# 2.8.1 / 2016-10-20
* Fixes parsing issues with arguments to `evaluate_now`
* Upgrades to Electron 1.4.4
# 2.8.0 / 2016-10-20
* Fixes a missing semicolon in the first readme example
* Fixes a reference error inside `.wait()` when using `node --use_strict`
* Adds missing documentation for `.mouseover()`
* Corrects a typo in the readme
* Removes dependency on `object-assign`
* Adds `.halt()` API to stop Nightmare execution immediately
* Fixes `blur` exception when elements are removed by keyboard events
* **Possibly breaking change:** Changes `.evaluate()` to allow for asynchronous execution. If the `.evaluate()`d function's arity is one less than the passed in parameters, it'll assume the last argument to the function is the callback. If the return value is a thenable, it'll call `then()` to wait for promise fulfillment. Otherwise, the call will behave synchronously as it does now.
# 2.7.0 / 2016-09-05
* Adds `.wait(element, timeout)` to wait for whichever comes first
* `.end()` will now end Electron gracefully instead of issuing a `SIGKILL`
* Touches up readme for `.end()`
# 2.6.1 / 2016-08-08
* Fixes treating provisional load failures as real load failures
# 2.6.0 / 2016-08-02
* Makes the CircleCI badge an SVG
* Adds an option for `.type()` to control time elapsed between keystrokes with `typeInterval`
* Adds `.cookies.clearAll()` to clear all cookies
* Fixes crashing if the Electron process is closed first
* Adds `pollInterval` as an option to control the tick time for `.wait()`
* Forces Nightmare to error on bad HTTP authentication
* Fixes a crash by omitting event data due to circular references
* Adds environment variable forwarding to the Electron process
* Fixes `openDevTools` docs to be more explicit about detaching the devtools tray
* Fixes the link to the preload script
# 2.5.3 / 2016-07-08
* Adds better proxy information to the readme
* Fixes a readme typo
* Updates `ipcRenderer` usage for preload scripts in readme
* Bumps Electron to version 1.2.5
# 2.5.2 / 2016-06-20
* Fixes `Referer` header support
* Removes timeout between keystrokes when using `.type()`
* Checks instance existence when calling `.end()`
* Adds a link to `nightmare-examples`
* Changes `yield` to `.then()` in readme
* Swaps `did-finish-loading` for `did-stop-loading` when waiting for page transitions
* Adds optional `loadTimeout` for server responses that do not end
# 2.5.1 / 2016-06-07
* Bumps Electron dependency to 1.2.1.
* Removes a `sender` workaround
* Moves the start of Electron from the constructor into the queue
# 2.5.0 / 2016-05-27
* adds a timeout to `.goto()` such that pages that load the DOM but never finish are considered successful, otherwise failing, preventing a hang.
* updates the example script and readme file for consistency.
* reports with more helpful messages when the element does not exist when running `.click()`, `.mousedown()` and `.mouseover()`.
* `.coookies.clear()` with no arguments will clear all cookies for the current domain.
* adds Node engine information to package and ensures CircleCI builds and tests against 4.x, 5.x and 6.x.
* removes extranneous `javascript` event listeners upon execution completion or error.
* adds `.once()` and `.removeListener()` for more complete Electron process event handling.
# 2.4.1 / 2016-05-19
* Points invalid test URLs to the `.tld` domain
* Switches javascript templates over to using template strings.
* Adds better switch tests
* Javascript `goto`s now only wait if the main frame is loading
* Allows a Nightmare instance to use `.catch()` without a `.then()`
* Fixes a deprecated IPC inclusion in tests
* `.goto()` rejects with a helpful message when `url` is not provided
# 2.4.0 / 2016-05-05
* adds call safety with IPC callbacks
* adds `.engineVersions()` to get Electron and Chrome versions, as well as Nightmare.version
* changes Yahoo example to use more robust selectors, adds `.catch()`
* adds a check for `runner` arguments
# 2.3.4 / 2016-04-23
* blurs text inputs when finished with `.type()` or `.input()`, including clearing selectors
* now errors properly for non-existent selectors when using `.type()` and `.input()`
* strips `sender` from Electron -> parent process forwarded events
* improves test speed for dev tools
* fixes `.then()` to comply with A+ promises
* pipes Electron output to `debug` prefixed with `electron:`
* cleans up several exception test cases using `.should.be.rejected` from `chai-as-promised`
* upgrades to Electron 0.37.7
* removes `process` event listeners when a Nightmare instance ends
* fixes support for `javascript:` urls
# 2.3.3 / 2016-04-19
* fixes `.goto()` failing when the page does not load
* fixes deprecated Electron APIs
* adds testing for deprecated API usage in Electron
# 2.3.2 / 2016-04-14
* fixes the `.wait(selector)` comment
* adds documentation about headers
* adds an interim gitter badge
* adds a unit test for `openDevTools`
* bumps to electron 0.37.5
* adds a wrapper to run unit tests when on CircleCI, when Xvfb is running, or the `HEADLESS` environment variable is set. Prevents Nightmare from hanging when running headlessly.
* `.evaluate()` errors if a function to evaluate is not supplied
# 2.3.1 / 2016-04-11
* fixes passing uncaught exceptions back to the default handler after cleanup
* fixes overhead due to automatic subscription to frame data for screenshots
* Adds unicode documentation for `.type()`
# 2.3.0 / 2016-04-02
* extends `.action()` to include adding actions on the Electron process
* adds a debugging message to inspect how Electron exited
* ensures multiple instances of Nightmare do not use the same `waitTimeout` value
* ensures cookies are not shared across tests
* adds basic HTTP authentication
* fixes `console.log` with injected/evaluated script
* ensures screenshots match the currently rendered frame
* adds ability to open and detach dev tools
* removes the double-injection from `.inject()`
* adds ability to save entire page as HTML
# 2.2.0 / 2016-02-16
* .then() now returns a full promise instead of nightmare. update yahoo example.
# 2.1.6 / 2016-02-01
* Fix failed wait with queued up functions
* fix fullscreen switching (#434)
# 2.1.5 / 2016-02-01
* add .insert(selector[, text]).
* improve .type(selector[, text]) robustness.
* bump electron and fix API updates.
# 2.1.4 / 2016-01-28
* added debugging flags to README
* Update use of electron APIs to kill deprecation warnings for 1.0
* Implement dock option
* added default waitTimout
* page event listener fix
# 2.1.3 / 2016-01-18
* added ability to uncheck
* now properly fails with integer wait time
* Added ability to return buffer from pdf
* add ability to clear cookies
* Added a documentation for .viewport(width, height)
* Uncomment OS X dock hide
* fix setting electron paths
# 2.1.2 / 2015-12-25
* Support typing in non-strings
* Support Chrome command line switches.
* fix eventemitter leak message
* Blur focussed on click. Fixes #400
# 2.1.1 / 2015-12-21
* clears inputs on falsey/empty values
# 2.1.0 / 2015-12-17
* **BREAKING**: changed `page-error`, `page-alert`, and `page-log` to `console` with types `error`, `alert`, `log`
* **BREAKING**: fixed signature on nightmare.on('console', ...), to act more like console.log(...)
* use native electron sendInputEvent for nightmare.type(...)
* properly shutdown nightmare after certain tests and update formatting on the readme
* add events for prompt, alert, confirm, and the other console events
* update docs for preload script
* support passing in a custom preload script
* Update PDF Options
* follow new BrowserWindow option naming
* remove useless mocha opt
* implement `electronPath` option
* Fixed 'args is not defined' error for paths option
# 2.0.9 / 2015-12-09
* add Nightmare.action(name, action|namespace) and nightmare.use(plugin)
* bump dependencies
* Add header() method, and optional headers param to goto()
* "manipulation" fixture fixed to correctly test horizontal scrolling
* Viewport size changed in the 'should set viewport' test (for test passing on small screen resolution).
* prevent alerts from blocking
* Add support to wait(fn) for passing arguments from node context to browser context, just like evaluate()
* better cross-platform tests
* add mousedown event
* add nightmare.cookies.get(...) and nightmare.cookies.set(...) support
* improve screenshot documentation
* remove `.only` from buffered image test case
* return a buffered image if no path is provided
* Allow overriding Electron app paths
* Update mocha-generators so tests run
# 2.0.8 / 2015-11-24
* pointing to versioned Electron documentation
* Use "did-stop-loading" event in "continue"
* Fix menu sub-section URL in documentation
* updating yahoo mocha example so it works with yahoo's changes, fixes #275
* adding a more complete example, fixes #295
* updating atom events links, fixes #312 and #258
* set make test as the npm test target
* log and error event clean up
* Added license to package.json
* replace co-mocha w/ mocha-generators
* Allow for user-specified web-preferences options.
* Add test case for 'type' The test case of 'type and click' doesn't ensure 'type' works
* Remove old evaluate method, fix #257
# 2.0.7 / 2015-10-01
* updated and clarified docs
* fixed package.json description, thanks @tscanlin
* better error handling for ipc, thanks @davidnaas
# 2.0.6 / 2015-09-25
* changing the tests to point to github to avoid the great firewall fix #249
* Use node-integration for electron, fix scripts loading fix #242 #247
* Remove after and util in test/index.js
* adding windows debug hint
# 2.0.5 / 2015-09-20
* adding .useragent() support back, thanks @jefeweisen!
# 2.0.4 / 2015-09-20
* improving logging for screenshot, events and goto
# 2.0.3 / 2015-09-19
* improving test cleanup, thanks @fritx!
* bumping electron from 0.32.2 to 0.33.0
# 2.0.2 / 2015-09-13
* improving tests for rendering
* adding support for screenshot clip rect #107
# 2.0.1 / 2015-09-13
* updated package.json
* credits to @matthewmueller!
# 2.0.0 / 2015-06-01
* see #200 for details
* added generator love
* switched to electron to speed things up
* many many thanks to @matthewmueller!
# 1.8.1 / 2015-04-27
* Fix escaping of selectors in .wait(selector) thanks @thotypous
* Updated Mocha link thanks @mortonfox
# 1.8.0 / 2015-03-23
* handling phantom crashes more gracefully
* fixing tests by using a local server and static fixtures
* feat(docs): add google-oauth2 plugin
* fixing links
* clearer ToC and clearer evaluate docs from #89
# 1.7.0 / 2015-01-26
* adding pdf ignore, fixing test timeout
* adding new resourceRequestStarted event for executing in phantomjs context
* Add scrollTo feature. Resolves #130.
* Adds zoom feature. Resolves #136.
* added error handling for requiring file extension in screenshot
* added documentation for supported filetypes for .screenshot
* add json parsing guard to test
* adding link to tests for more examples
* updating readme with clearer function lists and sections, and mocha test example
* add readme for headers()
* add tests for headers()
* add headers method
* upping timeouts
* Add ability to save an A4 sized PDF
* add check and select
# 1.6.5 / 2014-11-11
* updating tests and fixing global port issue
* Adding sequential test case
* adding multiple clicks across multiple pages test
# 1.6.4 / 2014-11-10
* fixing non-existent elem issue in .visible(), fixes #108
# 1.6.3 / 2014-11-09
* bumping circleci test version and timeout
* eliminating global phantom instance and state, fixes #104
# 1.6.2 / 2014-11-09
* .type() now uses uses phantom's sendEvent to trigger keypress events. Fixes #81. (by @johntitus)
# 1.6.1 / 2014-11-09
* bumping phantom to ~0.7.0, fixes #101
* readme tweaks
* adding resourceError event to docs
# 1.6.0 / 2014-11-02
* adding timeout handling (by @johntitus)
* cleaning up styles in tests, adding tests for timeout event
# 1.5.3 / 2014-11-02
* Add ability to specify a custom PhantomJS path (by @kevva)
# 1.5.2 / 2014-11-02
* updating readme to explain .on() before .goto()
* fixing callbacks for .wait()
* adding grep to makefile tests
* adding check for file existence before file upload, fixes #11
# 1.5.1 / 2014-10-26
* making clicks cancelable to allow for ajax forms
# 1.5.0 / 2014-10-22
* adding docs and support for ssl, proxy and other cli args
# 1.4.0 / 2014-10-22
* added .exists() (by @johntitus)
* Added .visible(selector) (by @johntitus)
* Added .authentication(user,password) (by @johntitus)
# 1.3.3 / 2014-10-20
* fix for 'Option to run phantom without weak' (by @securingsincity)
# 1.3.2 / 2014-10-15
* clarifying a readme example, see #55
# 1.3.1 / 2014-10-15
* expanding the readme (by @johntitus)
# 1.3.0 / 2014-10-15
* adding a on() action to handle phantom page events (by @johntitus)
# 1.2.0 / 2014-10-15
* adding .forward() method with test (by @stevenmiller888)
* adding .inject() action, test, and updated readme (by @johntitus)
# 1.1.1 / 2014-10-08
* adding wait(selector) test and clojure fix, fixes #39
* adding extraction readme example
* adding caveat to viewport docs, fixes #33
* updating readme example
* Remove OSX .DS_Store file
# 1.1.0 / 2014-10-05
* changing run structure to auto-terminate phantomjs instances
* naming goBack to back
# 1.0.5 / 2014-09-30
* added .goBack()
# 1.0.4 / 2014-05-12
* contain zalgo
# 1.0.3 / 2014-05-12
* cleaning up run based on ians feedback
# 1.0.2 / 2014-05-12
* fixing concat in place
* cleaning up naming, whitespace, structure.. thanks @ianstormtaylor!
* fixing readme and history
# 1.0.1 / 2014-05-10
* fixing queueing and .use() call order
* Merge pull request #15 from queckezz/fix/use-queueing
* fixing tests
* fixing history
* queue .use(). Closes #10
# 1.0.0 / 2014-05-10
* renaming methods, fixes #18 and #19
* Merge pull request #17 from queckezz/update/phantomjs-node
* Merge pull request #16 from stevenschobert/master
* update phantomjs-node for 0.11.x support
* add instance option for phantomjs port
# 0.1.7 / 2014-04-14
* Merge pull request #14 from queckezz/update/allow-no-args
* allow no args and fix debug for .evaluate()
* fixing history
# 0.1.6 / 2014-04-13
* adding .url(), more debug()s and a test for .url()
* fxiing histoyr
# 0.1.5 / 2014-04-12
* fixing impatient to only apply to upload since it breaks wait
* fixing history
# 0.1.4 / 2014-04-12
* making callbacks impatient based on timeouts
* fixing history
# 0.1.3 / 2014-04-12
* fixing upload not having a callback
* fixing history
# 0.1.2 / 2014-04-11
* clarifying readme
* adding refresh method and wait for fn on page refresh
* reworking wait function to make room for a new wait overload
* refactoring tests into sections
* fixing history
# 0.1.1 / 2014-04-08
* adding test to duplicate queue ordering issue, fixing issue, fixes #9
* adding nightmare-swiftly plugin mention with docs
* fixing history
# 0.1.0 / 2014-04-07
* adding .use() to docs
* Merge pull request #8 from segmentio/use-rewrite
* adding test for .use() pluggability
* changes .run() to .evaluate(), removes .error() and cleans up internal wrapping
* fixing history
# 0.0.13 / 2014-04-07
* Merge pull request #6 from segmentio/phantomjs-node
* fixing done callback, fixing agent setting and adding tests. fixes #4, #2, #3.
* fixing run callback hanging, fixes #3
* experimenting with phantomjs-node, for #5
* Merge branch 'master' of https://github.com/segmentio/nightmare
* Update Readme.md
# 0.0.12 / 2014-04-06
* adding .viewport() and .agent(), fixes #2
# 0.0.11 / 2014-04-06
* making debug output consistent
* consistent naming
* fixing .wait() readme docs
* fixing history
# 0.0.10 / 2014-04-06
* adding .run() method with docs and test. fixes #1
* Update Readme.md
* fixing history
# 0.0.9 / 2014-04-05
* adding more debug statements
* fixing history
# 0.0.8 / 2014-04-05
* updating readme for screen and options
* fixing timeout and adding debug for .screen() method
* fixing history
# 0.0.7 / 2014-04-05
* setting viewport
* fixing history
# 0.0.6 / 2014-04-05
* adding better debug logs for page load detection
* fixing history
# 0.0.5 / 2014-04-05
* fixing history
# 0.0.4 / 2014-04-05
* fixing main for require to work
* fixing history
# 0.0.3 / 2014-04-05
* fixing tests and getting screen working
* fixing history again
# 0.0.2 / 2014-04-05
* pkilling phantomjs more aggressively
* fixing phantom singletons
* fixing history.md
# 0.0.1 / 2014-04-05
* updating readme
* removing unneded circleci stuff
* adding circle badge to readme
* adding circle.yml
* adding tests with lots of fixes everywhere
* filling in remaining parts of api
* filling in wait function
* filling in lots of the first draft
* adding new done method
* blocks sync
* mvoing
* all before proceding
* copyright
* copy
* adding more wait options
* adding in scaffolding and readme outline
================================================
FILE: Makefile
================================================
GREP ?=.
#check if xvfb is running
XVFB_RUNNING = $(shell pgrep "Xvfb" > /dev/null; echo $$?)
#set the circle project name if not set
CIRCLE_PROJECT_REPONAME ?= 1
#set headless if not set
HEADLESS ?= 1
test: node_modules lint
@rm -rf /tmp/nightmare
#if this build is not on circle, is not headless, and xvfb is not already running,
#run mocha as usual
#otherwise, run mocha under the xvfb wrapper
ifeq ($(CIRCLE_PROJECT_REPONAME)$(HEADLESS)$(XVFB_RUNNING), 111)
@node_modules/.bin/mocha --grep "$(GREP)"
else
@./test/bb-xvfb node_modules/.bin/mocha --grep "$(GREP)"
endif
lint: node_modules
@./node_modules/.bin/eslint --fix lib/*.js test/*.js
node_modules: package.json
@npm install
.PHONY: test lint
================================================
FILE: Readme.md
================================================
*NOTICE: This library is no longer maintained.*
[](https://circleci.com/gh/segmentio/nightmare)
[](https://gitter.im/rosshinkley/nightmare?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
# Nightmare
Nightmare is a high-level browser automation library from [Segment](https://segment.com).
The goal is to expose a few simple methods that mimic user actions (like `goto`, `type` and `click`), with an API that feels synchronous for each block of scripting, rather than deeply nested callbacks. It was originally designed for automating tasks across sites that don't have APIs, but is most often used for UI testing and crawling.
Under the covers it uses [Electron](http://electron.atom.io/), which is similar to [PhantomJS](http://phantomjs.org/) but roughly [twice as fast](https://github.com/segmentio/nightmare/issues/484#issuecomment-184519591) and more modern.
**⚠️ Security Warning:** We've implemented [many](https://github.com/segmentio/nightmare/issues/1388) of the security recommendations [outlined by Electron](https://github.com/electron/electron/blob/master/docs/tutorial/security.md) to try and keep you safe, but undiscovered vulnerabilities may exist in Electron that could allow a malicious website to execute code on your computer. Avoid visiting untrusted websites.
**🛠 Migrating to 3.x:** You'll want to check out [this issue](https://github.com/segmentio/nightmare/issues/1396) before upgrading. We've worked hard to make improvements to nightmare while limiting the breaking changes and there's a good chance you won't need to do anything.
[Niffy](https://github.com/segmentio/niffy) is a perceptual diffing tool built on Nightmare. It helps you detect UI changes and bugs across releases of your web app.
[Daydream](https://github.com/segmentio/daydream) is a complementary chrome extension built by [@stevenmiller888](https://github.com/stevenmiller888) that generates Nightmare scripts for you while you browse.
Many thanks to [@matthewmueller](https://github.com/matthewmueller) and [@rosshinkley](https://github.com/rosshinkley) for their help on Nightmare.
* [Examples](#examples)
* [UI Testing Quick Start](https://segment.com/blog/ui-testing-with-nightmare/)
* [Perceptual Diffing with Niffy & Nightmare](https://segment.com/blog/perceptual-diffing-with-niffy/)
* [API](#api)
* [Set up an instance](#nightmareoptions)
* [Interact with the page](#interact-with-the-page)
* [Extract from the page](#extract-from-the-page)
* [Cookies](#cookies)
* [Proxies](#proxies)
* [Promises](#promises)
* [Extending Nightmare](#extending-nightmare)
* [Usage](#usage)
* [Debugging](#debugging)
* [Additional Resources](#additional-resources)
## Examples
Let's search on DuckDuckGo:
```js
const Nightmare = require('nightmare')
const nightmare = Nightmare({ show: true })
nightmare
.goto('https://duckduckgo.com')
.type('#search_form_input_homepage', 'github nightmare')
.click('#search_button_homepage')
.wait('#r1-0 a.result__a')
.evaluate(() => document.querySelector('#r1-0 a.result__a').href)
.end()
.then(console.log)
.catch(error => {
console.error('Search failed:', error)
})
```
You can run this with:
```shell
npm install --save nightmare
node example.js
```
Or, let's run some mocha tests:
```js
const Nightmare = require('nightmare')
const chai = require('chai')
const expect = chai.expect
describe('test duckduckgo search results', () => {
it('should find the nightmare github link first', function(done) {
this.timeout('10s')
const nightmare = Nightmare()
nightmare
.goto('https://duckduckgo.com')
.type('#search_form_input_homepage', 'github nightmare')
.click('#search_button_homepage')
.wait('#links .result__a')
.evaluate(() => document.querySelector('#links .result__a').href)
.end()
.then(link => {
expect(link).to.equal('https://github.com/segmentio/nightmare')
done()
})
})
})
```
You can see examples of every function [in the tests here](https://github.com/segmentio/nightmare/blob/master/test/index.js).
To get started with UI Testing, check out this [quick start guide](https://segment.com/blog/ui-testing-with-nightmare).
### To install dependencies
```
npm install
```
### To run the mocha tests
```
npm test
```
### Node versions
Nightmare is intended to be run on NodeJS 4.x or higher.
## API
#### Nightmare(options)
Creates a new instance that can navigate around the web. The available options are [documented here](https://github.com/atom/electron/blob/master/docs/api/browser-window.md#new-browserwindowoptions), along with the following nightmare-specific options.
##### waitTimeout (default: 30s)
Throws an exception if the `.wait()` didn't return `true` within the set timeframe.
```js
const nightmare = Nightmare({
waitTimeout: 1000 // in ms
})
```
##### gotoTimeout (default: 30s)
Throws an exception if the `.goto()` didn't finish loading within the set timeframe. Note that, even though `goto` normally waits for all the resources on a page to load, a timeout exception is only raised if the DOM itself has not yet loaded.
```js
const nightmare = Nightmare({
gotoTimeout: 1000 // in ms
})
```
##### loadTimeout (default: infinite)
Forces Nightmare to move on if a page transition caused by an action (eg, `.click()`) didn't finish within the set timeframe. If `loadTimeout` is shorter than `gotoTimeout`, the exceptions thrown by `gotoTimeout` will be suppressed.
```js
const nightmare = Nightmare({
loadTimeout: 1000 // in ms
})
```
##### executionTimeout (default: 30s)
The maximum amount of time to wait for an `.evaluate()` statement to complete.
```js
const nightmare = Nightmare({
executionTimeout: 1000 // in ms
})
```
##### paths
The default system paths that Electron knows about. Here's a list of available paths: https://github.com/atom/electron/blob/master/docs/api/app.md#appgetpathname
You can overwrite them in Nightmare by doing the following:
```js
const nightmare = Nightmare({
paths: {
userData: '/user/data'
}
})
```
##### switches
The command line switches used by the Chrome browser that are also supported by Electron. Here's a list of supported Chrome command line switches:
https://github.com/atom/electron/blob/master/docs/api/chrome-command-line-switches.md
```js
const nightmare = Nightmare({
switches: {
'proxy-server': '1.2.3.4:5678',
'ignore-certificate-errors': true
}
})
```
##### electronPath
The path to the prebuilt Electron binary. This is useful for testing on different versions of Electron. Note that Nightmare only supports the version on which this package depends. Use this option at your own risk.
```js
const nightmare = Nightmare({
electronPath: require('electron')
})
```
##### dock (OS X)
A boolean to optionally show the Electron icon in the dock (defaults to `false`). This is useful for testing purposes.
```js
const nightmare = Nightmare({
dock: true
})
```
##### openDevTools
Optionally shows the DevTools in the Electron window using `true`, or use an object hash containing `mode: 'detach'` to show in a separate window. The hash gets passed to [`contents.openDevTools()`](https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentsopendevtoolsoptions) to be handled. This is also useful for testing purposes. Note that this option is honored only if `show` is set to `true`.
```js
const nightmare = Nightmare({
openDevTools: {
mode: 'detach'
},
show: true
})
```
##### typeInterval (default: 100ms)
How long to wait between keystrokes when using `.type()`.
```js
const nightmare = Nightmare({
typeInterval: 20
})
```
##### pollInterval (default: 250ms)
How long to wait between checks for the `.wait()` condition to be successful.
```js
const nightmare = Nightmare({
pollInterval: 50 //in ms
})
```
##### maxAuthRetries (default: 3)
Defines the number of times to retry an authentication when set up with `.authenticate()`.
```js
const nightmare = Nightmare({
maxAuthRetries: 3
})
```
#### certificateSubjectName
A string to determine the client certificate selected by electron. If this options is set, the [`select-client-certificate`](https://github.com/electron/electron/blob/master/docs/api/app.md#event-select-client-certificate) event will be set to loop through the certificateList and find the first certificate that matches `subjectName` on the electron [`Certificate Object`](https://electronjs.org/docs/api/structures/certificate).
```js
const nightmare = Nightmare({
certificateSubjectName: 'tester'
})
```
#### .engineVersions()
Gets the versions for Electron and Chromium.
#### .useragent(useragent)
Sets the `useragent` used by electron.
#### .authentication(user, password)
Sets the `user` and `password` for accessing a web page using basic authentication. Be sure to set it before calling `.goto(url)`.
#### .end()
Completes any queue operations, disconnect and close the electron process. Note that if you're using promises, `.then()` must be called after `.end()` to run the `.end()` task. Also note that if using an `.end()` callback, the `.end()` call is equivalent to calling `.end()` followed by `.then(fn)`. Consider:
```js
nightmare
.goto(someUrl)
.end(() => 'some value')
//prints "some value"
.then(console.log)
```
#### .halt(error, done)
Clears all queued operations, kills the electron process, and passes error message or 'Nightmare Halted' to an unresolved promise. Done will be called after the process has exited.
### Interact with the Page
#### .goto(url[, headers])
Loads the page at `url`. Optionally, a `headers` hash can be supplied to set headers on the `goto` request.
When a page load is successful, `goto` returns an object with metadata about the page load, including:
* `url`: The URL that was loaded
* `code`: The HTTP status code (e.g. 200, 404, 500)
* `method`: The HTTP method used (e.g. "GET", "POST")
* `referrer`: The page that the window was displaying prior to this load or an empty string if this is the first page load.
* `headers`: An object representing the response headers for the request as in `{header1-name: header1-value, header2-name: header2-value}`
If the page load fails, the error will be an object with the following properties:
* `message`: A string describing the type of error
* `code`: The underlying error code describing what went wrong. Note this is NOT the HTTP status code. For possible values, see https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
* `details`: A string with additional details about the error. This may be null or an empty string.
* `url`: The URL that failed to load
Note that any valid response from a server is considered “successful.” That means things like 404 “not found” errors are successful results for `goto`. Only things that would cause no page to appear in the browser window, such as no server responding at the given address, the server hanging up in the middle of a response, or invalid URLs, are errors.
You can also adjust how long `goto` will wait before timing out by setting the [`gotoTimeout` option](#gototimeout-default-30s) on the Nightmare constructor.
#### .back()
Goes back to the previous page.
#### .forward()
Goes forward to the next page.
#### .refresh()
Refreshes the current page.
#### .click(selector)
Clicks the `selector` element once.
#### .mousedown(selector)
Mousedowns the `selector` element once.
#### .mouseup(selector)
Mouseups the `selector` element once.
#### .mouseover(selector)
Mouseovers the `selector` element once.
#### .mouseout(selector)
Mouseout the `selector` element once.
#### .type(selector[, text])
Enters the `text` provided into the `selector` element. Empty or falsey values provided for `text` will clear the selector's value.
`.type()` mimics a user typing in a textbox and will emit the proper keyboard events.
Key presses can also be fired using Unicode values with `.type()`. For example, if you wanted to fire an enter key press, you would write `.type('body', '\u000d')`.
> If you don't need the keyboard events, consider using `.insert()` instead as it will be faster and more robust.
#### .insert(selector[, text])
Similar to `.type()`, `.insert()` enters the `text` provided into the `selector` element. Empty or falsey values provided for `text` will clear the selector's value.
`.insert()` is faster than `.type()` but does not trigger the keyboard events.
#### .check(selector)
Checks the `selector` checkbox element.
#### .uncheck(selector)
Unchecks the `selector` checkbox element.
#### .select(selector, option)
Changes the `selector` dropdown element to the option with attribute [value=`option`]
#### .scrollTo(top, left)
Scrolls the page to desired position. `top` and `left` are always relative to the top left corner of the document.
#### .viewport(width, height)
Sets the viewport size.
#### .inject(type, file)
Injects a local `file` onto the current page. The file `type` must be either `js` or `css`.
#### .evaluate(fn[, arg1, arg2,...])
Invokes `fn` on the page with `arg1, arg2,...`. All the `args` are optional. On completion it returns the return value of `fn`. Useful for extracting information from the page. Here's an example:
```js
const selector = 'h1'
nightmare
.evaluate(selector => {
// now we're executing inside the browser scope.
return document.querySelector(selector).innerText
}, selector) // <-- that's how you pass parameters from Node scope to browser scope
.then(text => {
// ...
})
```
Error-first callbacks are supported as a part of `evaluate()`. If the arguments passed are one fewer than the arguments expected for the evaluated function, the evaluation will be passed a callback as the last parameter to the function. For example:
```js
const selector = 'h1'
nightmare
.evaluate((selector, done) => {
// now we're executing inside the browser scope.
setTimeout(
() => done(null, document.querySelector(selector).innerText),
2000
)
}, selector)
.then(text => {
// ...
})
```
Note that callbacks support only one value argument (eg `function(err, value)`). Ultimately, the callback will get wrapped in a native Promise and only be able to resolve a single value.
Promises are also supported as a part of `evaluate()`. If the return value of the function has a `then` member, `.evaluate()` assumes it is waiting for a promise. For example:
```js
const selector = 'h1';
nightmare
.evaluate((selector) => (
new Promise((resolve, reject) => {
setTimeout(() => resolve(document.querySelector(selector).innerText), 2000);
)}, selector)
)
.then((text) => {
// ...
})
```
#### .wait(ms)
Waits for `ms` milliseconds e.g. `.wait(5000)`.
#### .wait(selector)
Waits until the element `selector` is present e.g. `.wait('#pay-button')`.
#### .wait(fn[, arg1, arg2,...])
Waits until the `fn` evaluated on the page with `arg1, arg2,...` returns `true`. All the `args` are optional. See `.evaluate()` for usage.
#### .header(header, value)
Adds a header override for all HTTP requests. If `header` is undefined, the header overrides will be reset.
### Extract from the Page
#### .exists(selector)
Returns whether the selector exists or not on the page.
#### .visible(selector)
Returns whether the selector is visible or not.
#### .on(event, callback)
Captures page events with the callback. You have to call `.on()` before calling `.goto()`. Supported events are [documented here](http://electron.atom.io/docs/api/web-contents/#class-webcontents).
##### Additional "page" events
###### .on('page', function(type="error", message, stack))
This event is triggered if any javascript exception is thrown on the page. But this event is not triggered if the injected javascript code (e.g. via `.evaluate()`) is throwing an exception.
##### "page" events
Listens for `window.addEventListener('error')`, `alert(...)`, `prompt(...)` & `confirm(...)`.
###### .on('page', function(type="error", message, stack))
Listens for top-level page errors. This will get triggered when an error is thrown on the page.
###### .on('page', function(type="alert", message))
Nightmare disables `window.alert` from popping up by default, but you can still listen for the contents of the alert dialog.
###### .on('page', function(type="prompt", message, response))
Nightmare disables `window.prompt` from popping up by default, but you can still listen for the message to come up. If you need to handle the confirmation differently, you'll need to use your own preload script.
###### .on('page', function(type="confirm", message, response))
Nightmare disables `window.confirm` from popping up by default, but you can still listen for the message to come up. If you need to handle the confirmation differently, you'll need to use your own preload script.
###### .on('console', function(type [, arguments, ...]))
`type` will be either `log`, `warn` or `error` and `arguments` are what gets passed from the console. This event is not triggered if the injected javascript code (e.g. via `.evaluate()`) is using `console.log`.
#### .once(event, callback)
Similar to `.on()`, but captures page events with the callback one time.
#### .removeListener(event, callback)
Removes a given listener callback for an event.
#### .screenshot([path][, clip])
Takes a screenshot of the current page. Useful for debugging. The output is always a `png`. Both arguments are optional. If `path` is provided, it saves the image to the disk. Otherwise it returns a `Buffer` of the image data. If `clip` is provided (as [documented here](https://github.com/atom/electron/blob/master/docs/api/browser-window.md#wincapturepagerect-callback)), the image will be clipped to the rectangle.
#### .html(path, saveType)
Saves the current page as html as files to disk at the given path. Save type options are [here](https://github.com/atom/electron/blob/master/docs/api/web-contents.md#webcontentssavepagefullpath-savetype-callback).
#### .pdf(path, options)
Saves a PDF to the specified `path`. Options are [here](https://github.com/electron/electron/blob/v1.4.4/docs/api/web-contents.md#contentsprinttopdfoptions-callback).
#### .title()
Returns the title of the current page.
#### .url()
Returns the url of the current page.
#### .path()
Returns the path name of the current page.
### Cookies
#### .cookies.get(name)
Gets a cookie by it's `name`. The url will be the current url.
#### .cookies.get(query)
Queries multiple cookies with the `query` object. If a `query.name` is set, it will return the first cookie it finds with that name, otherwise it will query for an array of cookies. If no `query.url` is set, it will use the current url. Here's an example:
```js
// get all google cookies that are secure
// and have the path `/query`
nightmare
.goto('http://google.com')
.cookies.get({
path: '/query',
secure: true
})
.then(cookies => {
// do something with the cookies
})
```
Available properties are documented here: https://github.com/atom/electron/blob/master/docs/api/session.md#sescookiesgetdetails-callback
#### .cookies.get()
Gets all the cookies for the current url. If you'd like get all cookies for all urls, use: `.get({ url: null })`.
#### .cookies.set(name, value)
Sets a cookie's `name` and `value`. This is the most basic form, and the url will be the current url.
#### .cookies.set(cookie)
Sets a `cookie`. If `cookie.url` is not set, it will set the cookie on the current url. Here's an example:
```js
nightmare
.goto('http://google.com')
.cookies.set({
name: 'token',
value: 'some token',
path: '/query',
secure: true
})
// ... other actions ...
.then(() => {
// ...
})
```
Available properties are documented here: https://github.com/atom/electron/blob/master/docs/api/session.md#sescookiessetdetails-callback
#### .cookies.set(cookies)
Sets multiple cookies at once. `cookies` is an array of `cookie` objects. Take a look at the `.cookies.set(cookie)` documentation above for a better idea of what `cookie` should look like.
#### .cookies.clear([name])
Clears a cookie for the current domain. If `name` is not specified, all cookies for the current domain will be cleared.
```js
nightmare
.goto('http://google.com')
.cookies.clear('SomeCookieName')
// ... other actions ...
.then(() => {
// ...
})
```
#### .cookies.clearAll()
Clears all cookies for all domains.
```js
nightmare
.goto('http://google.com')
.cookies.clearAll()
// ... other actions ...
.then(() => {
//...
})
```
### Proxies
Proxies are supported in Nightmare through [switches](#switches).
If your proxy requires authentication you also need the [authentication](#authenticationuser-password) call.
The following example not only demonstrates how to use proxies, but you can run it to test if your proxy connection is working:
```js
import Nightmare from 'nightmare';
const proxyNightmare = Nightmare({
switches: {
'proxy-server': 'my_proxy_server.example.com:8080' // set the proxy server here ...
},
show: true
});
proxyNightmare
.authentication('proxyUsername', 'proxyPassword') // ... and authenticate here before `goto`
.goto('http://www.ipchicken.com')
.evaluate(() => {
return document.querySelector('b').innerText.replace(/[^\d\.]/g, '');
})
.end()
.then((ip) => { // This will log the Proxy's IP
console.log('proxy IP:', ip);
});
// The rest is just normal Nightmare to get your local IP
const regularNightmare = Nightmare({ show: true });
regularNightmare
.goto('http://www.ipchicken.com')
.evaluate(() =>
document.querySelector('b').innerText.replace(/[^\d\.]/g, '');
)
.end()
.then((ip) => { // This will log the your local IP
console.log('local IP:', ip);
});
```
### Promises
By default, Nightmare uses default native ES6 promises. You can plug in your favorite [ES6-style promises library](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) like [bluebird](https://www.npmjs.com/package/bluebird) or [q](https://www.npmjs.com/package/q) for convenience!
Here's an example:
```js
var Nightmare = require('nightmare')
Nightmare.Promise = require('bluebird')
// OR:
Nightmare.Promise = require('q').Promise
```
You can also specify a custom Promise library per-instance with the `Promise` constructor option like so:
```js
var Nightmare = require('nightmare')
var es6Nightmare = Nightmare()
var bluebirdNightmare = Nightmare({
Promise: require('bluebird')
})
var es6Promise = es6Nightmare
.goto('https://github.com/segmentio/nightmare')
.then()
var bluebirdPromise = bluebirdNightmare
.goto('https://github.com/segmentio/nightmare')
.then()
es6Promise.isFulfilled() // throws: `TypeError: es6EndPromise.isFulfilled is not a function`
bluebirdPromise.isFulfilled() // returns: `true | false`
```
### Extending Nightmare
#### Nightmare.action(name, [electronAction|electronNamespace], action|namespace)
You can add your own custom actions to the Nightmare prototype. Here's an example:
```js
Nightmare.action('size', function(done) {
this.evaluate_now(() => {
const w = Math.max(
document.documentElement.clientWidth,
window.innerWidth || 0
)
const h = Math.max(
document.documentElement.clientHeight,
window.innerHeight || 0
)
return {
height: h,
width: w
}
}, done)
})
Nightmare()
.goto('http://cnn.com')
.size()
.then(size => {
//... do something with the size information
})
```
> Remember, this is attached to the static class `Nightmare`, not the instance.
You'll notice we used an internal function `evaluate_now`. This function is different than `nightmare.evaluate` because it runs it immediately, whereas `nightmare.evaluate` is queued.
An easy way to remember: when in doubt, use `evaluate`. If you're creating custom actions, use `evaluate_now`. The technical reason is that since our action has already been queued and we're running it now, we shouldn't re-queue the evaluate function.
We can also create custom namespaces. We do this internally for `nightmare.cookies.get` and `nightmare.cookies.set`. These are useful if you have a bundle of actions you want to expose, but it will clutter up the main nightmare object. Here's an example of that:
```js
Nightmare.action('style', {
background(done) {
this.evaluate_now(
() => window.getComputedStyle(document.body, null).backgroundColor,
done
)
}
})
Nightmare()
.goto('http://google.com')
.style.background()
.then(background => {
// ... do something interesting with background
})
```
You can also add custom Electron actions. The additional Electron action or namespace actions take `name`, `options`, `parent`, `win`, `renderer`, and `done`. Note the Electron action comes first, mirroring how `.evaluate()` works. For example:
```javascript
Nightmare.action(
'clearCache',
(name, options, parent, win, renderer, done) => {
parent.respondTo('clearCache', done => {
win.webContents.session.clearCache(done)
})
done()
},
function(done) {
this.child.call('clearCache', done)
}
)
Nightmare()
.clearCache()
.goto('http://example.org')
//... more actions ...
.then(() => {
// ...
})
```
...would clear the browser’s cache before navigating to `example.org`.
See [this document](https://github.com/rosshinkley/nightmare-examples/blob/master/docs/beginner/action.md) for more details on creating custom actions.
#### .use(plugin)
`nightmare.use` is useful for reusing a set of tasks on an instance. Check out [nightmare-swiftly](https://github.com/segmentio/nightmare-swiftly) for some examples.
#### Custom preload script
If you need to do something custom when you first load the window environment, you
can specify a custom preload script. Here's how you do that:
```js
import path from 'path'
const nightmare = Nightmare({
webPreferences: {
preload: path.resolve('custom-script.js')
//alternative: preload: "absolute/path/to/custom-script.js"
}
})
```
The only requirement for that script is that you'll need the following prelude:
```js
window.__nightmare = {}
__nightmare.ipc = require('electron').ipcRenderer
```
To benefit of all of nightmare's feedback from the browser, you can instead copy the contents of nightmare's [preload script](lib/preload.js).
#### Storage Persistence between nightmare instances
By default nightmare will create an in-memory partition for each instance. This means that any localStorage or cookies or any other form of persistent state will be destroyed when nightmare is ended. If you would like to persist state between instances you can use the [webPreferences.partition](http://electron.atom.io/docs/api/browser-window/#new-browserwindowoptions) api in electron.
```js
import Nightmare from 'nightmare';
nightmare = Nightmare(); // non persistent paritition by default
yield nightmare
.evaluate(() => {
window.localStorage.setItem('testing', 'This will not be persisted');
})
.end();
nightmare = Nightmare({
webPreferences: {
partition: 'persist: testing'
}
});
yield nightmare
.evaluate(() => {
window.localStorage.setItem('testing', 'This is persisted for other instances with the same paritition name');
})
.end();
```
If you specify a `null` paritition then it will use the electron default behavior (persistent) or any string that starts with `'persist:'` will persist under that partition name, any other string will result in in-memory only storage.
## Usage
#### Installation
Nightmare is a Node.js module, so you'll need to [have Node.js installed](http://nodejs.org/). Then you just need to `npm install` the module:
```bash
$ npm install --save nightmare
```
#### Execution
Nightmare is a node module that can be used in a Node.js script or module. Here's a simple script to open a web page:
```js
import Nightmare from 'nightmare';
const nightmare = Nightmare();
nightmare.goto('http://cnn.com')
.evaluate(() => {
return document.title;
})
.end()
.then((title) => {
console.log(title);
})
```
If you save this as `cnn.js`, you can run it on the command line like this:
```bash
npm install --save nightmare
node cnn.js
```
#### Common Execution Problems
Nightmare heavily relies on [Electron](http://electron.atom.io/) for heavy lifting. And Electron in turn relies on several UI-focused dependencies (eg. libgtk+) which are often missing from server distros.
For help running nightmare on your server distro check out [How to run nightmare on Amazon Linux and CentOS](https://gist.github.com/dimkir/f4afde77366ff041b66d2252b45a13db) guide.
#### Debugging
There are three good ways to get more information about what's happening inside the headless browser:
1. Use the `DEBUG=*` flag described below.
2. Pass `{ show: true }` to the [nightmare constructor](#nightmareoptions) to have it create a visible, rendered window where you can watch what is happening.
3. Listen for [specific events](#onevent-callback).
To run the same file with debugging output, run it like this `DEBUG=nightmare node cnn.js` (on Windows use `set DEBUG=nightmare & node cnn.js`).
This will print out some additional information about what's going on:
```bash
nightmare queueing action "goto" +0ms
nightmare queueing action "evaluate" +4ms
Breaking News, U.S., World, Weather, Entertainment & Video News - CNN.com
```
##### Debug Flags
All nightmare messages
`DEBUG=nightmare*`
Only actions
`DEBUG=nightmare:actions*`
Only logs
`DEBUG=nightmare:log*`
## Additional Resources
* [Ross Hinkley's Nightmare Examples](https://github.com/rosshinkley/nightmare-examples) is a great resource for setting up nightmare, learning about custom actions, and avoiding common pitfalls.
* [Nightmare Issues](https://github.com/matthewmueller/nightmare-issues) has a bunch of standalone runnable examples. The script numbers correspond to nightmare issue numbers.
* [Nightmarishly good scraping](https://hackernoon.com/nightmarishly-good-scraping-with-nightmare-js-and-async-await-b7b20a38438f) is a great tutorial by [Ændrew Rininsland](https://twitter.com/@aendrew) on getting up & running with Nightmare using real-life data.
## Tests
Automated tests for nightmare itself are run using [Mocha](http://mochajs.org/) and Chai, both of which will be installed via `npm install`. To run nightmare's tests, just run `make test`.
When the tests are done, you'll see something like this:
```bash
make test
․․․․․․․․․․․․․․․․․․
18 passing (1m)
```
Note that if you are using `xvfb`, `make test` will automatically run the tests under an `xvfb-run` wrapper. If you are planning to run the tests headlessly without running `xvfb` first, set the `HEADLESS` environment variable to `0`.
## License (MIT)
```
WWWWWW||WWWWWW
W W W||W W W
||
( OO )__________
/ | \
/o o| MIT \
\___/||_||__||_|| *
|| || || ||
_||_|| _||_||
(__|__|(__|__|
```
Copyright (c) 2015 Segment.io, Inc. <mailto:friends@segment.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: example.js
================================================
/* eslint-disable no-console */
var Nightmare = require('nightmare')
var nightmare = Nightmare({ show: true })
nightmare
.goto('http://yahoo.com')
.type('form[action*="/search"] [name=p]', 'github nightmare')
.click('form[action*="/search"] [type=submit]')
.wait('#main')
.evaluate(function() {
return document.querySelector('#main .searchCenterMiddle li a').href
})
.end()
.then(function(result) {
console.log(result)
})
.catch(function(error) {
console.error('Search failed:', error)
})
================================================
FILE: lib/actions.js
================================================
/**
* Module Dependencies
*/
var debug = require('debug')('nightmare:actions')
var sliced = require('sliced')
var jsesc = require('jsesc')
var fs = require('fs')
/**
* Get the version info for Nightmare, Electron and Chromium.
* @param {Function} done
*/
exports.engineVersions = function(done) {
debug('.engineVersions()')
done(null, this.engineVersions)
}
/**
* Get the title of the page.
*
* @param {Function} done
*/
exports.title = function(done) {
debug('.title() getting it')
this.evaluate_now(function() {
return document.title
}, done)
}
/**
* Get the url of the page.
*
* @param {Function} done
*/
exports.url = function(done) {
debug('.url() getting it')
this.evaluate_now(function() {
return document.location.href
}, done)
}
/**
* Get the path of the page.
*
* @param {Function} done
*/
exports.path = function(done) {
debug('.path() getting it')
this.evaluate_now(function() {
return document.location.pathname
}, done)
}
/**
* Determine if a selector is visible on a page.
*
* @param {String} selector
* @param {Function} done
*/
exports.visible = function(selector, done) {
debug('.visible() for ' + selector)
this.evaluate_now(
function(selector) {
var elem = document.querySelector(selector)
if (elem) return elem.offsetWidth > 0 && elem.offsetHeight > 0
else return false
},
done,
selector
)
}
/**
* Determine if a selector exists on a page.
*
* @param {String} selector
* @param {Function} done
*/
exports.exists = function(selector, done) {
debug('.exists() for ' + selector)
this.evaluate_now(
function(selector) {
return document.querySelector(selector) !== null
},
done,
selector
)
}
/**
* Click an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.click = function(selector, done) {
debug('.click() on ' + selector)
this.evaluate_now(
function(selector) {
document.activeElement.blur()
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('click', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Mousedown on an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mousedown = function(selector, done) {
debug('.mousedown() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('mousedown', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Mouseup on an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mouseup = function(selector, done) {
debug('.mouseup() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('mouseup', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Hover over an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mouseover = function(selector, done) {
debug('.mouseover() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var bounding = element.getBoundingClientRect()
var event = new MouseEvent('mouseover', {
view: document.window,
bubbles: true,
cancelable: true,
clientX: bounding.left + bounding.width / 2,
clientY: bounding.top + bounding.height / 2
})
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Release hover from an element.
*
* @param {String} selector
* @param {Function} done
*/
exports.mouseout = function(selector, done) {
debug('.mouseout() on ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
if (!element) {
throw new Error('Unable to find element by selector: ' + selector)
}
var event = document.createEvent('MouseEvent')
event.initMouseEvent('mouseout', true, true)
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Helper functions for type() and insert() to focus/blur
* so that we trigger DOM events.
*/
var focusSelector = function(done, selector) {
return this.evaluate_now(
function(selector) {
document.querySelector(selector).focus()
},
done.bind(this),
selector
)
}
var blurSelector = function(done, selector) {
return this.evaluate_now(
function(selector) {
//it is possible the element has been removed from the DOM
//between the action and the call to blur the element
var element = document.querySelector(selector)
if (element) {
element.blur()
}
},
done.bind(this),
selector
)
}
/**
* Type into an element.
*
* @param {String} selector
* @param {String} text
* @param {Function} done
*/
exports.type = function() {
var selector = arguments[0],
text,
done
if (arguments.length == 2) {
done = arguments[1]
} else {
text = arguments[1]
done = arguments[2]
}
debug('.type() %s into %s', text, selector)
var self = this
focusSelector.bind(this)(function(err) {
if (err) {
debug('Unable to .type() into non-existent selector %s', selector)
return done(err)
}
var blurDone = blurSelector.bind(this, done, selector)
if ((text || '') == '') {
this.evaluate_now(
function(selector) {
document.querySelector(selector).value = ''
},
blurDone,
selector
)
} else {
self.child.call('type', text, blurDone)
}
}, selector)
}
/**
* Insert text
*
* @param {String} selector
* @param {String} text
* @param {Function} done
*/
exports.insert = function(selector, text, done) {
if (arguments.length === 2) {
done = text
text = null
}
debug('.insert() %s into %s', text, selector)
var child = this.child
focusSelector.bind(this)(function(err) {
if (err) {
debug('Unable to .insert() into non-existent selector %s', selector)
return done(err)
}
var blurDone = blurSelector.bind(this, done, selector)
if ((text || '') == '') {
this.evaluate_now(
function(selector) {
document.querySelector(selector).value = ''
},
blurDone,
selector
)
} else {
child.call('insert', text, blurDone)
}
}, selector)
}
/**
* Check a checkbox, fire change event
*
* @param {String} selector
* @param {Function} done
*/
exports.check = function(selector, done) {
debug('.check() ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
var event = document.createEvent('HTMLEvents')
element.checked = true
event.initEvent('change', true, true)
element.dispatchEvent(event)
},
done,
selector
)
}
/*
* Uncheck a checkbox, fire change event
*
* @param {String} selector
* @param {Function} done
*/
exports.uncheck = function(selector, done) {
debug('.uncheck() ' + selector)
this.evaluate_now(
function(selector) {
var element = document.querySelector(selector)
var event = document.createEvent('HTMLEvents')
element.checked = null
event.initEvent('change', true, true)
element.dispatchEvent(event)
},
done,
selector
)
}
/**
* Choose an option from a select dropdown
*
*
*
* @param {String} selector
* @param {String} option value
* @param {Function} done
*/
exports.select = function(selector, option, done) {
debug('.select() ' + selector)
this.evaluate_now(
function(selector, option) {
var element = document.querySelector(selector)
var event = document.createEvent('HTMLEvents')
element.value = option
event.initEvent('change', true, true)
element.dispatchEvent(event)
},
done,
selector,
option
)
}
/**
* Go back to previous url.
*
* @param {Function} done
*/
exports.back = function(done) {
debug('.back()')
this.evaluate_now(function() {
window.history.back()
}, done)
}
/**
* Go forward to previous url.
*
* @param {Function} done
*/
exports.forward = function(done) {
debug('.forward()')
this.evaluate_now(function() {
window.history.forward()
}, done)
}
/**
* Refresh the current page.
*
* @param {Function} done
*/
exports.refresh = function(done) {
debug('.refresh()')
this.evaluate_now(function() {
window.location.reload()
}, done)
}
/**
* Wait
*
* @param {...} args
*/
exports.wait = function() {
var args = sliced(arguments)
var done = args[args.length - 1]
if (args.length < 2) {
debug('Not enough arguments for .wait()')
return done()
}
var arg = args[0]
if (typeof arg === 'number') {
debug('.wait() for ' + arg + 'ms')
if (arg < this.options.waitTimeout) {
waitms(arg, done)
} else {
waitms(
this.options.waitTimeout,
function() {
done(
new Error(
'.wait() timed out after ' + this.options.waitTimeout + 'msec'
)
)
}.bind(this)
)
}
} else if (typeof arg === 'string') {
var timeout = null
if (typeof args[1] === 'number') {
timeout = args[1]
}
debug(
'.wait() for ' +
arg +
' element' +
(timeout ? ' or ' + timeout + 'msec' : '')
)
waitelem.apply({ timeout: timeout }, [this, arg, done])
} else if (typeof arg === 'function') {
debug('.wait() for fn')
args.unshift(this)
waitfn.apply(this, args)
} else {
done()
}
}
/**
* Wait for a specififed amount of time.
*
* @param {Number} ms
* @param {Function} done
*/
function waitms(ms, done) {
setTimeout(done, ms)
}
/**
* Wait for a specified selector to exist.
*
* @param {Nightmare} self
* @param {String} selector
* @param {Function} done
*/
function waitelem(self, selector, done) {
var elementPresent
eval(
'elementPresent = function() {' +
" var element = document.querySelector('" +
jsesc(selector) +
"');" +
' return (element ? true : false);' +
'};'
)
var newDone = function(err) {
if (err) {
return done(
new Error(
`.wait() for ${selector} timed out after ${
self.options.waitTimeout
}msec`
)
)
}
done()
}
waitfn.apply(this, [self, elementPresent, newDone])
}
/**
* Wait until evaluated function returns true.
*
* @param {Nightmare} self
* @param {Function} fn
* @param {...} args
* @param {Function} done
*/
function waitfn() {
var softTimeout = this.timeout || null
var executionTimer
var softTimeoutTimer
var self = arguments[0]
var args = sliced(arguments)
var done = args[args.length - 1]
var timeoutTimer = setTimeout(function() {
clearTimeout(executionTimer)
clearTimeout(softTimeoutTimer)
done(new Error(`.wait() timed out after ${self.options.waitTimeout}msec`))
}, self.options.waitTimeout)
return tick.apply(this, arguments)
function tick(self, fn /**, arg1, arg2..., done**/) {
if (softTimeout) {
softTimeoutTimer = setTimeout(function() {
clearTimeout(executionTimer)
clearTimeout(timeoutTimer)
done()
}, softTimeout)
}
var waitDone = function(err, result) {
if (result) {
clearTimeout(timeoutTimer)
clearTimeout(softTimeoutTimer)
return done()
} else if (err) {
clearTimeout(timeoutTimer)
clearTimeout(softTimeoutTimer)
return done(err)
} else {
executionTimer = setTimeout(function() {
tick.apply(self, args)
}, self.options.pollInterval)
}
}
var newArgs = [fn, waitDone].concat(args.slice(2, -1))
self.evaluate_now.apply(self, newArgs)
}
}
/**
* Execute a function on the page.
*
* @param {Function} fn
* @param {...} args
* @param {Function} done
*/
exports.evaluate = function(fn /**, arg1, arg2..., done**/) {
var args = sliced(arguments)
var done = args[args.length - 1]
var self = this
var newDone = function() {
clearTimeout(timeoutTimer)
done.apply(self, arguments)
}
var newArgs = [fn, newDone].concat(args.slice(1, -1))
if (typeof fn !== 'function') {
return done(new Error('.evaluate() fn should be a function'))
}
debug('.evaluate() fn on the page')
var timeoutTimer = setTimeout(function() {
done(
new Error(
`Evaluation timed out after ${
self.options.executionTimeout
}msec. Are you calling done() or resolving your promises?`
)
)
}, self.options.executionTimeout)
this.evaluate_now.apply(this, newArgs)
}
/**
* Inject a JavaScript or CSS file onto the page
*
* @param {String} type
* @param {String} file
* @param {Function} done
*/
exports.inject = function(type, file, done) {
debug('.inject()-ing a file')
if (type === 'js') {
var js = fs.readFileSync(file, { encoding: 'utf-8' })
this._inject(js, done)
} else if (type === 'css') {
var css = fs.readFileSync(file, { encoding: 'utf-8' })
this.child.call('css', css, done)
} else {
debug('unsupported file type in .inject()')
done()
}
}
/**
* Set the viewport.
*
* @param {Number} width
* @param {Number} height
* @param {Function} done
*/
exports.viewport = function(width, height, done) {
debug('.viewport()')
this.child.call('size', width, height, done)
}
/**
* Set the useragent.
*
* @param {String} useragent
* @param {Function} done
*/
exports.useragent = function(useragent, done) {
debug('.useragent() to ' + useragent)
this.child.call('useragent', useragent, done)
}
/**
* Set the scroll position.
*
* @param {Number} x
* @param {Number} y
* @param {Function} done
*/
exports.scrollTo = function(y, x, done) {
debug('.scrollTo()')
this.evaluate_now(
function(y, x) {
window.scrollTo(x, y)
},
done,
y,
x
)
}
/**
* Take a screenshot.
*
* @param {String} path
* @param {Object} clip
* @param {Function} done
*/
exports.screenshot = function(path, clip, done) {
debug('.screenshot()')
if (typeof path === 'function') {
done = path
clip = undefined
path = undefined
} else if (typeof clip === 'function') {
done = clip
clip = typeof path === 'string' ? undefined : path
path = typeof path === 'string' ? path : undefined
}
this.child.call('screenshot', path, clip, function(error, img) {
var buf = new Buffer(img.data)
debug('.screenshot() captured with length %s', buf.length)
path ? fs.writeFile(path, buf, done) : done(null, buf)
})
}
/**
* Save the current file as html to disk.
*
* @param {String} path the full path to the file to save to
* @param {String} saveType
* @param {Function} done
*/
exports.html = function(path, saveType, done) {
debug('.html()')
if (typeof path === 'function' && !saveType && !done) {
done = path
saveType = undefined
path = undefined
} else if (
typeof path === 'object' &&
typeof saveType === 'function' &&
!done
) {
done = saveType
saveType = path
path = undefined
} else if (typeof saveType === 'function' && !done) {
done = saveType
saveType = undefined
}
this.child.call('html', path, saveType, function(error) {
if (error) debug(error)
done(error)
})
}
/**
* Take a pdf.
*
* @param {String} path
* @param {Function} done
*/
exports.pdf = function(path, options, done) {
debug('.pdf()')
if (typeof path === 'function' && !options && !done) {
done = path
options = undefined
path = undefined
} else if (
typeof path === 'object' &&
typeof options === 'function' &&
!done
) {
done = options
options = path
path = undefined
} else if (typeof options === 'function' && !done) {
done = options
options = undefined
}
this.child.call('pdf', path, options, function(error, pdf) {
if (error) debug(error)
var buf = new Buffer(pdf.data)
debug('.pdf() captured with length %s', buf.length)
path ? fs.writeFile(path, buf, done) : done(null, buf)
})
}
/**
* Get and set cookies
*
* @param {String} name
* @param {Mixed} value (optional)
* @param {Function} done
*/
exports.cookies = {}
/**
* Get a cookie
*/
exports.cookies.get = function(name, done) {
debug('cookies.get()')
var query = {}
switch (arguments.length) {
case 2:
query = typeof name === 'string' ? { name: name } : name
break
case 1:
done = name
break
}
this.child.call('cookie.get', query, done)
}
/**
* Set a cookie
*/
exports.cookies.set = function(name, value, done) {
debug('cookies.set()')
var cookies = []
switch (arguments.length) {
case 3:
cookies.push({
name: name,
value: value
})
break
case 2:
cookies = [].concat(name)
done = value
break
case 1:
done = name
break
}
this.child.call('cookie.set', cookies, done)
}
/**
* Clear a cookie
*/
exports.cookies.clear = function(name, done) {
debug('cookies.clear()')
var cookies = []
switch (arguments.length) {
case 2:
cookies = [].concat(name)
break
case 1:
done = name
break
}
this.child.call('cookie.clear', cookies, done)
}
/**
* Clear all cookies
*/
exports.cookies.clearAll = function(done) {
this.child.call('cookie.clearAll', done)
}
/**
* Authentication
*/
exports.authentication = function(login, password, done) {
debug('.authentication()')
this.child.call('authentication', login, password, done)
}
================================================
FILE: lib/frame-manager.js
================================================
const parent = require('./ipc')(process)
const EventEmitter = require('events')
const util = require('util')
const HIGHLIGHT_STYLE = {
x: 0,
y: 0,
width: 1,
height: 1,
color: { r: 0, g: 0, b: 0, a: 0.1 }
}
module.exports = FrameManager
/**
* FrameManager is an event emitter that produces a 'data' event each time the
* browser window draws to the screen.
* The primary use for this is to ensure that calling `capturePage()` on a
* window will produce an image that is up-to-date with the state of the page.
*/
function FrameManager(window) {
if (!(this instanceof FrameManager)) return new FrameManager(window)
EventEmitter.call(this)
var requestedFrame = false
var frameRequestTimeout
var self = this
this.on('newListener', subscribe)
this.on('removeListener', unsubscribe)
function subscribe(eventName) {
if (!self.listenerCount('data') && eventName === 'data') {
parent.emit('log', 'subscribing to browser window frames')
window.webContents.beginFrameSubscription(receiveFrame)
}
}
function unsubscribe() {
if (!self.listenerCount('data')) {
parent.emit('log', 'unsubscribing from browser window frames')
window.webContents.endFrameSubscription()
}
}
function cancelFrame() {
requestedFrame = false
clearTimeout(frameRequestTimeout)
self.emit('data', null)
}
function receiveFrame(buffer) {
requestedFrame = false
clearTimeout(frameRequestTimeout)
self.emit('data', buffer)
}
/**
* In addition to listening for events, calling `requestFrame` will ensure
* that a frame is queued up to render (instead of just waiting for the next
* time the browser chooses to draw a frame).
* @param {Function} [callback] Called when the frame is rendered.
* @param {Number} [timeout=1000] If no frame has been rendered after this
many milliseconds, run the callback anyway. In this case, The
callback's first argument, an image buffer, will be `null`.
*/
this.requestFrame = function(callback, timeout) {
timeout = timeout == undefined ? 1000 : timeout
if (callback) {
this.once('data', callback)
}
if (!requestedFrame) {
requestedFrame = true
// Force the browser to render new content by using the debugger to
// highlight a portion of the page. This way, we can guarantee a change
// that both requires rendering a frame and does not actually affect
// the content of the page.
if (!window.webContents.debugger.isAttached()) {
try {
window.webContents.debugger.attach()
} catch (error) {
parent.emit(
'log',
`Failed to attach to debugger for frame subscriptions: ${error}`
)
cancelFrame()
return
}
}
if (timeout) {
frameRequestTimeout = setTimeout(function() {
parent.emit(
'log',
`FrameManager timing out after ${timeout} ms with no new rendered frames`
)
cancelFrame()
}, timeout)
}
parent.emit('log', 'Highlighting page to trigger rendering.')
window.webContents.debugger.sendCommand('DOM.enable')
window.webContents.debugger.sendCommand(
'DOM.highlightRect',
HIGHLIGHT_STYLE,
function(_error) {
window.webContents.debugger.sendCommand('DOM.hideHighlight')
window.webContents.debugger.detach()
}
)
}
}
}
util.inherits(FrameManager, EventEmitter)
================================================
FILE: lib/ipc.js
================================================
'use strict'
/**
* Module dependencies
*/
var Emitter = require('events').EventEmitter
var sliced = require('sliced')
var debug = require('debug')('nightmare:ipc')
// If this process has a parent, redirect debug logs to it
if (process.send) {
debug = function() {
process.send(['nightmare:ipc:debug'].concat(sliced(arguments)))
}
}
/**
* Export `IPC`
*/
module.exports = IPC
/**
* Initialize `IPC`
*/
var instance = Symbol()
function IPC(process) {
if (process[instance]) {
return process[instance]
}
var emitter = (process[instance] = new Emitter())
var emit = emitter.emit
var callId = 0
var responders = {}
// no parent
if (!process.send) {
return emitter
}
process.on('message', function(data) {
// handle debug logging specially
if (data[0] === 'nightmare:ipc:debug') {
debug.apply(null, sliced(data, 1))
}
emit.apply(emitter, sliced(data))
})
emitter.emit = function() {
if (process.connected) {
process.send(sliced(arguments))
}
}
/**
* Call a responder function in the associated process. (In the process,
* responders can be registered with `ipc.respondTo()`.) The last argument
* should be a callback function, which will called with the results of the
* responder.
* This returns an event emitter. You can listen for the results of the
* responder using the `end` event (this is the same as passing a callback).
* Additionally, you can listen for `data` events, which the responder may
* send to indicate some sort of progress.
* @param {String} name Name of the responder function to call
* @param {...Objects} [arguments] Any number of arguments to send
* @param {Function} [callback] A callback function that handles the results
* @return {Emitter}
*/
emitter.call = function(name) {
var args = sliced(arguments, 1)
var callback = args.pop()
if (typeof callback !== 'function') {
args.push(callback)
callback = undefined
}
var id = callId++
var progress = new Emitter()
emitter.on(`CALL_DATA_${id}`, function() {
progress.emit.apply(progress, ['data'].concat(sliced(arguments)))
})
emitter.once(`CALL_RESULT_${id}`, function(err) {
// unserialize errors
err = unserializeError(err)
progress.emit.apply(
progress,
['end'].concat(err).concat(sliced(arguments, 1))
)
emitter.removeAllListeners(`CALL_DATA_${id}`)
progress.removeAllListeners()
progress = undefined
if (callback) {
callback.apply(null, [err].concat(sliced(arguments, 1)))
}
})
emitter.emit.apply(emitter, ['CALL', id, name].concat(args))
return progress
}
/**
* Register a responder to be called from other processes with `ipc.call()`.
* The responder should be a function that accepts any number of arguments,
* where the last argument is a callback function. When the responder has
* finished its work, it MUST call the callback. The first argument should be
* an error, if any, and the second should be the results.
* Only one responder can be registered for a given name.
* @param {String} name The name to register the responder under.
* @param {Function} responder
*/
emitter.respondTo = function(name, responder) {
if (responders[name]) {
debug(`Replacing responder named "${name}"`)
}
responders[name] = responder
}
emitter.on('CALL', function(id, name) {
var responder = responders[name]
var done = function(err) {
err = serializeError(err)
emitter.emit.apply(
emitter,
[`CALL_RESULT_${id}`].concat(err).concat(sliced(arguments, 1))
)
}
done.progress = function() {
emitter.emit.apply(emitter, [`CALL_DATA_${id}`].concat(sliced(arguments)))
}
if (!responder) {
return done(new Error(`Nothing responds to "${name}"`))
}
try {
responder.apply(null, sliced(arguments, 2).concat([done]))
} catch (error) {
done(error)
}
})
return emitter
}
function serializeError(err) {
if (!(err instanceof Error)) return err
return {
code: err.code,
message: err.message,
details: err.detail,
stack: err.stack || ''
}
}
function unserializeError(err) {
if (!err || !err.message) return err
const e = new Error(err.message)
e.code = err.code || -1
if (err.stack) e.stack = err.stack
if (err.details) e.details = err.details
if (err.url) e.url = err.url
return e
}
================================================
FILE: lib/javascript.js
================================================
/**
* Module Dependencies
*/
var minstache = require('minstache')
/**
* Run the `src` function on the client-side, capture
* the response and logs, and send back via
* ipc to electron's main process
*/
var execute = `
(function javascript () {
var nightmare = window.__nightmare || window[''].nightmare;
try {
var fn = ({{!src}}),
response,
args = [];
{{#args}}args.push({{!argument}});{{/args}}
if(fn.length - 1 == args.length) {
args.push(((err, v) => {
if(err) return nightmare.reject(err);
nightmare.resolve(v);
}));
fn.apply(null, args);
}
else {
response = fn.apply(null, args);
if(response && response.then) {
response.then((v) => {
nightmare.resolve(v);
})
.catch((err) => {
nightmare.reject(err)
});
} else {
nightmare.resolve(response);
}
}
} catch (err) {
nightmare.reject(err);
}
})()
`
/**
* Inject the `src` on the client-side, capture
* the response and logs, and send back via
* ipc to electron's main process
*/
var inject = `
(function javascript () {
var nightmare = window.__nightmare || window[''].nightmare;
try {
var response = (function () { {{!src}} \n})()
nightmare.resolve(response);
} catch (e) {
nightmare.reject(e);
}
})()
`
/**
* Export the templates
*/
exports.execute = minstache.compile(execute)
exports.inject = minstache.compile(inject)
================================================
FILE: lib/nightmare.js
================================================
/**
* DEBUG=nightmare*
*/
var log = require('debug')('nightmare:log')
var debug = require('debug')('nightmare')
var electronLog = {
stdout: require('debug')('electron:stdout'),
stderr: require('debug')('electron:stderr')
}
/**
* Module dependencies
*/
var default_electron_path = require('electron')
var proc = require('child_process')
var actions = require('./actions')
var path = require('path')
var sliced = require('sliced')
var child = require('./ipc')
var once = require('once')
var split2 = require('split2')
var defaults = require('defaults')
var noop = function() {}
var keys = Object.keys
// Standard timeout for loading URLs
const DEFAULT_GOTO_TIMEOUT = 30 * 1000
// Standard timeout for wait(ms)
const DEFAULT_WAIT_TIMEOUT = 30 * 1000
// Timeout between keystrokes for `.type()`
const DEFAULT_TYPE_INTERVAL = 100
// timeout between `wait` polls
const DEFAULT_POLL_INTERVAL = 250
// max retry for authentication
const MAX_AUTH_RETRIES = 3
// max execution time for `.evaluate()`
const DEFAULT_EXECUTION_TIMEOUT = 30 * 1000
// Error message when halted
const DEFAULT_HALT_MESSAGE = 'Nightmare Halted'
// Non-persistent partition to use by defaults
const DEFAULT_PARTITION = 'nightmare'
/**
* Export `Nightmare`
*/
module.exports = Nightmare
/**
* runner script
*/
var runner = path.join(__dirname, 'runner.js')
/**
* Template
*/
var template = require('./javascript')
/**
* Initialize `Nightmare`
*
* @param {Object} options
*/
function Nightmare(options) {
if (!(this instanceof Nightmare)) return new Nightmare(options)
options = options || {}
var electronArgs = {}
var self = this
options.waitTimeout = options.waitTimeout || DEFAULT_WAIT_TIMEOUT
options.gotoTimeout = options.gotoTimeout || DEFAULT_GOTO_TIMEOUT
options.pollInterval = options.pollInterval || DEFAULT_POLL_INTERVAL
options.typeInterval = options.typeInterval || DEFAULT_TYPE_INTERVAL
options.executionTimeout =
options.executionTimeout || DEFAULT_EXECUTION_TIMEOUT
options.webPreferences = options.webPreferences || {}
// null is a valid value, which will result in the use of the electron default behavior, which is to persist storage.
// The default behavior for nightmare will be to use non-persistent storage.
// http://electron.atom.io/docs/api/browser-window/#new-browserwindowoptions
options.webPreferences.partition =
options.webPreferences.partition !== undefined
? options.webPreferences.partition
: DEFAULT_PARTITION
options.Promise = options.Promise || Nightmare.Promise || Promise
var electron_path = options.electronPath || default_electron_path
if (options.paths) {
electronArgs.paths = options.paths
}
if (options.switches) {
electronArgs.switches = options.switches
}
options.maxAuthRetries = options.maxAuthRetries || MAX_AUTH_RETRIES
electronArgs.loadTimeout = options.loadTimeout
if (
options.loadTimeout &&
options.gotoTimeout &&
options.loadTimeout < options.gotoTimeout
) {
debug(
`WARNING: load timeout of ${
options.loadTimeout
} is shorter than goto timeout of ${options.gotoTimeout}`
)
}
electronArgs.dock = options.dock || false
electronArgs.certificateSubjectName = options.certificateSubjectName || null
attachToProcess(this)
// initial state
this.state = 'initial'
this.running = false
this.ending = false
this.ended = false
this._queue = []
this._headers = {}
this.options = options
debug('queuing process start')
this.queue(done => {
this.proc = proc.spawn(
electron_path,
[runner].concat(JSON.stringify(electronArgs)),
{
stdio: [null, null, null, 'ipc'],
env: defaults(options.env || {}, process.env)
}
)
this.proc.stdout.pipe(split2()).on('data', data => {
electronLog.stdout(data)
})
this.proc.stderr.pipe(split2()).on('data', data => {
electronLog.stderr(data)
})
this.proc.on('close', code => {
if (!self.ended) {
handleExit(code, self, noop)
}
})
this.child = child(this.proc)
this.child.once('die', function(err) {
debug('dying: ' + err)
self.die = err
})
// propagate console.log(...) through
this.child.on('log', function() {
log.apply(log, arguments)
})
this.child.on('uncaughtException', function(err) {
const e = new Error('Nightmare runner error: ' + err.message)
e.stack = err.stack || ''
const onClose = () => {
throw err
}
endInstance(self, onClose, true)
})
this.child.on('page', function(type) {
log.apply(null, ['page-' + type].concat(sliced(arguments, 1)))
})
// propogate events through to debugging
this.child.on('did-finish-load', function() {
log('did-finish-load', JSON.stringify(sliced(arguments)))
})
this.child.on('did-fail-load', function() {
log('did-fail-load', JSON.stringify(sliced(arguments)))
})
this.child.on('did-fail-provisional-load', function() {
log('did-fail-provisional-load', JSON.stringify(sliced(arguments)))
})
this.child.on('did-frame-finish-load', function() {
log('did-frame-finish-load', JSON.stringify(sliced(arguments)))
})
this.child.on('did-start-loading', function() {
log('did-start-loading', JSON.stringify(sliced(arguments)))
})
this.child.on('did-stop-loading', function() {
log('did-stop-loading', JSON.stringify(sliced(arguments)))
})
this.child.on('did-get-response-details', function() {
log('did-get-response-details', JSON.stringify(sliced(arguments)))
})
this.child.on('did-get-redirect-request', function() {
log('did-get-redirect-request', JSON.stringify(sliced(arguments)))
})
this.child.on('dom-ready', function() {
log('dom-ready', JSON.stringify(sliced(arguments)))
})
this.child.on('page-favicon-updated', function() {
log('page-favicon-updated', JSON.stringify(sliced(arguments)))
})
this.child.on('new-window', function() {
log('new-window', JSON.stringify(sliced(arguments)))
})
this.child.on('will-navigate', function() {
log('will-navigate', JSON.stringify(sliced(arguments)))
})
this.child.on('crashed', function() {
log('crashed', JSON.stringify(sliced(arguments)))
})
this.child.on('plugin-crashed', function() {
log('plugin-crashed', JSON.stringify(sliced(arguments)))
})
this.child.on('destroyed', function() {
log('destroyed', JSON.stringify(sliced(arguments)))
})
this.child.on('media-started-playing', function() {
log('media-started-playing', JSON.stringify(sliced(arguments)))
})
this.child.on('media-paused', function() {
log('media-paused', JSON.stringify(sliced(arguments)))
})
this.child.once('ready', versions => {
this.engineVersions = versions
this.child.call('browser-initialize', options, function() {
self.state = 'ready'
done()
})
})
})
// initialize namespaces
Nightmare.namespaces.forEach(function(name) {
if ('function' === typeof this[name]) {
this[name] = this[name]()
}
}, this)
//prepend adding child actions to the queue
Object.keys(Nightmare.childActions).forEach(function(key) {
debug('queueing child action addition for "%s"', key)
this.queue(function(done) {
this.child.call('action', key, String(Nightmare.childActions[key]), done)
})
}, this)
}
function handleExit(code, instance, cb) {
var help = {
127: 'command not found - you may not have electron installed correctly',
126: 'permission problem or command is not an executable - you may not have all the necessary dependencies for electron',
1: 'general error - you may need xvfb',
0: 'success!'
}
debug('electron child process exited with code ' + code + ': ' + help[code])
instance.proc.removeAllListeners()
cb()
}
function endInstance(instance, cb, forceKill) {
instance.ended = true
detachFromProcess(instance)
if (instance.proc && instance.proc.connected) {
instance.proc.on('close', code => {
handleExit(code, instance, cb)
})
instance.child.call('quit', () => {
instance.child.removeAllListeners()
if (forceKill) {
instance.proc.kill('SIGINT')
}
})
} else {
debug('electron child process not started yet, skipping kill.')
cb()
}
}
/**
* Attach any instance-specific process-level events.
*/
function attachToProcess(instance) {
instance._endNow = endInstance.bind(null, instance, noop)
process.setMaxListeners(Infinity)
process.on('exit', instance._endNow)
process.on('SIGINT', instance._endNow)
process.on('SIGTERM', instance._endNow)
process.on('SIGQUIT', instance._endNow)
process.on('SIGHUP', instance._endNow)
process.on('SIGBREAK', instance._endNow)
}
function detachFromProcess(instance) {
process.removeListener('exit', instance._endNow)
process.removeListener('SIGINT', instance._endNow)
process.removeListener('SIGTERM', instance._endNow)
process.removeListener('SIGQUIT', instance._endNow)
process.removeListener('SIGHUP', instance._endNow)
process.removeListener('SIGBREAK', instance._endNow)
}
/**
* Namespaces to initialize
*/
Nightmare.namespaces = []
/**
* Child actions to create
*/
Nightmare.childActions = {}
/**
* Version
*/
Nightmare.version = require(path.resolve(
__dirname,
'..',
'package.json'
)).version
/**
* Promise library (can override)
*/
Nightmare.Promise = Promise
/**
* Override headers for all HTTP requests
*/
Nightmare.prototype.header = function(header, value) {
if (header && typeof value !== 'undefined') {
this._headers[header] = value
} else {
this._headers = header || {}
}
return this
}
/**
* Go to a `url`
*/
Nightmare.prototype.goto = function(url, headers) {
debug('queueing action "goto" for %s', url)
var self = this
headers = headers || {}
for (var key in this._headers) {
headers[key] = headers[key] || this._headers[key]
}
this.queue(function(fn) {
self.child.call('goto', url, headers, this.options.gotoTimeout, fn)
})
return this
}
/**
* run
*/
Nightmare.prototype.run = function(fn) {
debug('running')
var steps = this.queue()
this.running = true
this._queue = []
var self = this
// kick us off
next()
// next function
function next(err, _res) {
var item = steps.shift()
// Immediately halt execution if an error has been thrown, or we have no more queued up steps.
if (err || !item) return done.apply(self, arguments)
var args = item[1] || []
var method = item[0]
args.push(once(after))
method.apply(self, args)
}
function after(err, _res) {
err = err || self.die
var args = [err].concat(sliced(arguments, 1))
if (self.child) {
self.child.call('continue', () => next.apply(self, args))
} else {
next.apply(self, args)
}
}
function done() {
var doneargs = arguments
self.running = false
if (self.ending) {
return endInstance(self, () => fn.apply(self, doneargs))
}
return fn.apply(self, doneargs)
}
return this
}
/**
* run the code now (do not queue it)
*
* you should not use this, unless you know what you're doing
* it should be used for plugins and custom actions, not for
* normal API usage
*/
Nightmare.prototype.evaluate_now = function(js_fn, done) {
var args = Array.prototype.slice
.call(arguments)
.slice(2)
.map(a => {
return { argument: JSON.stringify(a) }
})
var source = template.execute({ src: String(js_fn), args: args })
this.child.call('javascript', source, done)
return this
}
/**
* inject javascript
*/
Nightmare.prototype._inject = function(js, done) {
this.child.call('javascript', template.inject({ src: js }), done)
return this
}
/**
* end
*/
Nightmare.prototype.end = function(done) {
this.ending = true
if (done && !this.running && !this.ended) {
return this.then(done)
}
return this
}
/**
* Halt - Force kills the electron process immediately and empties the queue
*
* @param {Error|String} error (Optional: defaults to 'Nightmare Halted'.) Error to pass to rejected promise
* @param {Function} done (Optional: defaults to no operation) callback when the child process exits
* @return {Nightmare} returns self
*/
Nightmare.prototype.halt = function(error, done) {
this.ending = true
var queue = this.queue() // empty the queue
queue.splice(0)
if (!this.ended) {
var message = error
if (error instanceof Error) {
message = error.message
}
this.die = message || DEFAULT_HALT_MESSAGE
if (typeof this._rejectActivePromise === 'function') {
this._rejectActivePromise(error || DEFAULT_HALT_MESSAGE)
}
var callback = done
if (!callback || typeof callback !== 'function') {
callback = noop
}
endInstance(this, callback, true)
}
return this
}
/**
* on
*/
Nightmare.prototype.on = function(event, handler) {
this.queue(function(done) {
this.child.on(event, handler)
done()
})
return this
}
/**
* once
*/
Nightmare.prototype.once = function(event, handler) {
this.queue(function(done) {
this.child.once(event, handler)
done()
})
return this
}
/**
* removeEventListener
*/
Nightmare.prototype.removeListener = function(event, handler) {
this.child.removeListener(event, handler)
return this
}
/**
* Queue
*/
Nightmare.prototype.queue = function(_done) {
if (!arguments.length) return this._queue
var args = sliced(arguments)
var fn = args.pop()
this._queue.push([fn, args])
}
/**
* then
*/
Nightmare.prototype.then = function(fulfill, reject) {
var self = this
return new this.options.Promise(function(success, failure) {
self._rejectActivePromise = failure
self.run(function(err, result) {
if (err) failure(err)
else success(result)
})
}).then(fulfill, reject)
}
/**
* catch
*/
Nightmare.prototype.catch = function(reject) {
this._rejectActivePromise = reject
return this.then(undefined, reject)
}
/**
* use
*/
Nightmare.prototype.use = function(fn) {
fn(this)
return this
}
// wrap all the functions in the queueing function
function queued(name, fn) {
return function action() {
debug('queueing action "' + name + '"')
var args = [].slice.call(arguments)
this._queue.push([fn, args])
return this
}
}
/**
* Static: Support attaching custom actions
*
* @param {String} name - method name
* @param {Function|Object} [childfn] - Electron implementation
* @param {Function|Object} parentfn - Nightmare implementation
* @return {Nightmare}
*/
Nightmare.action = function() {
var name = arguments[0],
childfn,
parentfn
if (arguments.length === 2) {
parentfn = arguments[1]
} else {
parentfn = arguments[2]
childfn = arguments[1]
}
// support functions and objects
// if it's an object, wrap it's
// properties in the queue function
if (parentfn) {
if (typeof parentfn === 'function') {
Nightmare.prototype[name] = queued(name, parentfn)
} else {
if (!~Nightmare.namespaces.indexOf(name)) {
Nightmare.namespaces.push(name)
}
Nightmare.prototype[name] = function() {
var self = this
return keys(parentfn).reduce(function(obj, key) {
obj[key] = queued(name, parentfn[key]).bind(self)
return obj
}, {})
}
}
}
if (childfn) {
if (typeof childfn === 'function') {
Nightmare.childActions[name] = childfn
} else {
for (var key in childfn) {
Nightmare.childActions[name + '.' + key] = childfn[key]
}
}
}
}
/**
* Attach all the actions.
*/
Object.keys(actions).forEach(function(name) {
var fn = actions[name]
Nightmare.action(name, fn)
})
================================================
FILE: lib/preload.js
================================================
/* eslint-disable no-console */
var ipc = require('electron').ipcRenderer
var sliced = require('sliced')
function send(_event) {
ipc.send.apply(ipc, arguments)
}
// offer limited access to allow
// .evaluate() and .inject()
// to continue to work as expected.
//
// TODO: this could be avoided by
// rewriting the evaluate to
// use promises instead. But
// for now this fixes the security
// issue in: segmentio/nightmare/#1358
window.__nightmare = {
resolve: function(value) {
send('response', value)
},
reject: function(err) {
send('error', error(err))
}
}
// Listen for error events
window.addEventListener(
'error',
function(err) {
send('page', 'error', error(err))
},
true
)
// prevent 'unload' and 'beforeunload' from being bound
var defaultAddEventListener = window.addEventListener
window.addEventListener = function(type) {
if (type === 'unload' || type === 'beforeunload') {
return
}
defaultAddEventListener.apply(window, arguments)
}
// prevent 'onunload' and 'onbeforeunload' from being set
Object.defineProperties(window, {
onunload: {
enumerable: true,
writable: false,
value: null
},
onbeforeunload: {
enumerable: true,
writable: false,
value: null
}
})
// listen for console.log
var defaultLog = console.log
console.log = function() {
send('console', 'log', sliced(arguments))
return defaultLog.apply(this, arguments)
}
// listen for console.warn
var defaultWarn = console.warn
console.warn = function() {
send('console', 'warn', sliced(arguments))
return defaultWarn.apply(this, arguments)
}
// listen for console.error
var defaultError = console.error
console.error = function() {
send('console', 'error', sliced(arguments))
return defaultError.apply(this, arguments)
}
// overwrite the default alert
window.alert = function(message) {
send('page', 'alert', message)
}
// overwrite the default prompt
window.prompt = function(message, defaultResponse) {
send('page', 'prompt', message, defaultResponse)
}
// overwrite the default confirm
window.confirm = function(message, defaultResponse) {
send('page', 'confirm', message, defaultResponse)
}
/**
* Make errors serializeable
*/
function error(err) {
if (!(err instanceof Error)) return err
return {
code: err.code,
message: err.message,
details: err.detail,
stack: err.stack || ''
}
}
================================================
FILE: lib/runner.js
================================================
/**
* Module Dependencies
*/
var parent = require('./ipc')(process)
var electron = require('electron')
var BrowserWindow = electron.BrowserWindow
var defaults = require('deep-defaults')
var join = require('path').join
var sliced = require('sliced')
var renderer = require('electron').ipcMain
var app = require('electron').app
var urlFormat = require('url')
var FrameManager = require('./frame-manager')
// URL protocols that don't need to be checked for validity
const KNOWN_PROTOCOLS = ['http', 'https', 'file', 'about', 'javascript']
// Property for tracking whether a window is ready for interaction
const IS_READY = Symbol('isReady')
/**
* Handle uncaught exceptions in the main electron process
*/
process.on('uncaughtException', function(err) {
parent.emit('uncaughtException', err.stack || err.message || String(err))
})
/**
* Update the app paths
*/
if (process.argv.length < 3) {
throw new Error(`Too few runner arguments: ${JSON.stringify(process.argv)}`)
}
var processArgs = JSON.parse(process.argv[2])
var paths = processArgs.paths
if (paths) {
for (let i in paths) {
app.setPath(i, paths[i])
}
}
var switches = processArgs.switches
if (switches) {
for (let i in switches) {
app.commandLine.appendSwitch(i, switches[i])
}
}
/**
* Hide the dock
*/
// app.dock is not defined when running
// electron in a platform other than OS X
if (!processArgs.dock && app.dock) {
app.dock.hide()
}
/**
* Set the client certificate by subjectName if processArgs.certificateSubjectName is defined
*/
if (processArgs.certificateSubjectName) {
app.on(
'select-client-certificate',
(event, webContents, url, list, callback) => {
for (var i = 0; i < list.length; i++) {
if (list[i].subjectName === processArgs.certificateSubjectName) {
callback(list[i])
return
}
}
// defaults to first if the subject name is not available
callback(list[0])
}
)
}
/**
* Listen for the app being "ready"
*/
app.on('ready', function() {
var win, frameManager, options, closed
/**
* create a browser window
*/
parent.respondTo('browser-initialize', function(opts, done) {
options = defaults(opts || {}, {
show: false,
alwaysOnTop: true,
webPreferences: {
preload: join(__dirname, 'preload.js'),
nodeIntegration: false
}
})
/**
* Create a new Browser Window
*/
win = new BrowserWindow(options)
if (options.show && options.openDevTools) {
if (typeof options.openDevTools === 'object') {
win.openDevTools(options.openDevTools)
} else {
win.openDevTools()
}
}
/**
* Window Docs:
* https://github.com/atom/electron/blob/master/docs/api/browser-window.md
*/
frameManager = FrameManager(win)
/**
* Window options
*/
win.webContents.setAudioMuted(true)
/**
* Sets user agent.
*/
if (options.userAgent) {
win.webContents.setUserAgent(options.userAgent)
}
/**
* Pass along web content events
*/
renderer.on('page', function(_sender /*, arguments, ... */) {
parent.emit.apply(parent, ['page'].concat(sliced(arguments, 1)))
})
renderer.on('console', function(sender, type, args) {
parent.emit.apply(parent, ['console', type].concat(args))
})
win.webContents.on('did-finish-load', forward('did-finish-load'))
win.webContents.on('did-fail-load', forward('did-fail-load'))
win.webContents.on(
'did-fail-provisional-load',
forward('did-fail-provisional-load')
)
win.webContents.on(
'did-frame-finish-load',
forward('did-frame-finish-load')
)
win.webContents.on('did-start-loading', forward('did-start-loading'))
win.webContents.on('did-stop-loading', forward('did-stop-loading'))
win.webContents.on(
'did-get-response-details',
forward('did-get-response-details')
)
win.webContents.on(
'did-get-redirect-request',
forward('did-get-redirect-request')
)
win.webContents.on('dom-ready', forward('dom-ready'))
win.webContents.on('page-favicon-updated', forward('page-favicon-updated'))
win.webContents.on('new-window', forward('new-window'))
win.webContents.on('will-navigate', forward('will-navigate'))
win.webContents.on('crashed', forward('crashed'))
win.webContents.on('plugin-crashed', forward('plugin-crashed'))
win.webContents.on('destroyed', forward('destroyed'))
win.webContents.on(
'media-started-playing',
forward('media-started-playing')
)
win.webContents.on('media-paused', forward('media-paused'))
win.webContents.on('close', _e => {
closed = true
})
var loadwatch
win.webContents.on('did-start-loading', function() {
if (win.webContents.isLoadingMainFrame()) {
if (options.loadTimeout) {
loadwatch = setTimeout(function() {
win.webContents.stop()
}, options.loadTimeout)
}
setIsReady(false)
}
})
win.webContents.on('did-stop-loading', function() {
clearTimeout(loadwatch)
setIsReady(true)
})
setIsReady(true)
done()
})
/**
* Parent actions
*/
/**
* goto
*/
parent.respondTo('goto', function(url, headers, timeout, done) {
if (!url || typeof url !== 'string') {
return done(new Error('goto: `url` must be a non-empty string'))
}
var httpReferrer = ''
var extraHeaders = ''
for (var key in headers) {
if (key.toLowerCase() == 'referer') {
httpReferrer = headers[key]
continue
}
extraHeaders += key + ': ' + headers[key] + '\n'
}
var loadUrlOptions = { extraHeaders: extraHeaders }
httpReferrer && (loadUrlOptions.httpReferrer = httpReferrer)
if (win.webContents.getURL() == url) {
done()
} else {
var responseData = {}
var domLoaded = false
var timer = setTimeout(function() {
// If the DOM loaded before timing out, consider the load successful.
var error = domLoaded
? undefined
: {
message: 'navigation error',
code: -7, // chromium's generic networking timeout code
details: `Navigation timed out after ${timeout} ms`,
url: url
}
// Even if "successful," note that some things didn't finish.
responseData.details = `Not all resources loaded after ${timeout} ms`
cleanup(error, responseData)
}, timeout)
function handleFailure(event, code, detail, failedUrl, isMainFrame) {
if (isMainFrame) {
cleanup({
message: 'navigation error',
code: code,
details: detail,
url: failedUrl || url
})
}
}
function handleDetails(
event,
status,
newUrl,
oldUrl,
statusCode,
method,
referrer,
headers,
resourceType
) {
if (resourceType === 'mainFrame') {
responseData = {
url: newUrl,
code: statusCode,
method: method,
referrer: referrer,
headers: headers
}
}
}
function handleDomReady() {
domLoaded = true
}
// We will have already unsubscribed if load failed, so assume success.
function handleFinish(_event) {
cleanup(null, responseData)
}
function cleanup(err, data) {
clearTimeout(timer)
win.webContents.removeListener('did-fail-load', handleFailure)
win.webContents.removeListener(
'did-fail-provisional-load',
handleFailure
)
win.webContents.removeListener(
'did-get-response-details',
handleDetails
)
win.webContents.removeListener('dom-ready', handleDomReady)
win.webContents.removeListener('did-finish-load', handleFinish)
setIsReady(true)
// wait a tick before notifying to resolve race conditions for events
setImmediate(() => done(err, data))
}
// In most environments, loadURL handles this logic for us, but in some
// it just hangs for unhandled protocols. Mitigate by checking ourselves.
function canLoadProtocol(protocol, callback) {
protocol = (protocol || '').replace(/:$/, '')
if (!protocol || KNOWN_PROTOCOLS.includes(protocol)) {
return callback(true)
}
electron.protocol.isProtocolHandled(protocol, callback)
}
function startLoading() {
// abort any pending loads first
if (win.webContents.isLoading()) {
parent.emit('log', 'aborting pending page load')
win.webContents.once('did-stop-loading', function() {
startLoading(true)
})
return win.webContents.stop()
}
win.webContents.on('did-fail-load', handleFailure)
win.webContents.on('did-fail-provisional-load', handleFailure)
win.webContents.on('did-get-response-details', handleDetails)
win.webContents.on('dom-ready', handleDomReady)
win.webContents.on('did-finish-load', handleFinish)
win.webContents.loadURL(url, loadUrlOptions)
// javascript: URLs *may* trigger page loads; wait a bit to see
if (protocol === 'javascript:') {
setTimeout(function() {
if (!win.webContents.isLoadingMainFrame()) {
done(null, {
url: url,
code: 200,
method: 'GET',
referrer: win.webContents.getURL(),
headers: {}
})
}
}, 10)
}
}
var protocol = urlFormat.parse(url).protocol
canLoadProtocol(protocol, function startLoad(canLoad) {
if (canLoad) {
parent.emit(
'log',
`Navigating: "${url}",
headers: ${extraHeaders || '[none]'},
timeout: ${timeout}`
)
return startLoading()
}
cleanup({
message: 'navigation error',
code: -1000,
details: 'unhandled protocol',
url: url
})
})
}
})
/**
* javascript
*/
parent.respondTo('javascript', function(src, done) {
var onresponse = (event, response) => {
renderer.removeListener('error', onerror)
renderer.removeListener('log', onlog)
done(null, response)
}
var onerror = (event, err) => {
renderer.removeListener('log', onlog)
renderer.removeListener('response', onresponse)
done(err)
}
var onlog = (event, args) => parent.emit.apply(parent, ['log'].concat(args))
renderer.once('response', onresponse)
renderer.once('error', onerror)
renderer.on('log', onlog)
//parent.emit('log', 'about to execute javascript: ' + src);
win.webContents.executeJavaScript(src)
})
/**
* css
*/
parent.respondTo('css', function(css, done) {
win.webContents.insertCSS(css)
done()
})
/**
* size
*/
parent.respondTo('size', function(width, height, done) {
win.setSize(width, height)
done()
})
parent.respondTo('useragent', function(useragent, done) {
win.webContents.setUserAgent(useragent)
done()
})
/**
* type
*/
parent.respondTo('type', function(value, done) {
var chars = String(value).split('')
function type() {
var ch = chars.shift()
if (ch === undefined) {
return done()
}
// keydown
win.webContents.sendInputEvent({
type: 'keyDown',
keyCode: ch
})
// keypress
win.webContents.sendInputEvent({
type: 'char',
keyCode: ch
})
// keyup
win.webContents.sendInputEvent({
type: 'keyUp',
keyCode: ch
})
// defer function into next event loop
setTimeout(type, options.typeInterval)
}
// start
type()
})
/**
* Insert
*/
parent.respondTo('insert', function(value, done) {
win.webContents.insertText(String(value))
done()
})
/**
* screenshot
*/
parent.respondTo('screenshot', function(path, clip, done) {
// https://gist.github.com/twolfson/0d374d9d7f26eefe7d38
var args = [
function handleCapture(img) {
done(null, img.toPNG())
}
]
if (clip) args.unshift(clip)
frameManager.requestFrame(function() {
win.capturePage.apply(win, args)
})
})
/**
* html
*/
parent.respondTo('html', function(path, saveType, done) {
// https://github.com/atom/electron/blob/master/docs/api/web-contents.md#webcontentssavepagefullpath-savetype-callback
saveType = saveType || 'HTMLComplete'
win.webContents.savePage(path, saveType, function(err) {
done(err)
})
})
/**
* pdf
*/
parent.respondTo('pdf', function(path, options, done) {
// https://github.com/fraserxu/electron-pdf/blob/master/index.js#L98
options = defaults(options || {}, {
marginType: 0,
printBackground: true,
printSelectionOnly: false,
landscape: false
})
win.webContents.printToPDF(options, function(err, data) {
if (err) return done(err)
done(null, data)
})
})
/**
* Get cookies
*/
parent.respondTo('cookie.get', function(query, done) {
var details = Object.assign(
{
url: win.webContents.getURL()
},
query
)
parent.emit('log', 'getting cookie: ' + JSON.stringify(details))
win.webContents.session.cookies.get(details, function(err, cookies) {
if (err) return done(err)
done(null, details.name ? cookies[0] : cookies)
})
})
/**
* Set cookies
*/
parent.respondTo('cookie.set', function(cookies, done) {
var pending = cookies.length
for (var i = 0, cookie; (cookie = cookies[i]); i++) {
var details = Object.assign(
{
url: win.webContents.getURL()
},
cookie
)
parent.emit('log', 'setting cookie: ' + JSON.stringify(details))
win.webContents.session.cookies.set(details, function(err) {
if (err) done(err)
else if (!--pending) done()
})
}
})
/**
* Clear cookie
*/
parent.respondTo('cookie.clear', function(cookies, done) {
var url = win.webContents.getURL()
var getCookies = cb => cb(null, cookies)
if (cookies.length == 0) {
getCookies = cb =>
win.webContents.session.cookies.get({ url: url }, (error, cookies) => {
cb(error, cookies.map(cookie => cookie.name))
})
}
getCookies((error, cookies) => {
var pending = cookies.length
//if no cookies, return
if (pending == 0) {
return done()
}
parent.emit('log', 'listing params', cookies)
//for each cookie name in cookies,
for (var i = 0, cookie; (cookie = cookies[i]); i++) {
//remove the cookie from the url
win.webContents.session.cookies.remove(url, cookie, function(err) {
if (err) done(err)
else if (!--pending) done()
})
}
})
})
/**
* Clear all cookies
*/
parent.respondTo('cookie.clearAll', function(done) {
win.webContents.session.clearStorageData(
{
storages: ['cookies']
},
function(err) {
if (err) return done(err)
return done()
}
)
})
/**
* Add custom functionality
*/
parent.respondTo('action', function(name, fntext, done) {
var fn = new Function(
'with(this){ parent.emit("log", "adding action for ' +
name +
'"); return ' +
fntext +
'}'
).call({
require: require,
parent: parent
})
fn(name, options, parent, win, renderer, function(err) {
if (err) return done(err)
return done()
})
})
/**
* Continue
*/
parent.respondTo('continue', function(done) {
if (isReady()) {
done()
} else {
parent.emit('log', 'waiting for window to load...')
win.once('did-change-is-ready', function() {
parent.emit('log', 'window became ready: ' + win.webContents.getURL())
done()
})
}
})
/**
* Authentication
*/
var loginListener
parent.respondTo('authentication', function(login, password, done) {
var currentUrl
var tries = 0
if (loginListener) {
win.webContents.removeListener('login', loginListener)
}
loginListener = function(webContents, request, authInfo, callback) {
tries++
parent.emit('log', `authenticating against ${request.url}, try #${tries}`)
if (currentUrl != request.url) {
currentUrl = request.url
tries = 1
}
if (tries >= options.maxAuthRetries) {
parent.emit('die', 'problem authenticating, check your credentials')
} else {
callback(login, password)
}
}
win.webContents.on('login', loginListener)
done()
})
/**
* Kill the electron app
*/
parent.respondTo('quit', function(done) {
app.quit()
done()
})
/**
* Send "ready" event to the parent process
*/
parent.emit('ready', {
electron: process.versions['electron'],
chrome: process.versions['chrome']
})
/**
* Check whether the window is ready for interaction
*/
function isReady() {
return win[IS_READY]
}
/**
* Set whether the window is ready for interaction
*/
function setIsReady(ready) {
ready = !!ready
if (ready !== win[IS_READY]) {
win[IS_READY] = ready
win.emit('did-change-is-ready', ready)
}
}
/**
* Forward events
*/
function forward(name) {
return function(_event) {
// NOTE: the raw Electron event used to be forwarded here, but we now send
// an empty event in its place -- the raw event is not JSON serializable.
if (!closed) {
parent.emit.apply(parent, [name, {}].concat(sliced(arguments, 1)))
}
}
}
})
================================================
FILE: package.json
================================================
{
"name": "nightmare",
"version": "3.0.2",
"license": "MIT",
"main": "lib/nightmare.js",
"scripts": {
"test": "make test",
"precommit": "make test"
},
"repository": {
"type": "git",
"url": "https://github.com/segmentio/nightmare.git"
},
"author": "Segment",
"keywords": [
"nightmare",
"electron"
],
"description": "A high-level browser automation library.",
"dependencies": {
"debug": "^2.2.0",
"deep-defaults": "^1.0.3",
"defaults": "^1.0.2",
"electron": "^2.0.18",
"enqueue": "^1.0.2",
"function-source": "^0.1.0",
"jsesc": "^0.5.0",
"minstache": "^1.2.0",
"mkdirp": "^0.5.1",
"multiline": "^1.0.2",
"once": "^1.3.3",
"rimraf": "^2.4.3",
"sliced": "1.0.1",
"split2": "^2.0.1"
},
"devDependencies": {
"async": "~2.1.4",
"basic-auth": "^1.0.3",
"basic-auth-connect": "^1.0.0",
"bluebird": "^3.4.0",
"chai": "^3.4.1",
"chai-as-promised": "^5.3.0",
"eslint": "4.18.1",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-prettier": "^2.6.0",
"express": "4.16.2",
"husky": "^0.14.3",
"mocha": "^2.3.0",
"mocha-generators": "^1.2.0",
"multer": "1.1.0",
"pngjs": "^2.2.0",
"prettier": "1.11.0",
"serve-static": "^1.10.0",
"split": "^1.0.0"
},
"engines": {
"node": ">=4.0.0"
}
}
================================================
FILE: test/Preferences
================================================
{"brightray":{"media":{"device_id_salt":"5F9qZMOFUibhFqg6msGAAQ=="}}}
================================================
FILE: test/bb-xvfb
================================================
#!/bin/sh
#
# bb-xvfb <command> <and> <its> <arguments...>
#
# Wrapper for XVFB. Capture XVFB logs where we can see them. Dump
# XVFB framebuffer where can we can see it. Use a standard display size
# and dpi for all of our XVFB-wrapped tests.
#
# pulled from: https://gist.github.com/tullmann/2d8d38444c5e81a41b6d
# original issue: https://github.com/angular/protractor/issues/2419#issuecomment-156527809
# Note: "32" is not a valid depth for xvfb: https://bugs.freedesktop.org/show_bug.cgi?id=17453 (from 2008)
SCREEN_SIZE="1280x1024x24+32"
BINDIR="$(dirname $0)"
TMPDIR=/tmp
if [ ! -d "$TMPDIR" ]; then
echo "Invalid tmp directory: ${TMPDIR}"
exit 1
fi
# Avoid race conditions in google-chrome startup by making sure there is a
# dbus message bus up and running.
# See https://code.google.com/p/chromium/issues/detail?id=309093
DBUS="dbus-launch --exit-with-session"
WAITFORX="${BINDIR}/waitForX"
XVFBDIR="${TMPDIR}/xvfb.$$"
mkdir -p "${XVFBDIR}"
ERRFILE="${XVFBDIR}/xvfb.log"
# Pass the "-fbdir" to force xvfb to use a memory-mapped file. This makes screenshots and debugging easier.
# To display said framebuffer contents: xwud -in ${XVFBDIR}/Xvfb_screen0
exec xvfb-run \
--error-file="${ERRFILE}" \
--auto-servernum \
--server-args="-fbdir ${XVFBDIR} -screen 0 ${SCREEN_SIZE} -dpi 96 -ac" \
${WAITFORX} ${DBUS} "$@"
#eof
================================================
FILE: test/files/globals.js
================================================
/** @type {Comment} [description] */
globalNumber = 7;
================================================
FILE: test/files/nightmare-created.js
================================================
// This script is used to create a nightmare run but not start it.
var Nightmare = require('../..');
var nightmare = Nightmare();
nightmare
.goto('about:blank');
process.send('ready');
================================================
FILE: test/files/nightmare-error.js
================================================
// This script is used to start nightmare
// but then throw a user space error
var Nightmare = require('../..');
var nightmare = Nightmare();
throw new Error("uncaught");
================================================
FILE: test/files/nightmare-unended.js
================================================
// This script is used to start a nightmare run but not end it. It reports its
// Electron process's pid, then we kill it and test to see whether that pid is
// still running.
var Nightmare = require('../..');
var nightmare = Nightmare();
nightmare
.goto('about:blank')
.run(function() {
process.send(nightmare.proc.pid);
});
================================================
FILE: test/files/server.crt
================================================
-----BEGIN CERTIFICATE-----
MIIB5zCCAVACCQC4FTxcJ7FcSTANBgkqhkiG9w0BAQUFADA4MRMwEQYDVQQIEwpT
b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcN
MTYwNTA2MTY1MTAwWhcNMTYwNjA1MTY1MTAwWjA4MRMwEQYDVQQIEwpTb21lLVN0
YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwgZ8wDQYJKoZI
hvcNAQEBBQADgY0AMIGJAoGBAKx2SRBWYxFprra2H0z6jnSqDYBjRNGj2zO8J5NC
ii9nNrgVJyrloKFQJgvUE+V9nHdzgxNpX6Ja4pWM3+saiifL1+TuOFjEaKofDSo+
AycoVVNFnUuqYVxd0COhVl5xHrza9Zs6Jf6DaDVPyFRgD7hyPTLp1Y9+7PTJOb3r
ICpRAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEASWRRZpfydjB1PGuHECN3B2phBLBi
ZG3DMwC4a7wHv9eA2l9ZNkfvJCeM0/1MuP/RrzKBKPNJ4YqnllRR4Y7njOVgjKye
8HdLdbbB0Rv8EY5knCiA8+YyIu6pIpTWV5eUOwPokP/uaLNWzVU3980ZXaNGzXrX
NY/j3UYG5Ms9Y8c=
-----END CERTIFICATE-----
================================================
FILE: test/files/server.key
================================================
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCsdkkQVmMRaa62th9M+o50qg2AY0TRo9szvCeTQoovZza4FScq
5aChUCYL1BPlfZx3c4MTaV+iWuKVjN/rGoony9fk7jhYxGiqHw0qPgMnKFVTRZ1L
qmFcXdAjoVZecR682vWbOiX+g2g1T8hUYA+4cj0y6dWPfuz0yTm96yAqUQIDAQAB
AoGALpHuXuQE8nHIRPxe7WmHSEeXR8EGl1mY2pqHUUOZjv1fEExd/D5vpr++1ljZ
WpIVy0e88GP2+B90qg+Vc6YCAhUtu+6Z8yTBdfukbfr2A/CeJk+QEuA0WJ7wNQxZ
Dwy8mjAe8c7oXC75e1LvZqVjCrg79HTRMPAPZcRCVlQISfECQQDkqelARmeRRebv
liib8o0KQYXcipBkahMjp6iiVnvgPKwiZsalJHEZtDoV53eAv1HfNy5j9ZrdpkL/
EZTzuL4NAkEAwRRc2vqGKFmeycPLwc1/N286ERKUneoU1ASic1iKKzkT6Er8tk3B
rDioHUxOSWVRC5mdsHtDYLXz8kiGnLlQVQJAdClw6gsaH+2/5KSGmrp8NeKVazUl
Jy3P7UQF4fpHUeHgnFVTwp8hqaop++irh8cpg1jYA0XI16LX1BYNcka+nQJBAIKV
ZwejEEER+9ax2YjFlxjC3R7W1jTHMDcEu2oPo8L/43rj3G7fv/DekLTf+sKhB2M1
DfViKHusE8T1UDWHD9ECQQC0uCNK9hkOrnlntJNeuBRlDdtgRdkwfU63QrIlatLD
eKR7hVMN8JhHt8ppbqbdtbdRx2tpcSFKWCN7CBqVtN+f
-----END RSA PRIVATE KEY-----
================================================
FILE: test/files/test.css
================================================
body, html {
background-color: #FF0000;
}
================================================
FILE: test/fixtures/cookies/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Cookies</title>
</head>
<body>
<h1 class="title">Hello World!</h1>
<span class="subtitle">Welcome to the Cookies Test!</span>
</body>
</html>
================================================
FILE: test/fixtures/evaluation/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Evaluation</title>
</head>
<body>
<h1 class="title">Hello World!</h1>
<span class="subtitle">Welcome to the Evaluation Test!</span>
<div class="hidden" style="display: none;">HIDDEN</div>
</body>
</html>
================================================
FILE: test/fixtures/events/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Options</title>
</head>
<body>
<h1>Hello World!</h1>
<button>click</button>
</body>
<script>
console.log('my log');
document.querySelector('button').addEventListener('click', function() {
console.log('clicked');
});
thisIsAnError(foobar);
</script>
</html>
================================================
FILE: test/fixtures/manipulation/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Manipulation</title>
</head>
<body>
<h1>Hello World!</h1>
<h2>this is nightmare</h2>
<form action="results.html">
<input type="search" name="q">
<input type="checkbox" name="advanced">
<input type='text' id='disappears'>
<select name="options">
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</select>
<button type="submit">Search</button>
<div data-test="test">attribute div</div>
</form>
<p style="width: 1000px;">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent suscipit
vitae eros ac rutrum. Sed tincidunt mi ut nulla placerat molestie. Nulla a
augue ante. Maecenas semper feugiat ante, nec mattis nisl sollicitudin
quis. Nulla facilisi. Nullam sollicitudin dignissim arcu, a finibus elit
vestibulum a. Cras felis libero, aliquam nec eleifend ut, vulputate sit
amet ex.
</p>
<p>
Nam faucibus porta elit ut varius. Sed sem diam, elementum nec dui sed,
porttitor semper odio. Pellentesque tempus tortor non velit convallis
gravida. Curabitur in erat laoreet, porta libero eget, hendrerit purus.
Praesent vitae nunc in neque feugiat condimentum a eget dolor. Nullam
mauris dui, ornare et commodo sit amet, iaculis vel ipsum. Morbi molestie
at lectus dapibus pulvinar. Duis at quam vel dolor molestie aliquet ac et
ipsum. Morbi urna sapien, tempus vel lorem quis, facilisis rutrum elit. Ut
vitae ultrices dui, ac aliquam tortor. Maecenas aliquam mi quis egestas
tempus. Donec id sapien a tortor suscipit vulputate. Cras lacus lacus,
euismod in porta et, pellentesque nec quam. Nulla sagittis fermentum
convallis. Duis eu urna tristique, maximus dui at, auctor tortor.
</p>
<p>
In nec leo auctor, pharetra ipsum et, molestie eros. Donec sed luctus
ante, vitae posuere sapien. Integer dignissim nibh eget tortor viverra
sodales. Vivamus condimentum tortor ut metus convallis aliquet. Donec nec
sagittis neque. Suspendisse potenti. Pellentesque tempor imperdiet
blandit. Morbi cursus, risus in dapibus finibus, diam nunc pharetra elit,
sed vehicula dui lectus eget purus. Aliquam erat volutpat. Fusce ipsum
metus, imperdiet quis blandit in, dapibus nec felis. Ut tincidunt arcu
augue, at commodo nulla aliquam id. Curabitur elit augue, malesuada et
faucibus in, rutrum sit amet elit. Cras aliquam iaculis libero non
molestie. Pellentesque feugiat pulvinar justo eget venenatis. Donec mi
metus, tincidunt vitae rhoncus sit amet, volutpat a diam. Quisque eleifend
ipsum eu ex ornare, sed tristique purus semper.
</p>
<p>
In consequat consequat urna non egestas. Vestibulum ante ipsum primis in
faucibus orci luctus et ultrices posuere cubilia Curae; Donec ut dignissim
augue, congue pharetra sem. Interdum et malesuada fames ac ante ipsum
primis in faucibus. Suspendisse feugiat sapien vitae commodo hendrerit.
Interdum et malesuada fames ac ante ipsum primis in faucibus. Nam
sagittis, tellus vel tempus feugiat, turpis lorem sollicitudin tortor, et
laoreet orci urna efficitur diam. Vivamus cursus nibh et erat gravida
consequat. Morbi fringilla mollis aliquam. Nullam placerat urna vitae
tellus molestie, non sodales sapien hendrerit. Donec feugiat ante at
consequat volutpat. Vestibulum nunc velit, aliquet non mi et, tincidunt
dapibus leo.
</p>
<p>
Morbi ligula diam, dapibus eget tincidunt nec, bibendum eu ligula. Cras
vel bibendum est. Aliquam in gravida justo. Ut varius lobortis tempor.
Proin sed lorem justo. Curabitur vestibulum erat vitae lacinia elementum.
Nunc ligula nisl, euismod et mi eget, viverra varius neque. Mauris turpis
justo, volutpat sed turpis sit amet, porta tempus ligula. Duis nulla
dolor, fermentum eget convallis id, vehicula quis felis. Mauris augue
risus, facilisis non quam vitae, fringilla egestas risus. Donec rutrum
quis massa non consectetur. Pellentesque mattis nibh congue erat congue
dictum. Etiam facilisis arcu id porttitor ullamcorper. Ut lobortis sodales
euismod. Aliquam molestie eros vel arcu vehicula, in vehicula nibh
pretium. Donec vulputate turpis nunc, non tempor urna tincidunt a.
</p>
</body>
<script>
var h1 = document.getElementsByTagName("h1");
var h2 = document.getElementsByTagName("h2");
var search = document.querySelector('input[type=search]')
var disappears = document.querySelector('#disappears');
disappears.addEventListener('keydown', function(e){
if(e.which == 69){
var form = document.querySelector('form');
form.removeChild(disappears);
}
});
h1[0].addEventListener("mouseover", function(){
this.style.background = "#66ff66";
});
h1[0].addEventListener("mousedown", function(){
this.style.background = "#ff0000";
})
h1[0].addEventListener("mouseup", function(){
this.style.background = "#0000ff";
})
h2[0].addEventListener("mouseover", function(){
this.style.background = "#00ffff";
})
h2[0].addEventListener("mouseout", function(){
this.style.background = "";
})
search.addEventListener("keyup", function(e) {
console.log('keyup', e.which)
})
search.addEventListener("keydown", function(e) {
console.log('keydown', e.which)
})
search.addEventListener('input', function(e) {
console.log('input', e.which)
})
</script>
</html>
================================================
FILE: test/fixtures/manipulation/result.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Manipulation - Result - Nightmare</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
================================================
FILE: test/fixtures/manipulation/results.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Manipulation - Results</title>
</head>
<body>
<h1>Hello World!</h1>
<ol class="results">
<li><a href="result.html">Nightmare!</a></li>
</ol>
</body>
</html>
================================================
FILE: test/fixtures/navigation/a.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>A</title>
</head>
<body>
<a class="a" href="a.html">A</a>
<a class="b" href="b.html">B</a>
<a class="c" href="c.html">C</a>
<a class="d" href="#">D</a>
</body>
</html>
================================================
FILE: test/fixtures/navigation/b.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>B</title>
</head>
<body>
<a class="a" href="a.html">A</a>
<a class="b" href="b.html">B</a>
<a class="c" href="c.html">C</a>
</body>
</html>
================================================
FILE: test/fixtures/navigation/c.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>C</title>
</head>
<body>
<a class="a" href="a.html">A</a>
<a class="b" href="b.html">B</a>
<a class="c" href="c.html">C</a>
</body>
</html>
================================================
FILE: test/fixtures/navigation/hanging-resources.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Hanging resource load</title>
</head>
<body>
<img src="/wait" />
</body>
</html>
================================================
FILE: test/fixtures/navigation/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Navigation</title>
</head>
<body>
<a class="a" href="a.html">A</a>
<a class="b" href="b.html">B</a>
<a class="c" href="c.html" id="escaping:test">C</a>
<a id='never-ends' href='/never-ends'>does not end</a>
</body>
</html>
================================================
FILE: test/fixtures/navigation/invalid-frame.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Navigation</title>
</head>
<body>
<iframe src="http://this-is-not-a-real-domain.com/non-existent.html" width="300" height="300">
</iframe>
</body>
</html>
================================================
FILE: test/fixtures/navigation/invalid-image.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Navigation</title>
</head>
<body>
<img src="http://this-is-not-a-real-domain.com/non-existent.jpg" />
</body>
</html>
================================================
FILE: test/fixtures/navigation/valid-frame.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Navigation</title>
</head>
<body>
<iframe src="/navigation" width="300" height="300">
</iframe>
</body>
</html>
================================================
FILE: test/fixtures/options/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Options</title>
</head>
<body>
<h1>Hello World!</h1>
<iframe id='example-iframe' src='http://example.com'></iframe>
</body>
</html>
================================================
FILE: test/fixtures/preload/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Simple</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
================================================
FILE: test/fixtures/preload/index.js
================================================
/* eslint-disable no-console */
var ipc = require('electron').ipcRenderer
var sliced = require('sliced')
function send(_event) {
ipc.send.apply(ipc, arguments)
}
// offer limited access to allow
// .evaluate() and .inject()
// to continue to work as expected.
//
// TODO: this could be avoided by
// rewriting the evaluate to
// use promises instead. But
// for now this fixes the security
// issue in: segmentio/nightmare/#1358
window.__nightmare = {
resolve: function(value) {
send('response', value)
},
reject: function(message) {
send('error', message)
}
}
// Listen for error events
window.addEventListener(
'error',
function(e) {
send('page', 'error', e.message, (e.error || {}).stack || '')
},
true
)
// prevent 'unload' and 'beforeunload' from being bound
var defaultAddEventListener = window.addEventListener
window.addEventListener = function(type) {
if (type === 'unload' || type === 'beforeunload') {
return
}
defaultAddEventListener.apply(window, arguments)
}
// prevent 'onunload' and 'onbeforeunload' from being set
Object.defineProperties(window, {
onunload: {
enumerable: true,
writable: false,
value: null
},
onbeforeunload: {
enumerable: true,
writable: false,
value: null
}
})
// listen for console.log
var defaultLog = console.log
console.log = function() {
send('console', 'log', sliced(arguments))
return defaultLog.apply(this, arguments)
}
// listen for console.warn
var defaultWarn = console.warn
console.warn = function() {
send('console', 'warn', sliced(arguments))
return defaultWarn.apply(this, arguments)
}
// listen for console.error
var defaultError = console.error
console.error = function() {
send('console', 'error', sliced(arguments))
return defaultError.apply(this, arguments)
}
// overwrite the default alert
window.alert = function(message) {
send('page', 'alert', message)
}
// overwrite the default prompt
window.prompt = function(message, defaultResponse) {
send('page', 'prompt', message, defaultResponse)
}
// overwrite the default confirm
window.confirm = function(message, defaultResponse) {
send('page', 'confirm', message, defaultResponse)
}
window.preload = 'custom'
================================================
FILE: test/fixtures/rendering/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Nightmare not lading fonts - DEMO</title>
<style>
body {
font-family: Oswald;
}
</style>
<link href='http://fonts.googleapis.com/css?family=Oswald' rel='stylesheet'>
</head>
<body>
<h1>This should be written in Oswald.</h1>
<script src="http://code.jquery.com/jquery.min.js"></script>
</body>
</html>
================================================
FILE: test/fixtures/security/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Simple</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
<script type="text/javascript">
console.log(typeof ipc, typeof window.ipc, typeof send, typeof window.send)
</script>
</html>
================================================
FILE: test/fixtures/simple/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Simple</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
================================================
FILE: test/fixtures/unload/add-event-listener.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Unload</title>
<script>
var unloadHandler = function (event) {
var confirmationMessage = 'Are you sure?';
event.returnValue = confirmationMessage;
return confirmationMessage;
};
window.addEventListener('unload', unloadHandler);
window.addEventListener('beforeunload', unloadHandler);
</script>
</head>
<body>
</body>
</html>
================================================
FILE: test/fixtures/unload/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<title>Unload</title>
<script>
window.onunload = window.onbeforeunload = function () {
return 'Are you sure?';
};
</script>
</head>
<body>
</body>
</html>
================================================
FILE: test/index.js
================================================
/* global it, describe, before, beforeEach, after, afterEach */
/**
* Module dependencies.
*/
require('mocha-generators').install()
var Nightmare = require('..')
var IPC = require('../lib/ipc')
var chai = require('chai')
var url = require('url')
var server = require('./server')
var https = require('https')
var fs = require('fs')
var mkdirp = require('mkdirp')
var path = require('path')
var rimraf = require('rimraf')
var child_process = require('child_process')
var PNG = require('pngjs').PNG
var should = chai.should()
var split = require('split')
var asPromised = require('chai-as-promised')
var assert = require('assert')
chai.use(asPromised)
/**
* Temporary directory
*/
var tmp_dir = path.join(__dirname, 'tmp')
/**
* Get rid of a warning.
*/
process.setMaxListeners(0)
/**
* Locals.
*/
var base = 'http://localhost:7500/'
describe('Nightmare', function() {
before(function(done) {
server.listen(7500, done)
Nightmare = withDeprecationTracking(Nightmare)
})
after(function() {
Nightmare.assertNoDeprecations()
})
it('should be constructable', function*() {
var nightmare = Nightmare()
nightmare.should.be.ok
yield nightmare.end()
})
it('should have version information', function*() {
var nightmare = Nightmare()
var versions = yield nightmare.engineVersions()
nightmare.engineVersions.electron.should.be.ok
nightmare.engineVersions.chrome.should.be.ok
versions.electron.should.be.ok
versions.chrome.should.be.ok
Nightmare.version.should.be.ok
yield nightmare.end()
})
it('should kill its electron process when it is killed', function(done) {
var child = child_process.fork(
path.join(__dirname, 'files', 'nightmare-unended.js')
)
child.once('message', function(electronPid) {
child.once('exit', function() {
try {
electronPid.should.not.be.a.process
} catch (error) {
// if the test failed, clean up the still-running process
process.kill(electronPid, 'SIGINT')
throw error
}
done()
})
child.kill()
})
})
it('should gracefully handle electron being killed', function(done) {
var child = child_process.fork(
path.join(__dirname, 'files', 'nightmare-unended.js')
)
child.once('message', function(electronPid) {
process.kill(electronPid, 'SIGINT')
child.once('exit', function() {
electronPid.should.not.be.a.process
done()
})
})
})
it('should end gracefully if the chain has not been started', function(done) {
var child = child_process.fork(
path.join(__dirname, 'files', 'nightmare-created.js')
)
child.once('message', function() {
child.once('exit', function(code) {
code.should.equal(0)
done()
})
child.kill()
})
})
it('should exit with a non-zero code on uncaughtExecption', function(done) {
var child = child_process.fork(
path.join(__dirname, 'files', 'nightmare-error.js'),
[],
{ silent: true }
)
child.once('exit', function(code) {
code.should.not.equal(0)
done()
})
})
it('should provide a .catch function', function(done) {
var nightmare = Nightmare()
nightmare
.goto('about:blank')
.evaluate(function() {
throw new Error('Test')
})
.catch(function(err) {
assert.equal(err instanceof Error, true)
assert.equal(err.message, 'Test')
done()
})
})
it('should allow ending more than once', function(done) {
var nightmare = Nightmare()
nightmare
.goto(fixture('navigation'))
.end()
.then(() => nightmare.end())
.then(() => done())
})
it('should allow end with a callback', function(done) {
var nightmare = Nightmare()
nightmare.goto(fixture('navigation')).end(() => done())
})
it('should allow end with a callback to be thenable', function(done) {
var nightmare = Nightmare()
nightmare
.goto(fixture('navigation'))
.end(() => 'nightmare')
.then(str => {
str.should.equal('nightmare')
done()
})
})
it('should kill electron process when halted', function() {
var nightmare = Nightmare()
const check1 = nightmare
.goto(fixture('navigation'))
.wait(1000)
.wait(500)
.end()
.then(() => {})
.should.be.rejectedWith('Nightmare Halted')
const electronPid = nightmare.proc.pid
const check2 = new Promise((resolve, _reject) => {
nightmare.halt('Nightmare Halted', () => {
electronPid.should.not.be.a.process
resolve()
})
})
return Promise.all([check1, check2])
})
it('should successfully end on pages setting onunload or onbeforeunload', function(done) {
var nightmare = Nightmare()
nightmare
.goto(fixture('unload'))
.end()
.then(() => done())
})
it('should successfully end on pages binding unload or beforeunload', function(done) {
var nightmare = Nightmare()
nightmare
.goto(fixture('unload/add-event-listener.html'))
.end()
.then(() => done())
})
it('should provide useful errors for .click', function(done) {
var nightmare = Nightmare()
nightmare
.goto('about:blank')
.click('a.not-here')
.catch(function(err) {
assert.equal(err instanceof Error, true)
err.message.should.include('a.not-here')
done()
})
})
it('should provide useful errors for .mousedown', function(done) {
var nightmare = Nightmare()
nightmare
.goto('about:blank')
.mousedown('a.not-here')
.catch(function(err) {
assert.equal(err instanceof Error, true)
err.message.should.include('a.not-here')
done()
})
})
it('should provide useful errors for .mouseup', function(done) {
var nightmare = Nightmare()
nightmare
.goto('about:blank')
.mouseup('a.not-here')
.catch(function(err) {
assert.equal(err instanceof Error, true)
err.message.should.include('a.not-here')
done()
})
})
it('should provide useful errors for .mouseover', function(done) {
var nightmare = Nightmare()
nightmare
.goto('about:blank')
.mouseover('a.not-here')
.catch(function(err) {
assert.equal(err instanceof Error, true)
err.message.should.include('a.not-here')
done()
})
})
describe('navigation', function() {
var nightmare
beforeEach(function() {
nightmare = Nightmare({
webPreferences: { partition: 'test-partition' + Math.random() },
loadTimeout: 45 * 1000,
waitTimeout: 5 * 1000
})
})
afterEach(function*() {
yield nightmare.end()
Nightmare.resetActions()
})
it('should return data about the response', function*() {
var data = yield nightmare.goto(fixture('navigation'))
data.should.contain.keys('url', 'code', 'method', 'referrer', 'headers')
})
it('should reject with a useful message when no URL', function() {
return nightmare.goto(undefined).then(
function() {
throw new Error('goto(undefined) didn’t cause an error')
},
function(err) {
assert.equal(err instanceof Error, true)
err.message.should.include('url')
}
)
})
it('should reject with a useful message for an empty URL', function() {
return nightmare.goto('').then(
function() {
throw new Error('goto(undefined) didn’t cause an error')
},
function(err) {
assert.equal(err instanceof Error, true)
err.message.should.include('url')
}
)
})
it('should click on a link and then go back', function*() {
var title = yield nightmare
.goto(fixture('navigation'))
.click('a')
.title()
title.should.equal('A')
title = yield nightmare.back().title()
title.should.equal('Navigation')
})
it('should work for links that dont go anywhere', function*() {
var title = yield nightmare
.goto(fixture('navigation'))
.click('a')
.title()
title.should.equal('A')
title = yield nightmare.click('.d').title()
title.should.equal('A')
})
it('should click on a link, go back, and then go forward', function*() {
yield nightmare
.goto(fixture('navigation'))
.click('a')
.back()
.forward()
})
it('should refresh the page', function*() {
yield nightmare.goto(fixture('navigation')).refresh()
})
it('should wait until element is present', function*() {
yield nightmare.goto(fixture('navigation')).wait('a')
})
it('should soft timeout if element does not appear', function*() {
yield nightmare.goto(fixture('navigation')).wait('ul', 150)
})
it('should wait until element is present with a modified poll interval', function*() {
nightmare = Nightmare({
pollInterval: 50
})
yield nightmare.goto(fixture('navigation')).wait('a')
})
it('should escape the css selector correctly when waiting for an element', function*() {
yield nightmare.goto(fixture('navigation')).wait('#escaping\\:test')
})
it('should wait until the evaluate fn returns true', function*() {
yield nightmare.goto(fixture('navigation')).wait(function() {
var text = document.querySelector('a').textContent
return text === 'A'
})
})
it('should wait until the evaluate fn with arguments returns true', function*() {
yield nightmare.goto(fixture('navigation')).wait(
function(expectedA, expectedB) {
var textA = document.querySelector('a.a').textContent
var textB = document.querySelector('a.b').textContent
return expectedA === textA && expectedB === textB
},
'A',
'B'
)
})
describe('asynchronous wait', function() {
it('should wait until the evaluate fn with arguments returns true with a callback', function*() {
yield nightmare.goto(fixture('navigation')).wait(
function(expectedA, expectedB, done) {
setTimeout(() => {
var textA = document.querySelector('a.a').textContent
var textB = document.querySelector('a.b').textContent
done(null, expectedA === textA && expectedB === textB)
}, 2000)
},
'A',
'B'
)
})
it('should wait until the evaluate fn with arguments returns true with a promise', function*() {
yield nightmare.goto(fixture('navigation')).wait(
function(expectedA, expectedB) {
return new Promise(function(resolve) {
setTimeout(() => {
var textA = document.querySelector('a.a').textContent
var textB = document.querySelector('a.b').textContent
resolve(expectedA === textA && expectedB === textB)
}, 2000)
})
},
'A',
'B'
)
})
it('should reject timeout on wait', function*() {
yield nightmare.goto(fixture('navigation')).wait(function(_done) {
//never call done
}).should.be.rejected
})
it('should reject timeout on wait with selector', function*() {
yield nightmare
.goto(fixture('navigation'))
.wait('#non-existent')
.should.be.rejected.then(function(error) {
error.message.should.include('#non-existent')
})
})
it('should run multiple times before timeout on wait', function*() {
yield nightmare.goto(fixture('navigation')).wait(function(done) {
setTimeout(() => done(null, false), 500)
}).should.be.rejected
})
})
it('should fail if navigation target is invalid', function() {
return nightmare.goto('http://this-is-not-a-real-domain.tld').then(
function() {
throw new Error('Navigation to an invalid domain succeeded')
},
function(err) {
assert.equal(err instanceof Error, true)
assert.equal(err.message, 'navigation error')
assert.equal(err.details, 'ERR_NAME_NOT_RESOLVED')
assert.equal(err.code, -105)
assert.equal(err.url, 'http://this-is-not-a-real-domain.tld/')
}
)
})
it('should fail if navigation target is a malformed URL', function(done) {
nightmare
.goto('somewhere out there')
.then(function() {
done(new Error('Navigation to an invalid domain succeeded'))
})
.catch(function(_error) {
done()
})
})
it('should fail if navigating to an unknown protocol', function(done) {
nightmare
.goto('fake-protocol://blahblahblah')
.then(function() {
done(new Error('Navigation to an invalid protocol succeeded'))
})
.catch(function(_error) {
done()
})
})
it('should not fail if the URL loads but a resource fails', function() {
return nightmare.goto(fixture('navigation/invalid-image'))
})
it('should not fail if a child frame fails', function() {
return nightmare.goto(fixture('navigation/invalid-frame'))
})
it('should return correct data when child frames are present', function*() {
var data = yield nightmare.goto(fixture('navigation/valid-frame'))
data.should.have.property('url')
data.url.should.equal(fixture('navigation/valid-frame'))
})
it('should not fail if response was a valid error (e.g. 404)', function() {
return nightmare.goto(fixture('navigation/not-a-real-page'))
})
it('should fail if the response dies in flight', function(done) {
nightmare
.goto(fixture('do-not-respond'))
.then(function() {
done(new Error('Navigation succeeded but server connection died'))
})
.catch(function(_error) {
done()
})
})
it('should not fail for a redirect', function() {
return nightmare.goto(fixture('redirect?url=%2Fnavigation'))
})
it('should fail for a redirect to an invalid URL', function(done) {
nightmare
.goto(
fixture('redirect?url=http%3A%2F%2Fthis-is-not-a-real-domain.tld')
)
.then(function() {
done(new Error('Navigation succeeded with redirect to bad location'))
})
.catch(function(_error) {
done()
})
})
it('should succeed properly if request handler is present', function() {
Nightmare.action(
'monitorRequest',
function(name, options, parent, win, renderer, done) {
win.webContents.session.webRequest.onBeforeRequest(
['*://localhost:*'],
function(details, callback) {
callback({ cancel: false })
}
)
done()
},
function(done) {
done()
return this
}
)
return Nightmare({ webPreferences: { partition: 'test-partition' } })
.goto(fixture('navigation'))
.end()
})
it('should fail properly if request handler is present', function(done) {
Nightmare.action(
'monitorRequest',
function(name, options, parent, win, renderer, done) {
win.webContents.session.webRequest.onBeforeRequest(
['*://localhost:*'],
function(details, callback) {
callback({ cancel: false })
}
)
done()
},
function(done) {
done()
return this
}
)
Nightmare({ webPreferences: { partition: 'test-partition' } })
.goto('http://this-is-not-a-real-domain.tld')
.then(function() {
done(new Error('Navigation to an invalid domain succeeded'))
})
.catch(function(_error) {
done()
})
})
it('should support javascript URLs', function*() {
var gotoResult = yield nightmare
.goto(fixture('navigation'))
.goto(
'javascript:void(document.querySelector(".a").textContent="LINK");'
)
gotoResult.should.be.an('object')
var linkText = yield nightmare.evaluate(function() {
return document.querySelector('.a').textContent
})
linkText.should.equal('LINK')
})
it('should support javascript URLs that load pages', function*() {
var data = yield nightmare
.goto(fixture('navigation'))
.goto(`javascript:window.location='${fixture('navigation/a.html')}'`)
data.should.contain.keys('url', 'code', 'method', 'referrer', 'headers')
data.url.should.equal(fixture('navigation/a.html'))
var linkText = yield nightmare.evaluate(function() {
return document.querySelector('.d').textContent
})
linkText.should.equal('D')
})
it('should fail immediately/not time out for 304 statuses', function() {
return Nightmare({ gotoTimeout: 500 })
.goto(fixture('not-modified'))
.end()
.then(
function() {
throw new Error('Navigating to a 304 should return an error')
},
function(error) {
if (error.code === -7) {
throw new Error('Navigating to a 304 should not time out')
}
}
)
})
it('should not time out for aborted loads', function() {
Nightmare.action(
'abortRequests',
function(name, options, parent, win, renderer, done) {
win.webContents.session.webRequest.onBeforeRequest(
['*://localhost:*'],
function(details, callback) {
setTimeout(() => win.webContents.stop(), 0)
callback({ cancel: false })
}
)
done()
},
function() {}
)
return Nightmare({ gotoTimeout: 500 })
.goto(fixture('navigation'))
.end()
.then(
function() {
throw new Error('An aborted page load should return an error')
},
function(error) {
if (error.code === -7) {
throw new Error('Aborting a page load should not time out')
}
}
)
})
describe('timeouts', function() {
it('should time out after 30 seconds of loading', function() {
// allow this test to go particularly long
this.timeout(40000)
return nightmare
.goto(fixture('wait'))
.should.be.rejected.then(function(error) {
error.code.should.equal(-7)
})
})
it('should allow custom goto timeout on the constructor', function() {
var startTime = Date.now()
return Nightmare({ gotoTimeout: 1000 })
.goto(fixture('wait'))
.end()
.should.be.rejected.then(function(_error) {
// allow a few extra seconds for browser startup
;(startTime - Date.now()).should.be.below(3000)
})
})
it('should allow a timeout to succeed if DOM loaded', function() {
return Nightmare({ gotoTimeout: 1000 })
.goto(fixture('navigation/hanging-resources.html'))
.end()
.then(function(data) {
data.details.should.include('1000 ms')
})
})
it('should allow actions on a hanging page', function() {
return Nightmare({ gotoTimeout: 500 })
.goto(fixture('navigation/hanging-resources.html'))
.evaluate(() => document.title)
.end()
.then(function(title) {
title.should.equal('Hanging resource load')
})
})
it('should allow loading a new page after timing out', function() {
nightmare.end().then()
nightmare = Nightmare({ gotoTimeout: 1000 })
return nightmare
.goto(fixture('wait'))
.should.be.rejected.then(function() {
return nightmare.goto(fixture('navigation'))
})
})
it('should allow for timeouts for non-goto loads', function*() {
// ###
this.timeout(40000)
var nightmare = Nightmare({ loadTimeout: 30000 })
yield nightmare.goto(fixture('navigation')).click('#never-ends')
yield nightmare.end()
})
})
})
describe('evaluation', function() {
var nightmare
beforeEach(function() {
nightmare = Nightmare()
})
afterEach(function*() {
yield nightmare.end()
})
it('should get the title', function*() {
var title = yield nightmare.goto(fixture('evaluation')).title()
title.should.eql('Evaluation')
})
it('should get the url', function*() {
var url = yield nightmare.goto(fixture('evaluation')).url()
url.should.have.string(fixture('evaluation'))
})
it('should get the path', function*() {
var path = yield nightmare.goto(fixture('evaluation')).path()
var formalUrl = fixture('evaluation') + '/'
formalUrl.should.have.string(path)
})
it('should check if the selector exists', function*() {
// existent element
var exists = yield nightmare
.goto(fixture('evaluation'))
.exists('h1.title')
exists.should.be.true
// non-existent element
exists = yield nightmare.exists('a.blahblahblah')
exists.should.be.false
})
it('should check if an element is visible', function*() {
// visible element
var visible = yield nightmare
.goto(fixture('evaluation'))
.visible('h1.title')
visible.should.be.true
// hidden element
visible = yield nightmare.visible('.hidden')
visible.should.be.false
// non-existent element
visible = yield nightmare.visible('#asdfasdfasdf')
visible.should.be.false
})
it('should evaluate javascript on the page, with parameters', function*() {
var title = yield nightmare
.goto(fixture('evaluation'))
.evaluate(function(parameter) {
return document.title + ' -- ' + parameter
}, 'testparameter')
title.should.equal('Evaluation -- testparameter')
})
it('should capture invalid evaluate fn', function() {
return nightmare.goto(fixture('evaluation')).evaluate('not_a_function')
.should.be.rejected
})
describe('asynchronous', function() {
it('should allow for asynchronous evaluation with a callback', function*() {
var asyncValue = yield nightmare
.goto(fixture('evaluation'))
.evaluate(function(done) {
setTimeout(() => done(null, 'nightmare'), 1000)
})
asyncValue.should.equal('nightmare')
})
it('should allow for arguments with asynchronous evaluation with a callback', function*() {
var asyncValue = yield nightmare
.goto(fixture('evaluation'))
.evaluate(function(str, done) {
setTimeout(() => done(null, str), 1000)
}, 'nightmare')
asyncValue.should.equal('nightmare')
})
it('should allow for errors in asynchronous evaluation with a callback', function*() {
yield nightmare.goto(fixture('evaluation')).evaluate(function(done) {
setTimeout(() => done(new Error('nightmare')), 1000)
}).should.be.rejected
})
it('should allow for timeouts in asynchronous evaluation with a callback', function*() {
this.timeout(40000)
yield nightmare.goto(fixture('evaluation')).evaluate(function(_done) {
//don't call done
}).should.be.rejected
})
it('should allow for asynchronous evaluation with a promise', function*() {
var asyncValue = yield nightmare
.goto(fixture('evaluation'))
.evaluate(function() {
return new Promise(resolve => {
setTimeout(() => resolve('nightmare'), 1000)
})
})
asyncValue.should.equal('nightmare')
})
it('should allow for arguments with asynchronous evaluation with a promise', function*() {
var asyncValue = yield nightmare
.goto(fixture('evaluation'))
.evaluate(function(str) {
return new Promise(resolve => {
setTimeout(() => resolve(str), 1000)
})
}, 'nightmare')
asyncValue.should.equal('nightmare')
})
it('should allow for errors in asynchronous evaluation with a promise', function*() {
yield nightmare.goto(fixture('evaluation')).evaluate(function() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('nightmare')), 1000)
})
}).should.be.rejected
})
it('should allow for timeouts in asynchronous evaluation with a promise', function*() {
this.timeout(40000)
yield nightmare.goto(fixture('evaluation')).evaluate(function() {
return new Promise((_resolve, _reject) => {
return 'nightmare'
})
}).should.be.rejected
})
})
})
describe('manipulation', function() {
var nightmare
beforeEach(function() {
nightmare = Nightmare()
})
afterEach(function*() {
yield nightmare.end()
})
it('should inject javascript onto the page', function*() {
var globalNumber = yield nightmare
.goto(fixture('manipulation'))
.inject('js', 'test/files/globals.js')
.evaluate(function() {
return globalNumber
})
globalNumber.should.equal(7)
var numAnchors = yield nightmare
.goto(fixture('manipulation'))
.inject('js', 'test/files/jquery-2.1.1.min.js')
.evaluate(function() {
return window.$('h1').length
})
numAnchors.should.equal(1)
})
it('should inject javascript onto the page ending with a comment', function*() {
var globalNumber = yield nightmare
.goto(fixture('manipulation'))
.inject('js', 'test/files/globals.js')
.evaluate(function() {
return globalNumber
})
globalNumber.should.equal(7)
var numAnchors = yield nightmare
.goto(fixture('manipulation'))
.inject('js', 'test/files/jquery-1.9.0.min.js')
.evaluate(function() {
return window.$('h1').length
})
numAnchors.should.equal(1)
})
it('should inject css onto the page', function*() {
var color = yield nightmare
.goto(fixture('manipulation'))
.inject('js', 'test/files/jquery-2.1.1.min.js')
.inject('css', 'test/files/test.css')
.evaluate(function() {
return window.$('body').css('background-color')
})
color.should.equal('rgb(255, 0, 0)')
})
it('should not inject unsupported types onto the page', function*() {
var color = yield nightmare
.goto(fixture('manipulation'))
.inject('js', 'test/files/jquery-2.1.1.min.js')
.inject('pdf', 'test/files/test.css')
.evaluate(function() {
return window.$('body').css('background-color')
})
color.should.not.equal('rgb(255, 0, 0)')
})
it('should type', function*() {
var input = 'nightmare'
var events = input.length * 3
var value = yield nightmare
.on('console', function(type, _input, _message) {
if (type === 'log') events--
})
.goto(fixture('manipulation'))
.type('input[type=search]', input)
.evaluate(function() {
return document.querySelector('input[type=search]').value
})
value.should.equal('nightmare')
events.should.equal(0)
})
it('should type integer', function*() {
var input = 10
var events = input.toString().length * 3
var value = yield nightmare
.on('console', function(type, _input, _message) {
if (type === 'log') events--
})
.goto(fixture('manipulation'))
.type('input[type=search]', input)
.evaluate(function() {
return document.querySelector('input[type=search]').value
})
value.should.equal('10')
events.should.equal(0)
})
it('should type object', function*() {
var input = {
foo: 'bar'
}
var events = input.toString().length * 3
var value = yield nightmare
.on('console', function(type, _input, _message) {
if (type === 'log') events--
})
.goto(fixture('manipulation'))
.type('input[type=search]', input)
.evaluate(function() {
return document.querySelector('input[type=search]').value
})
value.should.equal('[object Object]')
events.should.equal(0)
})
it('should clear inputs', function*() {
var input = 'nightmare'
var events = input.length * 3
var value = yield nightmare
.on('console', function(type, _input, _message) {
if (type === 'log') events--
})
.goto(fixture('manipulation'))
.type('input[type=search]', input)
.type('input[type=search]')
.evaluate(function() {
return document.querySelector('input[type=search]').value
})
value.should.equal('')
events.should.equal(0)
})
it('should support inserting text', function*() {
var input = 'nightmare insert typing'
var value = yield nightmare
.goto(fixture('manipulation'))
.insert('input[type=search]', input)
.evaluate(function() {
return document.querySelector('input[type=search]').value
})
value.should.equal('nightmare insert typing')
})
it('should support clearing inserted text', function*() {
var value = yield nightmare
.goto(fixture('manipulation'))
.insert('input[type=search]')
.evaluate(function() {
return document.querySelector('input[type=search]').value
})
value.should.equal('')
})
it('should not type in a nonexistent selector', function() {
return nightmare
.goto(fixture('manipulation'))
.type('does-not-exist', 'nightmare').should.be.rejected
})
it('should not insert in a nonexistent selector', function() {
return nightmare
.goto(fixture('manipulation'))
.insert('does-not-exist', 'nightmare').should.be.rejected
})
it('should blur the active element when something is clicked', function*() {
var isBody = yield nightmare
.goto(fixture('manipulation'))
.type('input[type=search]', 'test')
.click('p')
.evaluate(function() {
return document.activeElement === document.body
})
isBody.should.be.true
})
it('should allow for clicking on elements with attribute selectors', function*() {
yield nightmare
.goto(fixture('manipulation'))
.click('div[data-test="test"]')
})
it('should not allow for code injection with .click()', function(done) {
var exception
nightmare
.goto(fixture('manipulation'))
.click("\"]'); document.title = 'injected title'; ('\"")
.catch(e => (exception = e))
.then(() => nightmare.title())
.then(title => {
exception.should.exist
title.should.equal('Manipulation')
done()
})
})
it('should not fail if selector no longer exists to blur after typing', function*() {
yield nightmare
.on('console', function() {
// console.log(arguments)
})
.goto(fixture('manipulation'))
.type('input#disappears', 'nightmare')
})
it('should type and click', function*() {
var title = yield nightmare
.goto(fixture('manipulation'))
.type('input[type=search]', 'nightmare')
.click('button[type=submit]')
.wait(500)
.title()
title.should.equal('Manipulation - Results')
})
it('should type and click several times', function*() {
var title = yield nightmare
.goto(fixture('manipulation'))
.type('input[type=search]', 'github nightmare')
.click('button[type=submit]')
.wait(500)
.click('a')
.wait(500)
.title()
title.should.equal('Manipulation - Result - Nightmare')
})
it('should checkbox', function*() {
var checkbox = yield nightmare
.goto(fixture('manipulation'))
.check('input[type=checkbox]')
.evaluate(function() {
return document.querySelector('input[type=checkbox]').checked
})
checkbox.should.be.true
})
it('should uncheck', function*() {
var checkbox = yield nightmare
.goto(fixture('manipulation'))
.check('input[type=checkbox]')
.uncheck('input[type=checkbox]')
.evaluate(function() {
return document.querySelector('input[type=checkbox]').checked
})
checkbox.should.be.false
})
it('should select', function*() {
var select = yield nightmare
.goto(fixture('manipulation'))
.select('select', 'b')
.evaluate(function() {
return document.querySelector('select').value
})
select.should.equal('b')
})
it('should scroll to specified position', function*() {
// start at the top
var coordinates = yield nightmare
.viewport(320, 320)
.goto(fixture('manipulation'))
.evaluate(function() {
return {
top: document.scrollingElement.scrollTop,
left: document.scrollingElement.scrollLeft
}
})
coordinates.top.should.equal(0)
coordinates.left.should.equal(0)
// scroll down a bit
coordinates = yield nightmare.scrollTo(100, 50).evaluate(function() {
return {
top: document.scrollingElement.scrollTop,
left: document.scrollingElement.scrollLeft
}
})
coordinates.top.should.equal(100)
coordinates.left.should.equal(50)
})
it('should hover over an element', function*() {
var color = yield nightmare
.goto(fixture('manipulation'))
.mouseover('h1')
.evaluate(function() {
var element = document.querySelector('h1')
return element.style.background
})
color.should.equal('rgb(102, 255, 102)')
})
it('should release hover from an element', function*() {
var hoverColor = 'rgb(0, 255, 255)'
var hoveredColor = yield nightmare
.goto(fixture('manipulation'))
.mouseover('h2')
.evaluate(function() {
var element = document.querySelector('h2')
return element.style.background
})
hoveredColor.should.equal(hoverColor)
var nonHoveredColor = yield nightmare.mouseout('h2').evaluate(function() {
var element = document.querySelector('h2')
return element.style.background
})
nonHoveredColor.should.not.equal(hoverColor)
})
it('should mousedown on an element', function*() {
var color = yield nightmare
.goto(fixture('manipulation'))
.mousedown('h1')
.evaluate(function() {
var element = document.querySelector('h1')
return element.style.background
})
color.should.equal('rgb(255, 0, 0)')
})
})
describe('cookies', function() {
var nightmare
beforeEach(function() {
nightmare = Nightmare({
webPreferences: { partition: 'test-partition' }
}).goto(fixture('cookie'))
})
afterEach(function*() {
yield nightmare.end()
})
it('.set(name, value) & .get(name)', function*() {
var cookies = nightmare.cookies
yield cookies.set('hi', 'hello')
var cookie = yield cookies.get('hi')
cookie.name.should.equal('hi')
cookie.value.should.equal('hello')
cookie.path.should.equal('/')
cookie.secure.should.equal(false)
})
it('.set(obj) & .get(name)', function*() {
var cookies = nightmare.cookies
yield cookies.set({
name: 'nightmare',
value: 'rocks',
path: '/cookie'
})
var cookie = yield cookies.get('nightmare')
cookie.name.should.equal('nightmare')
cookie.value.should.equal('rocks')
cookie.path.should.equal('/cookie')
cookie.secure.should.equal(false)
})
it('.set([cookie1, cookie2]) & .get()', function*() {
var cookies = nightmare.cookies
yield cookies.set([
{
name: 'hi',
value: 'hello',
path: '/'
},
{
name: 'nightmare',
value: 'rocks',
path: '/cookie'
}
])
cookies = yield cookies.get()
cookies.length.should.equal(2)
// sort in case they come in a different order
cookies = cookies.sort(function(a, b) {
if (a.name > b.name) return 1
if (a.name < b.name) return -1
return 0
})
cookies[0].name.should.equal('hi')
cookies[0].value.should.equal('hello')
cookies[0].path.should.equal('/')
cookies[0].secure.should.equal(false)
cookies[1].name.should.equal('nightmare')
cookies[1].value.should.equal('rocks')
cookies[1].path.should.equal('/cookie')
cookies[1].secure.should.equal(false)
})
it('.set([cookie1, cookie2]) & .get(query)', function*() {
var cookies = nightmare.cookies
yield cookies.set([
{
name: 'hi',
value: 'hello',
path: '/'
},
{
name: 'nightmare',
value: 'rocks',
path: '/cookie'
}
])
cookies = yield cookies.get({ path: '/cookie' })
cookies.length.should.equal(1)
cookies[0].name.should.equal('nightmare')
cookies[0].value.should.equal('rocks')
cookies[0].path.should.equal('/cookie')
cookies[0].secure.should.equal(false)
})
it('.set([cookie]) & .clear(name) & .get(query)', function*() {
var cookies = nightmare.cookies
yield cookies.set([
{
name: 'hi',
value: 'hello',
path: '/'
},
{
name: 'nightmare',
value: 'rocks',
path: '/cookie'
}
])
yield cookies.clear('nightmare')
cookies = yield cookies.get({ path: '/cookie' })
cookies.length.should.equal(0)
})
it('.set([cookie]) & .clear() & .get()', function*() {
var cookies = nightmare.cookies
yield cookies.set([
{
name: 'hi',
value: 'hello',
path: '/'
},
{
name: 'nightmare',
value: 'rocks',
path: '/cookie'
}
])
yield cookies.clear()
cookies = yield cookies.get()
cookies.length.should.equal(0)
})
it('.set([cookie]) & .clearAll() & .get()', function*() {
yield nightmare.cookies.set([
{
name: 'hi',
value: 'hello',
path: '/'
},
{
name: 'nightmare',
value: 'rocks',
path: '/cookie'
}
])
yield nightmare.goto(fixture('simple'))
yield nightmare.cookies.set([
{
name: 'hi',
value: 'hello',
path: '/'
},
{
name: 'nightmare',
value: 'rocks',
path: '/cookie'
}
])
yield nightmare.cookies.clearAll()
var cookies = yield nightmare.cookies.get()
cookies.length.should.equal(0)
yield nightmare.goto(fixture('cookie'))
cookies = yield nightmare.cookies.get()
cookies.length.should.equal(0)
})
it('should return a proper error', function*() {
try {
yield nightmare.goto(fixture('cookie')).cookies.set({
name: 'hi',
value: 'there',
domain: 'https://www.google.com'
})
assert.fail("shouldn't have got here")
} catch (e) {
assert.equal(e.message, 'Setting cookie failed')
}
})
})
describe('rendering', function() {
this.timeout('30s')
var nightmare
before(function(done) {
mkdirp(tmp_dir, done)
})
after(function(done) {
rimraf(tmp_dir, done)
})
beforeEach(function() {
nightmare = Nightmare()
})
afterEach(function*() {
yield nightmare.end()
})
it('should take a screenshot', function*() {
yield nightmare
.goto('https://github.com/')
.screenshot(tmp_dir + '/test.png')
var stats = fs.statSync(tmp_dir + '/test.png')
stats.size.should.be.at.least(1000)
})
it('should buffer a screenshot', function*() {
var image = yield nightmare.goto('https://github.com').screenshot()
Buffer.isBuffer(image).should.be.true
image.length.should.be.at.least(1000)
})
it('should take a clipped screenshot', function*() {
yield nightmare
.goto('https://github.com/')
.screenshot(tmp_dir + '/test-clipped.png', {
x: 200,
y: 100,
width: 100,
height: 100
})
var stats = fs.statSync(tmp_dir + '/test.png')
var statsClipped = fs.statSync(tmp_dir + '/test-clipped.png')
statsClipped.size.should.be.at.least(300)
stats.size.should.be.at.least(10 * statsClipped.size)
})
it('should buffer a clipped screenshot', function*() {
var image = yield nightmare.goto('https://github.com').screenshot({
x: 200,
y: 100,
width: 100,
height: 100
})
Buffer.isBuffer(image).should.be.true
image.length.should.be.at.least(300)
})
// repeat this test 3 times, since the concern here is non-determinism in
// the timing accuracy of screenshots -- it might pass once, but likely not
// several times in a row.
// Temporarily disabled to allow Circle CI to pass, will revisit in 3.0 release
/*
for (var i = 0; i < 3; i++) {
it('should screenshot an up-to-date image of the page (' + i + ')', function*() {
var image = yield nightmare
.goto('about:blank')
.viewport(100, 100)
.evaluate(function() { document.body.style.background = '#090'; })
.evaluate(function() { document.body.style.background = '#090'; })
.wait(1000)
.screenshot();
var png = new PNG();
var imageData = yield png.parse.bind(png, image);
var firstPixel = Array.from(imageData.data.slice(0, 3));
firstPixel.should.deep.equal([0, 153, 0]);
});
}
*/
it('should screenshot an idle page', function*() {
var image = yield nightmare
.goto('about:blank')
.viewport(100, 100)
.evaluate(function() {
document.body.style.background = '#F0F'
})
.evaluate(function() {
document.body.style.background = '#0F0'
})
.wait(1000)
.screenshot()
var png = new PNG()
var imageData = yield png.parse.bind(png, image)
var firstPixel = Array.from(imageData.data.slice(0, 3))
// Since color profiles can affect the final output image depending
// on platform, the most we can expect is that the G channel is greater
// than the other two.
firstPixel[1].should.be.above(firstPixel[0])
firstPixel[1].should.be.above(firstPixel[2])
})
it('should not subscribe to frames until necessary', function() {
var didSubscribe = false
var FrameManager = require('../lib/frame-manager.js')
FrameManager({
webContents: {
beginFrameSubscription: function() {
didSubscribe = true
},
endFrameSubscription: function() {},
executeJavaScript: function() {}
}
})
didSubscribe.should.be.false
})
it('should subscribe to frames when requested necessary', function(done) {
var didSubscribe = false
var didUnsubscribe = false
var FrameManager = require('../lib/frame-manager.js')
var fn
var manager = FrameManager({
webContents: {
debugger: {
isAttached: function() {
return true
},
sendCommand: function(command) {
if (command === 'DOM.highlightRect') {
fn('mock-data')
}
}
},
beginFrameSubscription: function(_fn) {
didSubscribe = true
fn = _fn
},
endFrameSubscription: function() {
didUnsubscribe = true
},
executeJavaScript: function() {}
}
})
manager.requestFrame(function(data) {
didSubscribe.should.be.true
didUnsubscribe.should.be.true
data.should.equal('mock-data')
done()
})
})
it('should support multiple concurrent frame subscriptions', function(done) {
var subscribeCount = 0
var unsubscribeCount = 0
var FrameManager = require('../lib/frame-manager.js')
var fn = null
var async = require('async')
var manager = FrameManager({
webContents: {
debugger: {
isAttached: function() {
return true
},
sendCommand: function(command) {
if (command === 'DOM.highlightRect') {
setTimeout(function() {
fn('mock-data')
}, 100)
}
}
},
beginFrameSubscription: function(_fn) {
subscribeCount += 1
assert.strictEqual(fn, null)
fn = _fn
},
endFrameSubscription: function() {
unsubscribeCount += 1
fn = null
},
executeJavaScript: function() {}
}
})
async.times(
2,
function requestFrameFn(i, cb) {
manager.requestFrame(function handleFrame(data) {
cb(null, data)
})
},
function handleResults(err, results) {
if (err) {
done(err)
}
subscribeCount.should.equal(1)
unsubscribeCount.should.equal(1)
results[0].should.equal('mock-data')
results[1].should.equal('mock-data')
done()
}
)
})
it('should support multiple series frame subscriptions', function(done) {
var subscribeCount = 0
var unsubscribeCount = 0
var FrameManager = require('../lib/frame-manager.js')
var fn = null
var async = require('async')
var manager = FrameManager({
webContents: {
debugger: {
isAttached: function() {
return true
},
sendCommand: function(command) {
if (command === 'DOM.highlightRect') {
setTimeout(function() {
fn('mock-data')
}, 100)
}
}
},
beginFrameSubscription: function(_fn) {
subscribeCount += 1
assert.strictEqual(fn, null)
fn = _fn
},
endFrameSubscription: function() {
unsubscribeCount += 1
fn = null
},
executeJavaScript: function() {}
}
})
async.timesSeries(
2,
function requestFrameFn(i, cb) {
manager.requestFrame(function handleFrame(data) {
cb(null, data)
})
},
function handleResults(err, results) {
if (err) {
done(err)
}
subscribeCount.should.equal(2)
unsubscribeCount.should.equal(2)
results[0].should.equal('mock-data')
results[1].should.equal('mock-data')
done()
}
)
})
// DEV: We can have multiple timeouts if page is static
it('should support multiple series timing out frame subscriptions', function(done) {
var subscribeCount = 0
var unsubscribeCount = 0
var FrameManager = require('../lib/frame-manager.js')
var fn = null
var async = require('async')
var manager = FrameManager({
webContents: {
debugger: {
isAttached: function() {
return true
},
sendCommand: function() {
/* Ignore command so it times out */
}
},
beginFrameSubscription: function(_fn) {
subscribeCount += 1
assert.strictEqual(fn, null)
fn = _fn
},
endFrameSubscription: function() {
unsubscribeCount += 1
fn = null
},
executeJavaScript: function() {}
}
})
async.timesSeries(
2,
function requestFrameFn(i, cb) {
manager.requestFrame(function handleFrame(data) {
cb(null, data)
}, 100)
},
function handleResults(err, results) {
if (err) {
done(err)
}
subscribeCount.should.equal(2)
unsubscribeCount.should.equal(2)
should.equal(results[0], null)
should.equal(results[1], null)
done()
}
)
})
it('should load jquery correctly', function*() {
var loaded = yield nightmare
.goto(fixture('rendering'))
.wait(2000)
.evaluate(function() {
return !!window.jQuery
})
loaded.should.be.at.least(true)
})
it('should render fonts correctly', function*() {
yield nightmare
.goto(fixture('rendering'))
.wait(2000)
.screenshot(tmp_dir + '/font-rendering.png')
var stats = fs.statSync(tmp_dir + '/font-rendering.png')
stats.size.should.be.at.least(1000)
})
it('should save as html', function*() {
yield nightmare.goto(fixture('manipulation')).html(tmp_dir + '/test.html')
var stats = fs.statSync(tmp_dir + '/test.html')
stats.should.be.ok
})
it('should render a PDF', function*() {
yield nightmare.goto(fixture('manipulation')).pdf(tmp_dir + '/test.pdf')
var stats = fs.statSync(tmp_dir + '/test.pdf')
stats.size.should.be.at.least(1000)
})
it('should accept options to render a PDF', function*() {
yield nightmare
.goto(fixture('manipulation'))
.pdf(tmp_dir + '/test2.pdf', { printBackground: false })
var stats = fs.statSync(tmp_dir + '/test2.pdf')
stats.size.should.be.at.least(1000)
})
it('should return a buffer from a PDF with no path', function*() {
var buf = yield nightmare
.goto(fixture('manipulation'))
.pdf({ printBackground: false })
var isBuffer = Buffer.isBuffer(buf)
buf.length.should.be.at.least(1000)
isBuffer.should.be.true
})
})
describe('referer', function() {
var nightmare
beforeEach(function() {
nightmare = Nightmare({
webPreferences: { partition: 'test-partition' }
})
})
afterEach(function*() {
yield nightmare.end()
})
it('should return referer from headers', function*() {
var referer = 'http://my-referer.tld/'
var returnedReferer = yield nightmare
.goto(fixture('referer'), {
Referer: referer
})
.evaluate(function() {
return document.body.innerText
})
referer.should.be.equal(returnedReferer.trim())
})
})
describe('events', function() {
var nightmare
beforeEach(function() {
nightmare = Nightmare()
})
afterEach(function*() {
yield nightmare.end()
})
it('should fire an event on page load complete', function*() {
var fired = false
nightmare.on('did-finish-load', function() {
fired = true
})
yield nightmare.goto(fixture('events'))
fired.should.be.true
})
it('should fire an event on javascript error', function*() {
var fired = false
nightmare.on('page', function(type, _errorMessage, _errorStack) {
if (type === 'error') {
fired = true
}
})
yield nightmare.goto(fixture('events'))
fired.should.be.true
})
it('should fire an event on javascript console.log', function*() {
var log = ''
nightmare.on('console', function(type, str) {
if (type === 'log') log = str
})
yield nightmare.goto(fixture('events'))
log.should.equal('my log')
yield nightmare.click('button')
log.should.equal('clicked')
})
it('should fire an event on page load failure', function*() {
var fired = false
nightmare.on('did-fail-load', function() {
fired = true
})
try {
yield nightmare.goto('https://alskdjfasdfuuu.com')
} catch (_error) {
// do nothing
}
fired.should.be.true
})
it('should fire an event on javascript window.alert', function*() {
var alert = ''
nightmare.on('page', function(type, message) {
if (type === 'alert') {
alert = message
}
})
yield nightmare.goto(fixture('events')).evaluate(function() {
alert('my alert')
})
alert.should.equal('my alert')
})
it('should fire an event on javascript window.prompt', function*() {
var prompt = ''
var response = ''
nightmare.on('page', function(type, message, res) {
if (type === 'prompt') {
prompt = message
response = res
}
})
yield nightmare.goto(fixture('events')).evaluate(function() {
prompt('my prompt', 'hello!')
})
prompt.should.equal('my prompt')
response.should.equal('hello!')
})
it('should fire an event on javascript window.confirm', function*() {
var confirm = ''
var response = ''
nightmare.on('page', function(type, message, res) {
if (type === 'confirm') {
confirm = message
response = res
}
})
yield nightmare.goto(fixture('events')).evaluate(function() {
confirm('my confirm', 'hello!')
})
confirm.should.equal('my confirm')
response.should.equal('hello!')
})
it('should only fire once when using once', function*() {
var events = 0
nightmare.once('page', function(_type, _message) {
events++
})
yield nightmare.goto(fixture('events'))
events.should.equal(1)
})
it('should remove event listener', function*() {
var events = 0
var handler = function(type, _message) {
if (type === 'alert') {
events++
}
}
nightmare.on('page', handler)
yield nightmare.goto(fixture('events')).evaluate(function() {
alert('alert one')
})
nightmare.removeListener('page', handler)
yield nightmare.evaluate(function() {
alert('alert two')
})
events.should.equal(1)
})
})
describe('options', function() {
var nightmare
var server
before(function(done) {
// set up an HTTPS server using self-signed certificates -- Nightmare
// will only be able to talk to it if 'ignore-certificate-errors' is set.
server = https
.createServer(
{
key: fs.readFileSync(path.join(__dirname, 'files', 'server.key')),
cert: fs.readFileSync(path.join(__dirname, 'files', 'server.crt'))
},
function(request, response) {
response.end('ok\n')
}
)
.listen(0, 'localhost', function() {
var address = server.address()
server.url = `https://${address.address}:${address.port}`
done()
})
})
after(function() {
server.close()
server = null
})
afterEach(function*() {
gitextract_y9toqmvg/
├── .circleci/
│ └── config.yml
├── .eslintrc.js
├── .gitignore
├── History.md
├── Makefile
├── Readme.md
├── example.js
├── lib/
│ ├── actions.js
│ ├── frame-manager.js
│ ├── ipc.js
│ ├── javascript.js
│ ├── nightmare.js
│ ├── preload.js
│ └── runner.js
├── package.json
└── test/
├── Preferences
├── bb-xvfb
├── files/
│ ├── globals.js
│ ├── nightmare-created.js
│ ├── nightmare-error.js
│ ├── nightmare-unended.js
│ ├── server.crt
│ ├── server.key
│ └── test.css
├── fixtures/
│ ├── cookies/
│ │ └── index.html
│ ├── evaluation/
│ │ └── index.html
│ ├── events/
│ │ └── index.html
│ ├── manipulation/
│ │ ├── index.html
│ │ ├── result.html
│ │ └── results.html
│ ├── navigation/
│ │ ├── a.html
│ │ ├── b.html
│ │ ├── c.html
│ │ ├── hanging-resources.html
│ │ ├── index.html
│ │ ├── invalid-frame.html
│ │ ├── invalid-image.html
│ │ └── valid-frame.html
│ ├── options/
│ │ └── index.html
│ ├── preload/
│ │ ├── index.html
│ │ └── index.js
│ ├── rendering/
│ │ └── index.html
│ ├── security/
│ │ └── index.html
│ ├── simple/
│ │ └── index.html
│ └── unload/
│ ├── add-event-listener.html
│ └── index.html
├── index.js
├── mocha.opts
├── server.js
└── waitForX
SYMBOL INDEX (46 symbols across 8 files)
FILE: lib/actions.js
function waitms (line 520) | function waitms(ms, done) {
function waitelem (line 532) | function waitelem(self, selector, done) {
function waitfn (line 566) | function waitfn() {
FILE: lib/frame-manager.js
constant HIGHLIGHT_STYLE (line 5) | const HIGHLIGHT_STYLE = {
function FrameManager (line 21) | function FrameManager(window) {
FILE: lib/ipc.js
function IPC (line 29) | function IPC(process) {
function serializeError (line 150) | function serializeError(err) {
function unserializeError (line 160) | function unserializeError(err) {
FILE: lib/nightmare.js
constant DEFAULT_GOTO_TIMEOUT (line 29) | const DEFAULT_GOTO_TIMEOUT = 30 * 1000
constant DEFAULT_WAIT_TIMEOUT (line 31) | const DEFAULT_WAIT_TIMEOUT = 30 * 1000
constant DEFAULT_TYPE_INTERVAL (line 33) | const DEFAULT_TYPE_INTERVAL = 100
constant DEFAULT_POLL_INTERVAL (line 35) | const DEFAULT_POLL_INTERVAL = 250
constant MAX_AUTH_RETRIES (line 37) | const MAX_AUTH_RETRIES = 3
constant DEFAULT_EXECUTION_TIMEOUT (line 39) | const DEFAULT_EXECUTION_TIMEOUT = 30 * 1000
constant DEFAULT_HALT_MESSAGE (line 41) | const DEFAULT_HALT_MESSAGE = 'Nightmare Halted'
constant DEFAULT_PARTITION (line 43) | const DEFAULT_PARTITION = 'nightmare'
function Nightmare (line 69) | function Nightmare(options) {
function handleExit (line 262) | function handleExit(code, instance, cb) {
function endInstance (line 275) | function endInstance(instance, cb, forceKill) {
function attachToProcess (line 297) | function attachToProcess(instance) {
function detachFromProcess (line 308) | function detachFromProcess(instance) {
function next (line 392) | function next(err, _res) {
function after (line 402) | function after(err, _res) {
function done (line 413) | function done() {
function queued (line 577) | function queued(name, fn) {
FILE: lib/preload.js
function send (line 6) | function send(_event) {
function error (line 100) | function error(err) {
FILE: lib/runner.js
constant KNOWN_PROTOCOLS (line 17) | const KNOWN_PROTOCOLS = ['http', 'https', 'file', 'about', 'javascript']
constant IS_READY (line 19) | const IS_READY = Symbol('isReady')
function handleFailure (line 254) | function handleFailure(event, code, detail, failedUrl, isMainFrame) {
function handleDetails (line 265) | function handleDetails(
function handleDomReady (line 287) | function handleDomReady() {
function handleFinish (line 292) | function handleFinish(_event) {
function cleanup (line 296) | function cleanup(err, data) {
function canLoadProtocol (line 316) | function canLoadProtocol(protocol, callback) {
function startLoading (line 324) | function startLoading() {
function type (line 436) | function type() {
function isReady (line 706) | function isReady() {
function setIsReady (line 713) | function setIsReady(ready) {
function forward (line 725) | function forward(name) {
FILE: test/fixtures/preload/index.js
function send (line 6) | function send(_event) {
FILE: test/index.js
function select (line 2272) | function select(tagname) {
function opened (line 2316) | function opened() {
function fixture (line 2603) | function fixture(path) {
function Deferred (line 2611) | function Deferred() {
function withDeprecationTracking (line 2625) | function withDeprecationTracking(constructor) {
Condensed preview — 50 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (230K chars).
[
{
"path": ".circleci/config.yml",
"chars": 2039,
"preview": "test: &test\n working_directory: ~/repo\n steps:\n - checkout\n - run:\n name: Print system information\n "
},
{
"path": ".eslintrc.js",
"chars": 556,
"preview": "module.exports = \n{\n extends: [ 'eslint:recommended', 'prettier' ],\n plugins: ['prettier' ], // activating esling-plug"
},
{
"path": ".gitignore",
"chars": 62,
"preview": "node_modules\n.DS_Store\ntest/tmp\ntest/DevTools Extensions\n.idea"
},
{
"path": "History.md",
"chars": 18777,
"preview": "# 3.0.1 / 2018-03-29\n\n* bump electron to fix #1424\n\n# 3.0.0 / 2018-03-02\n\n* BREAKING: remove window.\\_\\_nightmare.ipc to"
},
{
"path": "Makefile",
"chars": 712,
"preview": "GREP ?=.\n#check if xvfb is running\nXVFB_RUNNING = $(shell pgrep \"Xvfb\" > /dev/null; echo $$?)\n#set the circle project na"
},
{
"path": "Readme.md",
"chars": 32444,
"preview": "*NOTICE: This library is no longer maintained.*\n\n[\nvar nightmare = Nightmare({ show: true })\n\nnightma"
},
{
"path": "lib/actions.js",
"chars": 18927,
"preview": "/**\n * Module Dependencies\n */\n\nvar debug = require('debug')('nightmare:actions')\nvar sliced = require('sliced')\nvar jse"
},
{
"path": "lib/frame-manager.js",
"chars": 3528,
"preview": "const parent = require('./ipc')(process)\nconst EventEmitter = require('events')\nconst util = require('util')\n\nconst HIGH"
},
{
"path": "lib/ipc.js",
"chars": 4520,
"preview": "'use strict'\n\n/**\n * Module dependencies\n */\n\nvar Emitter = require('events').EventEmitter\nvar sliced = require('sliced'"
},
{
"path": "lib/javascript.js",
"chars": 1489,
"preview": "/**\n * Module Dependencies\n */\n\nvar minstache = require('minstache')\n\n/**\n * Run the `src` function on the client-side, "
},
{
"path": "lib/nightmare.js",
"chars": 15916,
"preview": "/**\n * DEBUG=nightmare*\n */\n\nvar log = require('debug')('nightmare:log')\nvar debug = require('debug')('nightmare')\nvar e"
},
{
"path": "lib/preload.js",
"chars": 2383,
"preview": "/* eslint-disable no-console */\n\nvar ipc = require('electron').ipcRenderer\nvar sliced = require('sliced')\n\nfunction send"
},
{
"path": "lib/runner.js",
"chars": 18123,
"preview": "/**\n * Module Dependencies\n */\n\nvar parent = require('./ipc')(process)\nvar electron = require('electron')\nvar BrowserWin"
},
{
"path": "package.json",
"chars": 1364,
"preview": "{\n \"name\": \"nightmare\",\n \"version\": \"3.0.2\",\n \"license\": \"MIT\",\n \"main\": \"lib/nightmare.js\",\n \"scripts\": {\n \"tes"
},
{
"path": "test/Preferences",
"chars": 69,
"preview": "{\"brightray\":{\"media\":{\"device_id_salt\":\"5F9qZMOFUibhFqg6msGAAQ==\"}}}"
},
{
"path": "test/bb-xvfb",
"chars": 1365,
"preview": "#!/bin/sh\n#\n# bb-xvfb <command> <and> <its> <arguments...>\n#\n# Wrapper for XVFB. Capture XVFB logs where we can see the"
},
{
"path": "test/files/globals.js",
"chars": 55,
"preview": "/** @type {Comment} [description] */\nglobalNumber = 7;\n"
},
{
"path": "test/files/nightmare-created.js",
"chars": 188,
"preview": "// This script is used to create a nightmare run but not start it.\nvar Nightmare = require('../..');\nvar nightmare = Nig"
},
{
"path": "test/files/nightmare-error.js",
"chars": 172,
"preview": "// This script is used to start nightmare \n// but then throw a user space error\nvar Nightmare = require('../..');\nvar ni"
},
{
"path": "test/files/nightmare-unended.js",
"chars": 336,
"preview": "// This script is used to start a nightmare run but not end it. It reports its\n// Electron process's pid, then we kill i"
},
{
"path": "test/files/server.crt",
"chars": 721,
"preview": "-----BEGIN CERTIFICATE-----\nMIIB5zCCAVACCQC4FTxcJ7FcSTANBgkqhkiG9w0BAQUFADA4MRMwEQYDVQQIEwpT\nb21lLVN0YXRlMSEwHwYDVQQKExh"
},
{
"path": "test/files/server.key",
"chars": 887,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCsdkkQVmMRaa62th9M+o50qg2AY0TRo9szvCeTQoovZza4FScq\n5aChUCYL1BPlfZx3c4MTaV+"
},
{
"path": "test/files/test.css",
"chars": 42,
"preview": "body, html {\n\tbackground-color: #FF0000;\n}"
},
{
"path": "test/fixtures/cookies/index.html",
"chars": 199,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Cookies</title>\n </head>\n <body>\n <h1 class=\"title\">Hello World!</h1>\n "
},
{
"path": "test/fixtures/evaluation/index.html",
"chars": 265,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Evaluation</title>\n </head>\n <body>\n <h1 class=\"title\">Hello World!</h1>"
},
{
"path": "test/fixtures/events/index.html",
"chars": 340,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Options</title>\n </head>\n <body>\n <h1>Hello World!</h1>\n <button>clic"
},
{
"path": "test/fixtures/manipulation/index.html",
"chars": 5637,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Manipulation</title>\n </head>\n <body>\n <h1>Hello World!</h1>\n <h2>thi"
},
{
"path": "test/fixtures/manipulation/result.html",
"chars": 148,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Manipulation - Result - Nightmare</title>\n </head>\n <body>\n <h1>Hello Wo"
},
{
"path": "test/fixtures/manipulation/results.html",
"chars": 224,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Manipulation - Results</title>\n </head>\n <body>\n <h1>Hello World!</h1>\n "
},
{
"path": "test/fixtures/navigation/a.html",
"chars": 233,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>A</title>\n </head>\n <body>\n <a class=\"a\" href=\"a.html\">A</a>\n <a clas"
},
{
"path": "test/fixtures/navigation/b.html",
"chars": 201,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>B</title>\n </head>\n <body>\n <a class=\"a\" href=\"a.html\">A</a>\n <a clas"
},
{
"path": "test/fixtures/navigation/c.html",
"chars": 201,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>C</title>\n </head>\n <body>\n <a class=\"a\" href=\"a.html\">A</a>\n <a clas"
},
{
"path": "test/fixtures/navigation/hanging-resources.html",
"chars": 134,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Hanging resource load</title>\n </head>\n <body>\n <img src=\"/wait\" />\n </"
},
{
"path": "test/fixtures/navigation/index.html",
"chars": 288,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Navigation</title>\n </head>\n <body>\n <a class=\"a\" href=\"a.html\">A</a>\n "
},
{
"path": "test/fixtures/navigation/invalid-frame.html",
"chars": 212,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Navigation</title>\n </head>\n <body>\n <iframe src=\"http://this-is-not-a-r"
},
{
"path": "test/fixtures/navigation/invalid-image.html",
"chars": 171,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Navigation</title>\n </head>\n <body>\n <img src=\"http://this-is-not-a-real"
},
{
"path": "test/fixtures/navigation/valid-frame.html",
"chars": 169,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Navigation</title>\n </head>\n <body>\n <iframe src=\"/navigation\" width=\"30"
},
{
"path": "test/fixtures/options/index.html",
"chars": 189,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Options</title>\n </head>\n <body>\n <h1>Hello World!</h1>\n <iframe id='"
},
{
"path": "test/fixtures/preload/index.html",
"chars": 121,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Simple</title>\n </head>\n <body>\n <h1>Hello World!</h1>\n </body>\n</html>"
},
{
"path": "test/fixtures/preload/index.js",
"chars": 2219,
"preview": "/* eslint-disable no-console */\n\nvar ipc = require('electron').ipcRenderer\nvar sliced = require('sliced')\n\nfunction send"
},
{
"path": "test/fixtures/rendering/index.html",
"chars": 389,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Nightmare not lading fonts - DEMO</title>\n <style>\n body {\n fo"
},
{
"path": "test/fixtures/security/index.html",
"chars": 233,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <title>Simple</title>\n</head>\n\n<body>\n <h1>Hello World!</h1>\n</body>\n\n<script type=\"te"
},
{
"path": "test/fixtures/simple/index.html",
"chars": 121,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Simple</title>\n </head>\n <body>\n <h1>Hello World!</h1>\n </body>\n</html>"
},
{
"path": "test/fixtures/unload/add-event-listener.html",
"chars": 430,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Unload</title>\n <script>\n var unloadHandler = function (event) {\n "
},
{
"path": "test/fixtures/unload/index.html",
"chars": 225,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <title>Unload</title>\n <script>\n window.onunload = window.onbeforeunload = f"
},
{
"path": "test/index.js",
"chars": 77808,
"preview": "/* global it, describe, before, beforeEach, after, afterEach */\n\n/**\n * Module dependencies.\n */\n\nrequire('mocha-generat"
},
{
"path": "test/mocha.opts",
"chars": 24,
"preview": "--slow 3s\n--timeout 10s\n"
},
{
"path": "test/server.js",
"chars": 2269,
"preview": "/**\n * Module dependencies.\n */\n\nvar auth = require('basic-auth')\nvar basicAuth = require('basic-auth-connect')\nvar expr"
},
{
"path": "test/waitForX",
"chars": 846,
"preview": "#!/bin/bash\n#\n# waitForX [<cmd> [<arg> ...]]\n#\n# Wait for X Server to be ready, then run the given command once X server"
}
]
About this extraction
This page contains the full source code of the segment-boneyard/nightmare GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 50 files (213.4 KB), approximately 54.8k tokens, and a symbol index with 46 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.