Skip to content

Commit 50fbc75

Browse files
authored
fix: sync selectors (#1325)
1 parent 97c6490 commit 50fbc75

File tree

4 files changed

+369
-2
lines changed

4 files changed

+369
-2
lines changed

playwright/_impl/_playwright.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(
4141
self.webkit = from_channel(initializer["webkit"])
4242
self.webkit._playwright = self
4343

44-
self.selectors = Selectors(self._loop)
44+
self.selectors = Selectors(self._loop, self._dispatcher_fiber)
4545
selectors_owner: SelectorsOwner = from_channel(initializer["selectors"])
4646
self.selectors._add_channel(selectors_owner)
4747

playwright/_impl/_selectors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222

2323

2424
class Selectors:
25-
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
25+
def __init__(self, loop: asyncio.AbstractEventLoop, dispatcher_fiber: Any) -> None:
2626
self._loop = loop
2727
self._channels: Set[SelectorsOwner] = set()
2828
self._registrations: List[Dict] = []
29+
self._dispatcher_fiber = dispatcher_fiber
2930

3031
async def register(
3132
self,

tests/sync/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
BrowserType,
2424
Page,
2525
Playwright,
26+
Selectors,
2627
sync_playwright,
2728
)
2829

@@ -77,3 +78,8 @@ def page(context: BrowserContext) -> Generator[Page, None, None]:
7778
page = context.new_page()
7879
yield page
7980
page.close()
81+
82+
83+
@pytest.fixture(scope="session")
84+
def selectors(playwright: Playwright) -> Selectors:
85+
return playwright.selectors

tests/sync/test_queryselector.py

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from playwright.sync_api import Browser, Error, Page, Selectors
6+
7+
from .utils import Utils
8+
9+
10+
def test_selectors_register_should_work(
11+
selectors: Selectors, browser: Browser, browser_name: str
12+
) -> None:
13+
tag_selector = """
14+
{
15+
create(root, target) {
16+
return target.nodeName;
17+
},
18+
query(root, selector) {
19+
return root.querySelector(selector);
20+
},
21+
queryAll(root, selector) {
22+
return Array.from(root.querySelectorAll(selector));
23+
}
24+
}"""
25+
26+
selector_name = f"tag_{browser_name}"
27+
selector2_name = f"tag2_{browser_name}"
28+
29+
# Register one engine before creating context.
30+
selectors.register(selector_name, tag_selector)
31+
32+
context = browser.new_context()
33+
# Register another engine after creating context.
34+
selectors.register(selector2_name, tag_selector)
35+
36+
page = context.new_page()
37+
page.set_content("<div><span></span></div><div></div>")
38+
39+
assert page.eval_on_selector(f"{selector_name}=DIV", "e => e.nodeName") == "DIV"
40+
assert page.eval_on_selector(f"{selector_name}=SPAN", "e => e.nodeName") == "SPAN"
41+
assert page.eval_on_selector_all(f"{selector_name}=DIV", "es => es.length") == 2
42+
43+
assert page.eval_on_selector(f"{selector2_name}=DIV", "e => e.nodeName") == "DIV"
44+
assert page.eval_on_selector(f"{selector2_name}=SPAN", "e => e.nodeName") == "SPAN"
45+
assert page.eval_on_selector_all(f"{selector2_name}=DIV", "es => es.length") == 2
46+
47+
# Selector names are case-sensitive.
48+
with pytest.raises(Error) as exc:
49+
page.query_selector("tAG=DIV")
50+
assert 'Unknown engine "tAG" while parsing selector tAG=DIV' in exc.value.message
51+
52+
context.close()
53+
54+
55+
def test_selectors_register_should_work_with_path(
56+
selectors: Selectors, page: Page, utils: Utils, assetdir: Path
57+
) -> None:
58+
utils.register_selector_engine(
59+
selectors, "foo", path=assetdir / "sectionselectorengine.js"
60+
)
61+
page.set_content("<section></section>")
62+
assert page.eval_on_selector("foo=whatever", "e => e.nodeName") == "SECTION"
63+
64+
65+
def test_selectors_register_should_work_in_main_and_isolated_world(
66+
selectors: Selectors, page: Page, utils: Utils
67+
) -> None:
68+
dummy_selector_script = """{
69+
create(root, target) { },
70+
query(root, selector) {
71+
return window.__answer;
72+
},
73+
queryAll(root, selector) {
74+
return window['__answer'] ? [window['__answer'], document.body, document.documentElement] : [];
75+
}
76+
}"""
77+
78+
utils.register_selector_engine(selectors, "main", dummy_selector_script)
79+
utils.register_selector_engine(
80+
selectors, "isolated", dummy_selector_script, content_script=True
81+
)
82+
page.set_content("<div><span><section></section></span></div>")
83+
page.evaluate('() => window.__answer = document.querySelector("span")')
84+
# Works in main if asked.
85+
assert page.eval_on_selector("main=ignored", "e => e.nodeName") == "SPAN"
86+
assert page.eval_on_selector("css=div >> main=ignored", "e => e.nodeName") == "SPAN"
87+
assert page.eval_on_selector_all(
88+
"main=ignored", "es => window.__answer !== undefined"
89+
)
90+
assert (
91+
page.eval_on_selector_all("main=ignored", "es => es.filter(e => e).length") == 3
92+
)
93+
# Works in isolated by default.
94+
assert page.query_selector("isolated=ignored") is None
95+
assert page.query_selector("css=div >> isolated=ignored") is None
96+
# $$eval always works in main, to avoid adopting nodes one by one.
97+
assert page.eval_on_selector_all(
98+
"isolated=ignored", "es => window.__answer !== undefined"
99+
)
100+
assert (
101+
page.eval_on_selector_all("isolated=ignored", "es => es.filter(e => e).length")
102+
== 3
103+
)
104+
# At least one engine in main forces all to be in main.
105+
assert (
106+
page.eval_on_selector("main=ignored >> isolated=ignored", "e => e.nodeName")
107+
== "SPAN"
108+
)
109+
assert (
110+
page.eval_on_selector("isolated=ignored >> main=ignored", "e => e.nodeName")
111+
== "SPAN"
112+
)
113+
# Can be chained to css.
114+
assert (
115+
page.eval_on_selector("main=ignored >> css=section", "e => e.nodeName")
116+
== "SECTION"
117+
)
118+
119+
120+
def test_selectors_register_should_handle_errors(
121+
selectors: Selectors, page: Page, utils: Utils
122+
) -> None:
123+
with pytest.raises(Error) as exc:
124+
page.query_selector("neverregister=ignored")
125+
assert (
126+
'Unknown engine "neverregister" while parsing selector neverregister=ignored'
127+
in exc.value.message
128+
)
129+
130+
dummy_selector_engine_script = """{
131+
create(root, target) {
132+
return target.nodeName;
133+
},
134+
query(root, selector) {
135+
return root.querySelector('dummy');
136+
},
137+
queryAll(root, selector) {
138+
return Array.from(root.query_selector_all('dummy'));
139+
}
140+
}"""
141+
142+
with pytest.raises(Error) as exc:
143+
selectors.register("$", dummy_selector_engine_script)
144+
assert (
145+
exc.value.message
146+
== "Selector engine name may only contain [a-zA-Z0-9_] characters"
147+
)
148+
149+
# Selector names are case-sensitive.
150+
utils.register_selector_engine(selectors, "dummy", dummy_selector_engine_script)
151+
utils.register_selector_engine(selectors, "duMMy", dummy_selector_engine_script)
152+
153+
with pytest.raises(Error) as exc:
154+
selectors.register("dummy", dummy_selector_engine_script)
155+
assert exc.value.message == '"dummy" selector engine has been already registered'
156+
157+
with pytest.raises(Error) as exc:
158+
selectors.register("css", dummy_selector_engine_script)
159+
assert exc.value.message == '"css" is a predefined selector engine'
160+
161+
162+
def test_should_work_with_layout_selectors(page: Page) -> None:
163+
# +--+ +--+
164+
# | 1| | 2|
165+
# +--+ ++-++
166+
# | 3| | 4|
167+
# +-------+ ++-++
168+
# | 0 | | 5|
169+
# | +--+ +--+--+
170+
# | | 6| | 7|
171+
# | +--+ +--+
172+
# | |
173+
# O-------+
174+
# +--+
175+
# | 8|
176+
# +--++--+
177+
# | 9|
178+
# +--+
179+
180+
boxes = [
181+
# x, y, width, height
182+
[0, 0, 150, 150],
183+
[100, 200, 50, 50],
184+
[200, 200, 50, 50],
185+
[100, 150, 50, 50],
186+
[201, 150, 50, 50],
187+
[200, 100, 50, 50],
188+
[50, 50, 50, 50],
189+
[150, 50, 50, 50],
190+
[150, -51, 50, 50],
191+
[201, -101, 50, 50],
192+
]
193+
page.set_content(
194+
'<container style="width: 500px; height: 500px; position: relative;"></container>'
195+
)
196+
page.eval_on_selector(
197+
"container",
198+
"""(container, boxes) => {
199+
for (let i = 0; i < boxes.length; i++) {
200+
const div = document.createElement('div');
201+
div.style.position = 'absolute';
202+
div.style.overflow = 'hidden';
203+
div.style.boxSizing = 'border-box';
204+
div.style.border = '1px solid black';
205+
div.id = 'id' + i;
206+
div.textContent = 'id' + i;
207+
const box = boxes[i];
208+
div.style.left = box[0] + 'px';
209+
// Note that top is a flipped y coordinate.
210+
div.style.top = (250 - box[1] - box[3]) + 'px';
211+
div.style.width = box[2] + 'px';
212+
div.style.height = box[3] + 'px';
213+
container.appendChild(div);
214+
const span = document.createElement('span');
215+
span.textContent = '' + i;
216+
div.appendChild(span);
217+
}
218+
}""",
219+
boxes,
220+
)
221+
222+
assert page.eval_on_selector("div:right-of(#id6)", "e => e.id") == "id7"
223+
assert page.eval_on_selector("div:right-of(#id1)", "e => e.id") == "id2"
224+
assert page.eval_on_selector("div:right-of(#id3)", "e => e.id") == "id4"
225+
assert page.query_selector("div:right-of(#id4)") is None
226+
assert page.eval_on_selector("div:right-of(#id0)", "e => e.id") == "id7"
227+
assert page.eval_on_selector("div:right-of(#id8)", "e => e.id") == "id9"
228+
assert (
229+
page.eval_on_selector_all(
230+
"div:right-of(#id3)", "els => els.map(e => e.id).join(',')"
231+
)
232+
== "id4,id2,id5,id7,id8,id9"
233+
)
234+
assert (
235+
page.eval_on_selector_all(
236+
"div:right-of(#id3, 50)", "els => els.map(e => e.id).join(',')"
237+
)
238+
== "id2,id5,id7,id8"
239+
)
240+
assert (
241+
page.eval_on_selector_all(
242+
"div:right-of(#id3, 49)", "els => els.map(e => e.id).join(',')"
243+
)
244+
== "id7,id8"
245+
)
246+
247+
assert page.eval_on_selector("div:left-of(#id2)", "e => e.id") == "id1"
248+
assert page.query_selector("div:left-of(#id0)") is None
249+
assert page.eval_on_selector("div:left-of(#id5)", "e => e.id") == "id0"
250+
assert page.eval_on_selector("div:left-of(#id9)", "e => e.id") == "id8"
251+
assert page.eval_on_selector("div:left-of(#id4)", "e => e.id") == "id3"
252+
assert (
253+
page.eval_on_selector_all(
254+
"div:left-of(#id5)", "els => els.map(e => e.id).join(',')"
255+
)
256+
== "id0,id7,id3,id1,id6,id8"
257+
)
258+
assert (
259+
page.eval_on_selector_all(
260+
"div:left-of(#id5, 3)", "els => els.map(e => e.id).join(',')"
261+
)
262+
== "id7,id8"
263+
)
264+
265+
assert page.eval_on_selector("div:above(#id0)", "e => e.id") == "id3"
266+
assert page.eval_on_selector("div:above(#id5)", "e => e.id") == "id4"
267+
assert page.eval_on_selector("div:above(#id7)", "e => e.id") == "id5"
268+
assert page.eval_on_selector("div:above(#id8)", "e => e.id") == "id0"
269+
assert page.eval_on_selector("div:above(#id9)", "e => e.id") == "id8"
270+
assert page.query_selector("div:above(#id2)") is None
271+
assert (
272+
page.eval_on_selector_all(
273+
"div:above(#id5)", "els => els.map(e => e.id).join(',')"
274+
)
275+
== "id4,id2,id3,id1"
276+
)
277+
assert (
278+
page.eval_on_selector_all(
279+
"div:above(#id5, 20)", "els => els.map(e => e.id).join(',')"
280+
)
281+
== "id4,id3"
282+
)
283+
284+
assert page.eval_on_selector("div:below(#id4)", "e => e.id") == "id5"
285+
assert page.eval_on_selector("div:below(#id3)", "e => e.id") == "id0"
286+
assert page.eval_on_selector("div:below(#id2)", "e => e.id") == "id4"
287+
assert page.eval_on_selector("div:below(#id6)", "e => e.id") == "id8"
288+
assert page.eval_on_selector("div:below(#id7)", "e => e.id") == "id8"
289+
assert page.eval_on_selector("div:below(#id8)", "e => e.id") == "id9"
290+
assert page.query_selector("div:below(#id9)") is None
291+
assert (
292+
page.eval_on_selector_all(
293+
"div:below(#id3)", "els => els.map(e => e.id).join(',')"
294+
)
295+
== "id0,id5,id6,id7,id8,id9"
296+
)
297+
assert (
298+
page.eval_on_selector_all(
299+
"div:below(#id3, 105)", "els => els.map(e => e.id).join(',')"
300+
)
301+
== "id0,id5,id6,id7"
302+
)
303+
304+
assert page.eval_on_selector("div:near(#id0)", "e => e.id") == "id3"
305+
assert (
306+
page.eval_on_selector_all(
307+
"div:near(#id7)", "els => els.map(e => e.id).join(',')"
308+
)
309+
== "id0,id5,id3,id6"
310+
)
311+
assert (
312+
page.eval_on_selector_all(
313+
"div:near(#id0)", "els => els.map(e => e.id).join(',')"
314+
)
315+
== "id3,id6,id7,id8,id1,id5"
316+
)
317+
assert (
318+
page.eval_on_selector_all(
319+
"div:near(#id6)", "els => els.map(e => e.id).join(',')"
320+
)
321+
== "id0,id3,id7"
322+
)
323+
assert (
324+
page.eval_on_selector_all(
325+
"div:near(#id6, 10)", "els => els.map(e => e.id).join(',')"
326+
)
327+
== "id0"
328+
)
329+
assert (
330+
page.eval_on_selector_all(
331+
"div:near(#id0, 100)", "els => els.map(e => e.id).join(',')"
332+
)
333+
== "id3,id6,id7,id8,id1,id5,id4,id2"
334+
)
335+
336+
assert (
337+
page.eval_on_selector_all(
338+
"div:below(#id5):above(#id8)", "els => els.map(e => e.id).join(',')"
339+
)
340+
== "id7,id6"
341+
)
342+
assert page.eval_on_selector("div:below(#id5):above(#id8)", "e => e.id") == "id7"
343+
344+
assert (
345+
page.eval_on_selector_all(
346+
"div:right-of(#id0) + div:above(#id8)",
347+
"els => els.map(e => e.id).join(',')",
348+
)
349+
== "id5,id6,id3"
350+
)
351+
352+
with pytest.raises(Error) as exc_info:
353+
page.query_selector(":near(50)")
354+
assert (
355+
'"near" engine expects a selector list and optional maximum distance in pixels'
356+
in exc_info.value.message
357+
)
358+
with pytest.raises(Error) as exc_info:
359+
page.query_selector('left-of="div"')
360+
assert '"left-of" selector cannot be first' in exc_info.value.message

0 commit comments

Comments
 (0)