Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: Why use Promise.all when await works? #5470

Closed
roselan opened this issue Feb 16, 2021 · 3 comments
Closed

doc: Why use Promise.all when await works? #5470

roselan opened this issue Feb 16, 2021 · 3 comments

Comments

@roselan
Copy link

roselan commented Feb 16, 2021

in https://playwright.dev/docs/api/class-browsercontext#browsercontextonpage

there is

const [page] = await Promise.all([
  context.waitForEvent('page'),
  page.click('a[target=_blank]'),
]);
console.log(await page.evaluate('location.href'));

but this work too in our test:

await page.click('a[target=_blank]');
const newPage = await context.waitForEvent('page');
console.log(await newPage.evaluate('location.href'));

What is the reason Promise.all is used for this scenario?

edit: corrected code for correctness.

@pavelfeldman
Copy link
Member

if 'page' event emits before page.click finishes, second line of your snippet will stall. The way you read our recommended snippet is:

  • start waiting for the 'page' event first
  • perform click
  • wait for both click to finish and for 'page' event to arrive.

@roselan
Copy link
Author

roselan commented Feb 17, 2021

Why didn't I see this? ty!

@advename
Copy link

advename commented Oct 20, 2022

I'll hijack this issue as it's the first result searching for the need of Promise.all in Playwright.

I'll use the waitForResponse() method to showcase the issue.


Let's say, we have a Search Input field and a button that triggers the search, eventually making a request to an API (https://example.com/api/search, the search term is in the request body)

You would probably write something like this

await page.locator("button").click() // search button

await page.waitForResponse("https://example.com/api/search")  

With the above code, there's a (high) chance that we already received a response from https://example.com/api/search before we reached the await page.waitForResponse("https://example.com/api/search") line. The .click() method doesn't resolve immediately, but performs a range of (time-consuming) steps before resolving the await promise and continuing to the next line.

await executes code asynchronous in sequence, one after another.

What we want here is for await page.locator("button").click() and await page.waitForResponse("https://example.com/api/search") to be executed at the same time - so that both can do their job properly.
That's where Promise.all() comes into play.

Promise.all() executes promises concurrently, meaning,

const [response] = await Promise.all([
  page.locator("button").click(),
  page.waitForResponse("https://example.com/api/search")  
]);

executes both .click() and .waitForResponse() at the same time. The await Promise.all() as a whole only resolves when all of its argument promises passed. The issue we've noticed here is called race condition.

Many Playwright events (.waitForRequest(), .waitForResponse(), .waitForEvent(), ...) must execute concurrently with their triggers using Promise.all.

But we're not done yet

The order of the iterable promises inside Promise.all is important.

In short

Promise.all does not literally run the promises at the same time, but one after another. Mostly, the delay between each execution is insignificant (less than milliseconds). However, the delay is still important for some Playwright events and therefore should events be the first argument.

As a result, we switch around the order

const [response] = await Promise.all([
  page.waitForResponse("https://example.com/api/search")  
  page.locator("button").click(),
]);

and thereby prevent obscure situations.

Long explanation

To understand why the order matters, we need to understand several NodeJS/Programming concepts.

If all promises in Promise.all would literally run at the same time, then we would speak of "parallel" instead of "concurrent". However, due to the single-threaded nature of the NodeJS, this is impossible. NodeJS can only execute code one after another. Concurrency allows NodeJS to execute code one after another, but make progress independent of each other. (More about Concurrent vs Parallel)

To understand how progress is made and in NodeJS concurrency, I'd recommend reading up on the

  1. Basic execution flow with the Call Stack, Web API and the Task Queue (Callback queue) Read 1
  2. Promises execution flow, that introduces an additional queue, the Microtask Queue. Read 1, Read 2, Read 3
    (Remember that async/await is just a sugarcoat for callback promises, i.e. .then(<callback function>))

Additional Example
To even further illustrate the concurrency delay, we can execute two identical promises in NodeJS where we can track nanoseconds for measuring performance intervals.

const { hrtime } = require('node:process');

const getTime = new Promise((resolve, reject) =>{
    const time = hrtime.bigint();
    console.log("Current time 1: " , time);
    resolve(1)
})

const getTime2 = new Promise((resolve, reject) =>{
    const time = hrtime.bigint();
    console.log("Current time 2: " , time);
    resolve(1)
})

Promise.all([getTime, getTime2]).then(x=>console.log("finished"))

Above code yields in NodeJS

Current time 1:  715083667636625n
Current time 2:  715083668555083n
finished

With this knowledge, you will understand that it's important to run the "event listener" before it's counterpart that would trigger the event.


Bonus

The initial confusion may originate from conventional event listeners, as we know, in JavaScript (e.g. document.addEventListener("keydown", fn()). The above-mentioned events are promise-based, one-time events. Unless their event is triggered, you're just blocking the rest of the test.

Playwright provides "conventional" event listeners, as we know, but they may lead in many nested layers to achieve the same result.

E.g:

page.once('response', async response =>{
  if(response.url() === "https://example.com/api/search" ){
    // additional events have to be nested in here as this now is the scope of the rest of the test
  }
})

await page.locator("button").click(),

Bonus 2

You can also verify/inspect above statements by skipping the additional steps of the .click() method using the noWaitAfter and force option:

await page.locator("button").click({noWaitAfter: true, force: true}) // immediately resolve

await page.waitForResponse("https://example.com/api/search")  

If the API does not respond immediately, then the above code should work too. However, this likely ends in untrustworthy tests that should not be used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants