text: const path = require('path');
  const requireg = require('requireg');
  const urlResolve = require('url').resolve;
  
  const Helper = require('../helper');
  const stringIncludes = require('../assert/include').includes;
  const urlEquals = require('../assert/equal').urlEquals;
  const equals = require('../assert/equal').equals;
  const empty = require('../assert/empty').empty;
  const truth = require('../assert/truth').truth;
  const Locator = require('../locator');
  const ElementNotFound = require('./errors/ElementNotFound');
  const {
    xpathLocator,
    fileExists,
    screenshotOutputFolder,
  } = require('../utils');
  
  const specialKeys = {
    Backspace: '\u0008',
    Enter: '\u000d',
    Delete: '\u007f',
  };
  
  let withinStatus = false;
  
  /**
   * Nightmare helper wraps [Nightmare](https://github.com/segmentio/nightmare) library to provide
   * fastest headless testing using Electron engine. Unlike Selenium-based drivers this uses
   * Chromium-based browser with Electron with lots of client side scripts, thus should be less stable and
   * less trusted.
   *
   * Requires `nightmare` package to be installed.
   *
   * ## Configuration
   *
   * This helper should be configured in codecept.json or codecept.conf.js
   *
   * * `url` - base url of website to be tested
   * * `restart` (optional, default: true) - restart browser between tests.
   * * `disableScreenshots` (optional, default: false)  - don't save screenshot on failure.
   * * `uniqueScreenshotNames` (optional, default: false)  - option to prevent screenshot override if you have scenarios with the same name in different suites.
   * * `fullPageScreenshots` (optional, default: false) - make full page screenshots on failure.
   * * `keepBrowserState` (optional, default: false)  - keep browser state between tests when `restart` set to false.
   * * `keepCookies` (optional, default: false)  - keep cookies between tests when `restart` set to false.
   * * `waitForAction`: (optional) how long to wait after click, doubleClick or PressKey actions in ms. Default: 500.
   * * `waitForTimeout`: (optional) default wait* timeout in ms. Default: 1000.
   * * `windowSize`: (optional) default window size. Set a dimension like `640x480`.
   *
   * + options from [Nightmare configuration](https://github.com/segmentio/nightmare#api)
   *
   * ## Methods
   */
  class Nightmare extends Helper {
    constructor(config) {
      super(config);
  
      this.isRunning = false;
  
      // override defaults with config
      this._setConfig(config);
    }
  
    _validateConfig(config) {
      const defaults = {
        waitForAction: 500,
        waitForTimeout: 1000,
        fullPageScreenshots: false,
        disableScreenshots: false,
        uniqueScreenshotNames: false,
        rootElement: 'body',
        restart: true,
        keepBrowserState: false,
        keepCookies: false,
        js_errors: null,
        enableHAR: false,
      };
  
      return Object.assign(defaults, config);
    }
  
    static _config() {
      return [
        { name: 'url', message: 'Base url of site to be tested', default: 'http://localhost' },
        {
          name: 'show', message: 'Show browser window', default: true, type: 'confirm',
        },
      ];
    }
  
    static _checkRequirements() {
      try {
        requireg('nightmare');
      } catch (e) {
        return ['nightmare'];
      }
    }
  
    async _init() {
      this.Nightmare = requireg('nightmare');
  
      if (this.options.enableHAR) {
        require('nightmare-har-plugin').install(this.Nightmare);
      }
  
      this.Nightmare.action('findElements', function (locator, contextEl, done) {
        if (!done) {
          done = contextEl;
          contextEl = null;
        }
  
        const by = Object.keys(locator)[0];
        const value = locator[by];
  
        this.evaluate_now((by, locator, contextEl) => window.codeceptjs.findAndStoreElements(by, locator, contextEl), done, by, value, contextEl);
      });
  
      this.Nightmare.action('findElement', function (locator, contextEl, done) {
        if (!done) {
          done = contextEl;
          contextEl = null;
        }
  
        const by = Object.keys(locator)[0];
        const value = locator[by];
  
        this.evaluate_now((by, locator, contextEl) => {
          const res = window.codeceptjs.findAndStoreElement(by, locator, contextEl);
          if (res === null) {
            throw new Error(`Element ${locator} couldn't be located by ${by}`);
          }
          return res;
        }, done, by, value, contextEl);
      });
  
      this.Nightmare.action('asyncScript', function () {
        let args = Array.prototype.slice.call(arguments);
        const done = args.pop();
        args = args.splice(1, 0, done);
        this.evaluate_now.apply(this, args);
      });
  
      this.Nightmare.action('enterText', function (el, text, clean, done) {
        const child = this.child;
        const typeFn = () => child.call('type', text, done);
  
        this.evaluate_now((el, clean) => {
          const element = window.codeceptjs.fetchElement(el);
          if (clean) element.value = '';
          element.focus();
        }, () => {
          if (clean) return typeFn();
          child.call('pressKey', 'End', typeFn); // type End before
        }, el, clean);
      });
  
      this.Nightmare.action('pressKey', (ns, options, parent, win, renderer, done) => {
        parent.respondTo('pressKey', (ch, done) => {
          win.webContents.sendInputEvent({
            type: 'keyDown',
            keyCode: ch,
          });
  
          win.webContents.sendInputEvent({
            type: 'char',
            keyCode: ch,
          });
  
          win.webContents.sendInputEvent({
            type: 'keyUp',
            keyCode: ch,
          });
          done();
        });
        done();
      }, function (key, done) {
        this.child.call('pressKey', key, done);
      });
  
      this.Nightmare.action('triggerMouseEvent', (ns, options, parent, win, renderer, done) => {
        parent.respondTo('triggerMouseEvent', (evt, done) => {
          win.webContents.sendInputEvent(evt);
          done();
        });
        done();
      }, function (event, done) {
        this.child.call('triggerMouseEvent', event, done);
      });
  
      this.Nightmare.action(
        'upload',
        (ns, options, parent, win, renderer, done) => {
          parent.respondTo('upload', (selector, pathsToUpload, done) => {
            parent.emit('log', 'paths', pathsToUpload);
            try {
            // attach the debugger
            // NOTE: this will fail if devtools is open
              win.webContents.debugger.attach('1.1');
            } catch (e) {
              parent.emit('log', 'problem attaching', e);
              return done(e);
            }
  
            win.webContents.debugger.sendCommand('DOM.getDocument', {}, (err, domDocument) => {
              win.webContents.debugger.sendCommand('DOM.querySelector', {
                nodeId: domDocument.root.nodeId,
                selector,
              }, (err, queryResult) => {
              // HACK: chromium errors appear to be unpopulated objects?
                if (Object.keys(err)
                  .length > 0) {
                  parent.emit('log', 'problem selecting', err);
                  return done(err);
                }
                win.webContents.debugger.sendCommand('DOM.setFileInputFiles', {
                  nodeId: queryResult.nodeId,
                  files: pathsToUpload,
                }, (err, setFileResult) => {
                  if (Object.keys(err)
                    .length > 0) {
                    parent.emit('log', 'problem setting input', err);
                    return done(err);
                  }
                  win.webContents.debugger.detach();
                  done(null, pathsToUpload);
                });
              });
            });
          });
          done();
        },
        function (selector, pathsToUpload, done) {
          if (!Array.isArray(pathsToUpload)) {
            pathsToUpload = [pathsToUpload];
          }
          this.child.call('upload', selector, pathsToUpload, (err, stuff) => {
            done(err, stuff);
          });
        },
      );
  
      return Promise.resolve();
    }
  
    async _beforeSuite() {
      if (!this.options.restart && !this.isRunning) {
        this.debugSection('Session', 'Starting singleton browser session');
        return this._startBrowser();
      }
    }
  
    async _before() {
      if (this.options.restart) return this._startBrowser();
      if (!this.isRunning) return this._startBrowser();
      return this.browser;
    }
  
    async _after() {
      if (!this.isRunning) return;
      if (this.options.restart) {
        this.isRunning = false;
        return this._stopBrowser();
      }
      if (this.options.enableHAR) {
        await this.browser.resetHAR();
      }
      if (this.options.keepBrowserState) return;
      if (this.options.keepCookies) {
        await this.browser.cookies.clearAll();
      }
      this.debugSection('Session', 'cleaning up');
      return this.executeScript(() => localStorage.clear());
    }
  
    _afterSuite() {
    }
  
    _finishTest() {
      if (!this.options.restart && this.isRunning) {
        this._stopBrowser();
      }
    }
  
    async _startBrowser() {
      this.context = this.options.rootElement;
      if (this.options.enableHAR) {
        this.browser = this.Nightmare(Object.assign(require('nightmare-har-plugin').getDevtoolsOptions(), this.options));
        await this.browser;
        await this.browser.waitForDevtools();
      } else {
        this.browser = this.Nightmare(this.options);
        await this.browser;
      }
      await this.browser.goto('about:blank'); // Load a blank page so .saveScreenshot (/evaluate) will work
      this.isRunning = true;
      this.browser.on('dom-ready', () => this._injectClientScripts());
      this.browser.on('did-start-loading', () => this._injectClientScripts());
      this.browser.on('will-navigate', () => this._injectClientScripts());
      this.browser.on('console', (type, message) => {
        this.debug(`${type}: ${message}`);
      });
  
      if (this.options.windowSize) {
        const size = this.options.windowSize.split('x');
        return this.browser.viewport(parseInt(size[0], 10), parseInt(size[1], 10));
      }
    }
  
    /**
     * Get HAR
     *
     * ```js
     * let har = await I.grabHAR();
     * fs.writeFileSync('sample.har', JSON.stringify({log: har}));
     * ```
     */
    async grabHAR() {
      return this.browser.getHAR();
    }
  
    async saveHAR(fileName) {
      const outputFile = path.join(global.output_dir, fileName);
      this.debug(`HAR is saving to ${outputFile}`);
  
      await this.browser.getHAR().then((har) => {
        require('fs').writeFileSync(outputFile, JSON.stringify({ log: har }));
      });
    }
  
    async resetHAR() {
      await this.browser.resetHAR();
    }
  
    async _stopBrowser() {
      return this.browser.end().catch((error) => {
        this.debugSection('Error on End', error);
      });
    }
  
    async _withinBegin(locator) {
      this.context = locator;
      locator = new Locator(locator, 'css');
      withinStatus = true;
      return this.browser.evaluate((by, locator) => {
        const el = window.codeceptjs.findElement(by, locator);
        if (!el) throw new Error(`Element by ${by}: ${locator} not found`);
        window.codeceptjs.within = el;
      }, locator.type, locator.value);
    }
  
    _withinEnd() {
      this.context = this.options.rootElement;
      withinStatus = false;
      return this.browser.evaluate(() => {
        window.codeceptjs.within = null;
      });
    }
  
    /**
     * Locate elements by different locator types, including strict locator.
     * Should be used in custom helpers.
     *
     * This method return promise with array of IDs of found elements.
     * Actual elements can be accessed inside `evaluate` by using `codeceptjs.fetchElement()`
     * client-side function:
     *
     * ```js
     * // get an inner text of an element
     *
     * let browser = this.helpers['Nightmare'].browser;
     * let value = this.helpers['Nightmare']._locate({name: 'password'}).then(function(els) {
     *   return browser.evaluate(function(el) {
     *     return codeceptjs.fetchElement(el).value;
     *   }, els[0]);
     * });
     * ```
     */
    _locate(locator) {
      locator = new Locator(locator, 'css');
      return this.browser.evaluate((by, locator) => {
        return window.codeceptjs.findAndStoreElements(by, locator);
      }, locator.type, locator.value);
    }
  
  
    /**
     * Add a header override for all HTTP requests. If header is undefined, the header overrides will be reset.
     *
     * ```js
     * I.haveHeader('x-my-custom-header', 'some value');
     * I.haveHeader(); // clear headers
     * ```
     */
    haveHeader(header, value) {
      return this.browser.header(header, value);
    }
  
    /**
     * {{> amOnPage }}
     * @param {?object} headers list of request headers can be passed
     *
     */
    async amOnPage(url, headers = null) {
      if (!(/^\w+\:\/\//.test(url))) {
        url = urlResolve(this.options.url, url);
      }
      const currentUrl = await this.browser.url();
      if (url === currentUrl) {
        // navigating to the same url will cause an error in nightmare, so don't do it
        return;
      }
      return this.browser.goto(url, headers).then((res) => {
        this.debugSection('URL', res.url);
        this.debugSection('Code', res.code);
        this.debugSection('Headers', JSON.stringify(res.headers));
      });
    }
  
    /**
     * {{> seeInTitle }}
     */
    async seeInTitle(text) {
      const title = await this.browser.title();
      stringIncludes('web page title').assert(text, title);
    }
  
    /**
     * {{> dontSeeInTitle }}
     */
    async dontSeeInTitle(text) {
      const title = await this.browser.title();
      stringIncludes('web page title').negate(text, title);
    }
  
    /**
     * {{> grabTitle }}
     */
    async grabTitle() {
      return this.browser.title();
    }
  
    /**
     * {{> grabCurrentUrl }}
     */
    async grabCurrentUrl() {
      return this.browser.url();
    }
  
    /**
     * {{> seeInCurrentUrl }}
     */
    async seeInCurrentUrl(url) {
      const currentUrl = await this.browser.url();
      stringIncludes('url').assert(url, currentUrl);
    }
  
    /**
     * {{> dontSeeInCurrentUrl }}
     */
    async dontSeeInCurrentUrl(url) {
      const currentUrl = await this.browser.url();
      stringIncludes('url').negate(url, currentUrl);
    }
  
    /**
     * {{> seeCurrentUrlEquals }}
     */
    async seeCurrentUrlEquals(url) {
      const currentUrl = await this.browser.url();
      urlEquals(this.options.url).assert(url, currentUrl);
    }
  
    /**
     * {{> dontSeeCurrentUrlEquals }}
     */
    async dontSeeCurrentUrlEquals(url) {
      const currentUrl = await this.browser.url();
      urlEquals(this.options.url).negate(url, currentUrl);
    }
  
    /**
     * {{> see }}
     */
    async see(text, context = null) {
      return proceedSee.call(this, 'assert', text, context);
    }
  
    /**
     * {{> dontSee }}
     */
    dontSee(text, context = null) {
      return proceedSee.call(this, 'negate', text, context);
    }
  
    /**
     * {{> seeElement }}
     */
    async seeElement(locator) {
      locator = new Locator(locator, 'css');
      const num = await this.browser.evaluate((by, locator) => {
        return window.codeceptjs.findElements(by, locator).filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length;
      }, locator.type, locator.value);
      equals('number of elements on a page').negate(0, num);
    }
  
    /**
     * {{> dontSeeElement }}
     */
    async dontSeeElement(locator) {
      locator = new Locator(locator, 'css');
      locator = new Locator(locator, 'css');
      const num = await this.browser.evaluate((by, locator) => {
        return window.codeceptjs.findElements(by, locator).filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length;
      }, locator.type, locator.value);
      equals('number of elements on a page').assert(0, num);
    }
  
    /**
     * {{> seeElementInDOM }}
     */
    async seeElementInDOM(locator) {
      locator = new Locator(locator, 'css');
      const els = await this.browser.findElements(locator.toStrict());
      empty('elements').negate(els.fill('ELEMENT'));
    }
  
    /**
     * {{> dontSeeElementInDOM }}
     */
    async dontSeeElementInDOM(locator) {
      locator = new Locator(locator, 'css');
      const els = await this.browser.findElements(locator.toStrict());
      empty('elements').assert(els.fill('ELEMENT'));
    }
  
    /**
     * {{> seeInSource }}
     */
    async seeInSource(text) {
      const source = await this.browser.evaluate(() => document.documentElement.outerHTML);
      stringIncludes('HTML source of a page').assert(text, source);
    }
  
    /**
     * {{> dontSeeInSource }}
     */
    async dontSeeInSource(text) {
      const source = await this.browser.evaluate(() => document.documentElement.outerHTML);
      stringIncludes('HTML source of a page').negate(text, source);
    }
  
    /**
     * {{> seeNumberOfElements }}
     */
    async seeNumberOfElements(locator, num) {
      const elements = await this._locate(locator);
      return equals(`expected number of elements (${locator}) is ${num}, but found ${elements.length}`).assert(elements.length, num);
    }
  
    /**
     * {{> seeNumberOfVisibleElements }}
     */
    async seeNumberOfVisibleElements(locator, num) {
      const res = await this.grabNumberOfVisibleElements(locator);
      return equals(`expected number of visible elements (${locator}) is ${num}, but found ${res}`).assert(res, num);
    }
  
    /**
     * {{> grabNumberOfVisibleElements }}
     */
    async grabNumberOfVisibleElements(locator) {
      locator = new Locator(locator, 'css');
  
      const num = await this.browser.evaluate((by, locator) => {
        return window.codeceptjs.findElements(by, locator)
          .filter(e => e.offsetWidth > 0 && e.offsetHeight > 0).length;
      }, locator.type, locator.value);
  
      return num;
    }
  
    /**
     * {{> click }}
     */
    async click(locator, context = null) {
      const el = await findClickable.call(this, locator, context);
      assertElementExists(el, locator, 'Clickable');
      return this.browser.evaluate(el => window.codeceptjs.clickEl(el), el)
        .wait(this.options.waitForAction);
    }
  
    /**
     * {{> doubleClick }}
     */
    async doubleClick(locator, context = null) {
      const el = await findClickable.call(this, locator, context);
      assertElementExists(el, locator, 'Clickable');
      return this.browser.evaluate(el => window.codeceptjs.doubleClickEl(el), el)
        .wait(this.options.waitForAction);
    }
  
  
    /**
     * {{> rightClick }}
     */
    async rightClick(locator, context = null) {
      const el = await findClickable.call(this, locator, context);
      assertElementExists(el, locator, 'Clickable');
      return this.browser.evaluate(el => window.codeceptjs.rightClickEl(el), el)
        .wait(this.options.waitForAction);
    }
  
  
    /**
     * {{> moveCursorTo }}
     */
    async moveCursorTo(locator, offsetX = 0, offsetY = 0) {
      locator = new Locator(locator, 'css');
      const el = await this.browser.findElement(locator.toStrict());
      assertElementExists(el, locator);
      return this.browser.evaluate((el, x, y) => window.codeceptjs.hoverEl(el, x, y), el, offsetX, offsetY)
        .wait(this.options.waitForAction); // wait for hover event to happen
    }
  
  
    /**
     * {{> executeScript }}
     *
     * Wrapper for synchronous [evaluate](https://github.com/segmentio/nightmare#evaluatefn-arg1-arg2)
     */
    async executeScript(fn) {
      return this.browser.evaluate.apply(this.browser, arguments)
        .catch(err => err); // Nightmare's first argument is error :(
    }
  
    /**
     * {{> executeAsyncScript }}
     *
     * Wrapper for asynchronous [evaluate](https://github.com/segmentio/nightmare#evaluatefn-arg1-arg2).
     * Unlike NightmareJS implementation calling `done` will return its first argument.
     */
    async executeAsyncScript(fn) {
      return this.browser.evaluate.apply(this.browser, arguments)
        .catch(err => err); // Nightmare's first argument is error :(
    }
  
    /**
     * {{> resizeWindow }}
     */
    async resizeWindow(width, height) {
      if (width === 'maximize') {
        throw new Error('Nightmare doesn\'t support resizeWindow to maximum!');
      }
      return this.browser.viewport(width, height).wait(this.options.waitForAction);
    }
  
    /**
     * {{> checkOption }}
     */
    async checkOption(field, context = null) {
      const els = await findCheckable.call(this, field, context);
      assertElementExists(els[0], field, 'Checkbox or radio');
      return this.browser.evaluate(els => window.codeceptjs.checkEl(els[0]), els)
        .wait(this.options.waitForAction);
    }
  
    /**
     * {{> uncheckOption }}
     */
    async uncheckOption(field, context = null) {
      const els = await findCheckable.call(this, field, context);
      assertElementExists(els[0], field, 'Checkbox or radio');
      return this.browser.evaluate(els => window.codeceptjs.unCheckEl(els[0]), els)
        .wait(this.options.waitForAction);
    }
  
    /**
     * {{> fillField }}
     */
    async fillField(field, value) {
      const el = await findField.call(this, field);
      assertElementExists(el, field, 'Field');
      return this.browser.enterText(el, value.toString(), true)
        .wait(this.options.waitForAction);
    }
  
    /**
     * {{> clearField }}
     */
    async clearField(field) {
      return this.fillField(field, '');
    }
  
    /**
     * {{> appendField }}
     */
    async appendField(field, value) {
      const el = await findField.call(this, field);
      assertElementExists(el, field, 'Field');
      return this.browser.enterText(el, value, false)
        .wait(this.options.waitForAction);
    }
  
    /**
     * {{> seeInField }}
     */
    async seeInField(field, value) {
      return proceedSeeInField.call(this, 'assert', field, value);
    }
  
    /**
     * {{> dontSeeInField }}
     */
    async dontSeeInField(field, value) {
      return proceedSeeInField.call(this, 'negate', field, value);
    }
  
    /**
     * Sends [input event](http://electron.atom.io/docs/api/web-contents/#webcontentssendinputeventevent) on a page.
     * Can submit special keys like 'Enter', 'Backspace', etc
     */
    async pressKey(key) {
      if (Array.isArray(key)) {
        key = key.join('+'); // should work with accelerators...
      }
      if (Object.keys(specialKeys).indexOf(key) >= 0) {
        key = specialKeys[key];
      }
      return this.browser.pressKey(key).wait(this.options.waitForAction);
    }
  
    /**
     * Sends [input event](http://electron.atom.io/docs/api/web-contents/#contentssendinputeventevent) on a page.
     * Should be a mouse event like:
     *  {
          type: 'mouseDown',
          x: args.x,
          y: args.y,
          button: "left"
        }
     */
    async triggerMouseEvent(event) {
      return this.browser.triggerMouseEvent(event).wait(this.options.waitForAction);
    }
  
    /**
     * {{> seeCheckboxIsChecked }}
     */
    async seeCheckboxIsChecked(field) {
      return proceedIsChecked.call(this, 'assert', field);
    }
  
    /**
     * {{> dontSeeCheckboxIsChecked }}
     */
    async dontSeeCheckboxIsChecked(field) {
      return proceedIsChecked.call(this, 'negate', field);
    }
  
    /**
     * {{> attachFile }}
     *
     * Doesn't work if the Chromium DevTools panel is open (as Chromium allows only one attachment to the debugger at a time. [See more](https://github.com/rosshinkley/nightmare-upload#important-note-about-setting-file-upload-inputs))
     */
    async attachFile(locator, pathToFile) {
      const file = path.join(global.codecept_dir, pathToFile);
  
      locator = new Locator(locator, 'css');
      if (!locator.isCSS()) {
        throw new Error('Only CSS locator allowed for attachFile in Nightmare helper');
      }
  
      if (!fileExists(file)) {
        throw new Error(`File at ${file} can not be found on local system`);
      }
      return this.browser.upload(locator.value, file);
    }
  
    /**
     * {{> grabTextFrom }}
     */
    async grabTextFrom(locator) {
      locator = new Locator(locator, 'css');
      const els = await this.browser.findElements(locator.toStrict());
      assertElementExists(els[0], locator);
      const texts = [];
      const getText = el => window.codeceptjs.fetchElement(el).innerText;
      for (const el of els) {
        texts.push(await this.browser.evaluate(getText, el));
      }
      if (texts.length === 1) return texts[0];
      return texts;
    }
  
    /**
     * {{> grabValueFrom }}
     */
    async grabValueFrom(locator) {
      const el = await findField.call(this, locator);
      assertElementExists(el, locator, 'Field');
      return this.browser.evaluate(el => window.codeceptjs.fetchElement(el).value, el);
    }
  
    /**
     * {{> grabAttributeFrom }}
     */
    async grabAttributeFrom(locator, attr) {
      locator = new Locator(locator, 'css');
      const els = await this.browser.findElements(locator.toStrict());
      const array = [];
  
      for (let index = 0; index < els.length; index++) {
        const el = els[index];
        assertElementExists(el, locator);
        array.push(await this.browser.evaluate((el, attr) => window.codeceptjs.fetchElement(el).getAttribute(attr), el, attr));
      }
  
      return array.length === 1 ? array[0] : array;
    }
  
    /**
     * {{> grabHTMLFrom }}
     */
    async grabHTMLFrom(locator) {
      locator = new Locator(locator, 'css');
      const els = await this.browser.findElements(locator.toStrict());
      const array = [];
  
      for (let index = 0; index < els.length; index++) {
        const el = els[index];
        assertElementExists(el, locator);
        array.push(await this.browser.evaluate(el => window.codeceptjs.fetchElement(el).innerHTML, el));
      }
      this.debugSection('HTML', array);
  
      return array.length === 1 ? array[0] : array;
    }
  
  
    _injectClientScripts() {
      return this.browser.inject('js', path.join(__dirname, 'clientscripts', 'nightmare.js'));
    }
  
    /**
     * {{> selectOption }}
     */
    async selectOption(select, option) {
      const fetchAndCheckOption = function (el, locator) {
        el = window.codeceptjs.fetchElement(el);
        const found = document.evaluate(locator, el, null, 5, null);
        let current = null;
        const items = [];
        while (current = found.iterateNext()) {
          items.push(current);
        }
        for (let i = 0; i < items.length; i++) {
          current = items[i];
          if (current instanceof HTMLOptionElement) {
            current.selected = true;
            if (!el.multiple) el.value = current.value;
          }
          const event = document.createEvent('HTMLEvents');
          event.initEvent('change', true, true);
          el.dispatchEvent(event);
        }
        return !!current;
      };
  
      const el = await findField.call(this, select);
      assertElementExists(el, select, 'Selectable field');
      if (!Array.isArray(option)) {
        option = [option];
      }
      const promises = [];
  
      for (const key in option) {
        const opt = xpathLocator.literal(option[key]);
        const checked = await this.browser.evaluate(fetchAndCheckOption, el, Locator.select.byVisibleText(opt));
  
        if (!checked) {
          await this.browser.evaluate(fetchAndCheckOption, el, Locator.select.byValue(opt));
        }
      }
      return this.browser.wait(this.options.waitForAction);
    }
  
    /**
     * {{> setCookie }}
     *
     * Wrapper for `.cookies.set(cookie)`.
     * [See more](https://github.com/segmentio/nightmare/blob/master/Readme.md#cookiessetcookie)
     */
    async setCookie(cookie) {
      return this.browser.cookies.set(cookie);
    }
  
    /**
     * {{> seeCookie }}
     *
     */
    async seeCookie(name) {
      const res = await this.browser.cookies.get(name);
      truth(`cookie ${name}`, 'to be set').assert(res);
    }
  
    /**
     * {{> dontSeeCookie }}
     */
    async dontSeeCookie(name) {
      const res = await this.browser.cookies.get(name);
      truth(`cookie ${name}`, 'to be set').negate(res);
    }
  
    /**
     * {{> grabCookie }}
     *
     * Cookie in JSON format. If name not passed returns all cookies for this domain.
     *
     * Multiple cookies can be received by passing query object `I.grabCookie({ secure: true});`. If you'd like get all cookies for all urls, use: `.grabCookie({ url: null }).`
     */
    async grabCookie(name) {
      return this.browser.cookies.get(name);
    }
  
    /**
     * {{> clearCookie }}
     */
    async clearCookie(cookie) {
      if (!cookie) {
        return this.browser.cookies.clearAll();
      }
      return this.browser.cookies.clear(cookie);
    }
  
    /**
     * {{> waitForFunction }}
     */
    async waitForFunction(fn, argsOrSec = null, sec = null) {
      let args = [];
      if (argsOrSec) {
        if (Array.isArray(argsOrSec)) {
          args = argsOrSec;
        } else if (typeof argsOrSec === 'number') {
          sec = argsOrSec;
        }
      }
      this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
      return this.browser.wait(fn, ...args);
    }
  
    /**
     * {{> wait }}
     */
    async wait(sec) {
      return new Promise(((done) => {
        setTimeout(done, sec * 1000);
      }));
    }
  
    /**
     * {{> waitForText }}
     */
    async waitForText(text, sec, context = null) {
      if (!context) {
        context = this.context;
      }
      const locator = new Locator(context, 'css');
      this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
      return this.browser.wait((by, locator, text) => {
        return window.codeceptjs.findElement(by, locator).innerText.indexOf(text) > -1;
      }, locator.type, locator.value, text).catch((err) => {
        if (err.message.indexOf('Cannot read property') > -1) {
          throw new Error(`element (${JSON.stringify(context)}) is not in DOM. Unable to wait text.`);
        } else if (err.message && err.message.indexOf('.wait() timed out after') > -1) {
          throw new Error(`there is no element(${JSON.stringify(context)}) with text "${text}" after ${sec} sec`);
        } else throw err;
      });
    }
  
    /**
     * {{> waitForVisible }}
     */
    waitForVisible(locator, sec) {
      this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
      locator = new Locator(locator, 'css');
  
      return this.browser.wait((by, locator) => {
        const el = window.codeceptjs.findElement(by, locator);
        if (!el) return false;
        return el.offsetWidth > 0 && el.offsetHeight > 0;
      }, locator.type, locator.value).catch((err) => {
        if (err.message && err.message.indexOf('.wait() timed out after') > -1) {
          throw new Error(`element (${JSON.stringify(locator)}) still not visible on page after ${sec} sec`);
        } else throw err;
      });
    }
  
    /**
     * {{> waitToHide }}
     */
    async waitToHide(locator, sec = null) {
      return this.waitForInvisible(locator, sec);
    }
  
    /**
     * {{> waitForInvisible }}
     */
    waitForInvisible(locator, sec) {
      this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
      locator = new Locator(locator, 'css');
  
      return this.browser.wait((by, locator) => {
        const el = window.codeceptjs.findElement(by, locator);
        if (!el) return true;
        return !(el.offsetWidth > 0 && el.offsetHeight > 0);
      }, locator.type, locator.value).catch((err) => {
        if (err.message && err.message.indexOf('.wait() timed out after') > -1) {
          throw new Error(`element (${JSON.stringify(locator)}) still visible after ${sec} sec`);
        } else throw err;
      });
    }
  
    /**
     * {{> waitForElement }}
     */
    async waitForElement(locator, sec) {
      this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
      locator = new Locator(locator, 'css');
  
      return this.browser.wait((by, locator) => window.codeceptjs.findElement(by, locator) !== null, locator.type, locator.value).catch((err) => {
        if (err.message && err.message.indexOf('.wait() timed out after') > -1) {
          throw new Error(`element (${JSON.stringify(locator)}) still not present on page after ${sec} sec`);
        } else throw err;
      });
    }
  
    async waitUntilExists(locator, sec) {
      console.log(`waitUntilExists deprecated:
      * use 'waitForElement' to wait for element to be attached
      * use 'waitForDetached to wait for element to be removed'`);
      return this.waitForDetached(locator, sec);
    }
  
    /**
     * {{> waitForDetached }}
     */
    async waitForDetached(locator, sec) {
      this.browser.options.waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
      sec = this.browser.options.waitForTimeout / 1000;
      locator = new Locator(locator, 'css');
  
      return this.browser.wait((by, locator) => window.codeceptjs.findElement(by, locator) === null, locator.type, locator.value).catch((err) => {
        if (err.message && err.message.indexOf('.wait() timed out after') > -1) {
          throw new Error(`element (${JSON.stringify(locator)}) still on page after ${sec} sec`);
        } else throw err;
      });
    }
  
    /**
     * {{> refreshPage }}
     */
    async refreshPage() {
      return this.browser.refresh();
    }
  
    /**
     * Reload the page
     */
    refresh() {
      console.log('Deprecated in favor of refreshPage');
      return this.browser.refresh();
    }
  
    /**
     * {{> saveScreenshot }}
     */
    async saveScreenshot(fileName, fullPage = this.options.fullPageScreenshots) {
      const outputFile = screenshotOutputFolder(fileName);
  
      this.debug(`Screenshot is saving to ${outputFile}`);
      const recorder = require('../recorder');
  
      if (!fullPage) {
        return this.browser.screenshot(outputFile);
      }
      const { height, width } = await this.browser.evaluate(() => {
        return { height: document.body.scrollHeight, width: document.body.scrollWidth };
      });
      await this.browser.viewport(width, height);
      return this.browser.screenshot(outputFile);
    }
  
    async _failed(test) {
      if (withinStatus !== false) await this._withinEnd();
    }
  
    /**
     * {{> scrollTo }}
     */
    async scrollTo(locator, offsetX = 0, offsetY = 0) {
      if (typeof locator === 'number' && typeof offsetX === 'number') {
        offsetY = offsetX;
        offsetX = locator;
        locator = null;
      }
      if (locator) {
        locator = new Locator(locator, 'css');
        return this.browser.evaluate((by, locator, offsetX, offsetY) => {
          const el = window.codeceptjs.findElement(by, locator);
          if (!el) throw new Error(`Element not found ${by}: ${locator}`);
          const rect = el.getBoundingClientRect();
          window.scrollTo(rect.left + offsetX, rect.top + offsetY);
        }, locator.type, locator.value, offsetX, offsetY);
      }
      // eslint-disable-next-line prefer-arrow-callback
      return this.executeScript(function (x, y) { return window.scrollTo(x, y); }, offsetX, offsetY);
    }
  
    /**
     * {{> scrollPageToTop }}
     */
    async scrollPageToTop() {
      return this.executeScript(() => window.scrollTo(0, 0));
    }
  
    /**
     * {{> scrollPageToBottom }}
     */
    async scrollPageToBottom() {
      /* eslint-disable prefer-arrow-callback, comma-dangle */
      return this.executeScript(function () {
        const body = document.body;
        const html = document.documentElement;
        window.scrollTo(0, Math.max(
          body.scrollHeight, body.offsetHeight,
          html.clientHeight, html.scrollHeight, html.offsetHeight
        ));
      });
      /* eslint-enable */
    }
  
    /**
     * {{> grabPageScrollPosition }}
     */
    async grabPageScrollPosition() {
      /* eslint-disable comma-dangle */
      function getScrollPosition() {
        return {
          x: window.pageXOffset,
          y: window.pageYOffset
        };
      }
      /* eslint-enable comma-dangle */
      return this.executeScript(getScrollPosition);
    }
  }
  
  module.exports = Nightmare;
  
  async function proceedSee(assertType, text, context) {
    let description;
    let locator;
    if (!context) {
      if (this.context === this.options.rootElement) {
        locator = new Locator(this.context, 'css');
        description = 'web application';
      } else {
        description = `current context ${this.context}`;
        locator = new Locator({ xpath: './/*' });
      }
    } else {
      locator = new Locator(context, 'css');
      description = `element ${locator.toString()}`;
    }
  
    const texts = await this.browser.evaluate((by, locator) => {
      return window.codeceptjs.findElements(by, locator).map(el => el.innerText);
    }, locator.type, locator.value);
    const allText = texts.join(' | ');
    return stringIncludes(description)[assertType](text, allText);
  }
  
  async function proceedSeeInField(assertType, field, value) {
    const el = await findField.call(this, field);
    assertElementExists(el, field, 'Field');
    const tag = await this.browser.evaluate(el => window.codeceptjs.fetchElement(el).tagName, el);
    const fieldVal = await this.browser.evaluate(el => window.codeceptjs.fetchElement(el).value, el);
    if (tag === 'select') {
      // locate option by values and check them
      const text = await this.browser.evaluate((el, val) => {
        return el.querySelector(`option[value="${val}"]`).innerText;
      }, el, xpathLocator.literal(fieldVal));
      return equals(`select option by ${field}`)[assertType](value, text);
    }
    return stringIncludes(`field by ${field}`)[assertType](value, fieldVal);
  }
  
  async function proceedIsChecked(assertType, option) {
    const els = await findCheckable.call(this, option);
    assertElementExists(els, option, 'Checkable');
    const selected = await this.browser.evaluate((els) => {
      return els.map(el => window.codeceptjs.fetchElement(el).checked).reduce((prev, cur) => prev || cur);
    }, els);
    return truth(`checkable ${option}`, 'to be checked')[assertType](selected);
  }
  
  
  async function findCheckable(locator, context) {
    let contextEl = null;
    if (context) {
      contextEl = await this.browser.findElement((new Locator(context, 'css')).toStrict());
    }
  
    const matchedLocator = new Locator(locator);
    if (!matchedLocator.isFuzzy()) {
      return this.browser.findElements(matchedLocator.toStrict(), contextEl);
    }
  
    const literal = xpathLocator.literal(locator);
    let els = await this.browser.findElements({ xpath: Locator.checkable.byText(literal) }, contextEl);
    if (els.length) {
      return els;
    }
    els = await this.browser.findElements({ xpath: Locator.checkable.byName(literal) }, contextEl);
    if (els.length) {
      return els;
    }
    return this.browser.findElements({ css: locator }, contextEl);
  }
  
  async function findClickable(locator, context) {
    let contextEl = null;
    if (context) {
      contextEl = await this.browser.findElement((new Locator(context, 'css')).toStrict());
    }
  
    const matchedLocator = new Locator(locator);
    if (!matchedLocator.isFuzzy()) {
      return this.browser.findElement(matchedLocator.toStrict(), contextEl);
    }
  
    const literal = xpathLocator.literal(locator);
  
    let els = await this.browser.findElements({ xpath: Locator.clickable.narrow(literal) }, contextEl);
    if (els.length) {
      return els[0];
    }
  
    els = await this.browser.findElements({ xpath: Locator.clickable.wide(literal) }, contextEl);
    if (els.length) {
      return els[0];
    }
  
    return this.browser.findElement({ css: locator }, contextEl);
  }
  
  async function findField(locator) {
    const matchedLocator = new Locator(locator);
    if (!matchedLocator.isFuzzy()) {
      return this.browser.findElements(matchedLocator.toStrict());
    }
    const literal = xpathLocator.literal(locator);
  
    let els = await this.browser.findElements({ xpath: Locator.field.labelEquals(literal) });
    if (els.length) {
      return els[0];
    }
  
    els = await this.browser.findElements({ xpath: Locator.field.labelContains(literal) });
    if (els.length) {
      return els[0];
    }
    els = await this.browser.findElements({ xpath: Locator.field.byName(literal) });
    if (els.length) {
      return els[0];
    }
    return this.browser.findElement({ css: locator });
  }
  
  function assertElementExists(el, locator, prefix, suffix) {
    if (el === null) throw new ElementNotFound(locator, prefix, suffix);
  }
  