Multiple VUs using xk6 browser

I’m using k6/x/browser and while running in headed mode my interactions with the browser has no issues.

However when I want to use headless mode (because I want to run multiple concurrent vus) then I run in to issues and that interactions with the browser doesn’t want to happen.

Any ideas on how I can run in headless mode using multiple concurrent vus?

There are 2 API calls I make which then provides a redirect URL which I then navigate to using xk6 browser.

On that page I enter some payment details and click on a submit button which redirect to a 3DS page where I enter a password and click on a submit button again which then redirects back and will either say payment successful or failed.

This all works pretty well even using 3 concurrent users while in headed mode. But when headless it doesn’t even interact with the first page. I’m more than happy to share my script if that will help.

I am using the latest xk6 browser.

I’m just not sure if I’m doing something wrong or if xk6 browser was never intended to be used headless with multiple VUs.

I actually want to stress test this flow so wanted to throw a lot more than 3 VUs at it hence wanting to use headless mode.

Below the code I use for the browser interactions.

I had some issues making sure I wait for the locators to be available so played around a lot with waitForNavigation / waitForLoadState, using promises etc.

I would appreciate it a lot for any assistance on how best to approach this, with the main aim to stress test this flow.

const browser = chromium.launch();
const context = browser.newContext();
const page = context.newPage();

console.log(`VU ${__VU} Redirect URL: ${redirectUrl}`)

page
  .goto(redirectUrl, { waitUntil: 'networkidle', timeout: 10000, allowHttpErrors: true  })
  .then(() => {
  
    const cvvInput = page.locator('input[name="cvc"]')
    // cvvInput.waitFor({
    //   state: 'visible',
    // });

    console.log(`VU ${__VU} Enter CVV`)
    cvvInput.type(cvv)

    const submitInput = page.locator('button[type="submit"]')
    // submitInput.waitFor({
    //   state: 'visible',
    // });

    return Promise.all([
      console.log(`VU ${__VU} Click on Submit and Redirect to 3DS`),
      page.waitForNavigation({waitUntil: 'load'}),
      page.waitForLoadState('networkidle'),
      submitInput.click(),
    ]).then(() => {
      let pageContent = page.content()

      if (pageContent.includes('Payment Failed')) {
        console.log(`VU ${__VU} Transaction Status: Payment Failed Before 3DS Redirect`)
        sleep(1)
        //console.log(pageContent)
      } else {
        const passwordInput = page.locator('input[name="password"]')
        // passwordInput.waitFor({
        //   state: 'visible',
        // });

        console.log(`VU ${__VU} Enter Password`)
        passwordInput.type('test123')

        const threedsSubmit = page.locator('//input[@value=\'Submit\']')
        // threedsSubmit.waitFor({
        //   state: 'visible',
        // });

        return Promise.all([
          console.log(`VU ${__VU} Click 3DS Submit`),
          page.waitForNavigation({waitUntil: 'load'}),
          page.waitForLoadState('networkidle'),
          threedsSubmit.click(),
        ]).then(() => {
          page.waitForNavigation({waitUntil: 'load'})
          page.waitForLoadState('networkidle')

          let pageContent = page.content()
          if (pageContent.includes('Payment Successful')) {
            console.log(`VU ${__VU} Transaction Status: Payment Successful`)
          } else {
            console.log(`VU ${__VU} Transaction Status: Payment Failed`)
          }
          // const redirect = page.locator('a[title="Redirect Now"]');
          sleep(1)
        });
      }
      
    });
}).finally(() => {
  page.close();
  browser.close();

  const now = new Date();
  let testEndTimestamp = new Date();

  const durationInSeconds = (testEndTimestamp.getTime() - testStartTimestamp.getTime()) / 1000;

  console.log(`VU ${__VU} Test Complete: ${testEndTimestamp.toLocaleDateString()} ${testEndTimestamp.toLocaleTimeString()} Duration: ${durationInSeconds}`);
});

I believe that I might be doing something wrong with the promises and waits in any case because at times when running in headed mode I will get this error when I want to enter the password.

So clearly I’m not waiting correctly for this locator to become visible even though I have it in a promise and I use waitForNavigation and waitForLoadState.

ERRO[0012] Uncaught (in promise) GoError: waiting for “input[name="password"]”: execution context changed; most likely because of a navigation
running at reflect.methodValueCall (native)

Hi @Driesie,

Unfortunately, the parameter allowHttpErrors: true is not available. To make your scripts easier to maintain, you can use await instead of then. You can refer to this example for more information.

My recommendation would be to remove console.log from Promise.all, since waitForLoadState doesn’t return a promise, it shouldn’t be included. Additionally, locator.click does not support promises yet, so you might consider using page.click or element.click instead. Furthermore, it’s advisable to remove sleep calls as it could cause issues with our internals.

Could you try these first, and if they don’t work, please share with us an HTML page and a script, so we can reproduce the issue on our side.

Thanks.