microsoft / playwright-dotnet

.NET version of the Playwright testing and automation library.
https://playwright.dev/dotnet/
MIT License
2.46k stars 233 forks source link

[Feature] Deserialize JSON result from EvaluateAsync #2700

Closed WhitWaldo closed 1 year ago

WhitWaldo commented 1 year ago

The page.EvaluateAsync function allows me to pass a JavaScript expression to evaluate on the page and if it were to return a primitive value like a number or string, I can retrieve the output value and use it in my selector logic.

I'd like to request that this method be expanded on to allow it to deserialize JSON results as well. I'd like to pass a record into the generic type parameter (optionally decorated with the necessary [JsonProprertyName] attributes to match the expected property names in the response and be able to pass more than primitive results back from the JS evaluation.

For example, if I wrote some JavaScript that returned the following:

return {\"name\": \"Playwright\", \"count\": 5};

I would expect Playwright to determine if the returned value is a string (assume it's been "stringified") or not (serialize it first), then deserialize to the expected type:

public record PageCount([property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("count")] int Count);

[TestMethod]
public async Task ShouldWork()
{
  await Page.GotoAsync(MyPageUrl);
  var result = await Page.EvaluateAsync<PageCount>("return {\"name\": \"Playwright\", \"count\": 5};");
  Assert.AreEqual("Playwright", result.Name);
  Assert.AreEqual(5, result.Count);
)

It looks like some sort of parsing capability is in place, but it's not quite working. Here's what I see today when trying to make this work:

    internal record ParseResult([property: JsonPropertyName("name")] string Name,
    [property: JsonPropertyName("count")] int Count);

    [TestMethod]
    public async Task DeserializeObjectTest()
    {
        await Page.GotoAsync("http://www.github.com");

        var jsExpr = """
                     var doWork = function() {
                        var val = {"name": "Playwright", "count": 5};
                        return val;
                     }

                     doWork();
                     """;

        var result = await Page.EvaluateAsync<ParseResult>(jsExpr);
        Assert.AreEqual("Playwright", result.Name);
        Assert.AreEqual(5, result.Count);
    }

This throws the following exception:

Microsoft.Playwright.PlaywrightException: Return type mismatch. Expecting Scraper.Tests.Selectors.ParseResult, got Object

And if I serialize it first, I still get an exception (replace return val; with return JSON.stringify(val);:

System.InvalidCastException: Invalid cast from 'System.String' to 'Scraper.Tests.Selectors.ParseResult'.

But if I just have the JS expression return a primitive type:

[TestMethod]
public async Task DeserializeObjectTest()
{
    await Page.GotoAsync("http://www.github.com");

    var jsExpr = """
                 var doWork = function() {
                    var val = {"name": "Playwright", "count": 5};
                    return val.count
                 }

                 doWork();
                 """;

    var result = await Page.EvaluateAsync<int>(jsExpr);
    Assert.AreEqual(5, result);
}

Works fine.

Thank you for the consideration!

mxschmitt commented 1 year ago

Your jsExpr looks like its not returning any value (when looking at the last line, the doWork() which does not get returned (, I'd try the following, instead of what you have above:

var jsExpr = """() => {
    var val = {"name": "Playwright", "count": 5};
    return val;
}""";

(there is no difference between using an arrow function and a normal function in this case, but an arrow function is more convenient)

And yes, we have the logic already in place to serialise a JavaScript value from the browser back into a .NET data structure using the generics.

WhitWaldo commented 1 year ago

You're right - I didn't transpose my example correctly.

However, I've narrowed the deserialization issue - if I'm deserializing to a class, this will work exactly as anticipated. But if I deserialize to a record, it fails with the return type mismatch exception.

I'm going to close this issue as it's no longer a feature request but a bug report, I think.