11

After sending keys to an input field with selenium, the result is not as expected - the keys are inserted in incorrect order.

e.g. send_keys('4242424242424242') -> result is "4224242424242424"

EDIT: On some machines I observe the issue only randomly, 1 case out of 10 attempts. On another machine it is 10/10

This happens specifically with Stripe payment form + I see this problem only in Chrome version 69 (in previous versions it worked OK)

This can be easily reproduced on sample Stripe site: https://stripe.github.io/elements-examples/

Sample python code:

from selenium import webdriver
driver = webdriver.Chrome()
driver.get('https://stripe.github.io/elements-examples/')
driver.switch_to.frame(driver.find_element_by_tag_name('iframe'))  # First iframe
cc_input = driver.find_element_by_css_selector('input[name="cardnumber"]')
cc_input.send_keys('4242424242424242')

Result: enter image description here

I am able to get pass this by sending the keys one by one with slight delay - but this is also not 100% reliable (plus terribly slow)

I am not sure if this is a problem with selenium (3.14.1)/chromedriver (2.41.578737) or if I am doing something wrong.

Any ideas please?

Vafliik
  • 481
  • 3
  • 11
  • 2
    As a small point, Stripe generally doesn't recommend doing automated testing of the Elements form in this way(as the content is dynamically loaded in an iframe and things like IDs and DOM structure are subject to change at any time). Instead for testing you can mock the whole payment form and pass a [test token](https://stripe.com/docs/testing#cards) to your backend instead. – karllekko Oct 02 '18 at 12:51
  • Just tried your script, I can see the script is working properly. Its entering 4242 4242 4242 4242. –  Oct 02 '18 at 12:52
  • try to clear cookies first , and check – cruisepandey Oct 02 '18 at 12:52
  • @karllekko good point, I guess I will have to go with that. Unfortunately this was part of my E2E scripts that was e.g. testing proper error handling by the application in case the CC was not valid etc. I am not sure if I will be able to test the same using just the token – Vafliik Oct 02 '18 at 12:58
  • @dbachhav Thanks a lot for trying. Have you tried in Chrome 69? I have the same problem on different computers, also when trying services like Browserstack (although there I randomly get good results). It really looks like some random problem (although locally I get in 100%) – Vafliik Oct 02 '18 at 12:59
  • 1
    @Vafliik : Yes, I have tried in chrome version 69.0.3497.100 (Official Build) (64-bit). please clear your browsing history and try one more time :) –  Oct 02 '18 at 13:02
  • Have you/can you check the document status using javascript before sending the text to make sure jquery is not active? – Bill Hileman Oct 02 '18 at 14:15
  • @BillHileman I do not think the jQuery is active on the page. On the other hand, there is definitely some javascript handling of the form, as it does some checks after each key press – Vafliik Oct 02 '18 at 14:59

7 Answers7

4

We are having the exact same problem on MacOS and Ubuntu 18.04, as well as on our CI server with protractor 5.4.1 and the same version of selenium and chromedriver. It has only started failing since Chrome 69, worse in v70.

Update - Working (for the moment)

After much further investigation, I remembered that React tends to override change/input events, and that the values in the credit card input, ccv input etc are being rendered from the React Component State, not from just the input value. So I started looking, and found What is the best way to trigger onchange event in react js

Our tests are working (for the moment):

//Example credit input
function creditCardInput (): ElementFinder {
  return element(by.xpath('//input[contains(@name, "cardnumber")]'))
}

/// ... snippet of our method ...
await ensureCreditCardInputIsReady()
await stripeInput(creditCardInput, ccNumber)
await stripeInput(creditCardExpiry, ccExpiry)
await stripeInput(creditCardCvc, ccCvc)
await browser.wait(this.hasCreditCardZip(), undefined, 'Should have a credit card zip')
await stripeInput(creditCardZip, ccZip)
await browser.switchTo().defaultContent()
/// ... snip ...

async function ensureCreditCardInputIsReady (): Promise<void> {
  await browser.wait(ExpectedConditions.presenceOf(paymentIFrame()), undefined, 'Should have a payment iframe')
  await browser.switchTo().frame(await paymentIFrame().getWebElement())
  await browser.wait(
    ExpectedConditions.presenceOf(creditCardInput()),
    undefined,
    'Should have a credit card input'
  )
}

/**
 * SendKeys for the Stripe gateway was having issues in Chrome since version 69. Keys were coming in out of order,
 * which resulted in failed tests.
 */
async function stripeInput (inputElement: Function, value: string): Promise<void> {
  await browser.executeScript(`
      var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
          nativeInputValueSetter.call(arguments[0], '${value}');
      var inputEvent = new Event('input', { bubbles: true});
          arguments[0].dispatchEvent(inputEvent);
        `, inputElement()
  )
  await browser.sleep(100)
  const typedInValue = await inputElement().getWebElement().getAttribute('value')

  if (typedInValue.replace(/\s/g, '') === value) {
    return
  }

  throw new Error(`Failed set '${typedInValue}' on ${inputElement}`)
}

Previous Idea (only worked occasionally):

I have setup a minimal repro using https://stripe.com/docs/stripe-js/elements/quickstart and it succeeds when tests are run sequentially, but not in parallel (we think due to focus/blur issues when switching to the iframes).

Our solution is similar, although we noticed from watching the tests that input.clear() wasn't work on tel inputs which are used in the iframe.

This still fails occasionally, but far less frequently.

/**
 * Types a value into an input field, and checks if the value of the input
 * matches the expected value. If not, it attempts for `maxAttempts` times to
 * type the value into the input again.
 *
 * This works around an issue with ChromeDriver where sendKeys() can send keys out of order,
 * so a string like "0260" gets typed as "0206" for example.
 *
 * It also works around an issue with IEDriver where sendKeys() can press the SHIFT key too soon
 * and cause letters or numbers to be converted to their SHIFT variants, "6" gets typed as "^", for example.
 */
export async function slowlyTypeOutField (
  value: string,
  inputElement: Function,
  maxAttempts = 20
): Promise<void> {
  for (let attemptNumber = 0; attemptNumber < maxAttempts; attemptNumber++) {
    if (attemptNumber > 0) {
      await browser.sleep(100)
    }

    /*
      Executing a script seems to be a lot more reliable in setting these flaky fields than using the sendKeys built-in
      method. However, I struggled in finding out which JavaScript events Stripe listens to. So we send the last key to
      the input field to trigger all events we need.
     */
    const firstPart = value.substring(0, value.length - 1)
    const secondPart = value.substring(value.length - 1, value.length)
    await browser.executeScript(`
        arguments[0].focus();
        arguments[0].value = "${firstPart}";
      `,
      inputElement()
    )
    await inputElement().sendKeys(secondPart)
    const typedInValue = await inputElement().getAttribute('value')

    if (typedInValue === value) {
      return
    }

    console.log(`Tried to set value ${value}, but instead set ${typedInValue} on ${inputElement}`)
  }

  throw new Error(`Failed after ${maxAttempts} attempts to set value on ${inputElement}`)
}
Benno
  • 3,008
  • 3
  • 26
  • 41
2

I faced a similar issue in ubuntu 14.04, the following trick helped me. Have not got any issue since. First I used the regular send_keys method. Then I called the execute script to update the value

    input_data = "someimputdata"
    some_xpath = "//*[contains(@id,'input_fax.number_')]"
    element = web_driver_obj.find_element_by_xpath(some_xpath)
    element.clear()
    element.send_keys(input_data)
    web_driver_obj.execute_script("arguments[0].value = '{0}';".format(input_data), element)
0

Edit

Thanks a lot to @Benno - his answer was correct. I will just add python solution that worked for me, based on his JS

driver.get('https://stripe.github.io/elements-examples/')
driver.switch_to.frame(driver.find_element_by_tag_name('iframe'))  # First iframe
cc_input = driver.find_element_by_css_selector('input[name="cardnumber"]')
value = "4242424242424242"
driver.execute_script('''
 input = arguments[0];
 var nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; 
 nativeInputValueSetter.call(input, "{}");
 var eventCard = new Event("input", {{bubbles: true}});
 input.dispatchEvent(eventCard);
 '''.format(value), cc_input)
driver.switch_to.default_content()
driver.quit()

After couple of hours of trying, I gave up and accepted the fact that this is really a random issue and went with a workaround.

  1. Where it is not necessary to update, I will stay with Chrome version < 69
  2. In order to test latest Chrome, I will use React solution

What I've found out

The issue manifested itself mostly on MacOS, quite rarely on Windows (there are most probably other factors in play, this is just an observation)

I've run an experiment with 100 repetitions of filling the form.

Mac - 68 failures

Windows - 6 failures

The cookies/local history (as suggested in comments) do not seem to be the problem. The webdriver always spawned a "clean" instance of the browser, with no cookies or local storage.

Vafliik
  • 481
  • 3
  • 11
0

Maybe my solution will help for somebody:

I used sendKeys(" 4242424242424242")
Same for cvc field

With a space before string, it actually works for selenide + chrome + java

Diego Venâncio
  • 5,698
  • 2
  • 49
  • 68
0

You could make your own generic SendKeys method that takes the input element and the string you would like to send. The method would split the string into individual characters and then use the selenium sendkeys method on each character.

0

Adding in some backspaces worked for me for whatever reason:

from selenium.webdriver.common.keys import Keys

my_value = "123"
my_xpath="//input[@class='form-text']"
element = driver.find_element_by_xpath(my_xpath)
element.clear()
element.send_keys(Keys.BACKSPACE * 3, my_value)
Tahlor
  • 1,642
  • 1
  • 17
  • 21
0

I was having the same issue using RSelenium and got the idea to try adding spaces to the credit card number as they appear on the card from @Pavel's answer, since adding a space before the card number didn't work for me.

Using RSelenium this would be:

element$sendKeysToElement(list("4242 4242 4242 4242"))

Terrence
  • 33
  • 4