From 293a6ad512a6c37e26e054bd5c84cf89345783bf Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 26 May 2022 02:02:43 +0200 Subject: [PATCH] fix: sync selectors --- playwright/_impl/_playwright.py | 2 +- playwright/_impl/_selectors.py | 3 +- tests/sync/conftest.py | 6 + tests/sync/test_queryselector.py | 360 +++++++++++++++++++++++++++++++ 4 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 tests/sync/test_queryselector.py 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