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.* [![Build Status](https://img.shields.io/circleci/project/segmentio/nightmare/master.svg)](https://circleci.com/gh/segmentio/nightmare) [![Join the chat at https://gitter.im/rosshinkley/nightmare](https://badges.gitter.im/rosshinkley/nightmare.svg)](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. 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 # # 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 ================================================ Cookies

Hello World!

Welcome to the Cookies Test! ================================================ FILE: test/fixtures/evaluation/index.html ================================================ Evaluation

Hello World!

Welcome to the Evaluation Test! ================================================ FILE: test/fixtures/events/index.html ================================================ Options

Hello World!

================================================ FILE: test/fixtures/manipulation/index.html ================================================ Manipulation

Hello World!

this is nightmare

attribute div

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.

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.

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.

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.

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.

================================================ FILE: test/fixtures/manipulation/result.html ================================================ Manipulation - Result - Nightmare

Hello World!

================================================ FILE: test/fixtures/manipulation/results.html ================================================ Manipulation - Results

Hello World!

  1. Nightmare!
================================================ FILE: test/fixtures/navigation/a.html ================================================ A A B C D ================================================ FILE: test/fixtures/navigation/b.html ================================================ B A B C ================================================ FILE: test/fixtures/navigation/c.html ================================================ C A B C ================================================ FILE: test/fixtures/navigation/hanging-resources.html ================================================ Hanging resource load ================================================ FILE: test/fixtures/navigation/index.html ================================================ Navigation A B C does not end ================================================ FILE: test/fixtures/navigation/invalid-frame.html ================================================ Navigation ================================================ FILE: test/fixtures/navigation/invalid-image.html ================================================ Navigation ================================================ FILE: test/fixtures/navigation/valid-frame.html ================================================ Navigation ================================================ FILE: test/fixtures/options/index.html ================================================ Options

Hello World!

================================================ FILE: test/fixtures/preload/index.html ================================================ Simple

Hello World!

================================================ 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 ================================================ Nightmare not lading fonts - DEMO

This should be written in Oswald.

================================================ FILE: test/fixtures/security/index.html ================================================ Simple

Hello World!

================================================ FILE: test/fixtures/simple/index.html ================================================ Simple

Hello World!

================================================ FILE: test/fixtures/unload/add-event-listener.html ================================================ Unload ================================================ FILE: test/fixtures/unload/index.html ================================================ Unload ================================================ 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*() { yield nightmare.end() }) it('should set useragent', function*() { nightmare = new Nightmare() var useragent = yield nightmare .useragent('firefox') .goto(fixture('options')) .evaluate(function() { return window.navigator.userAgent }) useragent.should.eql('firefox') }) it('should wait and fail with waitTimeout', function() { nightmare = Nightmare({ waitTimeout: 254 }) return nightmare.goto(fixture('navigation')).wait('foobar').should.be .rejected }) it('should wait and fail with waitTimeout and a ms wait time', function() { nightmare = Nightmare({ waitTimeout: 254 }) return nightmare.goto(fixture('navigation')).wait(1000).should.be.rejected }) it('should wait and fail with waitTimeout with queued functions', function() { nightmare = Nightmare({ waitTimeout: 254 }) return nightmare .goto(fixture('navigation')) .wait('foobar') .exists('baz').should.be.rejected }) it('should set authentication', function*() { nightmare = Nightmare() var data = yield nightmare .authentication('my', 'auth') .goto(fixture('auth')) .evaluate(function() { return JSON.parse(document.querySelector('pre').innerHTML) }) data.should.eql({ name: 'my', pass: 'auth' }) }) it('should fail on authentication failure', function*() { nightmare = Nightmare() yield nightmare.authentication('my', 'wrong').goto(fixture('auth')).should .be.rejected }) it('should be able to update authentication', function*() { nightmare = Nightmare() var data = yield nightmare .authentication('my', 'auth') .goto(fixture('auth')) .authentication('my2', 'auth2') .goto(fixture('auth2')) .evaluate(function() { return JSON.parse(document.querySelector('pre').innerHTML) }) data.should.eql({ name: 'my2', pass: 'auth2' }) }) it('should set viewport', function*() { var size = { width: 400, height: 300, useContentSize: true } nightmare = Nightmare(size) var result = yield nightmare .goto(fixture('options')) .evaluate(function() { return { width: window.innerWidth, height: window.innerHeight } }) result.width.should.eql(size.width) result.height.should.eql(size.height) }) it('should set a single header', function*() { nightmare = Nightmare() var headers = yield nightmare .header('X-Nightmare-Header', 'hello world') .goto(fixture('headers')) .evaluate(function() { return JSON.parse(document.querySelector('pre').innerHTML) }) headers['x-nightmare-header'].should.equal('hello world') }) it('should set all headers', function*() { nightmare = Nightmare() var headers = yield nightmare .header({ 'X-Foo': 'foo', 'X-Bar': 'bar' }) .goto(fixture('headers')) .evaluate(function() { return JSON.parse(document.querySelector('pre').innerHTML) }) headers['x-foo'].should.equal('foo') headers['x-bar'].should.equal('bar') }) it('should set headers for that request', function*() { nightmare = Nightmare() var headers = yield nightmare .goto(fixture('headers'), { 'X-Nightmare-Header': 'hello world' }) .evaluate(function() { return JSON.parse(document.querySelector('pre').innerHTML) }) headers['x-nightmare-header'].should.equal('hello world') }) it('should allow webPreferences settings', function*() { nightmare = Nightmare({ webPreferences: { webSecurity: false } }) var result = yield nightmare .goto(fixture('options')) .evaluate(function() { return document.getElementById('example-iframe').contentDocument }) result.should.be.ok }) it('should be constructable with paths', function() { nightmare = Nightmare({ paths: { userData: __dirname } }) nightmare.should.be.ok }) it('should be constructable with switches', function*() { nightmare = Nightmare({ switches: { // empty string and non-string values all represent no value 'ignore-certificate-errors': null, 'touch-events': '' } }) nightmare.should.be.ok var touchEvents = yield nightmare.goto(server.url).evaluate(function() { return 'ontouchstart' in window }) touchEvents.should.be.true }) it('should support switches with values', function*() { nightmare = Nightmare({ switches: { 'force-device-scale-factor': '5' } }) nightmare.should.be.ok var scaleFactor = yield nightmare .goto('about:blank') .evaluate(function() { return window.devicePixelRatio }) scaleFactor.should.equal(5) }) it('should allow to use external Electron', function() { nightmare = Nightmare({ electronPath: require('electron') }) nightmare.should.be.ok }) it('should allow to use external Promise', function*() { nightmare = Nightmare({ Promise: require('bluebird') }) nightmare.should.be.ok const thenPromise = nightmare.goto('about:blank').then() thenPromise.should.be.an.instanceof(require('bluebird')) yield thenPromise const catchPromise = nightmare.goto('about:blank').catch() catchPromise.should.be.an.instanceof(require('bluebird')) yield catchPromise const endPromise = nightmare .goto('about:blank') .end() .then() endPromise.constructor.should.equal(require('bluebird')) endPromise.should.be.an.instanceof(require('bluebird')) yield endPromise }) }) describe('Nightmare.Promise', function() { var nightmare afterEach(function*() { // `withDeprecationTracking()` messes w/ prototype constructor references Nightmare.Promise = require('..').Promise = Promise yield nightmare.end() }) it('should default to native Promise', function*() { Nightmare.Promise.should.equal(Promise) nightmare = Nightmare() nightmare.should.be.ok var thenPromise = nightmare.goto('about:blank').then() thenPromise.should.be.an.instanceof(Promise) yield thenPromise }) it('should override default Promise library', function*() { // `withDeprecationTracking()` messes w/ prototype constructor references Nightmare.Promise = require('..').Promise = require('bluebird') Nightmare.Promise.should.equal(require('bluebird')) nightmare = Nightmare() nightmare.should.be.ok var thenPromise = nightmare.goto('about:blank').then() thenPromise.should.be.an.instanceof(require('bluebird')) yield thenPromise }) it('should not override per-instance Promise library', function*() { Nightmare.Promise.should.equal(Promise) nightmare = Nightmare({ Promise: require('bluebird') }) nightmare.should.be.ok var thenPromise = nightmare.goto('about:blank').then() thenPromise.should.not.be.an.instanceof(Promise) thenPromise.should.be.an.instanceof(require('bluebird')) yield thenPromise }) }) describe('Nightmare.action(name, fn)', function() { var nightmare afterEach(function*() { yield nightmare.end() }) it('should support custom actions', function*() { Nightmare.action('size', function(done) { this.evaluate_now(function() { var w = Math.max( document.documentElement.clientWidth, window.innerWidth || 0 ) var h = Math.max( document.documentElement.clientHeight, window.innerHeight || 0 ) return { height: h, width: w } }, done) }) nightmare = new Nightmare() var size = yield nightmare.goto(fixture('simple')).size() size.height.should.be.a('number') size.width.should.be.a('number') }) it('should support custom namespaces', function*() { Nightmare.action('style', { background: function(done) { this.evaluate_now(function() { return window.getComputedStyle(document.body, null).backgroundColor }, done) }, color: function(done) { this.evaluate_now(function() { return window.getComputedStyle(document.body, null).color }, done) } }) nightmare = Nightmare() yield nightmare.goto(fixture('simple')) var background = yield nightmare.style.background() var color = yield nightmare.style.color() background.should.equal('rgba(0, 0, 0, 0)') color.should.equal('rgb(0, 0, 0)') }) it('should allow env variables', function*() { Nightmare.action( 'envtest', function(name, options, parent, win, renderer, done) { parent.respondTo('envtest', function(done) { done(null, process.env) }) done() }, function(done) { this.child.call('envtest', done) } ) nightmare = Nightmare({ env: { TZ: 'UTC' } }) nightmare.should.be.ok var envTest = yield nightmare.goto('about:bank').envtest() envTest.should.be.ok envTest.should.have.property('TZ', 'UTC') }) }) describe('Nightmare.use', function() { var nightmare beforeEach(function() { nightmare = Nightmare() }) afterEach(function*() { yield nightmare.end() }) it('should support extending nightmare', function*() { var tagName = yield nightmare.goto(fixture('simple')).use(select('h1')) tagName.should.equal('H1') function select(tagname) { return function(nightmare) { nightmare.evaluate(function(tagname) { return document.querySelector(tagname).tagName }, tagname) } } }) }) describe('custom preload script', function() { var nightmare beforeEach(function() { nightmare = Nightmare() }) afterEach(function*() { yield nightmare.end() }) it('should support passing your own preload script in', function*() { var nightmare = Nightmare({ webPreferences: { preload: path.join(__dirname, 'fixtures', 'preload', 'index.js') } }) var value = yield nightmare.goto(fixture('preload')).evaluate(function() { return window.preload }) value.should.equal('custom') }) }) describe('devtools', function() { var nightmare beforeEach(function() { Nightmare.action( 'waitForDevTools', function(ns, options, parent, win, renderer, done) { parent.on('waitForDevTools', function() { function opened() { parent.emit('waitForDevTools', null, true) } if (win.webContents.isDevToolsOpened()) { return opened() } win.webContents.once('devtools-opened', opened) }) done() }, function(done) { this.child.once('waitForDevTools', done) this.child.emit('waitForDevTools') } ) nightmare = Nightmare({ show: true, openDevTools: true }) }) afterEach(function*() { yield nightmare.end() }) it('should open devtools', function*() { var devToolsOpen = yield nightmare .goto(fixture('simple')) .waitForDevTools() devToolsOpen.should.be.true }) }) describe('ipc', function() { var nightmare beforeEach(function() { Nightmare.action( 'test', function(_, __, parent, ___, ____, done) { parent.respondTo('test', function(arg1, done) { done.progress('one') done.progress('two') if (arg1 === 'error') { return done('Error!') } else { done(null, `Got ${arg1}`) } }) done() }, function(options, done) { var channel = this.child.call('test', options.arg || options, done) if (options.onData) { channel.on('data', options.onData) } if (options.onEnd) { channel.on('end', options.onEnd) } } ) Nightmare.action('noImplementation', function(done) { this.child.call('noImplementation', done) }) nightmare = Nightmare() }) afterEach(function*() { yield nightmare.end() }) it('should only make one IPC instance per process', function() { var processStub = { send: function() {}, on: function() {} } var ipc1 = IPC(processStub) var ipc2 = IPC(processStub) ipc1.should.equal(ipc2) }) it('should support basic call-response', function*() { var result = yield nightmare.test('x') result.should.equal('Got x') }) it('should support errors across IPC', function(done) { nightmare.test('error').then( function() { done(new Error('Action succeeded when it should have errored!')) }, function() { done() } ) }) it('should stream progress', function*() { var progress = [] yield nightmare.test({ arg: 'x', onData: data => progress.push(data), onEnd: (error, data) => progress.push([error, data]) }) progress.should.deep.equal(['one', 'two', [null, 'Got x']]) }) it('should trigger error if no responder is registered', function(done) { nightmare.noImplementation().then( function() { done(new Error('Action succeeded when it should have errored!')) }, function() { done() } ) }) it('should log a warning when replacing a responder', function*() { Nightmare.action( 'uhoh', function(_, __, parent, ___, ____, done) { parent.respondTo('test', function(done) { done() }) done() }, function(done) { this.child.call('test', done) } ) var logged = false var instance = Nightmare() instance._queue.splice(1, 0, [ function(done) { this.child.on('nightmare:ipc:debug', function(message) { if (message.toLowerCase().indexOf('replacing') > -1) { logged = true } }) done() }, [] ]) yield instance.goto('about:blank').end() logged.should.be.true }) }) describe('partitioning', function() { var nightmare afterEach(function*() { yield nightmare.end() }) // The default behavior for nightmare is to use a non-persistent partition name it('should not persist between instances by default', function*() { nightmare = Nightmare() yield nightmare .goto(fixture('simple')) .evaluate(function() { window.localStorage.setItem( 'testing', 'This string should not persist between instances.' ) }) .end() nightmare = Nightmare() var value = yield nightmare.goto(fixture('simple')).evaluate(function() { return window.localStorage.getItem('testing') || '' }) value.should.equal('') }) // Setting the partition to null we default to the electron default behavior // which is to use a persistent storage between instances. it('should persist between instances if partition is null', function*() { nightmare = Nightmare({ webPreferences: { partition: null } }) yield nightmare .goto(fixture('simple')) .evaluate(function() { window.localStorage.setItem( 'testing', 'This string should persist between instances.' ) }) .end() nightmare = Nightmare({ webPreferences: { partition: null } }) var value = yield nightmare.goto(fixture('simple')).evaluate(function() { return window.localStorage.getItem('testing') || '' }) value.should.equal('This string should persist between instances.') }) it('should not persist between instances if partition name doesnt start with "persist:"', function*() { nightmare = Nightmare({ webPreferences: { partition: 'nonpersist' } }) yield nightmare .goto(fixture('simple')) .evaluate(function() { window.localStorage.setItem( 'testing', 'This string not should persist between instances.' ) }) .end() nightmare = Nightmare({ webPreferences: { partition: 'nonpersist' } }) var value = yield nightmare.goto(fixture('simple')).evaluate(function() { return window.localStorage.getItem('testing') || '' }) value.should.equal('') }) it('should persist between instances if partition starts with "persist:"', function*() { nightmare = Nightmare({ webPreferences: { partition: 'persist: testing' } }) yield nightmare .goto(fixture('simple')) .evaluate(function() { window.localStorage.setItem( 'testing', 'This string should persist between instances.' ) }) .end() nightmare = Nightmare({ webPreferences: { partition: 'persist: testing' } }) var value = yield nightmare.goto(fixture('simple')).evaluate(function() { return window.localStorage.getItem('testing') || '' }) value.should.equal('This string should persist between instances.') }) }) describe('security', function() { it('should not expose the ipc in evaluate', function*() { const nightmare = Nightmare() const result = yield nightmare.goto(fixture('simple')).evaluate(() => { return this.send || this.window.send || this.ipc || window.ipc }) assert.strictEqual(result, null) }) it('should not expose the ipc to the 3rd party', function*() { const nightmare = Nightmare() const deferred = new Deferred() nightmare.on('console', function( _type, ipc, windowIPC, send, windowSend ) { // Since we cant define a Content-Security-Policy // and use restrictive rules. Rely on Electron providing // the warning string and making sure it clobbered these // into strings assert.strictEqual(typeof ipc, 'string') assert.strictEqual(typeof windowIPC, 'string') assert.strictEqual(typeof send, 'string') windowSend || undefined deferred.resolve() }) yield nightmare.goto(fixture('security')) yield deferred }) }) }) /** * Generate a URL to a specific fixture. * * @param {String} path * @returns {String} */ function fixture(path) { return url.resolve(base, path) } /** * Deferred helper */ function Deferred() { const p = new Promise((resolve, reject) => { this.resolve = resolve this.reject = reject }) this.then = p.then.bind(p) this.catch = p.catch.bind(p) } /** * Track deprecation warnings. */ function withDeprecationTracking(constructor) { var newConstructor = function() { var instance = constructor.apply(this, arguments) instance.queue(done => { instance.proc.stderr.pipe(split()).on('data', line => { if (line.indexOf('deprecated') > -1) { newConstructor.__deprecations.add(line) } }) done() }) return instance } newConstructor.__deprecations = new Set() newConstructor.assertNoDeprecations = function() { var deprecations = Nightmare.__deprecations if (deprecations.size) { var plural = deprecations.size === 1 ? '' : 's' throw new Error( `Used ${deprecations.size} deprecated Electron API${plural}: ${Array.from(deprecations).join('\n ')}` ) } } Object.setPrototypeOf(newConstructor, constructor) return newConstructor } /** * Make plugins resettable for tests */ var _action = Nightmare.action var _pluginNames = [] var _existingNamespaces = Nightmare.namespaces.slice() var _existingChildActions = Object.assign({}, Nightmare.childActions) Nightmare.action = function(name) { _pluginNames.push(name) return _action.apply(this, arguments) } // NOTE: this is somewhat fragile since there's no public API for removing // plugins. If you touch `Nightmare.action`, please be sure to update this. Nightmare.resetActions = function() { _pluginNames.splice(0, _pluginNames.length).forEach(name => { delete this.prototype[name] }) this.namespaces.splice(0, this.namespaces.length) this.namespaces.push.apply(this.namespaces, _existingNamespaces) Object.keys(this.childActions).forEach(name => { if (!_existingChildActions[name]) { delete this.childActions[name] } }) } /** * Simple assertion for running processes */ chai.Assertion.addProperty('process', function() { var running = true try { process.kill(this._obj, 0) } catch (e) { running = false } this.assert( running, 'expected process ##{this} to be running', 'expected process ##{this} not to be running' ) }) ================================================ FILE: test/mocha.opts ================================================ --slow 3s --timeout 10s ================================================ FILE: test/server.js ================================================ /** * Module dependencies. */ var auth = require('basic-auth') var basicAuth = require('basic-auth-connect') var express = require('express') var multer = require('multer') var path = require('path') var serve = require('serve-static') /** * Locals. */ var app = (module.exports = express()) /** * Accept file uploads. */ app.use(multer({ inMemory: true }).single('upload')) /** * Echo uploaded files for testing assertions. */ app.post('/upload', function(req, res) { res.send(req.files) }) /** * Echo HTTP Basic Auth for testing assertions. */ app.get('/auth', basicAuth('my', 'auth'), function(req, res) { res.send(auth(req)) }) app.get('/auth2', basicAuth('my2', 'auth2'), function(req, res) { res.send(auth(req)) }) /** * Echo HTTP Headers for testing assertions. */ app.get('/headers', function(req, res) { res.header('Cache-Control', 'no-cache') res.header('Expires', '-1') res.header('Pragma', 'no-cache') res.send(req.headers) }) /** * Redirect to the provided URL for testing redirects and headers */ app.get('/redirect', function(req, res) { var code = Number(req.query.code) || 301 var url = req.query.url || '/' res.redirect(code, url) }) /** * Start the response but do not end the request */ app.get('/not-modified', function(req, res) { res.sendStatus(304) }) /** * Simply hang up on the connection for testing interrupted page loads */ app.get('/do-not-respond', function(req, res) { res.socket.end() }) /** * Start the response but do not end the request */ app.get('/never-ends', function(req, res) { res.set('Content-Type', 'text/html') res.write(`this page will not stop`) }) /** * Wait forever and never respond */ app.get('/wait', function(_req, _res) {}) /** * Return 'Referer' header if presented */ app.get('/referer', function(req, res) { res.send( typeof req.headers.referer !== 'undefined' ? req.headers.referer : '' ) }) /** * Serve the fixtures directory as static files. */ app.use(serve(path.resolve(__dirname, 'fixtures'))) /** * Serve the test files so they can be accessed via HTML as well. */ app.use('/files', serve(path.resolve(__dirname, 'files'))) /** * Start if not required. */ if (!module.parent) app.listen(7500) ================================================ FILE: test/waitForX ================================================ #!/bin/bash # # waitForX [ [ ...]] # # Wait for X Server to be ready, then run the given command once X server # is ready. (Or simply return if no command is provided.) # # pulled from: https://gist.github.com/tullmann/476cc71169295d5c3fe6 # original issue: https://github.com/angular/protractor/issues/2419#issuecomment-156527809 function LOG { echo $(date -R): $0: $* } if [ -z "$DISPLAY" ]; then LOG "FATAL: No DISPLAY environment variable set. No X." exit 13 fi #LOG "Waiting for X Server $DISPLAY to be available" MAX=120 # About 60 seconds CT=0 while ! xdpyinfo >/dev/null 2>&1; do sleep 0.50s CT=$(( CT + 1 )) if [ "$CT" -ge "$MAX" ]; then LOG "FATAL: $0: Gave up waiting for X server $DISPLAY" exit 11 fi done #LOG "X is available" if [ -n "$1" ]; then exec "$@" fi #eof