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
28 changes: 28 additions & 0 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
19 changes: 19 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down Expand Up @@ -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).
Expand Down
22 changes: 22 additions & 0 deletions examples/playwright_sync_example.py
Original file line number Diff line number Diff line change
@@ -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()
73 changes: 49 additions & 24 deletions humantyping/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import time
import asyncio
from collections.abc import Iterable
from typing import Any

from .typer import MarkovTyper
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
66 changes: 66 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -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")