Skip to content

fix: sync selectors #1325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion playwright/_impl/_playwright.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
3 changes: 2 additions & 1 deletion playwright/_impl/_selectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions tests/sync/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
BrowserType,
Page,
Playwright,
Selectors,
sync_playwright,
)

Expand Down Expand Up @@ -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
360 changes: 360 additions & 0 deletions tests/sync/test_queryselector.py
Original file line number Diff line number Diff line change
@@ -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("<div><span></span></div><div></div>")

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("<section></section>")
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("<div><span><section></section></span></div>")
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(
'<container style="width: 500px; height: 500px; position: relative;"></container>'
)
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