Search code examples
jsontypescriptplaywright

How to loop through json object with playwright


hello I have this code below that makes an api call to the url.


const [response] = await Promise.all([
        page.waitForResponse(res =>
            res.status() ==200
            &&
            res.url() == 'https://apples.com/members/msg/get_inbox?filter=all&unread_only=0&last_msg_id=0'
            &&
            res.body().then(b=>{
                // console.log(b)
                return b.includes("my_id");
            })
        ),
        await page.getByRole('button', { name: 'Continue' }).click()
    ])

When I console.log(b) I get an object back that looks like this

{
    "threads": [
        {
            "thread_id": "116E451645000000",
            "unread_total": "1",
            "total": "1",
            "newest_unread_msg_id": "21011917985",
            "newest_msg_id": "21011917985",
            "other_id": "99",
            "message": "Hello and welcome. Get verified in less than a minute! It's quick and easy.\r\n\r\nThanks for joining us! 😘",
            "msg_id": "21011917985",
            "createdtime": "2024-01-18T00:37:42-05:00",
            "sender_id": "99",
            "thread_expire": null,
            "my_id": 8913,
            "billing_url": "\/\/apples.com\/members\/billing\/join\/?billing_header=msg_expire&sid=99"
        }
    ],
    "users": [
        {
            "id": "8913",
            "online_status": "ONLINE",
            "username": "Bensure240",
            "membership": "LIONHEART",
            "age": "18",
            "gender": "Man",
            "email_validated": "N",
        },
}

I want to only console.log the value of "threads[12]" which is "my_id". If I do return b[0][12] I get an error that the type is any and 0 cannot be used to index

Im trying to console log the value of "my_id" and only log just that. The issue is how Im iterating through the object


Solution

  • The waitForResponse callback is supposed to do one thing: validate whether a given response is the one you're looking for. To do this, it should return a boolean or a promise that resolves to a truthy value. Most of the time, you just want to check the URL, method and status, but it's also possible to use the body as a predicate (see bottom of this post). Technically, your res.body().then(b=>{ resolves to true, so this should work, but it doesn't help with your main goal of accessing the property you want in your main line of code after the response arrives. Try something like:

    const [response] = await Promise.all([
      page.waitForResponse(res =>
        res.status() === 200 &&
        res.url() === 'https://apples.com/members/msg/get_inbox?filter=all&unread_only=0&last_msg_id=0'
      ),
      page.getByRole('button', { name: 'Continue' }).click()
    ]);
    const data = await response.json();
    console.log(data.threads[0].my_id);
    
    // or to print all my_ids:
    for (const e of data.threads) {
      console.log(e.my_id);
    }
    

    Note that I removed the await before page.getByRole. Almost always, you want to pass promises to Promise.all(), not their resolved values. awaiting the click means it'll run and resolve before the waitForResponse gets a chance to register, causing the response to never be captured.

    If you're not sure how to find the key you want in the JSON, you can use this tool (make sure to close your ] and remove trailing commas so your JSON is valid).

    In case it helps, here's a runnable proof of concept:

    const playwright = require("playwright"); // ^1.39.0
    
    const sampleHTML = `<!DOCTYPE html><html><body>
    <button>click me</button>
    <script>
    document.querySelector("button").addEventListener("click", e => {
      fetch("https://jsonplaceholder.typicode.com/posts");
    });
    </script></body></html>`;
    
    let browser;
    (async () => {
      browser = await playwright.firefox.launch();
      const page = await browser.newPage();
      await page.setContent(sampleHTML);
      const [response] = await Promise.all([
        page.waitForResponse(res =>
          res.status() === 200 &&
          res.url() === "https://jsonplaceholder.typicode.com/posts"
        ),
        page.click("button"),
      ]);
      const data = await response.json();
      console.log(data[0].title);
    })()
      .catch(err => console.error(err))
      .finally(() => browser?.close());
    

    As mentioned above, if you want to wait for a response with a particular piece of data in the body, waitForResponse will await any promises you return, so something like the following is possible:

    page.waitForResponse(res =>
      // If a synchronous boolean condition fails, return false
      res.status() === 200 &&
      res.url() === "https://jsonplaceholder.typicode.com/posts" &&
    
      // Otherwise, if synchronous checks pass, return a promise
      // that resolves to a boolean, which Playwright will await.
      // Note that we can only return one promise if we're using '&&', and
      // that promise needs to be the last operand in the condition chain.
      res.json().then(data => data[0].userId === 1 && data.length === 100)
    )
    

    This is a bit more precise than using a substring match on the body. Note that the promise needs to be the last part of the boolean condition so it's implicitly returned and can be awaited by Playwright.