Skip to content

Commit 6ecce37

Browse files
authored
Use xpath query handler (#2230)
* Use xpath query handler * Disable obsolete errors * Match test
1 parent 9e27e33 commit 6ecce37

File tree

13 files changed

+111
-42
lines changed

13 files changed

+111
-42
lines changed

lib/PuppeteerSharp.Tests/ElementHandleTests/CustomQueriesTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public override Task DisposeAsync()
2121
return base.DisposeAsync();
2222
}
2323

24-
[PuppeteerTest("elementhandle.spec.ts", "Custom queries", "should reguster and unregister")]
24+
[PuppeteerTest("elementhandle.spec.ts", "Custom queries", "should register and unregister")]
2525
[PuppeteerFact]
2626
public async Task ShouldRegisterAndUnregister()
2727
{
@@ -49,7 +49,7 @@ public async Task ShouldRegisterAndUnregister()
4949
}
5050
catch (Exception ex)
5151
{
52-
Assert.Equal($"Query set to use \"getById\", but no query handler of that name was found", ex.Message);
52+
Assert.DoesNotContain($"Custom query handler name not set - throw expected", ex.Message);
5353
}
5454

5555
var handlerNamesAfterUnregistering = ((Browser)Browser).GetCustomQueryHandlerNames();

lib/PuppeteerSharp.Tests/QuerySelectorTests/ElementHandleXPathTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#pragma warning disable CS0618 // XPathAsync is obsolete but we test the funcionatlity anyway
12
using System.Threading.Tasks;
23
using PuppeteerSharp.Tests.Attributes;
34
using PuppeteerSharp.Xunit;
@@ -37,3 +38,4 @@ public async Task ShouldReturnNullForNonExistingElement()
3738
}
3839
}
3940
}
41+
#pragma warning restore CS0618

lib/PuppeteerSharp.Tests/QuerySelectorTests/PageXPathTests.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#pragma warning disable CS0618 // WaitForXPathAsync is obsolete but we test the funcionatlity anyway
12
using System.Threading.Tasks;
23
using PuppeteerSharp.Tests.Attributes;
34
using PuppeteerSharp.Xunit;
@@ -13,7 +14,7 @@ public PageXPathTests(ITestOutputHelper output) : base(output)
1314
{
1415
}
1516

16-
[PuppeteerTest("queryselector.spec.ts", "Path.$x", "should query existing element")]
17+
[PuppeteerTest("queryselector.spec.ts", "Page.$x", "should query existing element")]
1718
[PuppeteerFact]
1819
public async Task ShouldQueryExistingElement()
1920
{
@@ -23,15 +24,15 @@ public async Task ShouldQueryExistingElement()
2324
Assert.Single(elements);
2425
}
2526

26-
[PuppeteerTest("queryselector.spec.ts", "Path.$x", "should return empty array for non-existing element")]
27+
[PuppeteerTest("queryselector.spec.ts", "Page.$x", "should return empty array for non-existing element")]
2728
[PuppeteerFact]
2829
public async Task ShouldReturnEmptyArrayForNonExistingElement()
2930
{
3031
var elements = await Page.XPathAsync("/html/body/non-existing-element");
3132
Assert.Empty(elements);
3233
}
3334

34-
[PuppeteerTest("queryselector.spec.ts", "Path.$x", "should return multiple elements")]
35+
[PuppeteerTest("queryselector.spec.ts", "Page.$x", "should return multiple elements")]
3536
[PuppeteerFact]
3637
public async Task ShouldReturnMultipleElements()
3738
{
@@ -41,3 +42,4 @@ public async Task ShouldReturnMultipleElements()
4142
}
4243
}
4344
}
45+
#pragma warning restore CS0618

lib/PuppeteerSharp.Tests/WaitTaskTests/FrameWaitForXPathTests.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#pragma warning disable CS0618 // WaitForXPathAsync is obsolete but we test the funcionatlity anyway
12
using System;
23
using System.Linq;
34
using System.Threading.Tasks;
@@ -108,4 +109,5 @@ public async Task ShouldRespectTimeout()
108109
Assert.Contains($"Waiting failed: {timeout}ms exceeded", exception.Message);
109110
}
110111
}
111-
}
112+
}
113+
#pragma warning restore CS0618

lib/PuppeteerSharp.Tests/WaitTaskTests/PageWaitForTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#pragma warning disable CS0618 // WaitForXPathAsync is obsolete but we test the funcionatlity anyway
12
using System;
23
using System.Threading.Tasks;
34
using PuppeteerSharp.Tests.Attributes;
@@ -85,3 +86,4 @@ public async Task ShouldWaitForPredicateWithArguments()
8586
=> await Page.WaitForFunctionAsync("(arg1, arg2) => arg1 !== arg2", new WaitForFunctionOptions(), 1, 2);
8687
}
8788
}
89+
#pragma warning restore CS0618

lib/PuppeteerSharp/CustomQueriesManager.cs

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ namespace PuppeteerSharp
1010
{
1111
internal class CustomQueriesManager
1212
{
13+
private static readonly string[] CustomQuerySeparators = new[] { "=", "/" };
14+
private readonly Dictionary<string, PuppeteerQueryHandler> _internalQueryHandlers;
1315
private readonly Dictionary<string, PuppeteerQueryHandler> _queryHandlers = new();
1416
private readonly PuppeteerQueryHandler _pierceHandler = CreatePuppeteerQueryHandler(new CustomQueryHandler
1517
{
@@ -62,9 +64,7 @@ internal class CustomQueriesManager
6264
});
6365

6466
private readonly PuppeteerQueryHandler _ariaHandler = AriaQueryHandlerFactory.Create();
65-
private readonly Dictionary<string, PuppeteerQueryHandler> _builtInHandlers;
6667
private readonly Regex _customQueryHandlerNameRegex = new("[a-zA-Z]+$", RegexOptions.Compiled);
67-
private readonly Regex _customQueryHandlerParserRegex = new("(?<query>^[a-zA-Z]+)\\/(?<selector>.*)", RegexOptions.Compiled);
6868
private readonly PuppeteerQueryHandler _defaultHandler = CreatePuppeteerQueryHandler(new CustomQueryHandler
6969
{
7070
QueryOne = "(element, selector) => element.querySelector(selector)",
@@ -124,19 +124,54 @@ internal class CustomQueriesManager
124124
}",
125125
});
126126

127+
private readonly PuppeteerQueryHandler _xpathHandler = CreatePuppeteerQueryHandler(new CustomQueryHandler
128+
{
129+
QueryOne = @"(element, selector) => {
130+
const doc = element.ownerDocument || document;
131+
const result = doc.evaluate(
132+
selector,
133+
element,
134+
null,
135+
XPathResult.FIRST_ORDERED_NODE_TYPE
136+
);
137+
return result.singleNodeValue;
138+
}",
139+
QueryAll = @"(element, selector) => {
140+
const doc = element.ownerDocument || document;
141+
const iterator = doc.evaluate(
142+
selector,
143+
element,
144+
null,
145+
XPathResult.ORDERED_NODE_ITERATOR_TYPE
146+
);
147+
const array = [];
148+
let item;
149+
while ((item = iterator.iterateNext())) {
150+
array.push(item);
151+
}
152+
return array;
153+
},
154+
})",
155+
});
156+
127157
public CustomQueriesManager()
128158
{
129-
_builtInHandlers = new()
159+
_internalQueryHandlers = new()
130160
{
131161
["aria"] = _ariaHandler,
132162
["pierce"] = _pierceHandler,
133163
["text"] = _textQueryHandler,
164+
["xpath"] = _xpathHandler,
134165
};
135-
_queryHandlers = _builtInHandlers.Clone();
136166
}
137167

138168
internal void RegisterCustomQueryHandler(string name, CustomQueryHandler queryHandler)
139169
{
170+
if (_internalQueryHandlers.ContainsKey(name))
171+
{
172+
throw new PuppeteerException($"A query handler named \"{name}\" already exists");
173+
}
174+
140175
if (_queryHandlers.ContainsKey(name))
141176
{
142177
throw new PuppeteerException($"A custom query handler named \"{name}\" already exists");
@@ -155,21 +190,23 @@ internal void RegisterCustomQueryHandler(string name, CustomQueryHandler queryHa
155190

156191
internal (string UpdatedSelector, PuppeteerQueryHandler QueryHandler) GetQueryHandlerAndSelector(string selector)
157192
{
158-
var customQueryHandlerMatch = _customQueryHandlerParserRegex.Match(selector);
159-
if (!customQueryHandlerMatch.Success)
160-
{
161-
return (selector, _defaultHandler);
162-
}
163-
164-
var name = customQueryHandlerMatch.Groups["query"].Value;
165-
var updatedSelector = customQueryHandlerMatch.Groups["selector"].Value;
193+
var handlers = _internalQueryHandlers.Concat(_queryHandlers);
166194

167-
if (!_queryHandlers.TryGetValue(name, out var queryHandler))
195+
foreach (var kv in handlers)
168196
{
169-
throw new PuppeteerException($"Query set to use \"{name}\", but no query handler of that name was found");
197+
foreach (var separator in CustomQuerySeparators)
198+
{
199+
var prefix = $"{kv.Key}{separator}";
200+
201+
if (selector.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
202+
{
203+
selector = selector.Substring(prefix.Length);
204+
return (selector, kv.Value);
205+
}
206+
}
170207
}
171208

172-
return (updatedSelector, queryHandler);
209+
return (selector, _defaultHandler);
173210
}
174211

175212
internal IEnumerable<string> GetCustomQueryHandlerNames()
@@ -267,7 +304,6 @@ await handle.ExecutionContext.World.GetPuppeteerUtilAsync().ConfigureAwait(false
267304
return internalHandler;
268305
}
269306

270-
private IEnumerable<string> CustomQueryHandlerNames()
271-
=> _queryHandlers.Keys.ToArray().Where(k => !_builtInHandlers.ContainsKey(k));
307+
private IEnumerable<string> CustomQueryHandlerNames() => _queryHandlers.Keys.ToArray();
272308
}
273309
}

lib/PuppeteerSharp/ElementHandle.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ public async Task<Stream> ScreenshotStreamAsync(ScreenshotOptions options)
8181
/// <inheritdoc/>
8282
public async Task<IElementHandle> WaitForSelectorAsync(string selector, WaitForSelectorOptions options = null)
8383
{
84+
if (string.IsNullOrEmpty(selector))
85+
{
86+
throw new ArgumentNullException(nameof(selector));
87+
}
88+
8489
var customQueriesManager = ((Browser)Frame.FrameManager.Page.Browser).CustomQueriesManager;
8590
var (updatedSelector, queryHandler) = customQueriesManager.GetQueryHandlerAndSelector(selector);
8691
return await queryHandler.WaitFor(null, this, updatedSelector, options).ConfigureAwait(false);
@@ -242,20 +247,35 @@ public async Task PressAsync(string key, PressOptions options = null)
242247
/// <inheritdoc/>
243248
public Task<IElementHandle> QuerySelectorAsync(string selector)
244249
{
250+
if (string.IsNullOrEmpty(selector))
251+
{
252+
throw new ArgumentNullException(nameof(selector));
253+
}
254+
245255
var (updatedSelector, queryHandler) = CustomQueriesManager.GetQueryHandlerAndSelector(selector);
246256
return queryHandler.QueryOne(this, updatedSelector);
247257
}
248258

249259
/// <inheritdoc/>
250260
public Task<IElementHandle[]> QuerySelectorAllAsync(string selector)
251261
{
262+
if (string.IsNullOrEmpty(selector))
263+
{
264+
throw new ArgumentNullException(nameof(selector));
265+
}
266+
252267
var (updatedSelector, queryHandler) = CustomQueriesManager.GetQueryHandlerAndSelector(selector);
253268
return queryHandler.QueryAll(this, updatedSelector);
254269
}
255270

256271
/// <inheritdoc/>
257272
public async Task<IJSHandle> QuerySelectorAllHandleAsync(string selector)
258273
{
274+
if (string.IsNullOrEmpty(selector))
275+
{
276+
throw new ArgumentNullException(nameof(selector));
277+
}
278+
259279
var (updatedSelector, queryHandler) = CustomQueriesManager.GetQueryHandlerAndSelector(selector);
260280
var handles = await queryHandler.QueryAll(this, updatedSelector).ConfigureAwait(false);
261281

lib/PuppeteerSharp/Frame.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,24 +94,30 @@ public async Task<IExecutionContext> GetExecutionContextAsync()
9494
/// <inheritdoc/>
9595
public async Task<IElementHandle> WaitForSelectorAsync(string selector, WaitForSelectorOptions options = null)
9696
{
97+
if (string.IsNullOrEmpty(selector))
98+
{
99+
throw new ArgumentNullException(nameof(selector));
100+
}
101+
97102
var customQueriesManager = ((Browser)FrameManager.Page.Browser).CustomQueriesManager;
98103
var (updatedSelector, queryHandler) = customQueriesManager.GetQueryHandlerAndSelector(selector);
99104
return await queryHandler.WaitFor(this, null, updatedSelector, options).ConfigureAwait(false);
100105
}
101106

102107
/// <inheritdoc/>
103-
public async Task<IElementHandle> WaitForXPathAsync(string xpath, WaitForSelectorOptions options = null)
108+
public Task<IElementHandle> WaitForXPathAsync(string xpath, WaitForSelectorOptions options = null)
104109
{
105-
var handle = await PuppeteerWorld.WaitForXPathAsync(xpath, options).ConfigureAwait(false);
106-
if (handle == null)
110+
if (string.IsNullOrEmpty(xpath))
111+
{
112+
throw new ArgumentNullException(nameof(xpath));
113+
}
114+
115+
if (xpath.StartsWith("//", StringComparison.OrdinalIgnoreCase))
107116
{
108-
return null;
117+
xpath = $".{xpath}";
109118
}
110119

111-
var mainExecutionContext = await MainWorld.GetExecutionContextAsync().ConfigureAwait(false);
112-
var result = await mainExecutionContext.AdoptElementHandleAsync(handle).ConfigureAwait(false);
113-
await handle.DisposeAsync().ConfigureAwait(false);
114-
return result;
120+
return WaitForSelectorAsync($"xpath/{xpath}", options);
115121
}
116122

117123
/// <inheritdoc/>

lib/PuppeteerSharp/IElementHandle.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ public interface IElementHandle : IJSHandle
274274
/// </summary>
275275
/// <param name="expression">Expression to evaluate <see href="https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate"/>.</param>
276276
/// <returns>Task which resolves to an array of <see cref="IElementHandle"/>.</returns>
277+
[Obsolete("Use " + nameof(QuerySelectorAsync) + " instead")]
277278
Task<IElementHandle[]> XPathAsync(string expression);
278279
}
279280
}

lib/PuppeteerSharp/IFrame.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ public interface IFrame
381381
/// <param name="options">Optional waiting parameters.</param>
382382
/// <returns>A task that resolves when element specified by selector string is added to DOM.
383383
/// Resolves to `null` if waiting for `hidden: true` and selector is not found in DOM.</returns>
384-
/// <seealso cref="WaitForXPathAsync(string, WaitForSelectorOptions)"/>
385384
/// <seealso cref="IPage.WaitForSelectorAsync(string, WaitForSelectorOptions)"/>
386385
/// <exception cref="WaitTaskTimeoutException">If timeout occurred.</exception>
387386
Task<IElementHandle> WaitForSelectorAsync(string selector, WaitForSelectorOptions options = null);
@@ -421,16 +420,16 @@ public interface IFrame
421420
/// </code>
422421
/// </example>
423422
/// <seealso cref="WaitForSelectorAsync(string, WaitForSelectorOptions)"/>
424-
/// <seealso cref="IPage.WaitForXPathAsync(string, WaitForSelectorOptions)"/>
425423
/// <exception cref="WaitTaskTimeoutException">If timeout occurred.</exception>
424+
[Obsolete("Use " + nameof(WaitForSelectorAsync) + " instead")]
426425
Task<IElementHandle> WaitForXPathAsync(string xpath, WaitForSelectorOptions options = null);
427426

428427
/// <summary>
429428
/// Evaluates the XPath expression.
430429
/// </summary>
431430
/// <param name="expression">Expression to evaluate <see href="https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate"/>.</param>
432431
/// <returns>Task which resolves to an array of <see cref="IElementHandle"/>.</returns>
433-
/// <seealso cref="IPage.XPathAsync(string)"/>
432+
[Obsolete("Use " + nameof(QuerySelectorAsync) + " instead")]
434433
Task<IElementHandle[]> XPathAsync(string expression);
435434
}
436435
}

0 commit comments

Comments
 (0)