Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions src/PlaywrightNative.Tests/LibraryLocatorHighlightParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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("<button>Button</button>").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("<iframe name=\"frame\" srcdoc=\"<button>foo</button>\"></iframe><button>bar</button>").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);
}
}
}
25 changes: 25 additions & 0 deletions src/PlaywrightNative.Tests/LocatorQueryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(@"
<table>
<tr><td>a1</td><td>a2</td><td>a3</td></tr>
<tr><td>b1</td><td>b2</td><td>b3</td></tr>
<tr><td>c1</td><td>c2</td><td>c3</td></tr>
</table>
<span>outside</span>
").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);
}
}
}
32 changes: 32 additions & 0 deletions src/PlaywrightNative.Tests/SelectorsGetByParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,5 +521,37 @@ await page.SetContentAsync(@"
").ConfigureAwait(false);
Assert.That(await page.GetByRole("alert", description: " doc-2025.pdf \n was uploaded ").EvaluateAllAsync<string[]>("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(@"
<div>foo</div><div>foo</div><div>foo</div>
<label>foo<input></label><label>foo<input></label><label>foo<input></label>
<button>foo</button><button>foo</button><button>foo</button>
<input placeholder=foo><input placeholder=foo><input placeholder=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);
}
}
}
6 changes: 1 addition & 5 deletions src/PlaywrightNative/Firefox/FirefoxPage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,11 +1289,7 @@ Task<IResponse> IPage.GoForwardAsync(PageGoForwardOptions options)
Task<IResponse> 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);
Expand Down
41 changes: 29 additions & 12 deletions src/PlaywrightNative/Helpers/PageHighlights.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ internal static class PageHighlights
/// after a later navigation.
/// </summary>
/// <param name="page">The page that owns the overlay.</param>
/// <param name="frame">The frame the locator resolves relative to.</param>
/// <param name="locator">The highlighted locator.</param>
/// <param name="style">Optional extra inline CSS.</param>
/// <param name="id">Stable overlay id for this locator.</param>
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))
{
Expand All @@ -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,
Expand All @@ -69,8 +71,9 @@ internal static void Remember(IPage page, ILocator locator, string style, string
/// Forgets one locator overlay after <c>locator.hideHighlight()</c>.
/// </summary>
/// <param name="page">The page that owns the overlay.</param>
/// <param name="frame">The frame the locator resolves relative to.</param>
/// <param name="id">Stable overlay id for this locator.</param>
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))
{
Expand All @@ -79,24 +82,36 @@ internal static void Forget(IPage page, string id)

lock (registry.Sync)
{
RemoveId(registry, id);
RemoveId(registry, frame, id);
}
}

/// <summary>
/// Forgets every overlay after <c>page.hideHighlight()</c>.
/// <c>page.hideHighlight()</c>: forgets every overlay and removes it
/// from every frame, since locators may highlight in any frame.
/// </summary>
/// <param name="page">The page that owns the overlays.</param>
internal static void Clear(IPage page)
/// <returns>A task that completes when every frame is cleared.</returns>
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.
}
}
}

Expand Down Expand Up @@ -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);
}
Expand All @@ -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; }
Expand Down
Loading
Loading