Skip to content
Open
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
5 changes: 3 additions & 2 deletions public/docs/deep-dive/tab-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ classDiagram
-_cloudflare_captcha_callback_id: Optional[int]
+go_to(url: str, timeout: int)
+refresh()
+execute_script(script: str, element: WebElement)
+execute_script(script: str)
+execute_element_script(script: str, element: WebElement, **kwargs)
+find(**kwargs) WebElement|List[WebElement]
+query(expression: str) WebElement|List[WebElement]
+take_screenshot(path: str)
Expand Down Expand Up @@ -257,7 +258,7 @@ print(f"Window dimensions: {dimensions}")
heading = await tab.find(tag_name="h1")

# Execute JavaScript with the element as context
await tab.execute_script("""
await tab.execute_element_script("""
// 'argument' refers to the element
argument.style.color = 'red';
argument.style.fontSize = '32px';
Expand Down
5 changes: 3 additions & 2 deletions public/docs/zh/deep-dive/tab-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ classDiagram
-_cloudflare_captcha_callback_id: Optional[int]
+go_to(url: str, timeout: int)
+refresh()
+execute_script(script: str, element: WebElement)
+execute_script(script: str)
+execute_element_script(script: str, element: WebElement, **kwargs)
+find(**kwargs) WebElement|List[WebElement]
+query(expression: str) WebElement|List[WebElement]
+take_screenshot(path: str)
Expand Down Expand Up @@ -255,7 +256,7 @@ print(f"Window dimensions: {dimensions}")
heading = await tab.find(tag_name="h1")

# Execute JavaScript with the element as context
await tab.execute_script("""
await tab.execute_element_script("""
// 'argument' refers to the element
argument.style.color = 'red';
argument.style.fontSize = '32px';
Expand Down
133 changes: 69 additions & 64 deletions pydoll/browser/tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
IFrameNotFound,
InvalidFileExtension,
InvalidIFrame,
InvalidScriptWithElement,
InvalidTabInitialization,
MissingScreenshotPath,
NetworkEventsNotEnabled,
Expand Down Expand Up @@ -67,12 +66,14 @@
from pydoll.protocol.page.events import FileChooserOpenedEvent, PageEvent
from pydoll.protocol.page.methods import CaptureScreenshotResponse, PrintToPDFResponse
from pydoll.protocol.page.types import ScreenshotFormat
from pydoll.protocol.runtime.methods import CallFunctionOnResponse, EvaluateResponse
from pydoll.protocol.runtime.methods import (
EvaluateResponse,
SerializationOptions,
)
from pydoll.protocol.storage.methods import GetCookiesResponse
from pydoll.utils import (
decode_base64_to_bytes,
has_return_outside_function,
is_script_already_function,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -622,36 +623,80 @@ async def handle_dialog(self, accept: bool, prompt_text: Optional[str] = None):
PageCommands.handle_javascript_dialog(accept=accept, prompt_text=prompt_text)
)

@overload
async def execute_script(self, script: str) -> EvaluateResponse: ...

@overload
async def execute_script(self, script: str, element: WebElement) -> CallFunctionOnResponse: ...

async def execute_script(
self, script: str, element: Optional[WebElement] = None
) -> Union[EvaluateResponse, CallFunctionOnResponse]:
self,
script: str,
*,
object_group: Optional[str] = None,
include_command_line_api: Optional[bool] = None,
silent: Optional[bool] = None,
context_id: Optional[int] = None,
return_by_value: Optional[bool] = None,
generate_preview: Optional[bool] = None,
user_gesture: Optional[bool] = None,
await_promise: Optional[bool] = None,
throw_on_side_effect: Optional[bool] = None,
timeout: Optional[float] = None,
disable_breaks: Optional[bool] = None,
repl_mode: Optional[bool] = None,
allow_unsafe_eval_blocked_by_csp: Optional[bool] = None,
unique_context_id: Optional[str] = None,
serialization_options: Optional[SerializationOptions] = None,
) -> EvaluateResponse:
"""
Execute JavaScript in page context.

Args:
script: JavaScript code to execute.
element: Element context (use 'argument' in script to reference).
object_group: Symbolic group name for the result (Runtime.evaluate).
include_command_line_api: Whether to include command line API (Runtime.evaluate).
silent: Whether to silence exceptions (Runtime.evaluate).
context_id: ID of the execution context to evaluate in (Runtime.evaluate).
return_by_value: Whether to return the result by value instead of reference
(Runtime.evaluate).
generate_preview: Whether to generate a preview for the result
(Runtime.evaluate).
user_gesture: Whether to treat evaluation as initiated by user gesture
(Runtime.evaluate).
await_promise: Whether to await promise result (Runtime.evaluate).
throw_on_side_effect: Whether to throw if side effect cannot be ruled out
(Runtime.evaluate).
timeout: Timeout in milliseconds (Runtime.evaluate).
disable_breaks: Whether to disable breakpoints during evaluation (Runtime.evaluate).
repl_mode: Whether to execute in REPL mode (Runtime.evaluate).
allow_unsafe_eval_blocked_by_csp: Allow unsafe evaluation (Runtime.evaluate).
unique_context_id: Unique context ID for evaluation (Runtime.evaluate).
serialization_options: Serialization for the result (Runtime.evaluate).

Examples:
await page.execute_script('argument.click()', element)
await page.execute_script('argument.value = "Hello"', element)
Returns:
The result of the script execution.

Raises:
InvalidScriptWithElement: If script contains 'argument' but no element is provided.
Examples:
await page.execute_script('console.log("Hello World")')
await page.execute_script('return document.title')
"""
if 'argument' in script and element is None:
raise InvalidScriptWithElement('Script contains "argument" but no element was provided')

if element:
return await self._execute_script_with_element(script, element)
if has_return_outside_function(script):
script = f'(function(){{ {script} }})()'

return await self._execute_script_without_element(script)
command = RuntimeCommands.evaluate(
expression=script,
object_group=object_group,
include_command_line_api=include_command_line_api,
silent=silent,
context_id=context_id,
return_by_value=return_by_value,
generate_preview=generate_preview,
user_gesture=user_gesture,
await_promise=await_promise,
throw_on_side_effect=throw_on_side_effect,
timeout=timeout,
disable_breaks=disable_breaks,
repl_mode=repl_mode,
allow_unsafe_eval_blocked_by_csp=allow_unsafe_eval_blocked_by_csp,
unique_context_id=unique_context_id,
serialization_options=serialization_options,
)
return await self._execute_command(command)

# TODO: think about how to remove these duplications with the base class
async def continue_request(
Expand Down Expand Up @@ -954,46 +999,6 @@ def _get_connection_handler(self) -> ConnectionHandler:
return ConnectionHandler(ws_address=self._ws_address)
return ConnectionHandler(self._connection_port, self._target_id)

async def _execute_script_with_element(self, script: str, element: WebElement):
"""
Execute script with element context.

Args:
script: JavaScript code to execute.
element: Element context (use 'argument' in script to reference).

Returns:
The result of the script execution.
"""
if 'argument' not in script:
raise InvalidScriptWithElement('Script does not contain "argument"')

script = script.replace('argument', 'this')

if not is_script_already_function(script):
script = f'function(){{ {script} }}'

command = RuntimeCommands.call_function_on(
object_id=element._object_id, function_declaration=script, return_by_value=True
)
return await self._execute_command(command)

async def _execute_script_without_element(self, script: str):
"""
Execute script without element context.

Args:
script: JavaScript code to execute.

Returns:
The result of the script execution.
"""
if has_return_outside_function(script):
script = f'(function(){{ {script} }})()'

command = RuntimeCommands.evaluate(expression=script)
return await self._execute_command(command)

async def _refresh_if_url_not_changed(self, url: str) -> bool:
"""Refresh page if URL hasn't changed."""
current_url = await self.current_url
Expand Down Expand Up @@ -1036,7 +1041,7 @@ async def _bypass_cloudflare(
element = cast(WebElement, element)
if element:
# adjust the external div size to shadow root width (usually 300px)
await self.execute_script('argument.style="width: 300px"', element)
await element.execute_script('this.style="width: 300px"')
await asyncio.sleep(time_before_click)
await element.click()
except Exception as exc:
Expand Down
89 changes: 79 additions & 10 deletions pydoll/elements/web_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@
)
from pydoll.protocol.page.methods import CaptureScreenshotResponse
from pydoll.protocol.page.types import ScreenshotFormat, Viewport
from pydoll.protocol.runtime.methods import GetPropertiesResponse
from pydoll.protocol.runtime.methods import (
CallFunctionOnResponse,
GetPropertiesResponse,
SerializationOptions,
)
from pydoll.protocol.runtime.types import CallArgument
from pydoll.utils import (
decode_base64_to_bytes,
extract_text_from_html,
is_script_already_function,
)


Expand Down Expand Up @@ -473,19 +479,82 @@ async def is_interactable(self):
result = await self.execute_script(Scripts.ELEMENT_INTERACTIVE, return_by_value=True)
return result['result']['result']['value']

async def execute_script(self, script: str, return_by_value: bool = False):
async def execute_script(
self,
script: str,
*,
arguments: Optional[list[CallArgument]] = None,
silent: Optional[bool] = None,
return_by_value: Optional[bool] = None,
generate_preview: Optional[bool] = None,
user_gesture: Optional[bool] = None,
await_promise: Optional[bool] = None,
execution_context_id: Optional[int] = None,
object_group: Optional[str] = None,
throw_on_side_effect: Optional[bool] = None,
unique_context_id: Optional[str] = None,
serialization_options: Optional[SerializationOptions] = None,
) -> CallFunctionOnResponse:
"""
Execute JavaScript in element context.

Element is available as 'this' within the script.
"""
return await self._execute_command(
RuntimeCommands.call_function_on(
object_id=self._object_id,
function_declaration=script,
return_by_value=return_by_value,
)

Args:
script: JavaScript code to execute. Use 'this' to reference this element.
arguments: Arguments to pass to the function (Runtime.callFunctionOn).
silent: Whether to silence exceptions (Runtime.callFunctionOn).
return_by_value: Whether to return the result by value instead of reference
(Runtime.callFunctionOn).
generate_preview: Whether to generate a preview for the result
(Runtime.callFunctionOn).
user_gesture: Whether to treat the call as initiated by user gesture
(Runtime.callFunctionOn).
await_promise: Whether to await promise result (Runtime.callFunctionOn).
execution_context_id: ID of the execution context to call the function in
(Runtime.callFunctionOn).
object_group: Symbolic group name for the result (Runtime.callFunctionOn).
throw_on_side_effect: Whether to throw if side effect cannot be ruled out
(Runtime.callFunctionOn).
unique_context_id: Unique context ID for the function call
(Runtime.callFunctionOn).
serialization_options: Serialization options for the result
(Runtime.callFunctionOn).

Returns:
The result of the script execution.

Examples:
# Click the element
await element.execute_script('this.click()')

# Modify element style
await element.execute_script('this.style.border = "2px solid red"')

# Get element text
result = await element.execute_script('return this.textContent', return_by_value=True)

# Set element content
await element.execute_script('this.textContent = "Hello World"')
"""
if not is_script_already_function(script):
script = f'function(){{ {script} }}'

command = RuntimeCommands.call_function_on(
function_declaration=script,
object_id=self._object_id,
arguments=arguments,
silent=silent,
return_by_value=return_by_value,
generate_preview=generate_preview,
user_gesture=user_gesture,
await_promise=await_promise,
execution_context_id=execution_context_id,
object_group=object_group,
throw_on_side_effect=throw_on_side_effect,
unique_context_id=unique_context_id,
serialization_options=serialization_options,
)
return await self._execute_command(command)

async def _get_family_elements(
self, script: str, max_depth: int = 1, tag_filter: list[str] = []
Expand Down
6 changes: 0 additions & 6 deletions pydoll/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,6 @@ class ScriptException(PydollException):
message = 'A script execution error occurred'


class InvalidScriptWithElement(ScriptException):
"""Raised when a script contains 'argument' but no element is provided."""

message = 'Script contains "argument" but no element was provided'


class WrongPrefsDict(PydollException):
"""Raised when the prefs dict provided contains the 'prefs' key"""

Expand Down
Loading
Loading