diff --git a/src/PlaywrightNative.Tests/LibraryLocatorHighlightParityTests.cs b/src/PlaywrightNative.Tests/LibraryLocatorHighlightParityTests.cs
index 92b49225..1f93fdcd 100644
--- a/src/PlaywrightNative.Tests/LibraryLocatorHighlightParityTests.cs
+++ b/src/PlaywrightNative.Tests/LibraryLocatorHighlightParityTests.cs
@@ -148,5 +148,65 @@ public async Task PageHideHighlightClearsAllLocatorHighlights()
await page.HideHighlightAsync().ConfigureAwait(false);
await Assertions.Expect(page.Locator("x-pw-highlight")).ToHaveCountAsync(0).ConfigureAwait(false);
}
+
+ [PlaywrightTest("locator-highlight.spec.ts", "highlight should work with a custom selector engine that runs in the main world")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task HighlightShouldWorkWithACustomSelectorEngineThatRunsInTheMainWorld()
+ {
+ // Engines registered without "contentScript" run in the main world and can see page globals.
+ const string createEngine = @"() => ({
+ query(root, selector) {
+ return window['__engineEnabled'] ? root.querySelector(selector) : null;
+ },
+ queryAll(root, selector) {
+ return window['__engineEnabled'] ? Array.from(root.querySelectorAll(selector)) : [];
+ },
+ })";
+ await Playwright.Selectors.RegisterAsync("highlight-tag", createEngine).ConfigureAwait(false);
+
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ await page.SetContentAsync("").ConfigureAwait(false);
+ await page.EvaluateAsync("() => window['__engineEnabled'] = true").ConfigureAwait(false);
+
+ ILocator button = page.Locator("highlight-tag=button");
+ await button.HighlightAsync().ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("x-pw-highlight")).ToHaveCountAsync(1).ConfigureAwait(false);
+
+ await button.HideHighlightAsync().ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("x-pw-highlight")).ToHaveCountAsync(0).ConfigureAwait(false);
+
+ await button.HighlightAsync().ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("x-pw-highlight")).ToHaveCountAsync(1).ConfigureAwait(false);
+
+ await page.HideHighlightAsync().ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("x-pw-highlight")).ToHaveCountAsync(0).ConfigureAwait(false);
+ }
+
+ [PlaywrightTest("locator-highlight.spec.ts", "highlight should resolve relative to the frame of the locator")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task HighlightShouldResolveRelativeToTheFrameOfTheLocator()
+ {
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ await page.SetContentAsync("").ConfigureAwait(false);
+ IFrame frame = page.Frame("frame");
+
+ await page.Locator("button").HighlightAsync().ConfigureAwait(false);
+ await frame.Locator("button").HighlightAsync().ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("x-pw-highlight")).ToHaveCountAsync(1).ConfigureAwait(false);
+ await Assertions.Expect(frame.Locator("x-pw-highlight")).ToHaveCountAsync(1).ConfigureAwait(false);
+
+ await page.Locator("button").HideHighlightAsync().ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("x-pw-highlight")).ToHaveCountAsync(0).ConfigureAwait(false);
+ await Assertions.Expect(frame.Locator("x-pw-highlight")).ToHaveCountAsync(1).ConfigureAwait(false);
+
+ await frame.Locator("button").HideHighlightAsync().ConfigureAwait(false);
+ await Assertions.Expect(frame.Locator("x-pw-highlight")).ToHaveCountAsync(0).ConfigureAwait(false);
+ }
}
}
diff --git a/src/PlaywrightNative.Tests/LocatorQueryTests.cs b/src/PlaywrightNative.Tests/LocatorQueryTests.cs
index 327cda63..c6cb7dab 100644
--- a/src/PlaywrightNative.Tests/LocatorQueryTests.cs
+++ b/src/PlaywrightNative.Tests/LocatorQueryTests.cs
@@ -500,5 +500,30 @@ public void CountShouldNotThrowDuringNavigation()
{
Assert.Ignore("Node-only: playwright.selectors.register with a query-time navigation helper.");
}
+
+ [PlaywrightTest("locator-query.spec.ts", "should support locator.within")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task ShouldSupportLocatorWithin()
+ {
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+ await page.SetContentAsync(@"
+
+ | a1 | a2 | a3 |
+ | b1 | b2 | b3 |
+ | c1 | c2 | c3 |
+
+ outside
+ ").ConfigureAwait(false);
+
+ await Assertions.Expect(page.GetByRole("cell").Within(page.GetByRole("row"))).ToHaveTextAsync(new[] { "a1", "a2", "a3", "b1", "b2", "b3", "c1", "c2", "c3" }).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByRole("cell").Nth(1).Within(page.GetByRole("row"))).ToHaveTextAsync(new[] { "a2", "b2", "c2" }).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByRole("cell").Last.Within(page.GetByRole("row"))).ToHaveTextAsync(new[] { "a3", "b3", "c3" }).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByRole("cell").Nth(1).Within(page.GetByRole("row").Nth(2))).ToHaveTextAsync(new[] { "c2" }).ConfigureAwait(false);
+ await Assertions.Expect(page.Locator("span").Within(page.GetByRole("row"))).ToHaveCountAsync(0).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByRole("cell").Nth(1).Within(page.GetByRole("row")).Nth(1)).ToHaveTextAsync("b2").ConfigureAwait(false);
+ }
}
}
diff --git a/src/PlaywrightNative.Tests/SelectorsGetByParityTests.cs b/src/PlaywrightNative.Tests/SelectorsGetByParityTests.cs
index 91770aa6..f403ac6a 100644
--- a/src/PlaywrightNative.Tests/SelectorsGetByParityTests.cs
+++ b/src/PlaywrightNative.Tests/SelectorsGetByParityTests.cs
@@ -521,5 +521,37 @@ await page.SetContentAsync(@"
").ConfigureAwait(false);
Assert.That(await page.GetByRole("alert", description: " doc-2025.pdf \n was uploaded ").EvaluateAllAsync("els => els.map(e => e.textContent)").ConfigureAwait(false), Is.EqualTo(new[] { "Alert" }));
}
+
+ [PlaywrightTest("selectors-get-by.spec.ts", "locators should not be affected by global and sticky regex flags")]
+ [Test]
+ [Timeout(TestConstants.DefaultTestTimeout)]
+ public async Task LocatorsShouldNotBeAffectedByGlobalAndStickyRegexFlags()
+ {
+ await using IBrowser browser = await BrowserLauncher.LaunchAsync().ConfigureAwait(false);
+ await using IBrowserContext context = await browser.NewContextAsync().ConfigureAwait(false);
+ IPage page = await context.NewPageAsync().ConfigureAwait(false);
+
+ await page.SetContentAsync(@"
+ foo
foo
foo
+
+
+
+ ").ConfigureAwait(false);
+
+ // .NET Regex has no global (g) or sticky (y) flag, so the upstream
+ // /foo/g and /foo/y lines use the same pattern as /foo/.
+ await Assertions.Expect(page.GetByText(new Regex("foo"))).ToHaveCountAsync(9).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByText(new Regex("foo"))).ToHaveCountAsync(9).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByText(new Regex("foo"))).ToHaveCountAsync(9).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByLabel(new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByLabel(new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByLabel(new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByRole("button", nameRegex: new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByRole("button", nameRegex: new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByRole("button", nameRegex: new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByPlaceholder(new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByPlaceholder(new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ await Assertions.Expect(page.GetByPlaceholder(new Regex("foo"))).ToHaveCountAsync(3).ConfigureAwait(false);
+ }
}
}
diff --git a/src/PlaywrightNative/Firefox/FirefoxPage.cs b/src/PlaywrightNative/Firefox/FirefoxPage.cs
index 73110382..6fe22e0b 100644
--- a/src/PlaywrightNative/Firefox/FirefoxPage.cs
+++ b/src/PlaywrightNative/Firefox/FirefoxPage.cs
@@ -1289,11 +1289,7 @@ Task IPage.GoForwardAsync(PageGoForwardOptions options)
Task IPage.GotoAsync(string url, PageGotoOptions options)
=> GoToAsync(url, options?.WaitUntil ?? default, options?.Timeout, options?.Referer);
- async Task IPage.HideHighlightAsync()
- {
- PageHighlights.Clear(this);
- await EvaluateAsync(ElementStateScript.HideAllHighlightsFunction).ConfigureAwait(false);
- }
+ Task IPage.HideHighlightAsync() => PageHighlights.HideAllAsync(this);
Task IPage.HoverAsync(string selector, PageHoverOptions options)
=> HoverAsync(selector, options?.Position, options?.Modifiers, options?.Force, options?.Timeout, options?.Trial, ActionScrollBridge.FromScrollOption(options?.Scroll), options?.Strict);
diff --git a/src/PlaywrightNative/Helpers/PageHighlights.cs b/src/PlaywrightNative/Helpers/PageHighlights.cs
index bb836bf6..ba449370 100644
--- a/src/PlaywrightNative/Helpers/PageHighlights.cs
+++ b/src/PlaywrightNative/Helpers/PageHighlights.cs
@@ -36,10 +36,11 @@ internal static class PageHighlights
/// after a later navigation.
///
/// The page that owns the overlay.
+ /// The frame the locator resolves relative to.
/// The highlighted locator.
/// Optional extra inline CSS.
/// Stable overlay id for this locator.
- internal static void Remember(IPage page, ILocator locator, string style, string id)
+ internal static void Remember(IPage page, IFrame frame, ILocator locator, string style, string id)
{
if (page == null || locator == null || string.IsNullOrEmpty(id))
{
@@ -49,9 +50,10 @@ internal static void Remember(IPage page, ILocator locator, string style, string
Registry registry = _pages.GetOrCreateValue(page);
lock (registry.Sync)
{
- RemoveId(registry, id);
+ RemoveId(registry, frame, id);
registry.Entries.Add(new Entry
{
+ Frame = frame,
Locator = locator,
Style = style,
Id = id,
@@ -69,8 +71,9 @@ internal static void Remember(IPage page, ILocator locator, string style, string
/// Forgets one locator overlay after locator.hideHighlight().
///
/// The page that owns the overlay.
+ /// The frame the locator resolves relative to.
/// Stable overlay id for this locator.
- internal static void Forget(IPage page, string id)
+ internal static void Forget(IPage page, IFrame frame, string id)
{
if (page == null || string.IsNullOrEmpty(id) || !_pages.TryGetValue(page, out Registry registry))
{
@@ -79,24 +82,36 @@ internal static void Forget(IPage page, string id)
lock (registry.Sync)
{
- RemoveId(registry, id);
+ RemoveId(registry, frame, id);
}
}
///
- /// Forgets every overlay after page.hideHighlight().
+ /// page.hideHighlight(): forgets every overlay and removes it
+ /// from every frame, since locators may highlight in any frame.
///
/// The page that owns the overlays.
- internal static void Clear(IPage page)
+ /// A task that completes when every frame is cleared.
+ internal static async Task HideAllAsync(IPage page)
{
- if (page == null || !_pages.TryGetValue(page, out Registry registry))
+ if (_pages.TryGetValue(page, out Registry registry))
{
- return;
+ lock (registry.Sync)
+ {
+ registry.Entries.Clear();
+ }
}
- lock (registry.Sync)
+ foreach (IFrame frame in page.Frames)
{
- registry.Entries.Clear();
+ try
+ {
+ await frame.EvaluateAsync(ElementStateScript.HideAllHighlightsFunction).ConfigureAwait(false);
+ }
+ catch (PlaywrightException) when (frame != page.MainFrame)
+ {
+ // A child frame may detach or navigate while we clear it.
+ }
}
}
@@ -132,11 +147,11 @@ private static async Task ReapplyAsync(IPage page)
}
}
- private static void RemoveId(Registry registry, string id)
+ private static void RemoveId(Registry registry, IFrame frame, string id)
{
for (int i = registry.Entries.Count - 1; i >= 0; i--)
{
- if (string.Equals(registry.Entries[i].Id, id, StringComparison.Ordinal))
+ if (ReferenceEquals(registry.Entries[i].Frame, frame) && string.Equals(registry.Entries[i].Id, id, StringComparison.Ordinal))
{
registry.Entries.RemoveAt(i);
}
@@ -154,6 +169,8 @@ private sealed class Registry
private sealed class Entry
{
+ internal IFrame Frame { get; set; }
+
internal ILocator Locator { get; set; }
internal string Style { get; set; }
diff --git a/src/PlaywrightNative/Helpers/Selectors.cs b/src/PlaywrightNative/Helpers/Selectors.cs
index 2f6a0aba..dc3ae047 100644
--- a/src/PlaywrightNative/Helpers/Selectors.cs
+++ b/src/PlaywrightNative/Helpers/Selectors.cs
@@ -67,7 +67,8 @@ static bool NameIsValid(string value)
bool ok = (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
- || c == '_';
+ || c == '_'
+ || c == '-';
if (!ok)
{
return false;
diff --git a/src/PlaywrightNative/Locator.cs b/src/PlaywrightNative/Locator.cs
index d6f243b2..52fda64b 100644
--- a/src/PlaywrightNative/Locator.cs
+++ b/src/PlaywrightNative/Locator.cs
@@ -1053,8 +1053,8 @@ public async Task HideHighlightAsync(float? timeout = default)
{
_ = timeout;
string id = ToString();
- PageHighlights.Forget(Page, id);
- await Page.EvaluateAsync(ElementStateScript.HideHighlightByIdFunction, id).ConfigureAwait(false);
+ PageHighlights.Forget(Page, _frame, id);
+ await _frame.EvaluateAsync(ElementStateScript.HideHighlightByIdFunction, id).ConfigureAwait(false);
}
///
@@ -1340,7 +1340,7 @@ internal async Task HighlightInternalAsync(float? timeout, string style)
string tooltip = ToString();
string payload = "{\"tooltip\":" + JsonSerializer.Serialize(tooltip) + ",\"style\":" + JsonSerializer.Serialize(style ?? string.Empty) + ",\"id\":" + JsonSerializer.Serialize(tooltip) + "}";
await handle.EvaluateAsync(ElementStateScript.HighlightFunction, payload).ConfigureAwait(false);
- PageHighlights.Remember(Page, this, style, tooltip);
+ PageHighlights.Remember(Page, _frame, this, style, tooltip);
}
///
diff --git a/src/PlaywrightNative/LocatorCompatExtensions.cs b/src/PlaywrightNative/LocatorCompatExtensions.cs
index 35fb4da3..6a626997 100644
--- a/src/PlaywrightNative/LocatorCompatExtensions.cs
+++ b/src/PlaywrightNative/LocatorCompatExtensions.cs
@@ -90,6 +90,20 @@ public static Task HighlightAsync(this ILocator locator, string style = default)
public static ILocator Has(this ILocator locator, ILocator has)
=> RequireLocator(locator).Has(has);
+ ///
+ /// Returns a locator that matches this locator's elements inside each element matched by
+ /// . Same as parent.Locator(locator), but reads in natural order.
+ /// Relative locators such as Nth or First resolve separately inside each parent.
+ ///
+ /// The locator to scope.
+ /// Locator matching the parent elements to search within.
+ /// The scoped locator.
+ public static ILocator Within(this ILocator locator, ILocator parent)
+ {
+ ArgumentNullException.ThrowIfNull(parent);
+ return parent.Locator(locator);
+ }
+
/// Legacy hasNot filter.
public static ILocator HasNot(this ILocator locator, ILocator hasNot)
=> RequireLocator(locator).HasNot(hasNot);
diff --git a/src/PlaywrightNative/Page.cs b/src/PlaywrightNative/Page.cs
index 5b018218..33f2e3f7 100644
--- a/src/PlaywrightNative/Page.cs
+++ b/src/PlaywrightNative/Page.cs
@@ -2634,11 +2634,7 @@ Task IPage.GoForwardAsync(PageGoForwardOptions options)
Task IPage.GotoAsync(string url, PageGotoOptions options)
=> GoToAsync(url, options?.WaitUntil ?? default, options?.Timeout, options?.Referer);
- async Task IPage.HideHighlightAsync()
- {
- PageHighlights.Clear(this);
- await EvaluateAsync(ElementStateScript.HideAllHighlightsFunction).ConfigureAwait(false);
- }
+ Task IPage.HideHighlightAsync() => PageHighlights.HideAllAsync(this);
Task IPage.HoverAsync(string selector, PageHoverOptions options)
=> HoverAsync(selector, options?.Position, options?.Modifiers, options?.Force, options?.Timeout, options?.Trial, ActionScrollBridge.FromScrollOption(options?.Scroll), options?.Strict);
diff --git a/src/PlaywrightNative/WebKit/WKPage.cs b/src/PlaywrightNative/WebKit/WKPage.cs
index 9d5ee56b..4ec56064 100644
--- a/src/PlaywrightNative/WebKit/WKPage.cs
+++ b/src/PlaywrightNative/WebKit/WKPage.cs
@@ -12189,11 +12189,7 @@ Task IPage.GoForwardAsync(PageGoForwardOptions options)
Task IPage.GotoAsync(string url, PageGotoOptions options)
=> GoToAsync(url, options?.WaitUntil ?? default, options?.Timeout, options?.Referer);
- async Task IPage.HideHighlightAsync()
- {
- PageHighlights.Clear(this);
- await EvaluateAsync(ElementStateScript.HideAllHighlightsFunction).ConfigureAwait(false);
- }
+ Task IPage.HideHighlightAsync() => PageHighlights.HideAllAsync(this);
Task IPage.HoverAsync(string selector, PageHoverOptions options)
=> HoverAsync(selector, options?.Position, options?.Modifiers, options?.Force, options?.Timeout, options?.Trial, ActionScrollBridge.FromScrollOption(options?.Scroll), options?.Strict);