Skip to content

Commit faec0b1

Browse files
authored
Add shadow DOM support (#1994)
1 parent a12f106 commit faec0b1

File tree

3 files changed

+163
-2
lines changed

3 files changed

+163
-2
lines changed

appveyor/GenerateDocs.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ if($env:APPVEYOR_REPO_TAG -eq 'True' -And $env:framework -eq 'net6.0' -And $env:
44

55
git config --global user.email "[email protected]"
66
git config --global user.name "Dario Kondratiuk"
7-
git remote add pages https://github.com/kblok/puppeteer-sharp.git
7+
git remote add pages https://github.com/hardkoded/puppeteer-sharp.git
88
git fetch pages
99
git checkout master
1010
git subtree add --prefix docs pages/gh-pages
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using System.Xml.Linq;
5+
using PuppeteerSharp.Tests.Attributes;
6+
using PuppeteerSharp.Xunit;
7+
using Xunit;
8+
using Xunit.Abstractions;
9+
using static System.Net.Mime.MediaTypeNames;
10+
11+
namespace PuppeteerSharp.Tests.QuerySelectorTests
12+
{
13+
[Collection(TestConstants.TestFixtureCollectionName)]
14+
public class PierceHandlerTests : PuppeteerPageBaseTest
15+
{
16+
public PierceHandlerTests(ITestOutputHelper output) : base(output)
17+
{
18+
}
19+
20+
public override async Task InitializeAsync()
21+
{
22+
await base.InitializeAsync();
23+
24+
await Page.SetContentAsync(@"
25+
<script>
26+
const div = document.createElement('div');
27+
const shadowRoot = div.attachShadow({mode: 'open'});
28+
const div1 = document.createElement('div');
29+
div1.textContent = 'Hello';
30+
div1.className = 'foo';
31+
const div2 = document.createElement('div');
32+
div2.textContent = 'World';
33+
div2.className = 'foo';
34+
shadowRoot.appendChild(div1);
35+
shadowRoot.appendChild(div2);
36+
document.documentElement.appendChild(div);
37+
</script>
38+
");
39+
}
40+
public override Task DisposeAsync()
41+
{
42+
Browser.ClearCustomQueryHandlers();
43+
return base.DisposeAsync();
44+
}
45+
46+
[PuppeteerTest("queryselector.spec.ts", "pierceHandler", "should find first element in shadow")]
47+
[PuppeteerFact]
48+
public async Task ShouldFindFirstElementInShadow()
49+
{
50+
var div = await Page.QuerySelectorAsync("pierce/.foo");
51+
var text = await div.EvaluateFunctionAsync<string>(@"(element) => {
52+
return element.textContent;
53+
}");
54+
Assert.Equal("Hello", text);
55+
}
56+
57+
[PuppeteerTest("queryselector.spec.ts", "pierceHandler", "should find all elements in shadow")]
58+
[PuppeteerFact]
59+
public async Task ShouldFindAllElementsInShadow()
60+
{
61+
var divs = await Page.QuerySelectorAllAsync("pierce/.foo");
62+
var text = await Task.WhenAll(
63+
divs.Select(div => {
64+
return div.EvaluateFunctionAsync<string>(@"(element) => {
65+
return element.textContent;
66+
}");
67+
}));
68+
Assert.Equal("Hello World", string.Join(" ", text));
69+
}
70+
71+
[PuppeteerTest("queryselector.spec.ts", "pierceHandler", "should find first child element")]
72+
[PuppeteerFact]
73+
public async Task ShouldFindFirstChildElement()
74+
{
75+
var parentElement = await Page.QuerySelectorAsync("html > div");
76+
var childElement = await parentElement.QuerySelectorAsync("pierce/div");
77+
var text = await childElement.EvaluateFunctionAsync<string>(@"(element) => {
78+
return element.textContent;
79+
}");
80+
Assert.Equal("Hello", text);
81+
}
82+
83+
[PuppeteerTest("queryselector.spec.ts", "pierceHandler", "should find all child elements")]
84+
[PuppeteerFact]
85+
public async Task ShouldFindAllChildElements()
86+
{
87+
var parentElement = await Page.QuerySelectorAsync("html > div");
88+
var childElements = await parentElement.QuerySelectorAllAsync("pierce/div");
89+
var text = await Task.WhenAll(
90+
childElements.Select(div => {
91+
return div.EvaluateFunctionAsync<string>(@"(element) => {
92+
return element.textContent;
93+
}");
94+
}));
95+
Assert.Equal("Hello World", string.Join(" ", text));
96+
}
97+
}
98+
}

lib/PuppeteerSharp/CustomQueriesManager.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Text.RegularExpressions;
@@ -8,7 +9,58 @@ namespace PuppeteerSharp
89
internal class CustomQueriesManager
910
{
1011
private readonly Dictionary<string, InternalQueryHandler> _queryHandlers = new();
11-
private readonly Dictionary<string, InternalQueryHandler> _builtInHandlers = new();
12+
private readonly InternalQueryHandler _pierceHandler = MakeQueryHandler(new CustomQueryHandler
13+
{
14+
QueryOne = @"(element, selector) => {
15+
let found = null;
16+
const search = (root) => {
17+
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
18+
do {
19+
const currentNode = iter.currentNode;
20+
if (currentNode.shadowRoot) {
21+
search(currentNode.shadowRoot);
22+
}
23+
if (currentNode instanceof ShadowRoot) {
24+
continue;
25+
}
26+
if (currentNode !== root && !found && currentNode.matches(selector)) {
27+
found = currentNode;
28+
}
29+
} while (!found && iter.nextNode());
30+
};
31+
if (element instanceof Document) {
32+
element = element.documentElement;
33+
}
34+
search(element);
35+
return found;
36+
}",
37+
QueryAll = @"(element, selector) => {
38+
const result = [];
39+
const collect = (root) => {
40+
const iter = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
41+
do {
42+
const currentNode = iter.currentNode;
43+
if (currentNode.shadowRoot) {
44+
collect(currentNode.shadowRoot);
45+
}
46+
if (currentNode instanceof ShadowRoot) {
47+
continue;
48+
}
49+
if (currentNode !== root && currentNode.matches(selector)) {
50+
result.push(currentNode);
51+
}
52+
} while (iter.nextNode());
53+
};
54+
if (element instanceof Document) {
55+
element = element.documentElement;
56+
}
57+
collect(element);
58+
return result;
59+
}",
60+
});
61+
62+
private readonly Dictionary<string, InternalQueryHandler> _builtInHandlers;
63+
1264
private readonly Regex _customQueryHandlerNameRegex = new("[a-zA-Z]+$", RegexOptions.Compiled);
1365
private readonly Regex _customQueryHandlerParserRegex = new("(?<query>^[a-zA-Z]+)\\/(?<selector>.*)", RegexOptions.Compiled);
1466
private readonly InternalQueryHandler _defaultHandler = MakeQueryHandler(new CustomQueryHandler
@@ -17,6 +69,17 @@ internal class CustomQueriesManager
1769
QueryAll = "(element, selector) => element.querySelectorAll(selector)",
1870
});
1971

72+
public CustomQueriesManager()
73+
{
74+
_builtInHandlers = new()
75+
{
76+
["pierce"] = _pierceHandler,
77+
};
78+
_queryHandlers = _builtInHandlers.ToDictionary(
79+
entry => entry.Key,
80+
entry => entry.Value);
81+
}
82+
2083
internal void RegisterCustomQueryHandler(string name, CustomQueryHandler queryHandler)
2184
{
2285
if (_queryHandlers.ContainsKey(name))

0 commit comments

Comments
 (0)