diff --git a/public/docs/deep-dive/tab-domain.md b/public/docs/deep-dive/tab-domain.md index 1b304b38..027df68d 100644 --- a/public/docs/deep-dive/tab-domain.md +++ b/public/docs/deep-dive/tab-domain.md @@ -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) @@ -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'; diff --git a/public/docs/zh/deep-dive/tab-domain.md b/public/docs/zh/deep-dive/tab-domain.md index f552f78d..240ad0fd 100644 --- a/public/docs/zh/deep-dive/tab-domain.md +++ b/public/docs/zh/deep-dive/tab-domain.md @@ -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) @@ -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'; diff --git a/pydoll/browser/tab.py b/pydoll/browser/tab.py index aa18be2c..bfb8e69c 100644 --- a/pydoll/browser/tab.py +++ b/pydoll/browser/tab.py @@ -39,7 +39,6 @@ IFrameNotFound, InvalidFileExtension, InvalidIFrame, - InvalidScriptWithElement, InvalidTabInitialization, MissingScreenshotPath, NetworkEventsNotEnabled, @@ -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: @@ -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( @@ -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 @@ -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: diff --git a/pydoll/elements/web_element.py b/pydoll/elements/web_element.py index f0022c62..f0cb5a6b 100644 --- a/pydoll/elements/web_element.py +++ b/pydoll/elements/web_element.py @@ -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, ) @@ -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] = [] diff --git a/pydoll/exceptions.py b/pydoll/exceptions.py index 08e8e409..107a1b9c 100644 --- a/pydoll/exceptions.py +++ b/pydoll/exceptions.py @@ -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""" diff --git a/tests/test_browser/test_browser_tab.py b/tests/test_browser/test_browser_tab.py index 515e499a..3719d591 100644 --- a/tests/test_browser/test_browser_tab.py +++ b/tests/test_browser/test_browser_tab.py @@ -22,7 +22,6 @@ InvalidFileExtension, WaitElementTimeout, NetworkEventsNotEnabled, - InvalidScriptWithElement, ) @pytest_asyncio.fixture @@ -571,74 +570,6 @@ async def test_execute_script_simple(self, tab): assert_mock_called_at_least_once(tab._connection_handler) - @pytest.mark.asyncio - async def test_execute_script_with_element(self, tab): - """Test execute_script with element context.""" - # Mock element - element = MagicMock() - element._object_id = 'test-object-id' - - tab._connection_handler.execute_command.return_value = { - 'result': {'result': {'value': 'Element clicked'}} - } - - result = await tab.execute_script('argument.click()', element) - - assert_mock_called_at_least_once(tab._connection_handler) - - @pytest.mark.asyncio - async def test_execute_script_argument_without_element_raises_exception(self, tab): - """Test execute_script raises exception when script contains 'argument' but no element provided.""" - with pytest.raises(InvalidScriptWithElement) as exc_info: - await tab.execute_script('argument.click()') - - assert str(exc_info.value) == 'Script contains "argument" but no element was provided' - tab._connection_handler.execute_command.assert_not_called() - - @pytest.mark.asyncio - async def test_execute_script_element_without_argument_raises_exception(self, tab): - """Test execute_script raises exception when element is provided but script doesn't contain 'argument'.""" - element = MagicMock() - element._object_id = 'test-object-id' - - with pytest.raises(InvalidScriptWithElement) as exc_info: - await tab.execute_script('console.log("test")', element) - - assert str(exc_info.value) == 'Script does not contain "argument"' - tab._connection_handler.execute_command.assert_not_called() - - @pytest.mark.asyncio - async def test_execute_script_with_element_already_function(self, tab): - """Test execute_script with element when script is already a function.""" - element = MagicMock() - element._object_id = 'test-object-id' - - tab._connection_handler.execute_command.return_value = { - 'result': {'result': {'value': 'Function executed'}} - } - - # Script already wrapped in function - script = 'function() { argument.click(); return "done"; }' - result = await tab.execute_script(script, element) - - assert_mock_called_at_least_once(tab._connection_handler) - - @pytest.mark.asyncio - async def test_execute_script_with_element_arrow_function(self, tab): - """Test execute_script with element when script is already an arrow function.""" - element = MagicMock() - element._object_id = 'test-object-id' - - tab._connection_handler.execute_command.return_value = { - 'result': {'result': {'value': 'Arrow function executed'}} - } - - # Script already wrapped in arrow function - script = '() => { argument.click(); return "done"; }' - result = await tab.execute_script(script, element) - - assert_mock_called_at_least_once(tab._connection_handler) - @pytest.mark.asyncio async def test_execute_script_return_outside_function(self, tab): """Test execute_script wraps return statement outside function.""" @@ -1209,55 +1140,50 @@ async def test_expect_and_bypass_cloudflare_captcha(self, tab): async def test_bypass_cloudflare_with_element_found(self, tab): """Test _bypass_cloudflare when element is found.""" mock_element = AsyncMock() + mock_element.execute_script = AsyncMock() mock_find = AsyncMock(return_value=mock_element) - mock_execute_script = AsyncMock() with patch.object(tab, 'find_or_wait_element', mock_find): - with patch.object(tab, 'execute_script', mock_execute_script): - with patch('asyncio.sleep', AsyncMock()): - await tab._bypass_cloudflare({}) + with patch('asyncio.sleep', AsyncMock()): + await tab._bypass_cloudflare({}) mock_find.assert_called_once() - mock_execute_script.assert_called_once() + mock_element.execute_script.assert_called_once_with('this.style="width: 300px"') mock_element.click.assert_called_once() @pytest.mark.asyncio async def test_bypass_cloudflare_no_element_found(self, tab): """Test _bypass_cloudflare when no element is found.""" mock_find = AsyncMock(return_value=None) - mock_execute_script = AsyncMock() with patch.object(tab, 'find_or_wait_element', mock_find): - with patch.object(tab, 'execute_script', mock_execute_script): - await tab._bypass_cloudflare({}) + await tab._bypass_cloudflare({}) mock_find.assert_called_once() - # execute_script and click should not be called - mock_execute_script.assert_not_called() @pytest.mark.asyncio async def test_bypass_cloudflare_with_custom_selector(self, tab): """Test _bypass_cloudflare with custom selector.""" mock_element = AsyncMock() + mock_element.execute_script = AsyncMock() custom_selector = (By.ID, 'custom-captcha') mock_find = AsyncMock(return_value=mock_element) - mock_execute_script = AsyncMock() with patch.object(tab, 'find_or_wait_element', mock_find): - with patch.object(tab, 'execute_script', mock_execute_script): - with patch('asyncio.sleep', AsyncMock()): - await tab._bypass_cloudflare( - {}, - custom_selector=custom_selector, - time_before_click=3, - time_to_wait_captcha=10 - ) + with patch('asyncio.sleep', AsyncMock()): + await tab._bypass_cloudflare( + {}, + custom_selector=custom_selector, + time_before_click=3, + time_to_wait_captcha=10 + ) mock_find.assert_called_with( By.ID, 'custom-captcha', timeout=10, raise_exc=False ) + mock_element.execute_script.assert_called_once_with('this.style="width: 300px"') class TestTabFrameHandling: @@ -1646,17 +1572,6 @@ async def test_print_to_pdf_with_invalid_path(self, tab): assert result is not None assert_mock_called_at_least_once(tab._connection_handler) - @pytest.mark.asyncio - async def test_execute_script_with_none_element(self, tab): - """Test execute_script with None element.""" - tab._connection_handler.execute_command.return_value = { - 'result': {'result': {'value': 'Test Result'}} - } - - result = await tab.execute_script('return "Test Result"', None) - - assert_mock_called_at_least_once(tab._connection_handler) - @pytest.mark.asyncio async def test_network_logs_property(self, tab): """Test network_logs property access.""" diff --git a/tests/test_web_element.py b/tests/test_web_element.py index a8a9a27c..e8a74a33 100644 --- a/tests/test_web_element.py +++ b/tests/test_web_element.py @@ -844,8 +844,8 @@ def test_def_attributes_class_rename(self, mock_connection_handler): assert element._attributes == {'class_name': 'my-class', 'id': 'my-id'} @pytest.mark.asyncio - async def test_execute_script(self, web_element): - """Test _execute_script method.""" + async def test_execute_script_basic(self, web_element): + """Test execute_script basic functionality with return value.""" script = 'return this.tagName;' expected_response = {'result': {'result': {'value': 'DIV'}}} web_element._connection_handler.execute_command.return_value = expected_response @@ -855,13 +855,177 @@ async def test_execute_script(self, web_element): assert result == expected_response expected_command = RuntimeCommands.call_function_on( object_id='test-object-id', - function_declaration=script, + function_declaration='function(){ return this.tagName; }', return_by_value=True, ) web_element._connection_handler.execute_command.assert_called_once_with( expected_command, timeout=60 ) + @pytest.mark.asyncio + async def test_execute_script_with_this_syntax(self, web_element): + """Test execute_script method with 'this' syntax.""" + script = 'this.style.border = "2px solid red"' + expected_response = {'result': {'result': {'value': None}}} + web_element._connection_handler.execute_command.return_value = expected_response + + result = await web_element.execute_script(script) + + assert result == expected_response + expected_command = RuntimeCommands.call_function_on( + object_id='test-object-id', + function_declaration='function(){ this.style.border = "2px solid red" }', + ) + web_element._connection_handler.execute_command.assert_called_once_with( + expected_command, timeout=60 + ) + + @pytest.mark.asyncio + async def test_execute_script_already_function(self, web_element): + """Test execute_script when script is already a function.""" + script = 'function() { this.style.border = "2px solid red"; }' + expected_response = {'result': {'result': {'value': None}}} + web_element._connection_handler.execute_command.return_value = expected_response + + result = await web_element.execute_script(script) + + assert result == expected_response + expected_command = RuntimeCommands.call_function_on( + object_id='test-object-id', + function_declaration='function() { this.style.border = "2px solid red"; }', + ) + web_element._connection_handler.execute_command.assert_called_once_with( + expected_command, timeout=60 + ) + + @pytest.mark.asyncio + async def test_execute_script_with_parameters(self, web_element): + """Test execute_script with additional parameters.""" + script = 'this.value = "test"' + expected_response = {'result': {'result': {'value': 'test'}}} + web_element._connection_handler.execute_command.return_value = expected_response + + result = await web_element.execute_script( + script, + return_by_value=True, + user_gesture=True + ) + + assert result == expected_response + expected_command = RuntimeCommands.call_function_on( + object_id='test-object-id', + function_declaration='function(){ this.value = "test" }', + return_by_value=True, + user_gesture=True, + ) + web_element._connection_handler.execute_command.assert_called_once_with( + expected_command, timeout=60 + ) + + @pytest.mark.asyncio + async def test_execute_script_arrow_function(self, web_element): + """Test execute_script with arrow function syntax.""" + script = '() => { this.style.color = "red"; }' + expected_response = {'result': {'result': {'value': None}}} + web_element._connection_handler.execute_command.return_value = expected_response + + result = await web_element.execute_script(script) + + assert result == expected_response + expected_command = RuntimeCommands.call_function_on( + object_id='test-object-id', + function_declaration='() => { this.style.color = "red"; }', + ) + web_element._connection_handler.execute_command.assert_called_once_with( + expected_command, timeout=60 + ) + + @pytest.mark.asyncio + async def test_execute_script_multiline(self, web_element): + """Test execute_script with multiline script.""" + script = ''' + this.style.padding = "10px"; + this.style.margin = "5px"; + this.style.borderRadius = "8px"; + ''' + expected_response = {'result': {'result': {'value': None}}} + web_element._connection_handler.execute_command.return_value = expected_response + + result = await web_element.execute_script(script) + + assert result == expected_response + web_element._connection_handler.execute_command.assert_called_once() + call_args = web_element._connection_handler.execute_command.call_args[0][0] + + assert call_args['method'].value == 'Runtime.callFunctionOn' + assert call_args['params']['objectId'] == 'test-object-id' + + func_decl = call_args['params']['functionDeclaration'] + assert 'function(){' in func_decl + assert 'this.style.padding = "10px"' in func_decl + assert 'this.style.margin = "5px"' in func_decl + assert 'this.style.borderRadius = "8px"' in func_decl + + @pytest.mark.asyncio + async def test_execute_script_with_arguments(self, web_element): + """Test execute_script with custom arguments.""" + from pydoll.protocol.runtime.types import CallArgument + + script = 'this.value = arguments[0];' + arguments = [CallArgument(value="test_value")] + expected_response = {'result': {'result': {'value': None}}} + web_element._connection_handler.execute_command.return_value = expected_response + + result = await web_element.execute_script(script, arguments=arguments) + + assert result == expected_response + expected_command = RuntimeCommands.call_function_on( + object_id='test-object-id', + function_declaration='function(){ this.value = arguments[0]; }', + arguments=arguments, + ) + web_element._connection_handler.execute_command.assert_called_once_with( + expected_command, timeout=60 + ) + + @pytest.mark.asyncio + async def test_execute_script_all_parameters(self, web_element): + """Test execute_script with all optional parameters.""" + script = 'this.click()' + expected_response = {'result': {'result': {'value': None}}} + web_element._connection_handler.execute_command.return_value = expected_response + + result = await web_element.execute_script( + script, + silent=True, + return_by_value=True, + generate_preview=True, + user_gesture=True, + await_promise=True, + execution_context_id=123, + object_group="test_group", + throw_on_side_effect=True, + unique_context_id="unique_123" + ) + + assert result == expected_response + expected_command = RuntimeCommands.call_function_on( + object_id='test-object-id', + function_declaration='function(){ this.click() }', + silent=True, + return_by_value=True, + generate_preview=True, + user_gesture=True, + await_promise=True, + execution_context_id=123, + object_group="test_group", + throw_on_side_effect=True, + unique_context_id="unique_123", + ) + web_element._connection_handler.execute_command.assert_called_once_with( + expected_command, timeout=60 + ) + def test_repr(self, web_element): """Test __repr__ method.""" repr_str = repr(web_element)