oagi 0.4.1__tar.gz → 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of oagi might be problematic. Click here for more details.
- {oagi-0.4.1 → oagi-0.4.3}/.github/workflows/release.yml +10 -3
- {oagi-0.4.1 → oagi-0.4.3}/PKG-INFO +3 -1
- {oagi-0.4.1 → oagi-0.4.3}/README.md +2 -0
- {oagi-0.4.1 → oagi-0.4.3}/pyproject.toml +1 -1
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/pyautogui_action_handler.py +87 -3
- oagi-0.4.3/tests/test_pyautogui_action_handler.py +321 -0
- {oagi-0.4.1 → oagi-0.4.3}/uv.lock +1 -1
- oagi-0.4.1/tests/test_pyautogui_action_handler.py +0 -144
- {oagi-0.4.1 → oagi-0.4.3}/.github/workflows/ci.yml +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/.gitignore +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/.python-version +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/CONTRIBUTING.md +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/LICENSE +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/Makefile +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/examples/async_google_weather.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/examples/execute_task_auto.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/examples/execute_task_manual.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/examples/google_weather.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/examples/hotel_booking.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/examples/screenshot_with_config.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/examples/single_step.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/__init__.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_client.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_pyautogui_action_handler.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_screenshot_maker.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_short_task.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_single_step.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/async_task.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/exceptions.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/logging.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/pil_image.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/screenshot_maker.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/short_task.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/single_step.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/sync_client.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/task.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/__init__.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/action_handler.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/async_action_handler.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/async_image_provider.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/image.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/image_provider.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/__init__.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/action.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/image_config.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/src/oagi/types/models/step.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/__init__.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/conftest.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_async_client.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_async_handlers.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_async_task.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_logging.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_pil_image.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_screenshot_maker.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_short_task.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_single_step.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_sync_client.py +0 -0
- {oagi-0.4.1 → oagi-0.4.3}/tests/test_task.py +0 -0
|
@@ -32,10 +32,17 @@ jobs:
|
|
|
32
32
|
password: ${{ secrets.PYPI_API_TOKEN }}
|
|
33
33
|
# Another option: Use Trusted Publishing (recommended, no token needed)
|
|
34
34
|
# Configure at: https://pypi.org/manage/project/oagi/settings/publishing/
|
|
35
|
-
|
|
35
|
+
|
|
36
|
+
- name: Get commit message
|
|
37
|
+
id: commit_message
|
|
38
|
+
run: |
|
|
39
|
+
# Get the commit message for the tagged commit
|
|
40
|
+
echo "message<<EOF" >> $GITHUB_OUTPUT
|
|
41
|
+
git log -1 --pretty=%B >> $GITHUB_OUTPUT
|
|
42
|
+
echo "EOF" >> $GITHUB_OUTPUT
|
|
36
43
|
|
|
37
44
|
- name: Create GitHub Release
|
|
38
45
|
uses: softprops/action-gh-release@v1
|
|
39
46
|
with:
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
body: ${{ steps.commit_message.outputs.message }}
|
|
48
|
+
files: dist/*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: oagi
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Official API of OpenAGI Foundation
|
|
5
5
|
Project-URL: Homepage, https://github.com/agiopen-org/oagi
|
|
6
6
|
Author-email: OpenAGI Foundation <contact@agiopen.org>
|
|
@@ -93,6 +93,8 @@ config = PyautoguiConfig(
|
|
|
93
93
|
scroll_amount=50, # Larger scroll steps (default: 30)
|
|
94
94
|
wait_duration=2.0, # Longer waits (default: 1.0)
|
|
95
95
|
action_pause=0.2, # More pause between actions (default: 0.1)
|
|
96
|
+
hotkey_interval=0.1, # Interval between keys in hotkey combinations (default: 0.1)
|
|
97
|
+
capslock_mode="session" # Caps lock mode: 'session' or 'system' (default: 'session')
|
|
96
98
|
)
|
|
97
99
|
|
|
98
100
|
executor = PyautoguiActionHandler(config=config)
|
|
@@ -59,6 +59,8 @@ config = PyautoguiConfig(
|
|
|
59
59
|
scroll_amount=50, # Larger scroll steps (default: 30)
|
|
60
60
|
wait_duration=2.0, # Longer waits (default: 1.0)
|
|
61
61
|
action_pause=0.2, # More pause between actions (default: 0.1)
|
|
62
|
+
hotkey_interval=0.1, # Interval between keys in hotkey combinations (default: 0.1)
|
|
63
|
+
capslock_mode="session" # Caps lock mode: 'session' or 'system' (default: 'session')
|
|
62
64
|
)
|
|
63
65
|
|
|
64
66
|
executor = PyautoguiActionHandler(config=config)
|
|
@@ -15,6 +15,42 @@ from pydantic import BaseModel, Field
|
|
|
15
15
|
from .types import Action, ActionType
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class CapsLockManager:
|
|
19
|
+
"""Manages caps lock state for text transformation."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, mode: str = "session"):
|
|
22
|
+
"""Initialize caps lock manager.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
mode: Either "session" (internal state) or "system" (OS-level)
|
|
26
|
+
"""
|
|
27
|
+
self.mode = mode
|
|
28
|
+
self.caps_enabled = False
|
|
29
|
+
|
|
30
|
+
def toggle(self):
|
|
31
|
+
"""Toggle caps lock state in session mode."""
|
|
32
|
+
if self.mode == "session":
|
|
33
|
+
self.caps_enabled = not self.caps_enabled
|
|
34
|
+
|
|
35
|
+
def transform_text(self, text: str) -> str:
|
|
36
|
+
"""Transform text based on caps lock state.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
text: Input text to transform
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Transformed text (uppercase if caps enabled in session mode)
|
|
43
|
+
"""
|
|
44
|
+
if self.mode == "session" and self.caps_enabled:
|
|
45
|
+
# Transform letters to uppercase, preserve special characters
|
|
46
|
+
return "".join(c.upper() if c.isalpha() else c for c in text)
|
|
47
|
+
return text
|
|
48
|
+
|
|
49
|
+
def should_use_system_capslock(self) -> bool:
|
|
50
|
+
"""Check if system-level caps lock should be used."""
|
|
51
|
+
return self.mode == "system"
|
|
52
|
+
|
|
53
|
+
|
|
18
54
|
class PyautoguiConfig(BaseModel):
|
|
19
55
|
"""Configuration for PyautoguiActionHandler."""
|
|
20
56
|
|
|
@@ -30,6 +66,13 @@ class PyautoguiConfig(BaseModel):
|
|
|
30
66
|
action_pause: float = Field(
|
|
31
67
|
default=0.1, description="Pause between PyAutoGUI actions in seconds"
|
|
32
68
|
)
|
|
69
|
+
hotkey_interval: float = Field(
|
|
70
|
+
default=0.1, description="Interval between key presses in hotkey combinations"
|
|
71
|
+
)
|
|
72
|
+
capslock_mode: str = Field(
|
|
73
|
+
default="session",
|
|
74
|
+
description="Caps lock handling mode: 'session' (internal state) or 'system' (OS-level)",
|
|
75
|
+
)
|
|
33
76
|
|
|
34
77
|
|
|
35
78
|
class PyautoguiActionHandler:
|
|
@@ -54,11 +97,30 @@ class PyautoguiActionHandler:
|
|
|
54
97
|
self.screen_width, self.screen_height = pyautogui.size()
|
|
55
98
|
# Set default delay between actions
|
|
56
99
|
pyautogui.PAUSE = self.config.action_pause
|
|
100
|
+
# Initialize caps lock manager
|
|
101
|
+
self.caps_manager = CapsLockManager(mode=self.config.capslock_mode)
|
|
57
102
|
|
|
58
103
|
def _denormalize_coords(self, x: float, y: float) -> tuple[int, int]:
|
|
59
|
-
"""Convert coordinates from 0-1000 range to actual screen coordinates.
|
|
104
|
+
"""Convert coordinates from 0-1000 range to actual screen coordinates.
|
|
105
|
+
|
|
106
|
+
Also handles corner coordinates to prevent PyAutoGUI fail-safe trigger.
|
|
107
|
+
Corner coordinates (0,0), (0,max), (max,0), (max,max) are offset by 1 pixel.
|
|
108
|
+
"""
|
|
60
109
|
screen_x = int(x * self.screen_width / 1000)
|
|
61
110
|
screen_y = int(y * self.screen_height / 1000)
|
|
111
|
+
|
|
112
|
+
# Prevent fail-safe by adjusting corner coordinates
|
|
113
|
+
# Check if coordinates are at screen corners (with small tolerance)
|
|
114
|
+
if screen_x < 1:
|
|
115
|
+
screen_x = 1
|
|
116
|
+
elif screen_x > self.screen_width - 1:
|
|
117
|
+
screen_x = self.screen_width - 1
|
|
118
|
+
|
|
119
|
+
if screen_y < 1:
|
|
120
|
+
screen_y = 1
|
|
121
|
+
elif screen_y > self.screen_height - 1:
|
|
122
|
+
screen_y = self.screen_height - 1
|
|
123
|
+
|
|
62
124
|
return screen_x, screen_y
|
|
63
125
|
|
|
64
126
|
def _parse_coords(self, args_str: str) -> tuple[int, int]:
|
|
@@ -94,12 +156,20 @@ class PyautoguiActionHandler:
|
|
|
94
156
|
direction = match.group(3).lower()
|
|
95
157
|
return x, y, direction
|
|
96
158
|
|
|
159
|
+
def _normalize_key(self, key: str) -> str:
|
|
160
|
+
"""Normalize key names for consistency."""
|
|
161
|
+
key = key.strip().lower()
|
|
162
|
+
# Normalize caps lock variations
|
|
163
|
+
if key in ["caps_lock", "caps", "capslock"]:
|
|
164
|
+
return "capslock"
|
|
165
|
+
return key
|
|
166
|
+
|
|
97
167
|
def _parse_hotkey(self, args_str: str) -> list[str]:
|
|
98
168
|
"""Parse hotkey string into list of keys."""
|
|
99
169
|
# Remove parentheses if present
|
|
100
170
|
args_str = args_str.strip("()")
|
|
101
171
|
# Split by '+' to get individual keys
|
|
102
|
-
keys = [
|
|
172
|
+
keys = [self._normalize_key(key) for key in args_str.split("+")]
|
|
103
173
|
return keys
|
|
104
174
|
|
|
105
175
|
def _execute_single_action(self, action: Action) -> None:
|
|
@@ -132,11 +202,25 @@ class PyautoguiActionHandler:
|
|
|
132
202
|
|
|
133
203
|
case ActionType.HOTKEY:
|
|
134
204
|
keys = self._parse_hotkey(arg)
|
|
135
|
-
|
|
205
|
+
# Check if this is a caps lock key press
|
|
206
|
+
if len(keys) == 1 and keys[0] == "capslock":
|
|
207
|
+
if self.caps_manager.should_use_system_capslock():
|
|
208
|
+
# System mode: use OS-level caps lock
|
|
209
|
+
pyautogui.hotkey(
|
|
210
|
+
"capslock", interval=self.config.hotkey_interval
|
|
211
|
+
)
|
|
212
|
+
else:
|
|
213
|
+
# Session mode: toggle internal state
|
|
214
|
+
self.caps_manager.toggle()
|
|
215
|
+
else:
|
|
216
|
+
# Regular hotkey combination
|
|
217
|
+
pyautogui.hotkey(*keys, interval=self.config.hotkey_interval)
|
|
136
218
|
|
|
137
219
|
case ActionType.TYPE:
|
|
138
220
|
# Remove quotes if present
|
|
139
221
|
text = arg.strip("\"'")
|
|
222
|
+
# Apply caps lock transformation if needed
|
|
223
|
+
text = self.caps_manager.transform_text(text)
|
|
140
224
|
pyautogui.typewrite(text)
|
|
141
225
|
|
|
142
226
|
case ActionType.SCROLL:
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# -----------------------------------------------------------------------------
|
|
2
|
+
# Copyright (c) OpenAGI Foundation
|
|
3
|
+
# All rights reserved.
|
|
4
|
+
#
|
|
5
|
+
# This file is part of the official API project.
|
|
6
|
+
# Licensed under the MIT License.
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from oagi.pyautogui_action_handler import (
|
|
14
|
+
CapsLockManager,
|
|
15
|
+
PyautoguiActionHandler,
|
|
16
|
+
PyautoguiConfig,
|
|
17
|
+
)
|
|
18
|
+
from oagi.types import Action, ActionType
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_pyautogui():
|
|
23
|
+
with patch("oagi.pyautogui_action_handler.pyautogui") as mock:
|
|
24
|
+
mock.size.return_value = (1920, 1080)
|
|
25
|
+
yield mock
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def config():
|
|
30
|
+
return PyautoguiConfig()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def handler(mock_pyautogui):
|
|
35
|
+
return PyautoguiActionHandler()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.mark.parametrize(
|
|
39
|
+
"action_type,argument,expected_method,expected_coords",
|
|
40
|
+
[
|
|
41
|
+
(ActionType.CLICK, "500, 300", "click", (960, 324)),
|
|
42
|
+
(ActionType.LEFT_DOUBLE, "400, 250", "doubleClick", (768, 270)),
|
|
43
|
+
(ActionType.LEFT_TRIPLE, "350, 200", "tripleClick", (672, 216)),
|
|
44
|
+
(ActionType.RIGHT_SINGLE, "600, 400", "rightClick", (1152, 432)),
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
def test_coordinate_based_actions(
|
|
48
|
+
handler, mock_pyautogui, action_type, argument, expected_method, expected_coords
|
|
49
|
+
):
|
|
50
|
+
action = Action(type=action_type, argument=argument, count=1)
|
|
51
|
+
handler([action])
|
|
52
|
+
|
|
53
|
+
getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_coords)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_drag_action(handler, mock_pyautogui, config):
|
|
57
|
+
action = Action(type=ActionType.DRAG, argument="100, 100, 500, 300", count=1)
|
|
58
|
+
handler([action])
|
|
59
|
+
|
|
60
|
+
mock_pyautogui.moveTo.assert_any_call(192, 108)
|
|
61
|
+
mock_pyautogui.dragTo.assert_called_once_with(
|
|
62
|
+
960, 324, duration=config.drag_duration, button="left"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_hotkey_action(handler, mock_pyautogui, config):
|
|
67
|
+
action = Action(type=ActionType.HOTKEY, argument="ctrl+c", count=1)
|
|
68
|
+
handler([action])
|
|
69
|
+
|
|
70
|
+
mock_pyautogui.hotkey.assert_called_once_with(
|
|
71
|
+
"ctrl", "c", interval=config.hotkey_interval
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_type_action(handler, mock_pyautogui):
|
|
76
|
+
action = Action(type=ActionType.TYPE, argument="Hello World", count=1)
|
|
77
|
+
handler([action])
|
|
78
|
+
|
|
79
|
+
mock_pyautogui.typewrite.assert_called_once_with("Hello World")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize(
|
|
83
|
+
"direction,expected_amount_multiplier",
|
|
84
|
+
[("up", 1), ("down", -1)],
|
|
85
|
+
)
|
|
86
|
+
def test_scroll_actions(
|
|
87
|
+
handler, mock_pyautogui, config, direction, expected_amount_multiplier
|
|
88
|
+
):
|
|
89
|
+
action = Action(type=ActionType.SCROLL, argument=f"500, 300, {direction}", count=1)
|
|
90
|
+
handler([action])
|
|
91
|
+
|
|
92
|
+
mock_pyautogui.moveTo.assert_called_once_with(960, 324)
|
|
93
|
+
expected_scroll_amount = config.scroll_amount * expected_amount_multiplier
|
|
94
|
+
mock_pyautogui.scroll.assert_called_once_with(expected_scroll_amount)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_wait_action(handler, mock_pyautogui, config):
|
|
98
|
+
with patch("time.sleep") as mock_sleep:
|
|
99
|
+
action = Action(type=ActionType.WAIT, argument="", count=1)
|
|
100
|
+
handler([action])
|
|
101
|
+
mock_sleep.assert_called_once_with(config.wait_duration)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_hotkey_with_custom_interval(mock_pyautogui):
|
|
105
|
+
custom_config = PyautoguiConfig(hotkey_interval=0.5)
|
|
106
|
+
handler = PyautoguiActionHandler(config=custom_config)
|
|
107
|
+
|
|
108
|
+
action = Action(type=ActionType.HOTKEY, argument="cmd+shift+a", count=1)
|
|
109
|
+
handler([action])
|
|
110
|
+
|
|
111
|
+
mock_pyautogui.hotkey.assert_called_once_with("cmd", "shift", "a", interval=0.5)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_finish_action(handler, mock_pyautogui):
|
|
115
|
+
action = Action(type=ActionType.FINISH, argument="", count=1)
|
|
116
|
+
handler([action])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_call_user_action(handler, mock_pyautogui, capsys):
|
|
120
|
+
action = Action(type=ActionType.CALL_USER, argument="", count=1)
|
|
121
|
+
handler([action])
|
|
122
|
+
|
|
123
|
+
captured = capsys.readouterr()
|
|
124
|
+
assert "User intervention requested" in captured.out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestActionExecution:
|
|
128
|
+
def test_multiple_count(self, handler, mock_pyautogui):
|
|
129
|
+
action = Action(type=ActionType.CLICK, argument="500, 300", count=3)
|
|
130
|
+
handler([action])
|
|
131
|
+
|
|
132
|
+
assert mock_pyautogui.click.call_count == 3
|
|
133
|
+
|
|
134
|
+
def test_multiple_actions(self, handler, mock_pyautogui):
|
|
135
|
+
actions = [
|
|
136
|
+
Action(type=ActionType.CLICK, argument="100, 100", count=1),
|
|
137
|
+
Action(type=ActionType.TYPE, argument="test", count=1),
|
|
138
|
+
Action(type=ActionType.HOTKEY, argument="ctrl+s", count=1),
|
|
139
|
+
]
|
|
140
|
+
handler(actions)
|
|
141
|
+
|
|
142
|
+
mock_pyautogui.click.assert_called_once()
|
|
143
|
+
mock_pyautogui.typewrite.assert_called_once_with("test")
|
|
144
|
+
mock_pyautogui.hotkey.assert_called_once_with("ctrl", "s", interval=0.1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TestInputValidation:
|
|
148
|
+
def test_invalid_coordinates_format(self, handler, mock_pyautogui):
|
|
149
|
+
action = Action(type=ActionType.CLICK, argument="invalid", count=1)
|
|
150
|
+
|
|
151
|
+
with pytest.raises(ValueError, match="Invalid coordinates format"):
|
|
152
|
+
handler([action])
|
|
153
|
+
|
|
154
|
+
def test_type_with_quotes(self, handler, mock_pyautogui):
|
|
155
|
+
action = Action(type=ActionType.TYPE, argument='"Hello World"', count=1)
|
|
156
|
+
handler([action])
|
|
157
|
+
|
|
158
|
+
mock_pyautogui.typewrite.assert_called_once_with("Hello World")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TestCapsLockManager:
|
|
162
|
+
def test_session_mode_text_transformation(self):
|
|
163
|
+
manager = CapsLockManager(mode="session")
|
|
164
|
+
|
|
165
|
+
# Initially caps is off
|
|
166
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
167
|
+
assert manager.transform_text("123!@#") == "123!@#"
|
|
168
|
+
|
|
169
|
+
# Toggle caps on
|
|
170
|
+
manager.toggle()
|
|
171
|
+
assert manager.caps_enabled is True
|
|
172
|
+
assert manager.transform_text("Hello World") == "HELLO WORLD"
|
|
173
|
+
assert manager.transform_text("test123!") == "TEST123!"
|
|
174
|
+
assert manager.transform_text("123!@#") == "123!@#"
|
|
175
|
+
|
|
176
|
+
# Toggle caps off
|
|
177
|
+
manager.toggle()
|
|
178
|
+
assert manager.caps_enabled is False
|
|
179
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
180
|
+
|
|
181
|
+
def test_system_mode_no_transformation(self):
|
|
182
|
+
manager = CapsLockManager(mode="system")
|
|
183
|
+
|
|
184
|
+
# System mode doesn't transform text
|
|
185
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
186
|
+
|
|
187
|
+
manager.toggle() # Should not affect state in system mode
|
|
188
|
+
assert manager.caps_enabled is False
|
|
189
|
+
assert manager.transform_text("Hello World") == "Hello World"
|
|
190
|
+
|
|
191
|
+
def test_should_use_system_capslock(self):
|
|
192
|
+
session_manager = CapsLockManager(mode="session")
|
|
193
|
+
system_manager = CapsLockManager(mode="system")
|
|
194
|
+
|
|
195
|
+
assert session_manager.should_use_system_capslock() is False
|
|
196
|
+
assert system_manager.should_use_system_capslock() is True
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class TestCornerCoordinatesHandling:
|
|
200
|
+
"""Test that corner coordinates are adjusted to prevent PyAutoGUI fail-safe."""
|
|
201
|
+
|
|
202
|
+
@pytest.mark.parametrize(
|
|
203
|
+
"input_coords,expected_coords",
|
|
204
|
+
[
|
|
205
|
+
# Top-left corner
|
|
206
|
+
("0, 0", (1, 1)),
|
|
207
|
+
("1, 1", (1, 1)),
|
|
208
|
+
# Top-right corner (assuming 1920x1080 screen)
|
|
209
|
+
("1000, 0", (1919, 1)),
|
|
210
|
+
("999, 1", (1918, 1)),
|
|
211
|
+
# Bottom-left corner
|
|
212
|
+
("0, 1000", (1, 1079)),
|
|
213
|
+
("1, 999", (1, 1078)),
|
|
214
|
+
# Bottom-right corner
|
|
215
|
+
("1000, 1000", (1919, 1079)),
|
|
216
|
+
("999, 999", (1918, 1078)),
|
|
217
|
+
# Middle coordinates should not be affected
|
|
218
|
+
("500, 500", (960, 540)),
|
|
219
|
+
("250, 750", (480, 810)),
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
def test_corner_coordinate_adjustment(
|
|
223
|
+
self, mock_pyautogui, input_coords, expected_coords
|
|
224
|
+
):
|
|
225
|
+
handler = PyautoguiActionHandler()
|
|
226
|
+
action = Action(type=ActionType.CLICK, argument=input_coords, count=1)
|
|
227
|
+
handler([action])
|
|
228
|
+
mock_pyautogui.click.assert_called_once_with(*expected_coords)
|
|
229
|
+
|
|
230
|
+
def test_drag_with_corner_coordinates(self, mock_pyautogui, config):
|
|
231
|
+
"""Test drag operations with corner coordinates."""
|
|
232
|
+
handler = PyautoguiActionHandler()
|
|
233
|
+
# Drag from top-left corner to bottom-right corner
|
|
234
|
+
action = Action(type=ActionType.DRAG, argument="0, 0, 1000, 1000", count=1)
|
|
235
|
+
handler([action])
|
|
236
|
+
|
|
237
|
+
# Should adjust corner coordinates to prevent fail-safe
|
|
238
|
+
mock_pyautogui.moveTo.assert_called_once_with(1, 1)
|
|
239
|
+
mock_pyautogui.dragTo.assert_called_once_with(
|
|
240
|
+
1919, 1079, duration=config.drag_duration, button="left"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
def test_scroll_with_corner_coordinates(self, mock_pyautogui, config):
|
|
244
|
+
"""Test scroll operations at corner coordinates."""
|
|
245
|
+
handler = PyautoguiActionHandler()
|
|
246
|
+
action = Action(type=ActionType.SCROLL, argument="0, 0, up", count=1)
|
|
247
|
+
handler([action])
|
|
248
|
+
|
|
249
|
+
# Should adjust corner coordinates
|
|
250
|
+
mock_pyautogui.moveTo.assert_called_once_with(1, 1)
|
|
251
|
+
mock_pyautogui.scroll.assert_called_once_with(config.scroll_amount)
|
|
252
|
+
|
|
253
|
+
def test_multiple_clicks_at_corners(self, mock_pyautogui):
|
|
254
|
+
"""Test multiple clicks at corner positions."""
|
|
255
|
+
handler = PyautoguiActionHandler()
|
|
256
|
+
actions = [
|
|
257
|
+
Action(type=ActionType.LEFT_DOUBLE, argument="0, 0", count=1),
|
|
258
|
+
Action(type=ActionType.LEFT_TRIPLE, argument="1000, 0", count=1),
|
|
259
|
+
Action(type=ActionType.RIGHT_SINGLE, argument="0, 1000", count=1),
|
|
260
|
+
]
|
|
261
|
+
handler(actions)
|
|
262
|
+
|
|
263
|
+
# All corner coordinates should be adjusted
|
|
264
|
+
mock_pyautogui.doubleClick.assert_called_once_with(1, 1)
|
|
265
|
+
mock_pyautogui.tripleClick.assert_called_once_with(1919, 1)
|
|
266
|
+
mock_pyautogui.rightClick.assert_called_once_with(1, 1079)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestCapsLockIntegration:
|
|
270
|
+
def test_caps_lock_key_normalization(self, mock_pyautogui):
|
|
271
|
+
handler = PyautoguiActionHandler()
|
|
272
|
+
|
|
273
|
+
# Test different caps lock variations
|
|
274
|
+
for variant in ["caps", "caps_lock", "capslock"]:
|
|
275
|
+
keys = handler._parse_hotkey(variant)
|
|
276
|
+
assert keys == ["capslock"]
|
|
277
|
+
|
|
278
|
+
def test_caps_lock_session_mode(self, mock_pyautogui):
|
|
279
|
+
config = PyautoguiConfig(capslock_mode="session")
|
|
280
|
+
handler = PyautoguiActionHandler(config=config)
|
|
281
|
+
|
|
282
|
+
# Type without caps
|
|
283
|
+
type_action = Action(type=ActionType.TYPE, argument="test", count=1)
|
|
284
|
+
handler([type_action])
|
|
285
|
+
mock_pyautogui.typewrite.assert_called_with("test")
|
|
286
|
+
|
|
287
|
+
# Toggle caps lock
|
|
288
|
+
caps_action = Action(type=ActionType.HOTKEY, argument="caps_lock", count=1)
|
|
289
|
+
handler([caps_action])
|
|
290
|
+
# In session mode, should not call pyautogui.hotkey for capslock
|
|
291
|
+
assert mock_pyautogui.hotkey.call_count == 0
|
|
292
|
+
|
|
293
|
+
# Type with caps enabled
|
|
294
|
+
mock_pyautogui.typewrite.reset_mock()
|
|
295
|
+
type_action = Action(type=ActionType.TYPE, argument="test", count=1)
|
|
296
|
+
handler([type_action])
|
|
297
|
+
mock_pyautogui.typewrite.assert_called_with("TEST")
|
|
298
|
+
|
|
299
|
+
def test_caps_lock_system_mode(self, mock_pyautogui):
|
|
300
|
+
config = PyautoguiConfig(capslock_mode="system")
|
|
301
|
+
handler = PyautoguiActionHandler(config=config)
|
|
302
|
+
|
|
303
|
+
# Toggle caps lock in system mode
|
|
304
|
+
caps_action = Action(type=ActionType.HOTKEY, argument="caps", count=1)
|
|
305
|
+
handler([caps_action])
|
|
306
|
+
# In system mode, should call pyautogui.hotkey
|
|
307
|
+
mock_pyautogui.hotkey.assert_called_once_with("capslock", interval=0.1)
|
|
308
|
+
|
|
309
|
+
# Type action should not transform text in system mode
|
|
310
|
+
mock_pyautogui.typewrite.reset_mock()
|
|
311
|
+
type_action = Action(type=ActionType.TYPE, argument="test", count=1)
|
|
312
|
+
handler([type_action])
|
|
313
|
+
mock_pyautogui.typewrite.assert_called_with("test")
|
|
314
|
+
|
|
315
|
+
def test_regular_hotkey_not_affected(self, mock_pyautogui):
|
|
316
|
+
handler = PyautoguiActionHandler()
|
|
317
|
+
|
|
318
|
+
# Regular hotkeys should work normally
|
|
319
|
+
action = Action(type=ActionType.HOTKEY, argument="ctrl+c", count=1)
|
|
320
|
+
handler([action])
|
|
321
|
+
mock_pyautogui.hotkey.assert_called_once_with("ctrl", "c", interval=0.1)
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# -----------------------------------------------------------------------------
|
|
2
|
-
# Copyright (c) OpenAGI Foundation
|
|
3
|
-
# All rights reserved.
|
|
4
|
-
#
|
|
5
|
-
# This file is part of the official API project.
|
|
6
|
-
# Licensed under the MIT License.
|
|
7
|
-
# -----------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
from unittest.mock import patch
|
|
10
|
-
|
|
11
|
-
import pytest
|
|
12
|
-
|
|
13
|
-
from oagi.pyautogui_action_handler import PyautoguiActionHandler, PyautoguiConfig
|
|
14
|
-
from oagi.types import Action, ActionType
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@pytest.fixture
|
|
18
|
-
def mock_pyautogui():
|
|
19
|
-
with patch("oagi.pyautogui_action_handler.pyautogui") as mock:
|
|
20
|
-
mock.size.return_value = (1920, 1080)
|
|
21
|
-
yield mock
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@pytest.fixture
|
|
25
|
-
def config():
|
|
26
|
-
return PyautoguiConfig()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@pytest.fixture
|
|
30
|
-
def handler(mock_pyautogui):
|
|
31
|
-
return PyautoguiActionHandler()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
@pytest.mark.parametrize(
|
|
35
|
-
"action_type,argument,expected_method,expected_coords",
|
|
36
|
-
[
|
|
37
|
-
(ActionType.CLICK, "500, 300", "click", (960, 324)),
|
|
38
|
-
(ActionType.LEFT_DOUBLE, "400, 250", "doubleClick", (768, 270)),
|
|
39
|
-
(ActionType.LEFT_TRIPLE, "350, 200", "tripleClick", (672, 216)),
|
|
40
|
-
(ActionType.RIGHT_SINGLE, "600, 400", "rightClick", (1152, 432)),
|
|
41
|
-
],
|
|
42
|
-
)
|
|
43
|
-
def test_coordinate_based_actions(
|
|
44
|
-
handler, mock_pyautogui, action_type, argument, expected_method, expected_coords
|
|
45
|
-
):
|
|
46
|
-
action = Action(type=action_type, argument=argument, count=1)
|
|
47
|
-
handler([action])
|
|
48
|
-
|
|
49
|
-
getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_coords)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def test_drag_action(handler, mock_pyautogui, config):
|
|
53
|
-
action = Action(type=ActionType.DRAG, argument="100, 100, 500, 300", count=1)
|
|
54
|
-
handler([action])
|
|
55
|
-
|
|
56
|
-
mock_pyautogui.moveTo.assert_any_call(192, 108)
|
|
57
|
-
mock_pyautogui.dragTo.assert_called_once_with(
|
|
58
|
-
960, 324, duration=config.drag_duration, button="left"
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@pytest.mark.parametrize(
|
|
63
|
-
"action_type,argument,expected_method,expected_args",
|
|
64
|
-
[
|
|
65
|
-
(ActionType.HOTKEY, "ctrl+c", "hotkey", ("ctrl", "c")),
|
|
66
|
-
(ActionType.TYPE, "Hello World", "typewrite", ("Hello World",)),
|
|
67
|
-
],
|
|
68
|
-
)
|
|
69
|
-
def test_text_based_actions(
|
|
70
|
-
handler, mock_pyautogui, action_type, argument, expected_method, expected_args
|
|
71
|
-
):
|
|
72
|
-
action = Action(type=action_type, argument=argument, count=1)
|
|
73
|
-
handler([action])
|
|
74
|
-
|
|
75
|
-
getattr(mock_pyautogui, expected_method).assert_called_once_with(*expected_args)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@pytest.mark.parametrize(
|
|
79
|
-
"direction,expected_amount_multiplier",
|
|
80
|
-
[("up", 1), ("down", -1)],
|
|
81
|
-
)
|
|
82
|
-
def test_scroll_actions(
|
|
83
|
-
handler, mock_pyautogui, config, direction, expected_amount_multiplier
|
|
84
|
-
):
|
|
85
|
-
action = Action(type=ActionType.SCROLL, argument=f"500, 300, {direction}", count=1)
|
|
86
|
-
handler([action])
|
|
87
|
-
|
|
88
|
-
mock_pyautogui.moveTo.assert_called_once_with(960, 324)
|
|
89
|
-
expected_scroll_amount = config.scroll_amount * expected_amount_multiplier
|
|
90
|
-
mock_pyautogui.scroll.assert_called_once_with(expected_scroll_amount)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def test_wait_action(handler, mock_pyautogui, config):
|
|
94
|
-
with patch("time.sleep") as mock_sleep:
|
|
95
|
-
action = Action(type=ActionType.WAIT, argument="", count=1)
|
|
96
|
-
handler([action])
|
|
97
|
-
mock_sleep.assert_called_once_with(config.wait_duration)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def test_finish_action(handler, mock_pyautogui):
|
|
101
|
-
action = Action(type=ActionType.FINISH, argument="", count=1)
|
|
102
|
-
handler([action])
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def test_call_user_action(handler, mock_pyautogui, capsys):
|
|
106
|
-
action = Action(type=ActionType.CALL_USER, argument="", count=1)
|
|
107
|
-
handler([action])
|
|
108
|
-
|
|
109
|
-
captured = capsys.readouterr()
|
|
110
|
-
assert "User intervention requested" in captured.out
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
class TestActionExecution:
|
|
114
|
-
def test_multiple_count(self, handler, mock_pyautogui):
|
|
115
|
-
action = Action(type=ActionType.CLICK, argument="500, 300", count=3)
|
|
116
|
-
handler([action])
|
|
117
|
-
|
|
118
|
-
assert mock_pyautogui.click.call_count == 3
|
|
119
|
-
|
|
120
|
-
def test_multiple_actions(self, handler, mock_pyautogui):
|
|
121
|
-
actions = [
|
|
122
|
-
Action(type=ActionType.CLICK, argument="100, 100", count=1),
|
|
123
|
-
Action(type=ActionType.TYPE, argument="test", count=1),
|
|
124
|
-
Action(type=ActionType.HOTKEY, argument="ctrl+s", count=1),
|
|
125
|
-
]
|
|
126
|
-
handler(actions)
|
|
127
|
-
|
|
128
|
-
mock_pyautogui.click.assert_called_once()
|
|
129
|
-
mock_pyautogui.typewrite.assert_called_once_with("test")
|
|
130
|
-
mock_pyautogui.hotkey.assert_called_once_with("ctrl", "s")
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
class TestInputValidation:
|
|
134
|
-
def test_invalid_coordinates_format(self, handler, mock_pyautogui):
|
|
135
|
-
action = Action(type=ActionType.CLICK, argument="invalid", count=1)
|
|
136
|
-
|
|
137
|
-
with pytest.raises(ValueError, match="Invalid coordinates format"):
|
|
138
|
-
handler([action])
|
|
139
|
-
|
|
140
|
-
def test_type_with_quotes(self, handler, mock_pyautogui):
|
|
141
|
-
action = Action(type=ActionType.TYPE, argument='"Hello World"', count=1)
|
|
142
|
-
handler([action])
|
|
143
|
-
|
|
144
|
-
mock_pyautogui.typewrite.assert_called_once_with("Hello World")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|