From c8e827643cdff89cc318c4254bb47aab9e4df959 Mon Sep 17 00:00:00 2001 From: user user Date: Fri, 22 May 2026 00:38:09 +0000 Subject: [PATCH] Add playwright sync support --- QUICKSTART.md | 28 +++++++++++ README.md | 19 ++++++++ examples/README.md | 19 ++++++++ examples/playwright_sync_example.py | 22 +++++++++ humantyping/integration.py | 73 +++++++++++++++++++---------- tests/test_integration.py | 66 ++++++++++++++++++++++++++ 6 files changed, 203 insertions(+), 24 deletions(-) create mode 100644 examples/playwright_sync_example.py create mode 100644 tests/test_integration.py diff --git a/QUICKSTART.md b/QUICKSTART.md index b422caa..95e5165 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -148,6 +148,33 @@ typer = HumanTyper(wpm=70) await typer.type(page.locator("#username"), "john_doe") ``` +#### `type_sync(element, text)` + +Types text into a Playwright sync Locator/ElementHandle or Selenium WebElement +with human-like behavior. + +**Parameters:** +- `element`: Playwright sync Locator/ElementHandle or Selenium WebElement +- `text` (str): Text to type + +**Playwright sync example:** +```python +from playwright.sync_api import sync_playwright +from humantyping import HumanTyper + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + page.goto("https://example.com") + + typer = HumanTyper(wpm=70) + field = page.locator("#username") + field.click() + typer.type_sync(field, "john_doe") + + browser.close() +``` + ## Troubleshooting ### "Element is not focused" @@ -172,6 +199,7 @@ typer = HumanTyper(wpm=120) Check the `examples/` folder for: - `playwright_example.py` - Basic Playwright integration +- `playwright_sync_example.py` - Basic Playwright sync integration - `selenium_example.py` - Selenium integration (sync) ## Support diff --git a/README.md b/README.md index 3c1c226..1a42275 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,25 @@ if __name__ == "__main__": 2. Create an instance: `typer = HumanTyper(wpm=70)` 3. Type: `await typer.type(element, "your text")` +### Playwright (Sync) + +```python +from playwright.sync_api import sync_playwright +from humantyping import HumanTyper + +with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + page.goto("https://example.com") + + typer = HumanTyper(wpm=70) + search_box = page.locator("input[name='search']") + search_box.click() + typer.type_sync(search_box, "Typing with Playwright sync") + + browser.close() +``` + ### Selenium & Appium (Sync) diff --git a/examples/README.md b/examples/README.md index b9049d0..fdd9d2a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -20,6 +20,9 @@ python examples/simple_example.py # Full Playwright example python examples/playwright_example.py +# Playwright sync example +python examples/playwright_sync_example.py + # Selenium example python examples/selenium_example.py ``` @@ -65,6 +68,22 @@ python examples/playwright_example.py --- +### `playwright_sync_example.py` - Playwright Sync Support + +Shows how to use HumanTyping with Playwright's synchronous API. + +**Features:** +- Synchronous Playwright API (`type_sync`) +- Compatible with Playwright sync Locator and ElementHandle +- Same realistic behavior as async Playwright + +**Run it:** +```bash +python examples/playwright_sync_example.py +``` + +--- + ### `selenium_example.py` - Selenium Support Shows how to use HumanTyping with Selenium (synchronous). diff --git a/examples/playwright_sync_example.py b/examples/playwright_sync_example.py new file mode 100644 index 0000000..3eef8a1 --- /dev/null +++ b/examples/playwright_sync_example.py @@ -0,0 +1,22 @@ +from playwright.sync_api import sync_playwright + +from humantyping import HumanTyper + + +def main() -> None: + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + page = browser.new_page() + page.goto("https://google.com") + + typer = HumanTyper(wpm=70) + search_box = page.locator("[name='q']") + search_box.click() + typer.type_sync(search_box, "Playwright sync with human typing") + + page.wait_for_timeout(2000) + browser.close() + + +if __name__ == "__main__": + main() diff --git a/humantyping/integration.py b/humantyping/integration.py index b35fcbe..e19befe 100644 --- a/humantyping/integration.py +++ b/humantyping/integration.py @@ -2,6 +2,7 @@ import time import asyncio +from collections.abc import Iterable from typing import Any from .typer import MarkovTyper @@ -14,6 +15,15 @@ def _extract_char(action: str) -> str: return action[first_quote + 1:last_quote] +def _typing_history(text: str, wpm: float, layout: str) -> Iterable[tuple[float, str, Any]]: + if not isinstance(text, str) or len(text) == 0: + raise ValueError("text must be a non-empty string") + + typer = MarkovTyper(text, target_wpm=wpm, layout=layout) + _, history = typer.run() + return history + + class HumanTyper: """ A helper class to integrate realistic typing into automation frameworks @@ -46,15 +56,9 @@ async def type(self, page_element: Any, text: str) -> None: await input_box.click() await typer.type(input_box, "Hello world!") """ - if not isinstance(text, str) or len(text) == 0: - raise ValueError("text must be a non-empty string") - - typer = MarkovTyper(text, target_wpm=self.wpm, layout=self.layout) - _, history = typer.run() - last_time = 0.0 - for t, action, _ in history: + for t, action, _ in _typing_history(text, self.wpm, self.layout): delay = t - last_time if delay > 0: await asyncio.sleep(delay) @@ -87,17 +91,12 @@ def type_appium(self, driver: Any, text: str) -> None: search_box.click() # Ensure focus typer.type_appium(driver, "Hello Appium") """ - if not isinstance(text, str) or len(text) == 0: - raise ValueError("text must be a non-empty string") + history = _typing_history(text, self.wpm, self.layout) + last_time = 0.0 from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.keys import Keys - typer = MarkovTyper(text, target_wpm=self.wpm, layout=self.layout) - _, history = typer.run() - - last_time = 0.0 - for t, action, _ in history: delay = t - last_time if delay > 0: @@ -115,30 +114,56 @@ def type_appium(self, driver: Any, text: str) -> None: char = _extract_char(action) actions.send_keys(char).perform() - def type_sync(self, selenium_element: Any, text: str) -> None: + def type_sync(self, element: Any, text: str) -> None: """ - Types text into a Selenium WebElement with realistic human behavior. + Types text into a Playwright sync element or Selenium WebElement with + realistic human behavior. Args: - selenium_element: The Selenium WebElement to type into. + element: The Playwright sync Locator/ElementHandle or Selenium + WebElement to type into. text: The text to type with human-like behavior. Example: typer = HumanTyper(wpm=65) - input_box = driver.find_element(By.NAME, "search") + input_box = page.locator("input[name='search']") input_box.click() - typer.type_sync(input_box, "Hello Selenium!") + typer.type_sync(input_box, "Hello Playwright sync!") """ - if not isinstance(text, str) or len(text) == 0: - raise ValueError("text must be a non-empty string") + if hasattr(element, "press"): + self._type_playwright_sync(element, text) + return - from selenium.webdriver.common.keys import Keys + self._type_selenium_sync(element, text) - typer = MarkovTyper(text, target_wpm=self.wpm, layout=self.layout) - _, history = typer.run() + def _type_playwright_sync(self, page_element: Any, text: str) -> None: + history = _typing_history(text, self.wpm, self.layout) + last_time = 0.0 + for t, action, _ in history: + delay = t - last_time + if delay > 0: + time.sleep(delay) + last_time = t + + if "BACKSPACE" in action: + page_element.press("Backspace") + elif "TYPED_SWAP" in action: + for char in _extract_char(action): + page_element.press(char) + elif "TYPED_ERROR" in action: + char = _extract_char(action) + page_element.press(char) + elif "TYPED" in action: + char = _extract_char(action) + page_element.press(char) + + def _type_selenium_sync(self, selenium_element: Any, text: str) -> None: + history = _typing_history(text, self.wpm, self.layout) last_time = 0.0 + from selenium.webdriver.common.keys import Keys + for t, action, _ in history: delay = t - last_time if delay > 0: diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..14c7d70 --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from unittest.mock import patch + +from humantyping.integration import HumanTyper + + +class FakeMarkovTyper: + def __init__(self, text: str, target_wpm: float, layout: str) -> None: + self.text = text + self.target_wpm = target_wpm + self.layout = layout + + def run(self): + return self.text, [ + (0.0, "TYPED 'a'", None), + (0.0, "TYPED_SWAP 'bc'", None), + (0.0, "TYPED_ERROR 'x'", None), + (0.0, "BACKSPACE", None), + ] + + +class FakePlaywrightElement: + def __init__(self) -> None: + self.pressed: list[str] = [] + + def press(self, key: str) -> None: + self.pressed.append(key) + + +class FakeSeleniumElement: + def __init__(self) -> None: + self.sent_keys: list[str] = [] + + def send_keys(self, key: str) -> None: + self.sent_keys.append(key) + + +@patch("humantyping.integration.MarkovTyper", FakeMarkovTyper) +def test_type_sync_supports_playwright_sync_elements() -> None: + element = FakePlaywrightElement() + + HumanTyper(wpm=70).type_sync(element, "abc") + + assert element.pressed == ["a", "b", "c", "x", "Backspace"] + + +@patch("humantyping.integration.MarkovTyper", FakeMarkovTyper) +def test_type_sync_keeps_selenium_fallback() -> None: + element = FakeSeleniumElement() + + HumanTyper(wpm=70).type_sync(element, "abc") + + assert element.sent_keys[:4] == ["a", "b", "c", "x"] + assert len(element.sent_keys) == 5 + + +def test_type_sync_rejects_empty_text() -> None: + element = FakePlaywrightElement() + + try: + HumanTyper().type_sync(element, "") + except ValueError as exc: + assert str(exc) == "text must be a non-empty string" + else: + raise AssertionError("Expected ValueError")