diff --git a/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs b/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs
index b84b58df6..ba8ea099c 100644
--- a/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs
+++ b/src/Playwright.Tests/Assertions/LocatorAssertionsTests.cs
@@ -635,6 +635,25 @@ public async Task ShouldSupportToHaveJSProperty()
await Expect(locator).ToHaveJSPropertyAsync("itsNull", null);
}
+ [PlaywrightTest("playwright-test/playwright.expect.misc.spec.ts", "toHaveJSProperty pass undefined")]
+ public async Task ShouldSupportToHaveJSPropertyUndefined()
+ {
+ await Page.SetContentAsync("
");
+ var locator = Page.Locator("div");
+ await Expect(locator).ToHaveJSPropertyAsync("foo", JSUndefined.Value);
+
+ await Page.EvalOnSelectorAsync("div", "e => e.foo = null");
+ await Expect(locator).ToHaveJSPropertyAsync("foo", null);
+ var undefinedWhenNull = await PlaywrightAssert.ThrowsAsync(
+ () => Expect(locator).ToHaveJSPropertyAsync("foo", JSUndefined.Value, new() { Timeout = 200 }));
+ StringAssert.Contains("undefined", undefinedWhenNull.Message);
+
+ await Page.EvalOnSelectorAsync("div", "e => { delete e.foo; }");
+ var nullWhenUndefined = await PlaywrightAssert.ThrowsAsync(
+ () => Expect(locator).ToHaveJSPropertyAsync("foo", null, new() { Timeout = 200 }));
+ StringAssert.Contains("unexpected value \"undefined\"", nullWhenUndefined.Message);
+ }
+
[PlaywrightTest("playwright-test/playwright.expect.misc.spec.ts", "should support toHaveValue")]
public async Task ShouldSupportToHaveValue()
{
diff --git a/src/Playwright/API/Types/JSUndefined.cs b/src/Playwright/API/Types/JSUndefined.cs
new file mode 100644
index 000000000..3e5632e15
--- /dev/null
+++ b/src/Playwright/API/Types/JSUndefined.cs
@@ -0,0 +1,54 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+namespace Microsoft.Playwright;
+
+///
+/// Sentinel representing the JavaScript undefined value.
+///
+///
+///
+/// Use this as the expected value of
+/// to assert that a JavaScript property is undefined rather than null.
+/// C# continues to mean JavaScript null.
+///
+///
+/// This sentinel is only supported by .
+/// Passing it to evaluate APIs does not produce JavaScript undefined.
+///
+///
+public sealed class JSUndefined
+{
+ private JSUndefined()
+ {
+ }
+
+ ///
+ /// The JavaScript undefined value.
+ ///
+ public static JSUndefined Value { get; } = new();
+
+ ///
+ public override string ToString() => "undefined";
+}
diff --git a/src/Playwright/Core/LocatorAssertions.cs b/src/Playwright/Core/LocatorAssertions.cs
index f1aa62c0c..78aa84a9e 100644
--- a/src/Playwright/Core/LocatorAssertions.cs
+++ b/src/Playwright/Core/LocatorAssertions.cs
@@ -196,7 +196,9 @@ public Task ToHaveJSPropertyAsync(string name, object value, LocatorAssertionsTo
{
var commonOptions = ConvertToFrameExpectOptions(options);
commonOptions.ExpressionArg = name;
- commonOptions.ExpectedValue = ScriptsHelper.SerializedArgument(value);
+ commonOptions.ExpectedValue = value is JSUndefined
+ ? ScriptsHelper.SerializedUndefinedArgument()
+ : ScriptsHelper.SerializedArgument(value);
return ExpectImplAsync("to.have.property", null as ExpectedTextValue[], value, $"Locator expected to have JavaScript property '{name}'", "Expect \"ToHaveJSPropertyAsync\"", commonOptions);
}
diff --git a/src/Playwright/Core/ScriptsHelper.cs b/src/Playwright/Core/ScriptsHelper.cs
index 36ca9a2dd..d7ede7074 100644
--- a/src/Playwright/Core/ScriptsHelper.cs
+++ b/src/Playwright/Core/ScriptsHelper.cs
@@ -74,6 +74,9 @@ internal static object SerializedArgument(object? arg)
return new { value = EvaluateArgumentValueConverter.Serialize(arg, handles, new()), handles };
}
+ internal static object SerializedUndefinedArgument()
+ => new { value = new { v = "undefined" }, handles = new List() };
+
internal static string EvaluationScript(string? content, string? path, bool addSourceUrl)
{
if (!content.IsNullOrEmpty())