Skip to content

Commit 6ca26ec

Browse files
authored
feat: added selector engine (microsoft#76)
1 parent d9847e6 commit 6ca26ec

9 files changed

+234
-8
lines changed

playwright/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
firefox = playwright_object.firefox
2121
webkit = playwright_object.webkit
2222
devices = playwright_object.devices
23+
selectors = playwright_object.selectors
2324
browser_types = playwright_object.browser_types
2425
Error = helper.Error
2526
TimeoutError = helper.TimeoutError
@@ -30,6 +31,7 @@
3031
"firefox",
3132
"webkit",
3233
"devices",
34+
"selectors",
3335
"Error",
3436
"TimeoutError",
3537
]

playwright/js_handle.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ async def getProperty(self, name: str) -> "JSHandle":
7373
return from_channel(await self._channel.send("getProperty", dict(name=name)))
7474

7575
async def getProperties(self) -> Dict[str, "JSHandle"]:
76-
map = dict()
77-
for property in await self._channel.send("getPropertyList"):
78-
map[property["name"]] = from_channel(property["value"])
79-
return map
76+
return {
77+
prop["name"]: from_channel(prop["value"])
78+
for prop in await self._channel.send("getPropertyList")
79+
}
8080

8181
def asElement(self) -> Optional["ElementHandle"]:
8282
return None

playwright/object_factory.py

+3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from playwright.network import Request, Response, Route
2929
from playwright.page import BindingCall, Page
3030
from playwright.playwright import Playwright
31+
from playwright.selectors import Selectors
3132
from playwright.worker import Worker
3233

3334

@@ -73,4 +74,6 @@ def create_remote_object(
7374
return Route(scope, guid, initializer)
7475
if type == "worker":
7576
return Worker(scope, guid, initializer)
77+
if type == "selectors":
78+
return Selectors(scope, guid, initializer)
7679
return DummyObject(scope, guid, initializer)

playwright/playwright.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from playwright.browser_type import BrowserType
1818
from playwright.connection import ChannelOwner, ConnectionScope, from_channel
19+
from playwright.selectors import Selectors
1920

2021

2122
class Playwright(ChannelOwner):
@@ -24,9 +25,11 @@ def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None
2425
self.chromium: BrowserType = from_channel(initializer["chromium"])
2526
self.firefox: BrowserType = from_channel(initializer["firefox"])
2627
self.webkit: BrowserType = from_channel(initializer["webkit"])
27-
self.devices = dict()
28-
for device in initializer["deviceDescriptors"]:
29-
self.devices[device["name"]] = device["descriptor"]
28+
self.selectors: Selectors = from_channel(initializer["selectors"])
29+
self.devices = {
30+
device["name"]: device["descriptor"]
31+
for device in initializer["deviceDescriptors"]
32+
}
3033
self.browser_types: Dict[str, BrowserType] = dict(
3134
chromium=self.chromium, webkit=self.webkit, firefox=self.firefox
3235
)

playwright/selectors.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (c) Microsoft Corporation.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Dict, Optional
16+
17+
from playwright.connection import ChannelOwner, ConnectionScope
18+
from playwright.element_handle import ElementHandle
19+
20+
21+
class Selectors(ChannelOwner):
22+
def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None:
23+
super().__init__(scope, guid, initializer)
24+
25+
async def register(
26+
self, name: str, source: str = "", path: str = None, contentScript: bool = False
27+
) -> None:
28+
if path:
29+
with open(path, "r") as file:
30+
source = file.read()
31+
await self._channel.send(
32+
"register",
33+
dict(name=name, source=source, options={"contentScript": contentScript}),
34+
)
35+
36+
async def _createSelector(self, name: str, handle: ElementHandle) -> Optional[str]:
37+
return await self._channel.send(
38+
"createSelector", dict(name=name, handle=handle._channel)
39+
)

tests/assets/sectionselectorengine.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
({
2+
create(root, target) {
3+
},
4+
query(root, selector) {
5+
return root.querySelector('section');
6+
},
7+
queryAll(root, selector) {
8+
return Array.from(root.querySelectorAll('section'));
9+
}
10+
})

tests/conftest.py

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def event_loop():
4242
loop.close()
4343

4444

45+
@pytest.fixture(scope="session")
46+
def selectors():
47+
return playwright.selectors
48+
49+
4550
@pytest.fixture(scope="session")
4651
def browser_type(browser_name: str):
4752
return playwright.browser_types[browser_name]

tests/test_queryselector.py

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import os
2+
3+
import pytest
4+
5+
from playwright.helper import Error
6+
from playwright.page import Page
7+
8+
9+
async def test_selectors_register_should_work(selectors, page: Page, utils):
10+
await utils.register_selector_engine(
11+
selectors,
12+
"tag",
13+
"""{
14+
create(root, target) {
15+
return target.nodeName;
16+
},
17+
query(root, selector) {
18+
return root.querySelector(selector);
19+
},
20+
queryAll(root, selector) {
21+
return Array.from(root.querySelectorAll(selector));
22+
}
23+
}""",
24+
)
25+
await page.setContent("<div><span></span></div><div></div>")
26+
assert (
27+
await selectors._createSelector("tag", await page.querySelector("div")) == "DIV"
28+
)
29+
assert await page.evalOnSelector("tag=DIV", "e => e.nodeName") == "DIV"
30+
assert await page.evalOnSelector("tag=SPAN", "e => e.nodeName") == "SPAN"
31+
assert await page.evalOnSelectorAll("tag=DIV", "es => es.length") == 2
32+
33+
# Selector names are case-sensitive.
34+
with pytest.raises(Error) as exc:
35+
await page.querySelector("tAG=DIV")
36+
assert 'Unknown engine "tAG" while parsing selector tAG=DIV' in exc.value.message
37+
38+
39+
async def test_selectors_register_should_work_with_path(selectors, page: Page, utils):
40+
await utils.register_selector_engine(
41+
selectors,
42+
"foo",
43+
path=os.path.join(
44+
os.path.dirname(os.path.abspath(__file__)),
45+
"assets/sectionselectorengine.js",
46+
),
47+
)
48+
await page.setContent("<section></section>")
49+
assert await page.evalOnSelector("foo=whatever", "e => e.nodeName") == "SECTION"
50+
51+
52+
async def test_selectors_register_should_work_in_main_and_isolated_world(
53+
selectors, page: Page, utils
54+
):
55+
dummy_selector_script = """{
56+
create(root, target) { },
57+
query(root, selector) {
58+
return window.__answer;
59+
},
60+
queryAll(root, selector) {
61+
return [document.body, document.documentElement, window.__answer];
62+
}
63+
}"""
64+
65+
await utils.register_selector_engine(selectors, "main", dummy_selector_script)
66+
await utils.register_selector_engine(
67+
selectors, "isolated", dummy_selector_script, contentScript=True
68+
)
69+
await page.setContent("<div><span><section></section></span></div>")
70+
await page.evaluate('() => window.__answer = document.querySelector("span")')
71+
# Works in main if asked.
72+
assert await page.evalOnSelector("main=ignored", "e => e.nodeName") == "SPAN"
73+
assert (
74+
await page.evalOnSelector("css=div >> main=ignored", "e => e.nodeName")
75+
== "SPAN"
76+
)
77+
assert await page.evalOnSelectorAll(
78+
"main=ignored", "es => window.__answer !== undefined"
79+
)
80+
assert (
81+
await page.evalOnSelectorAll("main=ignored", "es => es.filter(e => e).length")
82+
== 3
83+
)
84+
# Works in isolated by default.
85+
assert await page.querySelector("isolated=ignored") is None
86+
assert await page.querySelector("css=div >> isolated=ignored") is None
87+
# $$eval always works in main, to avoid adopting nodes one by one.
88+
assert await page.evalOnSelectorAll(
89+
"isolated=ignored", "es => window.__answer !== undefined"
90+
)
91+
assert (
92+
await page.evalOnSelectorAll(
93+
"isolated=ignored", "es => es.filter(e => e).length"
94+
)
95+
== 3
96+
)
97+
# At least one engine in main forces all to be in main.
98+
assert (
99+
await page.evalOnSelector("main=ignored >> isolated=ignored", "e => e.nodeName")
100+
== "SPAN"
101+
)
102+
assert (
103+
await page.evalOnSelector("isolated=ignored >> main=ignored", "e => e.nodeName")
104+
== "SPAN"
105+
)
106+
# Can be chained to css.
107+
assert (
108+
await page.evalOnSelector("main=ignored >> css=section", "e => e.nodeName")
109+
== "SECTION"
110+
)
111+
112+
113+
async def test_selectors_register_should_handle_errors(selectors, page: Page, utils):
114+
with pytest.raises(Error) as exc:
115+
await page.querySelector("neverregister=ignored")
116+
assert (
117+
'Unknown engine "neverregister" while parsing selector neverregister=ignored'
118+
in exc.value.message
119+
)
120+
121+
dummy_selector_engine_script = """{
122+
create(root, target) {
123+
return target.nodeName;
124+
},
125+
query(root, selector) {
126+
return root.querySelector('dummy');
127+
},
128+
queryAll(root, selector) {
129+
return Array.from(root.querySelectorAll('dummy'));
130+
}
131+
}"""
132+
133+
with pytest.raises(Error) as exc:
134+
await selectors.register("$", dummy_selector_engine_script)
135+
assert (
136+
"Selector engine name may only contain [a-zA-Z0-9_] characters"
137+
== exc.value.message
138+
)
139+
140+
# Selector names are case-sensitive.
141+
await utils.register_selector_engine(
142+
selectors, "dummy", dummy_selector_engine_script
143+
)
144+
await utils.register_selector_engine(
145+
selectors, "duMMy", dummy_selector_engine_script
146+
)
147+
148+
with pytest.raises(Error) as exc:
149+
await selectors.register("dummy", dummy_selector_engine_script)
150+
assert exc.value.message == '"dummy" selector engine has been already registered'
151+
152+
with pytest.raises(Error) as exc:
153+
await selectors.register("css", dummy_selector_engine_script)
154+
assert exc.value.message == '"css" is a predefined selector engine'

tests/utils.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717

1818
from playwright.element_handle import ElementHandle
1919
from playwright.frame import Frame
20-
from playwright.helper import Viewport
20+
from playwright.helper import Error, Viewport
2121
from playwright.page import Page
22+
from playwright.selectors import Selectors
2223

2324

2425
class Utils:
@@ -60,5 +61,14 @@ async def verify_viewport(self, page: Page, width: int, height: int):
6061
assert await page.evaluate("window.innerWidth") == width
6162
assert await page.evaluate("window.innerHeight") == height
6263

64+
async def register_selector_engine(
65+
self, selectors: Selectors, *args, **kwargs
66+
) -> None:
67+
try:
68+
await selectors.register(*args, **kwargs)
69+
except Error as exc:
70+
if "has been already registered" not in exc.message:
71+
raise exc
72+
6373

6474
utils = Utils()

0 commit comments

Comments
 (0)