Skip to content

Commit 50fbc75

Browse files
authored
fix: sync selectors (microsoft#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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy