# 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.
import json
import pathlib
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
Dict,
List,
Literal,
Optional,
Pattern,
Sequence,
Tuple,
TypeVar,
Union,
)
from playwright._impl._api_structures import (
AriaRole,
FilePayload,
FloatRect,
FrameExpectOptions,
FrameExpectResult,
Position,
)
from playwright._impl._element_handle import ElementHandle
from playwright._impl._helper import (
Error,
KeyboardModifier,
MouseButton,
locals_to_params,
monotonic_time,
to_impl,
)
from playwright._impl._js_handle import Serializable, parse_value, serialize_argument
from playwright._impl._str_utils import (
escape_for_attribute_selector,
escape_for_text_selector,
)
if TYPE_CHECKING: # pragma: no cover
from playwright._impl._frame import Frame
from playwright._impl._js_handle import JSHandle
from playwright._impl._page import Page
T = TypeVar("T")
class Locator:
def __init__(
self,
frame: "Frame",
selector: str,
has_text: Union[str, Pattern[str]] = None,
has_not_text: Union[str, Pattern[str]] = None,
has: "Locator" = None,
has_not: "Locator" = None,
) -> None:
self._frame = frame
self._selector = selector
self._loop = frame._loop
self._dispatcher_fiber = frame._connection._dispatcher_fiber
if has_text:
self._selector += f" >> internal:has-text={escape_for_text_selector(has_text, exact=False)}"
if has:
if has._frame != frame:
raise Error('Inner "has" locator must belong to the same frame.')
self._selector += " >> internal:has=" + json.dumps(
has._selector, ensure_ascii=False
)
if has_not_text:
self._selector += f" >> internal:has-not-text={escape_for_text_selector(has_not_text, exact=False)}"
if has_not:
locator = has_not
if locator._frame != frame:
raise Error('Inner "has_not" locator must belong to the same frame.')
self._selector += " >> internal:has-not=" + json.dumps(locator._selector)
def __repr__(self) -> str:
return f"<Locator frame={self._frame!r} selector={self._selector!r}>"
async def _with_element(
self,
task: Callable[[ElementHandle, float], Awaitable[T]],
timeout: float = None,
) -> T:
timeout = self._frame.page._timeout_settings.timeout(timeout)
deadline = (monotonic_time() + timeout) if timeout else 0
handle = await self.element_handle(timeout=timeout)
if not handle:
raise Error(f"Could not resolve {self._selector} to DOM Element")
try:
return await task(
handle,
(deadline - monotonic_time()) if deadline else 0,
)
finally:
await handle.dispose()
def _equals(self, locator: "Locator") -> bool:
return self._frame == locator._frame and self._selector == locator._selector
@property
def page(self) -> "Page":
return self._frame.page
async def bounding_box(self, timeout: float = None) -> Optional[FloatRect]:
return await self._with_element(
lambda h, _: h.bounding_box(),
timeout,
)
async def check(
self,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.check(self._selector, strict=True, **params)
async def click(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
delay: float = None,
button: MouseButton = None,
clickCount: int = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.click(self._selector, strict=True, **params)
async def dblclick(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
delay: float = None,
button: MouseButton = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.dblclick(self._selector, strict=True, **params)
async def dispatch_event(
self,
type: str,
eventInit: Dict = None,
timeout: float = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.dispatch_event(self._selector, strict=True, **params)
async def evaluate(
self, expression: str, arg: Serializable = None, timeout: float = None
) -> Any:
return await self._with_element(
lambda h, _: h.evaluate(expression, arg),
timeout,
)
async def evaluate_all(self, expression: str, arg: Serializable = None) -> Any:
params = locals_to_params(locals())
return await self._frame.eval_on_selector_all(self._selector, **params)
async def evaluate_handle(
self, expression: str, arg: Serializable = None, timeout: float = None
) -> "JSHandle":
return await self._with_element(
lambda h, _: h.evaluate_handle(expression, arg), timeout
)
async def fill(
self,
value: str,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.fill(self._selector, strict=True, **params)
async def clear(
self,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
) -> None:
await self.fill("", timeout=timeout, force=force)
def locator(
self,
selectorOrLocator: Union[str, "Locator"],
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: "Locator" = None,
hasNot: "Locator" = None,
) -> "Locator":
if isinstance(selectorOrLocator, str):
return Locator(
self._frame,
f"{self._selector} >> {selectorOrLocator}",
has_text=hasText,
has_not_text=hasNotText,
has_not=hasNot,
has=has,
)
selectorOrLocator = to_impl(selectorOrLocator)
if selectorOrLocator._frame != self._frame:
raise Error("Locators must belong to the same frame.")
return Locator(
self._frame,
f"{self._selector} >> internal:chain={json.dumps(selectorOrLocator._selector)}",
has_text=hasText,
has_not_text=hasNotText,
has_not=hasNot,
has=has,
)
def get_by_alt_text(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_alt_text_selector(text, exact=exact))
def get_by_label(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_label_selector(text, exact=exact))
def get_by_placeholder(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_placeholder_selector(text, exact=exact))
def get_by_role(
self,
role: AriaRole,
checked: bool = None,
disabled: bool = None,
expanded: bool = None,
includeHidden: bool = None,
level: int = None,
name: Union[str, Pattern[str]] = None,
pressed: bool = None,
selected: bool = None,
exact: bool = None,
) -> "Locator":
return self.locator(
get_by_role_selector(
role,
checked=checked,
disabled=disabled,
expanded=expanded,
includeHidden=includeHidden,
level=level,
name=name,
pressed=pressed,
selected=selected,
exact=exact,
)
)
def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator":
return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId))
def get_by_text(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_text_selector(text, exact=exact))
def get_by_title(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_title_selector(text, exact=exact))
def frame_locator(self, selector: str) -> "FrameLocator":
return FrameLocator(self._frame, self._selector + " >> " + selector)
async def element_handle(
self,
timeout: float = None,
) -> ElementHandle:
params = locals_to_params(locals())
handle = await self._frame.wait_for_selector(
self._selector, strict=True, state="attached", **params
)
assert handle
return handle
async def element_handles(self) -> List[ElementHandle]:
return await self._frame.query_selector_all(self._selector)
@property
def first(self) -> "Locator":
return Locator(self._frame, f"{self._selector} >> nth=0")
@property
def last(self) -> "Locator":
return Locator(self._frame, f"{self._selector} >> nth=-1")
def nth(self, index: int) -> "Locator":
return Locator(self._frame, f"{self._selector} >> nth={index}")
@property
def content_frame(self) -> "FrameLocator":
return FrameLocator(self._frame, self._selector)
def filter(
self,
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: "Locator" = None,
hasNot: "Locator" = None,
) -> "Locator":
return Locator(
self._frame,
self._selector,
has_text=hasText,
has_not_text=hasNotText,
has=has,
has_not=hasNot,
)
def or_(self, locator: "Locator") -> "Locator":
if locator._frame != self._frame:
raise Error("Locators must belong to the same frame.")
return Locator(
self._frame,
self._selector + " >> internal:or=" + json.dumps(locator._selector),
)
def and_(self, locator: "Locator") -> "Locator":
if locator._frame != self._frame:
raise Error("Locators must belong to the same frame.")
return Locator(
self._frame,
self._selector + " >> internal:and=" + json.dumps(locator._selector),
)
async def focus(self, timeout: float = None) -> None:
params = locals_to_params(locals())
return await self._frame.focus(self._selector, strict=True, **params)
async def blur(self, timeout: float = None) -> None:
await self._frame._channel.send(
"blur",
{
"selector": self._selector,
"strict": True,
**locals_to_params(locals()),
},
)
async def all(
self,
) -> List["Locator"]:
result = []
for index in range(await self.count()):
result.append(self.nth(index))
return result
async def count(
self,
) -> int:
return await self._frame._query_count(self._selector)
async def drag_to(
self,
target: "Locator",
force: bool = None,
noWaitAfter: bool = None,
timeout: float = None,
trial: bool = None,
sourcePosition: Position = None,
targetPosition: Position = None,
) -> None:
params = locals_to_params(locals())
del params["target"]
return await self._frame.drag_and_drop(
self._selector, target._selector, strict=True, **params
)
async def get_attribute(self, name: str, timeout: float = None) -> Optional[str]:
params = locals_to_params(locals())
return await self._frame.get_attribute(
self._selector,
strict=True,
**params,
)
async def hover(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.hover(
self._selector,
strict=True,
**params,
)
async def inner_html(self, timeout: float = None) -> str:
params = locals_to_params(locals())
return await self._frame.inner_html(
self._selector,
strict=True,
**params,
)
async def inner_text(self, timeout: float = None) -> str:
params = locals_to_params(locals())
return await self._frame.inner_text(
self._selector,
strict=True,
**params,
)
async def input_value(self, timeout: float = None) -> str:
params = locals_to_params(locals())
return await self._frame.input_value(
self._selector,
strict=True,
**params,
)
async def is_checked(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_checked(
self._selector,
strict=True,
**params,
)
async def is_disabled(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_disabled(
self._selector,
strict=True,
**params,
)
async def is_editable(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_editable(
self._selector,
strict=True,
**params,
)
async def is_enabled(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_editable(
self._selector,
strict=True,
**params,
)
async def is_hidden(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_hidden(
self._selector,
strict=True,
**params,
)
async def is_visible(self, timeout: float = None) -> bool:
params = locals_to_params(locals())
return await self._frame.is_visible(
self._selector,
strict=True,
**params,
)
async def press(
self,
key: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.press(self._selector, strict=True, **params)
async def screenshot(
self,
timeout: float = None,
type: Literal["jpeg", "png"] = None,
path: Union[str, pathlib.Path] = None,
quality: int = None,
omitBackground: bool = None,
animations: Literal["allow", "disabled"] = None,
caret: Literal["hide", "initial"] = None,
scale: Literal["css", "device"] = None,
mask: Sequence["Locator"] = None,
maskColor: str = None,
style: str = None,
) -> bytes:
params = locals_to_params(locals())
return await self._with_element(
lambda h, timeout: h.screenshot(
**{**params, "timeout": timeout},
),
)
async def aria_snapshot(self, timeout: float = None) -> str:
return await self._frame._channel.send(
"ariaSnapshot",
{
"selector": self._selector,
**locals_to_params(locals()),
},
)
async def scroll_into_view_if_needed(
self,
timeout: float = None,
) -> None:
return await self._with_element(
lambda h, timeout: h.scroll_into_view_if_needed(timeout=timeout),
timeout,
)
async def select_option(
self,
value: Union[str, Sequence[str]] = None,
index: Union[int, Sequence[int]] = None,
label: Union[str, Sequence[str]] = None,
element: Union["ElementHandle", Sequence["ElementHandle"]] = None,
timeout: float = None,
noWaitAfter: bool = None,
force: bool = None,
) -> List[str]:
params = locals_to_params(locals())
return await self._frame.select_option(
self._selector,
strict=True,
**params,
)
async def select_text(self, force: bool = None, timeout: float = None) -> None:
params = locals_to_params(locals())
return await self._with_element(
lambda h, timeout: h.select_text(**{**params, "timeout": timeout}),
timeout,
)
async def set_input_files(
self,
files: Union[
str,
pathlib.Path,
FilePayload,
Sequence[Union[str, pathlib.Path]],
Sequence[FilePayload],
],
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.set_input_files(
self._selector,
strict=True,
**params,
)
async def tap(
self,
modifiers: Sequence[KeyboardModifier] = None,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.tap(
self._selector,
strict=True,
**params,
)
async def text_content(self, timeout: float = None) -> Optional[str]:
params = locals_to_params(locals())
return await self._frame.text_content(
self._selector,
strict=True,
**params,
)
async def type(
self,
text: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.type(
self._selector,
strict=True,
**params,
)
async def press_sequentially(
self,
text: str,
delay: float = None,
timeout: float = None,
noWaitAfter: bool = None,
) -> None:
await self.type(text, delay=delay, timeout=timeout)
async def uncheck(
self,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
params = locals_to_params(locals())
return await self._frame.uncheck(
self._selector,
strict=True,
**params,
)
async def all_inner_texts(
self,
) -> List[str]:
return await self._frame.eval_on_selector_all(
self._selector, "ee => ee.map(e => e.innerText)"
)
async def all_text_contents(
self,
) -> List[str]:
return await self._frame.eval_on_selector_all(
self._selector, "ee => ee.map(e => e.textContent || '')"
)
async def wait_for(
self,
timeout: float = None,
state: Literal["attached", "detached", "hidden", "visible"] = None,
) -> None:
await self._frame.wait_for_selector(
self._selector, strict=True, timeout=timeout, state=state
)
async def set_checked(
self,
checked: bool,
position: Position = None,
timeout: float = None,
force: bool = None,
noWaitAfter: bool = None,
trial: bool = None,
) -> None:
if checked:
await self.check(
position=position,
timeout=timeout,
force=force,
trial=trial,
)
else:
await self.uncheck(
position=position,
timeout=timeout,
force=force,
trial=trial,
)
async def _expect(
self, expression: str, options: FrameExpectOptions
) -> FrameExpectResult:
if "expectedValue" in options:
options["expectedValue"] = serialize_argument(options["expectedValue"])
result = await self._frame._channel.send_return_as_dict(
"expect",
{
"selector": self._selector,
"expression": expression,
**options,
},
)
if result.get("received"):
result["received"] = parse_value(result["received"])
return result
async def highlight(self) -> None:
await self._frame._highlight(self._selector)
class FrameLocator:
def __init__(self, frame: "Frame", frame_selector: str) -> None:
self._frame = frame
self._loop = frame._loop
self._dispatcher_fiber = frame._connection._dispatcher_fiber
self._frame_selector = frame_selector
def locator(
self,
selectorOrLocator: Union["Locator", str],
hasText: Union[str, Pattern[str]] = None,
hasNotText: Union[str, Pattern[str]] = None,
has: Locator = None,
hasNot: Locator = None,
) -> Locator:
if isinstance(selectorOrLocator, str):
return Locator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator}",
has_text=hasText,
has_not_text=hasNotText,
has=has,
has_not=hasNot,
)
selectorOrLocator = to_impl(selectorOrLocator)
if selectorOrLocator._frame != self._frame:
raise ValueError("Locators must belong to the same frame.")
return Locator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selectorOrLocator._selector}",
has_text=hasText,
has_not_text=hasNotText,
has=has,
has_not=hasNot,
)
def get_by_alt_text(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_alt_text_selector(text, exact=exact))
def get_by_label(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_label_selector(text, exact=exact))
def get_by_placeholder(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_placeholder_selector(text, exact=exact))
def get_by_role(
self,
role: AriaRole,
checked: bool = None,
disabled: bool = None,
expanded: bool = None,
includeHidden: bool = None,
level: int = None,
name: Union[str, Pattern[str]] = None,
pressed: bool = None,
selected: bool = None,
exact: bool = None,
) -> "Locator":
return self.locator(
get_by_role_selector(
role,
checked=checked,
disabled=disabled,
expanded=expanded,
includeHidden=includeHidden,
level=level,
name=name,
pressed=pressed,
selected=selected,
exact=exact,
)
)
def get_by_test_id(self, testId: Union[str, Pattern[str]]) -> "Locator":
return self.locator(get_by_test_id_selector(test_id_attribute_name(), testId))
def get_by_text(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_text_selector(text, exact=exact))
def get_by_title(
self, text: Union[str, Pattern[str]], exact: bool = None
) -> "Locator":
return self.locator(get_by_title_selector(text, exact=exact))
def frame_locator(self, selector: str) -> "FrameLocator":
return FrameLocator(
self._frame,
f"{self._frame_selector} >> internal:control=enter-frame >> {selector}",
)
@property
def first(self) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth=0")
@property
def last(self) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth=-1")
@property
def owner(self) -> "Locator":
return Locator(self._frame, self._frame_selector)
def nth(self, index: int) -> "FrameLocator":
return FrameLocator(self._frame, f"{self._frame_selector} >> nth={index}")
def __repr__(self) -> str:
return f"<FrameLocator frame={self._frame!r} selector={self._frame_selector!r}>"
_test_id_attribute_name: str = "data-testid"
def test_id_attribute_name() -> str:
return _test_id_attribute_name
def set_test_id_attribute_name(attribute_name: str) -> None:
global _test_id_attribute_name
_test_id_attribute_name = attribute_name
def get_by_test_id_selector(
test_id_attribute_name: str, test_id: Union[str, Pattern[str]]
) -> str:
return f"internal:testid=[{test_id_attribute_name}={escape_for_attribute_selector(test_id, True)}]"
def get_by_attribute_text_selector(
attr_name: str, text: Union[str, Pattern[str]], exact: bool = None
) -> str:
return f"internal:attr=[{attr_name}={escape_for_attribute_selector(text, exact=exact)}]"
def get_by_label_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return "internal:label=" + escape_for_text_selector(text, exact=exact)
def get_by_alt_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return get_by_attribute_text_selector("alt", text, exact=exact)
def get_by_title_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return get_by_attribute_text_selector("title", text, exact=exact)
def get_by_placeholder_selector(
text: Union[str, Pattern[str]], exact: bool = None
) -> str:
return get_by_attribute_text_selector("placeholder", text, exact=exact)
def get_by_text_selector(text: Union[str, Pattern[str]], exact: bool = None) -> str:
return "internal:text=" + escape_for_text_selector(text, exact=exact)
def bool_to_js_bool(value: bool) -> str:
return "true" if value else "false"
def get_by_role_selector(
role: AriaRole,
checked: bool = None,
disabled: bool = None,
expanded: bool = None,
includeHidden: bool = None,
level: int = None,
name: Union[str, Pattern[str]] = None,
pressed: bool = None,
selected: bool = None,
exact: bool = None,
) -> str:
props: List[Tuple[str, str]] = []
if checked is not None:
props.append(("checked", bool_to_js_bool(checked)))
if disabled is not None:
props.append(("disabled", bool_to_js_bool(disabled)))
if selected is not None:
props.append(("selected", bool_to_js_bool(selected)))
if expanded is not None:
props.append(("expanded", bool_to_js_bool(expanded)))
if includeHidden is not None:
props.append(("include-hidden", bool_to_js_bool(includeHidden)))
if level is not None:
props.append(("level", str(level)))
if name is not None:
props.append(
(
"name",
escape_for_attribute_selector(name, exact=exact),
)
)
if pressed is not None:
props.append(("pressed", bool_to_js_bool(pressed)))
props_str = "".join([f"[{t[0]}={t[1]}]" for t in props])
return f"internal:role={role}{props_str}"