diff --git a/src/PlaywrightNative.Tests/JSHandlePropertiesTests.cs b/src/PlaywrightNative.Tests/JSHandlePropertiesTests.cs index fba05e04..14dc5218 100644 --- a/src/PlaywrightNative.Tests/JSHandlePropertiesTests.cs +++ b/src/PlaywrightNative.Tests/JSHandlePropertiesTests.cs @@ -160,6 +160,53 @@ public async Task GetPropertiesShouldReturnEmptyMapForNonObjects() Dictionary properties = await aHandle.GetPropertiesAsync().ConfigureAwait(false); Assert.That(properties, Is.Empty); } + + [PlaywrightTest("jshandle-properties.spec.ts", "getProperties should return properties inhertied from super class")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task GetPropertiesShouldReturnPropertiesInhertiedFromSuperClass() + { + IJSHandle aHandle = await Page.EvaluateHandleAsync(@"() => { + class A { + constructor() { + this.a = '1'; + } + } + class B extends A { + constructor() { + super(); + this.b = '2'; + } + } + return new B(); + }").ConfigureAwait(false); + Dictionary properties = await aHandle.GetPropertiesAsync().ConfigureAwait(false); + Assert.That(await properties["a"].JsonValueAsync().ConfigureAwait(false), Is.EqualTo("1")); + Assert.That(await properties["b"].JsonValueAsync().ConfigureAwait(false), Is.EqualTo("2")); + } + + [PlaywrightTest("jshandle-properties.spec.ts", "getProperties should return own enumerable properties only")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task GetPropertiesShouldReturnOwnEnumerablePropertiesOnly() + { + IJSHandle aHandle = await Page.EvaluateHandleAsync(@"() => { + class A { + constructor() { + this.own = 1; + Object.defineProperty(this, 'ownHidden', { value: 2, enumerable: false }); + } + } + A.prototype.inherited = 3; + return new A(); + }").ConfigureAwait(false); + Assert.That((await aHandle.GetPropertiesAsync().ConfigureAwait(false)).Keys, Is.EqualTo(new[] { "own" })); + + await Page.SetContentAsync("
ab
").ConfigureAwait(false); + IJSHandle collection = await Page.EvaluateHandleAsync("() => document.querySelector('#d').children").ConfigureAwait(false); + Assert.That((await collection.GetPropertiesAsync().ConfigureAwait(false)).Keys, Is.EqualTo(new[] { "0", "1" })); + } + [PlaywrightTest("jshandle-properties.spec.ts", "getProperties should work with elements")] [Test] [Timeout(30_000)] diff --git a/src/PlaywrightNative.Tests/PageEvaluateParityTests.cs b/src/PlaywrightNative.Tests/PageEvaluateParityTests.cs index 0884ed5b..7f5b138e 100644 --- a/src/PlaywrightNative.Tests/PageEvaluateParityTests.cs +++ b/src/PlaywrightNative.Tests/PageEvaluateParityTests.cs @@ -300,6 +300,21 @@ public async Task ShouldTransferMapsAsEmptyObjects() Assert.That(result, Is.EqualTo("Object {}")); } + [PlaywrightTest("page-evaluate.spec.ts", "should transfer sets as empty objects")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task ShouldTransferSetsAsEmptyObjects() + { + // .NET has no JS Set, so build it in the page and transfer it back as the argument. + Dictionary set = await Page.EvaluateAsync>("() => new Set([1, 2])").ConfigureAwait(false); + string result = await Page.EvaluateAsync( + "a => a.x.constructor.name + ' ' + JSON.stringify(a.x)", + new Dictionary { ["x"] = set }) + .ConfigureAwait(false); + Assert.That(result, Is.EqualTo("Object {}")); + Assert.That(set, Is.Empty); + } + [PlaywrightTest("page-evaluate.spec.ts", "should modify global environment")] [Test] [Timeout(TestConstants.DefaultTestTimeout)] @@ -482,6 +497,35 @@ public void ShouldSupportThrownNumbersAsErrorMessages() Assert.That(error.Message, Does.Contain("100500")); } + [PlaywrightTest("page-evaluate.spec.ts", "should reject when a falsy value is thrown")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public void ShouldRejectWhenAFalsyValueIsThrown() + { + foreach (string value in new[] { "null", "undefined", "0", "''", "false", "NaN", "-0", "0n" }) + { + Assert.CatchAsync( + () => Page.EvaluateAsync($"(() => {{ throw {value}; }})()"), + $"throw {value}"); + Assert.CatchAsync( + () => Page.EvaluateAsync($"Promise.reject({value})"), + $"Promise.reject({value})"); + } + } + + [PlaywrightTest("page-evaluate.spec.ts", "should include a non-error rejection value in the error message")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public void ShouldIncludeANonErrorRejectionValueInTheErrorMessage() + { + PlaywrightException error = Assert.CatchAsync( + () => Page.EvaluateAsync("() => Promise.reject(7)")); + Assert.That(error.Message, Does.Contain("7")); + PlaywrightException nullError = Assert.CatchAsync( + () => Page.EvaluateAsync("() => { throw null; }")); + Assert.That(nullError.Message, Does.Contain("null")); + } + [PlaywrightTest("page-evaluate.spec.ts", "should return complex objects")] [Test] [Timeout(TestConstants.DefaultTestTimeout)] diff --git a/src/PlaywrightNative.Tests/PageEventConsoleTests.cs b/src/PlaywrightNative.Tests/PageEventConsoleTests.cs index d0adada6..4648d2c1 100644 --- a/src/PlaywrightNative.Tests/PageEventConsoleTests.cs +++ b/src/PlaywrightNative.Tests/PageEventConsoleTests.cs @@ -253,6 +253,32 @@ await Task.WhenAll( } } + [PlaywrightTest("page-event-console.spec.ts", "should not fail for object that can't be coerced to a primitive")] + [Test] + [Timeout(TestConstants.DefaultTestTimeout)] + public async Task ShouldNotFailForObjectThatCantBeCoercedToAPrimitive() + { + IConsoleMessage message = null; + void OnConsole(object sender, IConsoleMessage received) + { + Page.Console -= OnConsole; + message = received; + } + + Page.Console += OnConsole; + await Task.WhenAll( + Page.EvaluateAsync("() => console.log({ toString() { return {}; }, valueOf() { return {}; } })"), + Page.WaitForConsoleMessageAsync()).ConfigureAwait(false); + if (!TestConstants.IsFirefox) + { + Assert.That(message.Text, Does.Match(@"\{toString: (Function)?, valueOf: (Function)?\}")); + } + else + { + Assert.That(message.Text, Is.EqualTo("JSHandle@object")); + } + } + [PlaywrightTest("page-event-console.spec.ts", "should trigger correct Log")] [Test] [Retry(2)] diff --git a/src/PlaywrightNative/Chromium/CRExecutionContext.cs b/src/PlaywrightNative/Chromium/CRExecutionContext.cs index 6a97c934..dd6f91fd 100644 --- a/src/PlaywrightNative/Chromium/CRExecutionContext.cs +++ b/src/PlaywrightNative/Chromium/CRExecutionContext.cs @@ -584,6 +584,11 @@ private static void ThrowIfExceptionDetails(JsonElement response) { message = description.GetString(); } + else if (!exception.TryGetProperty("value", out JsonElement rawValue) || rawValue.ValueKind == JsonValueKind.Null) + { + // Official getExceptionMessage: String(exception.value) for thrown null / undefined. + message = rawValue.ValueKind == JsonValueKind.Null ? "null" : "undefined"; + } if (exception.TryGetProperty("value", out JsonElement thrown) && thrown.ValueKind != JsonValueKind.Undefined diff --git a/src/PlaywrightNative/Helpers/JsonValueHelper.cs b/src/PlaywrightNative/Helpers/JsonValueHelper.cs index a072598f..857efa3f 100644 --- a/src/PlaywrightNative/Helpers/JsonValueHelper.cs +++ b/src/PlaywrightNative/Helpers/JsonValueHelper.cs @@ -41,12 +41,12 @@ internal static class JsonValueHelper internal const string SerializeFunction = EvaluateSerialization.SerializeJs; /// - /// Returns own and inherited (prototype) property names, or an empty list for - /// primitives / null. Stops at Object.prototype so getProperties - /// includes constructor-assigned parent fields without walking built-ins. + /// Returns own enumerable data property names, or an empty list for + /// primitives / null. Matches the protocol getProperties + /// filter (ownProperties, enumerable, has a value). /// internal const string EnumerablePropertyNamesFunction = - "o => { if (o === null || o === undefined || (typeof o !== 'object' && typeof o !== 'function')) return []; const names = []; const seen = new Set(); for (let obj = o; obj && obj !== Object.prototype; obj = Object.getPrototypeOf(obj)) { const own = Object.getOwnPropertyNames(obj); for (let i = 0; i < own.length; i++) { const name = own[i]; if (!seen.has(name)) { seen.add(name); names.push(name); } } } return names; }"; + "o => { if (o === null || o === undefined || (typeof o !== 'object' && typeof o !== 'function')) return []; return Object.keys(o).filter(name => { const descriptor = Object.getOwnPropertyDescriptor(o, name); return !!descriptor && 'value' in descriptor; }); }"; private static readonly JsonSerializerOptions PreserveOptions = new JsonSerializerOptions {