Search code examples
typescriptautomationautomated-testsplaywrightplaywright-test

Square brackets property accessor doesn't work in evaluate method [Playwright]


I'm trying to get a property value of web element, setting property name dynamically. Each time, when I use square brackets property accessor (Ex: node[propertyName]), I get an error "elementHandle.evaluate: ReferenceError: propertyName is not defined". But if I try the same property, using dot property accessor, it works fine and I get expected value.

The documentation of class JSHandle and method evaluate is here.

I tried to reproduce the same steps in Browser console.

const elements = $$("thead th");
const element = elements[0];
propertyName = "innerText";

const propertyValue2 = element.innerText;
const propertyValue = element[propertyName];

console.log(propertyValue2); // Expected property value
console.log(propertyValue); // Expected property value

But in my IDE, a got a little bit other results:

protected async getElementProperty(): Promise<string> {
    const elements = await this.page.$$("thead th");
    const element: JSHandle = elements[0];
    propertyName = "innerText";
    

    const propertyValue1: string = await element.evaluate((node) => node.innerText); // Expected property value
    const propertyValue2: string = await element.evaluate((node) => node[propertyName]); // ReferenceError: propertyName is not defined
    const propertyValue3: string = await element.evaluate(`(node) => node[${propertyName}]`); // Undefined
    
    return propertyValue;
  }

Solution

  • Playwright's evaluate method is tricky: It converts the function you give it to source code, then rebuilds the function within the page environment. So your function can't close over variables, because they don't exist in the page environment, only in your script's environment.

    You can see this in the "right" and "wrong" examples in the documentation:

    Right:

    const data = { text: 'some data', value: 1 };
    // Pass |data| as a parameter.
    const result = await page.evaluate(data => {
      window.myApp.use(data);
    }, data);
    

    Wrong:

    const data = { text: 'some data', value: 1 };
    const result = await page.evaluate(() => {
      // There is no |data| in the web page.
      window.myApp.use(data);
    });
    

    Instead, pass propertyName as a parameter as they've done in that "right" example, although your code is closer to this example further up the page:

    await button.evaluate((button, from) => button.textContent.substring(from), 5);
    

    More in the JSHandle.evaluate docs.

    So:

    const propertyValue2: string = await element.evaluate(
        (node, propertyName) => node[propertyName],
        propertyName
    );
    

    Notice that the function receives a second parameter, and we pass a second argument to evaluate.