diff --git a/playwright/_impl/_playwright.py b/playwright/_impl/_playwright.py
index 17287d145..354e3d11c 100644
--- a/playwright/_impl/_playwright.py
+++ b/playwright/_impl/_playwright.py
@@ -41,7 +41,7 @@ def __init__(
self.webkit = from_channel(initializer["webkit"])
self.webkit._playwright = self
- self.selectors = Selectors(self._loop)
+ self.selectors = Selectors(self._loop, self._dispatcher_fiber)
selectors_owner: SelectorsOwner = from_channel(initializer["selectors"])
self.selectors._add_channel(selectors_owner)
diff --git a/playwright/_impl/_selectors.py b/playwright/_impl/_selectors.py
index 961d2b2e0..03188ea5c 100644
--- a/playwright/_impl/_selectors.py
+++ b/playwright/_impl/_selectors.py
@@ -22,10 +22,11 @@
class Selectors:
- def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
+ def __init__(self, loop: asyncio.AbstractEventLoop, dispatcher_fiber: Any) -> None:
self._loop = loop
self._channels: Set[SelectorsOwner] = set()
self._registrations: List[Dict] = []
+ self._dispatcher_fiber = dispatcher_fiber
async def register(
self,
diff --git a/tests/sync/conftest.py b/tests/sync/conftest.py
index 868729cee..d7942bf90 100644
--- a/tests/sync/conftest.py
+++ b/tests/sync/conftest.py
@@ -23,6 +23,7 @@
BrowserType,
Page,
Playwright,
+ Selectors,
sync_playwright,
)
@@ -77,3 +78,8 @@ def page(context: BrowserContext) -> Generator[Page, None, None]:
page = context.new_page()
yield page
page.close()
+
+
+@pytest.fixture(scope="session")
+def selectors(playwright: Playwright) -> Selectors:
+ return playwright.selectors
diff --git a/tests/sync/test_queryselector.py b/tests/sync/test_queryselector.py
new file mode 100644
index 000000000..367b90e9a
--- /dev/null
+++ b/tests/sync/test_queryselector.py
@@ -0,0 +1,360 @@
+from pathlib import Path
+
+import pytest
+
+from playwright.sync_api import Browser, Error, Page, Selectors
+
+from .utils import Utils
+
+
+def test_selectors_register_should_work(
+ selectors: Selectors, browser: Browser, browser_name: str
+) -> None:
+ tag_selector = """
+ {
+ create(root, target) {
+ return target.nodeName;
+ },
+ query(root, selector) {
+ return root.querySelector(selector);
+ },
+ queryAll(root, selector) {
+ return Array.from(root.querySelectorAll(selector));
+ }
+ }"""
+
+ selector_name = f"tag_{browser_name}"
+ selector2_name = f"tag2_{browser_name}"
+
+ # Register one engine before creating context.
+ selectors.register(selector_name, tag_selector)
+
+ context = browser.new_context()
+ # Register another engine after creating context.
+ selectors.register(selector2_name, tag_selector)
+
+ page = context.new_page()
+ page.set_content("
")
+
+ assert page.eval_on_selector(f"{selector_name}=DIV", "e => e.nodeName") == "DIV"
+ assert page.eval_on_selector(f"{selector_name}=SPAN", "e => e.nodeName") == "SPAN"
+ assert page.eval_on_selector_all(f"{selector_name}=DIV", "es => es.length") == 2
+
+ assert page.eval_on_selector(f"{selector2_name}=DIV", "e => e.nodeName") == "DIV"
+ assert page.eval_on_selector(f"{selector2_name}=SPAN", "e => e.nodeName") == "SPAN"
+ assert page.eval_on_selector_all(f"{selector2_name}=DIV", "es => es.length") == 2
+
+ # Selector names are case-sensitive.
+ with pytest.raises(Error) as exc:
+ page.query_selector("tAG=DIV")
+ assert 'Unknown engine "tAG" while parsing selector tAG=DIV' in exc.value.message
+
+ context.close()
+
+
+def test_selectors_register_should_work_with_path(
+ selectors: Selectors, page: Page, utils: Utils, assetdir: Path
+) -> None:
+ utils.register_selector_engine(
+ selectors, "foo", path=assetdir / "sectionselectorengine.js"
+ )
+ page.set_content("")
+ assert page.eval_on_selector("foo=whatever", "e => e.nodeName") == "SECTION"
+
+
+def test_selectors_register_should_work_in_main_and_isolated_world(
+ selectors: Selectors, page: Page, utils: Utils
+) -> None:
+ dummy_selector_script = """{
+ create(root, target) { },
+ query(root, selector) {
+ return window.__answer;
+ },
+ queryAll(root, selector) {
+ return window['__answer'] ? [window['__answer'], document.body, document.documentElement] : [];
+ }
+ }"""
+
+ utils.register_selector_engine(selectors, "main", dummy_selector_script)
+ utils.register_selector_engine(
+ selectors, "isolated", dummy_selector_script, content_script=True
+ )
+ page.set_content("
")
+ page.evaluate('() => window.__answer = document.querySelector("span")')
+ # Works in main if asked.
+ assert page.eval_on_selector("main=ignored", "e => e.nodeName") == "SPAN"
+ assert page.eval_on_selector("css=div >> main=ignored", "e => e.nodeName") == "SPAN"
+ assert page.eval_on_selector_all(
+ "main=ignored", "es => window.__answer !== undefined"
+ )
+ assert (
+ page.eval_on_selector_all("main=ignored", "es => es.filter(e => e).length") == 3
+ )
+ # Works in isolated by default.
+ assert page.query_selector("isolated=ignored") is None
+ assert page.query_selector("css=div >> isolated=ignored") is None
+ # $$eval always works in main, to avoid adopting nodes one by one.
+ assert page.eval_on_selector_all(
+ "isolated=ignored", "es => window.__answer !== undefined"
+ )
+ assert (
+ page.eval_on_selector_all("isolated=ignored", "es => es.filter(e => e).length")
+ == 3
+ )
+ # At least one engine in main forces all to be in main.
+ assert (
+ page.eval_on_selector("main=ignored >> isolated=ignored", "e => e.nodeName")
+ == "SPAN"
+ )
+ assert (
+ page.eval_on_selector("isolated=ignored >> main=ignored", "e => e.nodeName")
+ == "SPAN"
+ )
+ # Can be chained to css.
+ assert (
+ page.eval_on_selector("main=ignored >> css=section", "e => e.nodeName")
+ == "SECTION"
+ )
+
+
+def test_selectors_register_should_handle_errors(
+ selectors: Selectors, page: Page, utils: Utils
+) -> None:
+ with pytest.raises(Error) as exc:
+ page.query_selector("neverregister=ignored")
+ assert (
+ 'Unknown engine "neverregister" while parsing selector neverregister=ignored'
+ in exc.value.message
+ )
+
+ dummy_selector_engine_script = """{
+ create(root, target) {
+ return target.nodeName;
+ },
+ query(root, selector) {
+ return root.querySelector('dummy');
+ },
+ queryAll(root, selector) {
+ return Array.from(root.query_selector_all('dummy'));
+ }
+ }"""
+
+ with pytest.raises(Error) as exc:
+ selectors.register("$", dummy_selector_engine_script)
+ assert (
+ exc.value.message
+ == "Selector engine name may only contain [a-zA-Z0-9_] characters"
+ )
+
+ # Selector names are case-sensitive.
+ utils.register_selector_engine(selectors, "dummy", dummy_selector_engine_script)
+ utils.register_selector_engine(selectors, "duMMy", dummy_selector_engine_script)
+
+ with pytest.raises(Error) as exc:
+ selectors.register("dummy", dummy_selector_engine_script)
+ assert exc.value.message == '"dummy" selector engine has been already registered'
+
+ with pytest.raises(Error) as exc:
+ selectors.register("css", dummy_selector_engine_script)
+ assert exc.value.message == '"css" is a predefined selector engine'
+
+
+def test_should_work_with_layout_selectors(page: Page) -> None:
+ # +--+ +--+
+ # | 1| | 2|
+ # +--+ ++-++
+ # | 3| | 4|
+ # +-------+ ++-++
+ # | 0 | | 5|
+ # | +--+ +--+--+
+ # | | 6| | 7|
+ # | +--+ +--+
+ # | |
+ # O-------+
+ # +--+
+ # | 8|
+ # +--++--+
+ # | 9|
+ # +--+
+
+ boxes = [
+ # x, y, width, height
+ [0, 0, 150, 150],
+ [100, 200, 50, 50],
+ [200, 200, 50, 50],
+ [100, 150, 50, 50],
+ [201, 150, 50, 50],
+ [200, 100, 50, 50],
+ [50, 50, 50, 50],
+ [150, 50, 50, 50],
+ [150, -51, 50, 50],
+ [201, -101, 50, 50],
+ ]
+ page.set_content(
+ ''
+ )
+ page.eval_on_selector(
+ "container",
+ """(container, boxes) => {
+ for (let i = 0; i < boxes.length; i++) {
+ const div = document.createElement('div');
+ div.style.position = 'absolute';
+ div.style.overflow = 'hidden';
+ div.style.boxSizing = 'border-box';
+ div.style.border = '1px solid black';
+ div.id = 'id' + i;
+ div.textContent = 'id' + i;
+ const box = boxes[i];
+ div.style.left = box[0] + 'px';
+ // Note that top is a flipped y coordinate.
+ div.style.top = (250 - box[1] - box[3]) + 'px';
+ div.style.width = box[2] + 'px';
+ div.style.height = box[3] + 'px';
+ container.appendChild(div);
+ const span = document.createElement('span');
+ span.textContent = '' + i;
+ div.appendChild(span);
+ }
+ }""",
+ boxes,
+ )
+
+ assert page.eval_on_selector("div:right-of(#id6)", "e => e.id") == "id7"
+ assert page.eval_on_selector("div:right-of(#id1)", "e => e.id") == "id2"
+ assert page.eval_on_selector("div:right-of(#id3)", "e => e.id") == "id4"
+ assert page.query_selector("div:right-of(#id4)") is None
+ assert page.eval_on_selector("div:right-of(#id0)", "e => e.id") == "id7"
+ assert page.eval_on_selector("div:right-of(#id8)", "e => e.id") == "id9"
+ assert (
+ page.eval_on_selector_all(
+ "div:right-of(#id3)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id4,id2,id5,id7,id8,id9"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:right-of(#id3, 50)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id2,id5,id7,id8"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:right-of(#id3, 49)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id7,id8"
+ )
+
+ assert page.eval_on_selector("div:left-of(#id2)", "e => e.id") == "id1"
+ assert page.query_selector("div:left-of(#id0)") is None
+ assert page.eval_on_selector("div:left-of(#id5)", "e => e.id") == "id0"
+ assert page.eval_on_selector("div:left-of(#id9)", "e => e.id") == "id8"
+ assert page.eval_on_selector("div:left-of(#id4)", "e => e.id") == "id3"
+ assert (
+ page.eval_on_selector_all(
+ "div:left-of(#id5)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id0,id7,id3,id1,id6,id8"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:left-of(#id5, 3)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id7,id8"
+ )
+
+ assert page.eval_on_selector("div:above(#id0)", "e => e.id") == "id3"
+ assert page.eval_on_selector("div:above(#id5)", "e => e.id") == "id4"
+ assert page.eval_on_selector("div:above(#id7)", "e => e.id") == "id5"
+ assert page.eval_on_selector("div:above(#id8)", "e => e.id") == "id0"
+ assert page.eval_on_selector("div:above(#id9)", "e => e.id") == "id8"
+ assert page.query_selector("div:above(#id2)") is None
+ assert (
+ page.eval_on_selector_all(
+ "div:above(#id5)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id4,id2,id3,id1"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:above(#id5, 20)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id4,id3"
+ )
+
+ assert page.eval_on_selector("div:below(#id4)", "e => e.id") == "id5"
+ assert page.eval_on_selector("div:below(#id3)", "e => e.id") == "id0"
+ assert page.eval_on_selector("div:below(#id2)", "e => e.id") == "id4"
+ assert page.eval_on_selector("div:below(#id6)", "e => e.id") == "id8"
+ assert page.eval_on_selector("div:below(#id7)", "e => e.id") == "id8"
+ assert page.eval_on_selector("div:below(#id8)", "e => e.id") == "id9"
+ assert page.query_selector("div:below(#id9)") is None
+ assert (
+ page.eval_on_selector_all(
+ "div:below(#id3)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id0,id5,id6,id7,id8,id9"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:below(#id3, 105)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id0,id5,id6,id7"
+ )
+
+ assert page.eval_on_selector("div:near(#id0)", "e => e.id") == "id3"
+ assert (
+ page.eval_on_selector_all(
+ "div:near(#id7)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id0,id5,id3,id6"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:near(#id0)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id3,id6,id7,id8,id1,id5"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:near(#id6)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id0,id3,id7"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:near(#id6, 10)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id0"
+ )
+ assert (
+ page.eval_on_selector_all(
+ "div:near(#id0, 100)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id3,id6,id7,id8,id1,id5,id4,id2"
+ )
+
+ assert (
+ page.eval_on_selector_all(
+ "div:below(#id5):above(#id8)", "els => els.map(e => e.id).join(',')"
+ )
+ == "id7,id6"
+ )
+ assert page.eval_on_selector("div:below(#id5):above(#id8)", "e => e.id") == "id7"
+
+ assert (
+ page.eval_on_selector_all(
+ "div:right-of(#id0) + div:above(#id8)",
+ "els => els.map(e => e.id).join(',')",
+ )
+ == "id5,id6,id3"
+ )
+
+ with pytest.raises(Error) as exc_info:
+ page.query_selector(":near(50)")
+ assert (
+ '"near" engine expects a selector list and optional maximum distance in pixels'
+ in exc_info.value.message
+ )
+ with pytest.raises(Error) as exc_info:
+ page.query_selector('left-of="div"')
+ assert '"left-of" selector cannot be first' in exc_info.value.message