segment-boneyard / nightmare

A high-level browser automation library.
https://open.segment.com
19.55k stars 1.08k forks source link

Simulate arrow keys #932

Closed nickforddev closed 7 years ago

nickforddev commented 7 years ago

I've seen several threads and issues about mimicking a user hitting the enter or delete keys, but can't find anything about arrow keys. I've already tried converting the keycode values to hex ie. 40, which is the down arrow on a Mac, would be \u0028 I suppose, but that is (. I tried every variation I could find, including a hex value I found in some Apple native API, can't find anything that works for arrow keys in Nightmare.js.

So, is this even possible?

Also, is this still the way to go about it? This actually returns an error: Cannot read property 'focus' of null


`.type('document', '\u000d')`
rosshinkley commented 7 years ago

Hm, the only way I can think of off the top of my head to do this is with an accelerator. Accelerators let you send keys like the arrow keys. Out of the box, I don't think Nightmare supports accelerators, but I think you could write something with .action() and webview.sendInputEvent() to accomplish what you're after.

josephrocca commented 7 years ago

To help others who also need this, I managed to get it working without too much trouble using Ross's advice:

  Nightmare.action('specialKeyPress',
  function(name, options, parent, win, renderer, done) {
    parent.respondTo('specialKeyPress', async function(keyCode, done) {
      // See this: https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentssendinputeventevent
      // and this: https://github.com/electron/electron/blob/master/docs/api/accelerator.md
      win.focus();
      win.webContents.sendInputEvent({ type:'keyDown', keyCode });
      await new Promise(resolve => setTimeout(resolve, 50));
      win.webContents.sendInputEvent({ type:'keyUp', keyCode });
      done();
    });
    done();
  },
  function(keyCode, done) {
    this.child.call('specialKeyPress', keyCode, done);
  });

It takes any of the keyCodes listed in this document. Use it like this:

await nightmare
  .goto('http://blahblah.com')
  .specialKeyPress('Down'); // or `Up` or `Right` etc.

Note that you don't need this custom action for Enter and Tab, which can be triggered with .type('body','\u000d') and .type('body','\u0009'), respectively.

casesandberg commented 7 years ago

Looks like this is resolved. I am going to close this for now but feel free to open a new issue if you are still having problems.

bitnom commented 7 years ago

This isn't working for me. I'm trying to use it to backspace pre-existing text in an input box. I also tried sending PrintScreen as a test just to see if the action is working but that didn't work either:

const Nightmare = require('nightmare');
const vo = require('vo');

Nightmare.action('specialKeyPress',
    function(name, options, parent, win, renderer, done) {
        parent.respondTo('specialKeyPress', async function(keyCode, done) {
            // See this: https://github.com/electron/electron/blob/master/docs/api/web-contents.md#contentssendinputeventevent
            // and this: https://github.com/electron/electron/blob/master/docs/api/accelerator.md
            win.focus();
            win.webContents.sendInputEvent({ type:'keyDown', keyCode });
            await new Promise(resolve => setTimeout(resolve, 50));
            win.webContents.sendInputEvent({ type:'keyUp', keyCode });
            done();
        });
        done();
    },
    function(keyCode, done) {
        this.child.call('specialKeyPress', keyCode, done);
    });

var run = function * () {
    const nightmare = Nightmare({
        show: true,
        switches: {
            /////'proxy-server': 'http://' + proxyHost + ':' + proxyPort,
            'ignore-certificate-errors': true
        },
        waitTimeout: 90000,
        gotoTimeout: 90000
    });

    yield nightmare.goto('https://www.google.com');
    yield nightmare.click('div.innerWrap input[aria-haspopup="true"]'); // Click city input
    yield nightmare.specialKeyPress('PrintScreen');
    for(i = 0; i < 40; i++){
        yield nightmare.specialKeyPress('Right'); // Move cursor right of any text
    }
    for(i = 0; i < 40; i++){
        yield nightmare.type('div.innerWrap input[aria-haspopup="true"]','\u0008'); // Backspace city input
    }

    yield nightmare.end(() => "some value");
    yield nightmare.then(function () {
        console.log("ended");
        //////xvfb.stopSync();
    });
}

vo(run)(function(err, titles) {
    console.dir(titles);
});
matthewmueller commented 6 years ago

For anyone coming back to this, the above solution no longer works, instead you should use the Press custom action in this runnable example:

const Nightmare = require('nightmare')

main().catch(console.error)

async function main() {
  Press(Nightmare)

  const nightmare = Nightmare({ show: true })

  await nightmare.goto('https://google.com')
  await nightmare.type('#lst-ib', 'nightmare is cool')

  await nightmare.press('#lst-ib', 'Backspace')
  await nightmare.press('#lst-ib', 'Backspace')
  await nightmare.press('#lst-ib', 'Backspace')
  await nightmare.press('#lst-ib', 'Backspace')

  await nightmare.type('#lst-ib', 'awesome!')

  await sleep(5000)
  await nightmare.end()
}

function sleep(ms) {
  return new Promise(res => setTimeout(res, ms))
}

function Press(Nightmare) {
  Nightmare.action(
    'press',
    function(name, options, parent, win, renderer, done) {
      parent.respondTo('press', function(keyCode, done) {
        win.webContents.sendInputEvent({ type: 'keyDown', keyCode: keyCode })
        win.webContents.sendInputEvent({ type: 'keyUp', keyCode: keyCode })
        done()
      })
      done()
    },
    function(selector, keyCode, done) {
      // focus, press, blur
      // TODO: clean me up
      return this.evaluate_now(
        selector => document.querySelector(selector).focus(),
        () =>
          this.child.call('press', keyCode, () => {
            this.evaluate_now(
              selector => document.querySelector(selector).blur(),
              () => done(),
              selector
            )
          }),
        selector
      )
    }
  )
}
adelriosantiago commented 6 years ago

@matthewmueller 's solution works fine. Thanks.

This would be a nice feature in NightmareJS!