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/RoleSelectorEngine.cs b/src/PlaywrightNative/Helpers/RoleSelectorEngine.cs
index 3832e005..cacd45cb 100644
--- a/src/PlaywrightNative/Helpers/RoleSelectorEngine.cs
+++ b/src/PlaywrightNative/Helpers/RoleSelectorEngine.cs
@@ -41,7 +41,7 @@ internal static class RoleSelectorEngine
return null;
};
- const elementSafeTagName = (el) => el && el.tagName ? el.tagName : '';
+ const elementSafeTagName = (el) => el && typeof el.tagName === 'string' ? el.tagName.toUpperCase() : '';
const getExplicitAriaRole = (element) => {
const roles = String(element.getAttribute('role') || '').split(' ');
@@ -52,34 +52,194 @@ internal static class RoleSelectorEngine
return null;
};
- const getImplicitAriaRole = (element) => {
- const tag = elementSafeTagName(element);
- if (tag === 'BUTTON') return 'button';
- if (tag === 'DETAILS') return 'group';
- if (tag === 'OPTION') return 'option';
- if (tag === 'OUTPUT') return 'status';
- if (tag === 'LI') return 'listitem';
- if (tag === 'TEXTAREA') return 'textbox';
- if (tag === 'A' && element.hasAttribute('href')) return 'link';
- if (tag === 'IMG') return 'img';
- if (tag === 'H1' || tag === 'H2' || tag === 'H3' || tag === 'H4' || tag === 'H5' || tag === 'H6') return 'heading';
- if (tag === 'SELECT') {
- const size = Number(element.size);
- if (element.hasAttribute('multiple') || size > 1) return 'listbox';
- return 'combobox';
+ const closestCrossShadow = (element, css) => {
+ while (element) {
+ const closest = element.closest(css);
+ if (closest) return closest;
+ while (element.parentElement) element = element.parentElement;
+ element = parentElementOrShadowHost(element);
}
- if (tag === 'INPUT') {
- const type = String(element.type || '').toLowerCase();
+ return null;
+ };
+
+ const hasExplicitAccessibleName = (e) => e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby');
+
+ const kAncestorPreventingLandmark = 'article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]';
+
+ const kNameProhibitedRoles = ['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong', 'subscript', 'superscript'];
+ const kGlobalAriaAttributes = [
+ ['aria-atomic', undefined],
+ ['aria-busy', undefined],
+ ['aria-controls', undefined],
+ ['aria-current', undefined],
+ ['aria-describedby', undefined],
+ ['aria-details', undefined],
+ ['aria-dropeffect', undefined],
+ ['aria-flowto', undefined],
+ ['aria-grabbed', undefined],
+ ['aria-hidden', undefined],
+ ['aria-keyshortcuts', undefined],
+ ['aria-label', kNameProhibitedRoles],
+ ['aria-labelledby', kNameProhibitedRoles],
+ ['aria-live', undefined],
+ ['aria-owns', undefined],
+ ['aria-relevant', undefined],
+ ['aria-roledescription', ['generic']],
+ ];
+
+ const hasGlobalAriaAttribute = (element, forRole) => kGlobalAriaAttributes.some(([attr, prohibited]) =>
+ !(prohibited && prohibited.includes(forRole || '')) && element.hasAttribute(attr));
+
+ const hasTabIndex = (element) => !Number.isNaN(Number(String(element.getAttribute('tabindex'))));
+
+ const isNativelyFocusable = (element) => {
+ const tag = elementSafeTagName(element);
+ if (['BUTTON', 'DETAILS', 'SELECT', 'TEXTAREA'].includes(tag)) return true;
+ if (tag === 'A' || tag === 'AREA') return element.hasAttribute('href');
+ if (tag === 'INPUT') return !element.hidden;
+ return false;
+ };
+
+ const isFocusable = (element) => !isNativelyDisabled(element) && (isNativelyFocusable(element) || hasTabIndex(element));
+
+ const inputTypeToRole = { button: 'button', checkbox: 'checkbox', image: 'button', number: 'spinbutton', radio: 'radio', range: 'slider', reset: 'button', submit: 'button' };
+
+ const isHeaderCell = (element) => !!element && elementSafeTagName(element) === 'TH';
+
+ const isNonEmptyDataCell = (element) => {
+ if (!element || elementSafeTagName(element) !== 'TD') return false;
+ return !!((element.textContent && element.textContent.trim()) || element.children.length > 0);
+ };
+
+ // https://w3c.github.io/html-aam/#html-element-role-mappings
+ const kImplicitRoleByTagName = {
+ A: (e) => e.hasAttribute('href') ? 'link' : null,
+ AREA: (e) => e.hasAttribute('href') ? 'link' : null,
+ ARTICLE: () => 'article',
+ ASIDE: () => 'complementary',
+ BLOCKQUOTE: () => 'blockquote',
+ BUTTON: () => 'button',
+ CAPTION: () => 'caption',
+ CODE: () => 'code',
+ DATALIST: () => 'listbox',
+ DD: () => 'definition',
+ DEL: () => 'deletion',
+ DETAILS: () => 'group',
+ DFN: () => 'term',
+ DIALOG: () => 'dialog',
+ DT: () => 'term',
+ EM: () => 'emphasis',
+ FIELDSET: () => 'group',
+ FIGURE: () => 'figure',
+ FOOTER: (e) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'contentinfo',
+ FORM: (e) => hasExplicitAccessibleName(e) ? 'form' : null,
+ H1: () => 'heading',
+ H2: () => 'heading',
+ H3: () => 'heading',
+ H4: () => 'heading',
+ H5: () => 'heading',
+ H6: () => 'heading',
+ HEADER: (e) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'banner',
+ HR: () => 'separator',
+ HTML: () => 'document',
+ IMG: (e) => (e.getAttribute('alt') === '') && !e.getAttribute('title') && !hasGlobalAriaAttribute(e) && !hasTabIndex(e) ? 'presentation' : 'img',
+ INPUT: (e) => {
+ const type = String(e.type || '').toLowerCase();
+ if (['email', 'search', 'tel', 'text', 'url', ''].includes(type)) {
+ const list = getIdRefs(e, e.getAttribute('list'))[0];
+ if (list && elementSafeTagName(list) === 'DATALIST') return 'combobox';
+ return type === 'search' ? 'searchbox' : 'textbox';
+ }
if (type === 'hidden') return null;
- if (type === 'checkbox') return 'checkbox';
- if (type === 'radio') return 'radio';
- if (type === 'button' || type === 'submit' || type === 'reset' || type === 'image' || type === 'file') return 'button';
- if (type === 'number') return 'spinbutton';
- if (type === 'range') return 'slider';
- if (type === 'search') return 'searchbox';
- return 'textbox';
+ if (type === 'file') return 'button';
+ return inputTypeToRole[type] || 'textbox';
+ },
+ INS: () => 'insertion',
+ LI: () => 'listitem',
+ MAIN: () => 'main',
+ MARK: () => 'mark',
+ MATH: () => 'math',
+ MENU: () => 'list',
+ METER: () => 'meter',
+ NAV: () => 'navigation',
+ OL: () => 'list',
+ OPTGROUP: () => 'group',
+ OPTION: () => 'option',
+ OUTPUT: () => 'status',
+ P: () => 'paragraph',
+ PROGRESS: () => 'progressbar',
+ SEARCH: () => 'search',
+ SECTION: (e) => hasExplicitAccessibleName(e) ? 'region' : null,
+ SELECT: (e) => (e.multiple || e.size > 1) ? 'listbox' : 'combobox',
+ STRONG: () => 'strong',
+ SUB: () => 'subscript',
+ SUP: () => 'superscript',
+ SVG: () => 'img',
+ TABLE: () => 'table',
+ TBODY: () => 'rowgroup',
+ TD: (e) => {
+ const table = closestCrossShadow(e, 'table');
+ const role = table ? getExplicitAriaRole(table) : '';
+ return (role === 'grid' || role === 'treegrid') ? 'gridcell' : 'cell';
+ },
+ TEXTAREA: () => 'textbox',
+ TFOOT: () => 'rowgroup',
+ TH: (e) => {
+ const scope = e.getAttribute('scope');
+ if (scope === 'col' || scope === 'colgroup') return 'columnheader';
+ if (scope === 'row' || scope === 'rowgroup') return 'rowheader';
+ const nextSibling = e.nextElementSibling;
+ const prevSibling = e.previousElementSibling;
+ const row = !!e.parentElement && elementSafeTagName(e.parentElement) === 'TR' ? e.parentElement : undefined;
+ if (!nextSibling && !prevSibling) {
+ if (row) {
+ const table = closestCrossShadow(row, 'table');
+ if (table && table.rows.length <= 1) return null;
+ }
+ return 'columnheader';
+ }
+ if (isHeaderCell(nextSibling) && isHeaderCell(prevSibling)) return 'columnheader';
+ if (isNonEmptyDataCell(nextSibling) || isNonEmptyDataCell(prevSibling)) return 'rowheader';
+ return 'columnheader';
+ },
+ THEAD: () => 'rowgroup',
+ TIME: () => 'time',
+ TR: () => 'row',
+ UL: () => 'list',
+ };
+
+ const kPresentationInheritanceParents = {
+ DD: ['DL', 'DIV'],
+ DIV: ['DL'],
+ DT: ['DL', 'DIV'],
+ LI: ['OL', 'UL'],
+ TBODY: ['TABLE'],
+ TD: ['TR'],
+ TFOOT: ['TABLE'],
+ TH: ['TR'],
+ THEAD: ['TABLE'],
+ TR: ['THEAD', 'TBODY', 'TFOOT', 'TABLE'],
+ };
+
+ const hasPresentationConflictResolution = (element, role) => hasGlobalAriaAttribute(element, role) || isFocusable(element);
+
+ const getImplicitAriaRole = (element) => {
+ const factory = kImplicitRoleByTagName[elementSafeTagName(element)];
+ const implicitRole = (factory && factory(element)) || '';
+ if (!implicitRole) return null;
+ // Inherit presentation role when required.
+ // https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none
+ let ancestor = element;
+ while (ancestor) {
+ const parent = parentElementOrShadowHost(ancestor);
+ const parents = kPresentationInheritanceParents[elementSafeTagName(ancestor)];
+ if (!parents || !parent || !parents.includes(elementSafeTagName(parent))) break;
+ const parentExplicitRole = getExplicitAriaRole(parent);
+ if ((parentExplicitRole === 'none' || parentExplicitRole === 'presentation') && !hasPresentationConflictResolution(parent, parentExplicitRole))
+ return parentExplicitRole;
+ ancestor = parent;
}
- return null;
+ return implicitRole;
};
const getAriaRole = (element) => getExplicitAriaRole(element) || getImplicitAriaRole(element);
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 0c319fff..b3e9ec14 100644
--- a/src/PlaywrightNative/Locator.cs
+++ b/src/PlaywrightNative/Locator.cs
@@ -1076,8 +1076,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);
}
///
@@ -1363,7 +1363,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);
}
internal Locator WithAnyFrame()
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 b9853b40..84a38455 100644
--- a/src/PlaywrightNative/WebKit/WKPage.cs
+++ b/src/PlaywrightNative/WebKit/WKPage.cs
@@ -12184,11 +12184,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);