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

page.evaluate returns empty object #32864

Closed
okadurin opened this issue Sep 28, 2024 · 6 comments
Closed

page.evaluate returns empty object #32864

okadurin opened this issue Sep 28, 2024 · 6 comments

Comments

@okadurin
Copy link

okadurin commented Sep 28, 2024

Hi there,
I am trying to return a class instance or a function from page.evaluate and it returns either an empty object or undefined. Here are some examples:

  const input = await page.locator('css=input');  
  await input.focus();
  const updatedConfig = await page.evaluate(() => {
    const config = {
      hasDropdownFlashed: false,
      observer: null
    };
    const dialog = document.querySelector('complex-object-combobox').shadowRoot.querySelector('dialog');
    config.observer = new MutationObserver((mutationList, observer) => {
      for (const mutation of mutationList) {
          if (dialog.style.display === '') {
            config.hasDropdownFlashed = true;                
          }              
      }
    });
    config.observer.observe(dialog, { attributeFilter: ['style'] });
    return config;
  });

  console.log('updatedConfig: ', updatedConfig);   // updatedConfig:  { hasDropdownFlashed: false, observer: {} }
  await page.keyboard.type('a');  
  await page.waitForTimeout(1000);
  await page.keyboard.type('r');  
  await page.waitForTimeout(1000);
  // expect config.hasDropdownFlashed to be false
  const input = await page.locator('css=input');  
  await input.focus();
  const updatedConfig = await page.evaluate(() => {
    const config = {
      hasDropdownFlashed: false,
      observer: null
    };
    const dialog = document.querySelector('complex-object-combobox').shadowRoot.querySelector('dialog');
    config.observer = new MutationObserver((mutationList, observer) => {
      for (const mutation of mutationList) {
          if (dialog.style.display === '') {
            config.hasDropdownFlashed = true;                
          }              
      }
    });
    config.observer.observe(dialog, { attributeFilter: ['style'] });
    return config.observer.disconnect;
  });

  console.log('updatedConfig: ', updatedConfig);   // updatedConfig:  undefined
  await page.keyboard.type('a');  
  await page.waitForTimeout(1000);
  await page.keyboard.type('r');  
  await page.waitForTimeout(1000);
  // expect config.hasDropdownFlashed to be false

The idea is to check whether a dialog flashes for some milliseconds while a users types the text. For that I setup a MutationObserver. As a result of page.evaluate I would like to get access to hasDropdownFlashed and observer to disconnect the observer.
However as I can see the returned instance of Observer is replaced with an empty object.
Is there any way to achieve what I explained? Thank you!

@okadurin
Copy link
Author

okadurin commented Sep 28, 2024

Ok I worked it around by using global object at the browser side and then requesting the property of a global object later as follows:

  const input = await page.locator('css=input');  
  await input.focus();
  await page.evaluate(() => {
    const config = {
      hasDropdownFlashed: false,
      observer: null
    };
    const dialog = document.querySelector('complex-object-combobox').shadowRoot.querySelector('dialog');
    config.observer = new MutationObserver((mutationList, observer) => {
      for (const mutation of mutationList) {
          if (dialog.style.display === '') {
            config.hasDropdownFlashed = true;                
          }              
      }
    });
    config.observer.observe(dialog, { attributeFilter: ['style'] });
    document.my = { config };
  });

  await page.keyboard.type('a');  
  await page.waitForTimeout(1000);
  await page.keyboard.type('r');  
  await page.waitForTimeout(1000);
  const hasDropdownFlashed = await page.evaluate(() => {
    console.log('hasDropdownFlashed: ', document.my.config.hasDropdownFlashed); // that value that will be passed to Node
    console.log('document.my.config.observer: ', document.my.config.observer); // actual observer in the browser
    document.my.config.observer.disconnect();
    return document.my.config.hasDropdownFlashed;
  });
  console.log('hasDropdownFlashed: ', hasDropdownFlashed);  // `true` in Node
  // expect config.hasDropdownFlashed to be false

@Skn0tt
Copy link
Member

Skn0tt commented Sep 30, 2024

Hi! You ran into the behaviour that's documented here: https://playwright.dev/docs/api/class-page#page-evaluate

If the function passed to the page.evaluate() returns a non-Serializable value, then page.evaluate() resolves to undefined. Playwright also supports transferring some additional values that are not serializable by JSON: -0, NaN, Infinity, -Infinity.

Instead of storing your config on document.my, you can keep a handle to it:

  // ...
  const config = await page.evaluateHandle(() => {
    // ...
    return config;
  });
  // ...
  const hasDropdownFlashed = await page.evaluate(config => {
    config.observer.disconnect();
    return config.hasDropdownFlashed;
  }, config);
  // ...

Let me know if that helps!

@okadurin
Copy link
Author

Hi @Skn0tt , thank you for quick response! I have just tried your suggestion and I got the error Error: page.evaluate: TypeError: config.observer.disconnect is not a function

@Skn0tt
Copy link
Member

Skn0tt commented Sep 30, 2024

Hmm, unsure why that happens. I don't have access to your full code so I can't make sure that my snippet works. This example works for me:

import { test } from "@playwright/test";

test("repro", async ({ page }) => {
    await page.goto("https://example.com");

    const config = await page.evaluateHandle(() => {
        const observer = new MutationObserver(() => {});
        const config = { observer }
        return config;
    })

    await page.evaluate(config => {
        config.observer.disconnect();
    }, config);
})

If you're getting config.observer.disconnect is not a function, then that's probably related to your specific implementation.

@okadurin
Copy link
Author

Yes you are right! I double checked the code and I noticed that first time I did not use evaluateHandle and used evaluate instead. Thank you! The solution works.

@Skn0tt
Copy link
Member

Skn0tt commented Sep 30, 2024

That's wonderful to hear! Closing then 😁

@Skn0tt Skn0tt closed this as completed Sep 30, 2024
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

2 participants