Selenium send keys incorrect order in Stripe credit card input

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}`)
}

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)