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(@" + + + + +
a1a2a3
b1b2b3
c1c2c3
+ 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);