Skip to content
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: 2 additions & 0 deletions playwright/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
firefox = playwright_object.firefox
webkit = playwright_object.webkit
devices = playwright_object.devices
selectors = playwright_object.selectors
browser_types = playwright_object.browser_types
Error = helper.Error
TimeoutError = helper.TimeoutError
Expand All @@ -30,6 +31,7 @@
"firefox",
"webkit",
"devices",
"selectors",
"Error",
"TimeoutError",
]
8 changes: 4 additions & 4 deletions playwright/js_handle.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ async def getProperty(self, name: str) -> "JSHandle":
return from_channel(await self._channel.send("getProperty", dict(name=name)))

async def getProperties(self) -> Dict[str, "JSHandle"]:
map = dict()
for property in await self._channel.send("getPropertyList"):
map[property["name"]] = from_channel(property["value"])
return map
return {
prop["name"]: from_channel(prop["value"])
for prop in await self._channel.send("getPropertyList")
}

def asElement(self) -> Optional["ElementHandle"]:
return None
Expand Down
3 changes: 3 additions & 0 deletions playwright/object_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from playwright.network import Request, Response, Route
from playwright.page import BindingCall, Page
from playwright.playwright import Playwright
from playwright.selectors import Selectors
from playwright.worker import Worker


Expand Down Expand Up @@ -73,4 +74,6 @@ def create_remote_object(
return Route(scope, guid, initializer)
if type == "worker":
return Worker(scope, guid, initializer)
if type == "selectors":
return Selectors(scope, guid, initializer)
return DummyObject(scope, guid, initializer)
9 changes: 6 additions & 3 deletions playwright/playwright.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from playwright.browser_type import BrowserType
from playwright.connection import ChannelOwner, ConnectionScope, from_channel
from playwright.selectors import Selectors


class Playwright(ChannelOwner):
Expand All @@ -24,9 +25,11 @@ def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None
self.chromium: BrowserType = from_channel(initializer["chromium"])
self.firefox: BrowserType = from_channel(initializer["firefox"])
self.webkit: BrowserType = from_channel(initializer["webkit"])
self.devices = dict()
for device in initializer["deviceDescriptors"]:
self.devices[device["name"]] = device["descriptor"]
self.selectors: Selectors = from_channel(initializer["selectors"])
self.devices = {
device["name"]: device["descriptor"]
for device in initializer["deviceDescriptors"]
}
self.browser_types: Dict[str, BrowserType] = dict(
chromium=self.chromium, webkit=self.webkit, firefox=self.firefox
)
39 changes: 39 additions & 0 deletions playwright/selectors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Dict, Optional

from playwright.connection import ChannelOwner, ConnectionScope
from playwright.element_handle import ElementHandle


class Selectors(ChannelOwner):
def __init__(self, scope: ConnectionScope, guid: str, initializer: Dict) -> None:
super().__init__(scope, guid, initializer)

async def register(
self, name: str, source: str = "", path: str = None, contentScript: bool = False
) -> None:
if path:
with open(path, "r") as file:
source = file.read()
await self._channel.send(
"register",
dict(name=name, source=source, options={"contentScript": contentScript}),
)

async def _createSelector(self, name: str, handle: ElementHandle) -> Optional[str]:
return await self._channel.send(
"createSelector", dict(name=name, handle=handle._channel)
)
10 changes: 10 additions & 0 deletions tests/assets/sectionselectorengine.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
({
create(root, target) {
},
query(root, selector) {
return root.querySelector('section');
},
queryAll(root, selector) {
return Array.from(root.querySelectorAll('section'));
}
})
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ def event_loop():
loop.close()


@pytest.fixture(scope="session")
def selectors():
return playwright.selectors


@pytest.fixture(scope="session")
def browser_type(browser_name: str):
return playwright.browser_types[browser_name]
Expand Down
154 changes: 154 additions & 0 deletions tests/test_queryselector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import os

import pytest

from playwright.helper import Error
from playwright.page import Page


async def test_selectors_register_should_work(selectors, page: Page, utils):
await utils.register_selector_engine(
selectors,
"tag",
"""{
create(root, target) {
return target.nodeName;
},
query(root, selector) {
return root.querySelector(selector);
},
queryAll(root, selector) {
return Array.from(root.querySelectorAll(selector));
}
}""",
)
await page.setContent("<div><span></span></div><div></div>")
assert (
await selectors._createSelector("tag", await page.querySelector("div")) == "DIV"
)
assert await page.evalOnSelector("tag=DIV", "e => e.nodeName") == "DIV"
assert await page.evalOnSelector("tag=SPAN", "e => e.nodeName") == "SPAN"
assert await page.evalOnSelectorAll("tag=DIV", "es => es.length") == 2

# Selector names are case-sensitive.
with pytest.raises(Error) as exc:
await page.querySelector("tAG=DIV")
assert 'Unknown engine "tAG" while parsing selector tAG=DIV' in exc.value.message


async def test_selectors_register_should_work_with_path(selectors, page: Page, utils):
await utils.register_selector_engine(
selectors,
"foo",
path=os.path.join(
os.path.dirname(os.path.abspath(__file__)),
"assets/sectionselectorengine.js",
),
)
await page.setContent("<section></section>")
assert await page.evalOnSelector("foo=whatever", "e => e.nodeName") == "SECTION"


async def test_selectors_register_should_work_in_main_and_isolated_world(
selectors, page: Page, utils
):
dummy_selector_script = """{
create(root, target) { },
query(root, selector) {
return window.__answer;
},
queryAll(root, selector) {
return [document.body, document.documentElement, window.__answer];
}
}"""

await utils.register_selector_engine(selectors, "main", dummy_selector_script)
await utils.register_selector_engine(
selectors, "isolated", dummy_selector_script, contentScript=True
)
await page.setContent("<div><span><section></section></span></div>")
await page.evaluate('() => window.__answer = document.querySelector("span")')
# Works in main if asked.
assert await page.evalOnSelector("main=ignored", "e => e.nodeName") == "SPAN"
assert (
await page.evalOnSelector("css=div >> main=ignored", "e => e.nodeName")
== "SPAN"
)
assert await page.evalOnSelectorAll(
"main=ignored", "es => window.__answer !== undefined"
)
assert (
await page.evalOnSelectorAll("main=ignored", "es => es.filter(e => e).length")
== 3
)
# Works in isolated by default.
assert await page.querySelector("isolated=ignored") is None
assert await page.querySelector("css=div >> isolated=ignored") is None
# $$eval always works in main, to avoid adopting nodes one by one.
assert await page.evalOnSelectorAll(
"isolated=ignored", "es => window.__answer !== undefined"
)
assert (
await page.evalOnSelectorAll(
"isolated=ignored", "es => es.filter(e => e).length"
)
== 3
)
# At least one engine in main forces all to be in main.
assert (
await page.evalOnSelector("main=ignored >> isolated=ignored", "e => e.nodeName")
== "SPAN"
)
assert (
await page.evalOnSelector("isolated=ignored >> main=ignored", "e => e.nodeName")
== "SPAN"
)
# Can be chained to css.
assert (
await page.evalOnSelector("main=ignored >> css=section", "e => e.nodeName")
== "SECTION"
)


async def test_selectors_register_should_handle_errors(selectors, page: Page, utils):
with pytest.raises(Error) as exc:
await page.querySelector("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.querySelectorAll('dummy'));
}
}"""

with pytest.raises(Error) as exc:
await selectors.register("$", dummy_selector_engine_script)
assert (
"Selector engine name may only contain [a-zA-Z0-9_] characters"
== exc.value.message
)

# Selector names are case-sensitive.
await utils.register_selector_engine(
selectors, "dummy", dummy_selector_engine_script
)
await utils.register_selector_engine(
selectors, "duMMy", dummy_selector_engine_script
)

with pytest.raises(Error) as exc:
await selectors.register("dummy", dummy_selector_engine_script)
assert exc.value.message == '"dummy" selector engine has been already registered'

with pytest.raises(Error) as exc:
await selectors.register("css", dummy_selector_engine_script)
assert exc.value.message == '"css" is a predefined selector engine'
12 changes: 11 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@

from playwright.element_handle import ElementHandle
from playwright.frame import Frame
from playwright.helper import Viewport
from playwright.helper import Error, Viewport
from playwright.page import Page
from playwright.selectors import Selectors


class Utils:
Expand Down Expand Up @@ -60,5 +61,14 @@ async def verify_viewport(self, page: Page, width: int, height: int):
assert await page.evaluate("window.innerWidth") == width
assert await page.evaluate("window.innerHeight") == height

async def register_selector_engine(
self, selectors: Selectors, *args, **kwargs
) -> None:
try:
await selectors.register(*args, **kwargs)
except Error as exc:
if "has been already registered" not in exc.message:
raise exc


utils = Utils()