rootbrowse 0.2.0__py3-none-any.whl
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.
- rootbrowse/__init__.py +39 -0
- rootbrowse/_version.py +3 -0
- rootbrowse/browser.py +181 -0
- rootbrowse/constants.py +62 -0
- rootbrowse/element_operator.py +219 -0
- rootbrowse/exceptions.py +41 -0
- rootbrowse/page_scanner.py +467 -0
- rootbrowse/tab_manager.py +97 -0
- rootbrowse/types.py +66 -0
- rootbrowse/utils/__init__.py +0 -0
- rootbrowse-0.2.0.dist-info/METADATA +203 -0
- rootbrowse-0.2.0.dist-info/RECORD +13 -0
- rootbrowse-0.2.0.dist-info/WHEEL +4 -0
rootbrowse/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""RootBrowse - Python 浏览器自动化 MCP 框架"""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .browser import Browser
|
|
5
|
+
from .tab_manager import TabManager
|
|
6
|
+
from .element_operator import ElementOperator
|
|
7
|
+
from .page_scanner import PageScanner
|
|
8
|
+
from .types import Region, Element, RegionSummary, ElementPreview, OperationResult
|
|
9
|
+
from .exceptions import (
|
|
10
|
+
RootBrowseError,
|
|
11
|
+
BrowserError,
|
|
12
|
+
ElementNotFoundError,
|
|
13
|
+
RegionNotFoundError,
|
|
14
|
+
TabNotFoundError,
|
|
15
|
+
OperationError,
|
|
16
|
+
StateFileError,
|
|
17
|
+
PageLoadError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"Browser",
|
|
23
|
+
"TabManager",
|
|
24
|
+
"ElementOperator",
|
|
25
|
+
"PageScanner",
|
|
26
|
+
"Region",
|
|
27
|
+
"Element",
|
|
28
|
+
"RegionSummary",
|
|
29
|
+
"ElementPreview",
|
|
30
|
+
"OperationResult",
|
|
31
|
+
"RootBrowseError",
|
|
32
|
+
"BrowserError",
|
|
33
|
+
"ElementNotFoundError",
|
|
34
|
+
"RegionNotFoundError",
|
|
35
|
+
"TabNotFoundError",
|
|
36
|
+
"OperationError",
|
|
37
|
+
"StateFileError",
|
|
38
|
+
"PageLoadError",
|
|
39
|
+
]
|
rootbrowse/_version.py
ADDED
rootbrowse/browser.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""浏览器主入口"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .constants import DEFAULT_URL
|
|
7
|
+
from .tab_manager import TabManager
|
|
8
|
+
from .element_operator import ElementOperator
|
|
9
|
+
from .page_scanner import PageScanner
|
|
10
|
+
from .types import Region, RegionSummary, Element, ElementPreview, OperationResult
|
|
11
|
+
from .exceptions import BrowserError, StateFileError, PageLoadError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Browser:
|
|
15
|
+
"""
|
|
16
|
+
浏览器主入口,组合 TabManager、ElementOperator、PageScanner
|
|
17
|
+
|
|
18
|
+
AI 调用方式:
|
|
19
|
+
browser = Browser() # 默认打开 bing.com
|
|
20
|
+
browser = Browser('https://example.com') # 指定 URL
|
|
21
|
+
browser.view.get_regions()
|
|
22
|
+
browser.act.click('r1')
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, page: Any | None = None, url: str | None = None, headless: bool = True):
|
|
26
|
+
"""
|
|
27
|
+
初始化 Browser
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
page: DrissionPage ChromiumPage 实例,None 则自动创建
|
|
31
|
+
url: 默认打开的 URL,配合 page 为 None 时使用
|
|
32
|
+
headless: 是否使用无头模式,默认 Ture(无头浏览器)
|
|
33
|
+
"""
|
|
34
|
+
page_2 = page
|
|
35
|
+
if page is None:
|
|
36
|
+
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
37
|
+
opts = ChromiumOptions()
|
|
38
|
+
# 有头还是无头浏览器
|
|
39
|
+
if headless:
|
|
40
|
+
opts.headless(True)
|
|
41
|
+
else:
|
|
42
|
+
opts.headless(False)
|
|
43
|
+
|
|
44
|
+
page = ChromiumPage(addr_or_opts=opts)
|
|
45
|
+
|
|
46
|
+
self._page = page
|
|
47
|
+
self._closed = False
|
|
48
|
+
|
|
49
|
+
# 初始化子模块
|
|
50
|
+
self._page_scanner = PageScanner(self._page)
|
|
51
|
+
self._tab_manager = TabManager(self._page)
|
|
52
|
+
self._element_operator = ElementOperator(self._page, self._page_scanner._element_map)
|
|
53
|
+
|
|
54
|
+
# 打开默认 URL(只有当 page 和 url 都没提供时才自动打开)
|
|
55
|
+
if page_2 is None and url is None:
|
|
56
|
+
self.get(DEFAULT_URL)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def create(cls, url: str | None = None) -> "Browser":
|
|
60
|
+
"""
|
|
61
|
+
工厂方法:创建 Browser 并打开指定 URL
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
url: 目标 URL,None 则打开 DEFAULT_URL (bing.com)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Browser 实例
|
|
68
|
+
"""
|
|
69
|
+
if url is None:
|
|
70
|
+
url = DEFAULT_URL
|
|
71
|
+
return cls(url=url)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def tabs(self) -> TabManager:
|
|
75
|
+
"""标签页管理器"""
|
|
76
|
+
return self._tab_manager
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def view(self) -> PageScanner:
|
|
80
|
+
"""页面扫描器"""
|
|
81
|
+
return self._page_scanner
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def act(self) -> ElementOperator:
|
|
85
|
+
"""元素操作器"""
|
|
86
|
+
return self._element_operator
|
|
87
|
+
|
|
88
|
+
def get(self, url: str, timeout: float = 30) -> dict:
|
|
89
|
+
"""
|
|
90
|
+
打开 URL
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
url: 目标 URL
|
|
94
|
+
timeout: 超时时间(秒)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
dict: {url, title}
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
PageLoadError: 页面加载失败
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
self._page.get(url, timeout=timeout)
|
|
104
|
+
title = self._page.title or ""
|
|
105
|
+
return {"url": url, "title": title}
|
|
106
|
+
except Exception as e:
|
|
107
|
+
raise PageLoadError(f"Failed to load page: {url}, error: {e}")
|
|
108
|
+
|
|
109
|
+
def screenshot(
|
|
110
|
+
self,
|
|
111
|
+
path: str | None = None,
|
|
112
|
+
annotate: list[list[int]] | None = None
|
|
113
|
+
) -> str:
|
|
114
|
+
"""
|
|
115
|
+
页面截图
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
path: 保存路径,None 则返回 base64 字符串
|
|
119
|
+
annotate: 标注区域列表 [[x1,y1,x2,y2], ...]
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
str: 文件路径或 base64 字符串
|
|
123
|
+
"""
|
|
124
|
+
if path:
|
|
125
|
+
self._page.get_screenshot(path=path)
|
|
126
|
+
return path
|
|
127
|
+
else:
|
|
128
|
+
return self._page.screenshot()
|
|
129
|
+
|
|
130
|
+
def save_state(self, path: str) -> None:
|
|
131
|
+
"""
|
|
132
|
+
保存浏览器状态(cookies)
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
path: 保存路径
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
StateFileError: 保存失败
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
cookies = self._page.cookies()
|
|
142
|
+
data = {
|
|
143
|
+
'version': '0.1.0',
|
|
144
|
+
'cookies': cookies if isinstance(cookies, list) else list(cookies),
|
|
145
|
+
'saved_at': self._get_timestamp(),
|
|
146
|
+
}
|
|
147
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
148
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
raise StateFileError(f"Failed to save state: {e}")
|
|
151
|
+
|
|
152
|
+
def load_state(self, path: str) -> None:
|
|
153
|
+
"""
|
|
154
|
+
恢复浏览器状态(cookies)
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
path: 状态文件路径
|
|
158
|
+
|
|
159
|
+
Raises:
|
|
160
|
+
StateFileError: 恢复失败
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
164
|
+
data = json.load(f)
|
|
165
|
+
cookies = data.get('cookies', [])
|
|
166
|
+
if cookies:
|
|
167
|
+
self._page.set.cookies(cookies)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise StateFileError(f"Failed to load state: {e}")
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _get_timestamp() -> str:
|
|
173
|
+
"""返回当前时间戳 ISO 格式"""
|
|
174
|
+
from datetime import datetime, timezone
|
|
175
|
+
return datetime.now(timezone.utc).isoformat()
|
|
176
|
+
|
|
177
|
+
def close(self) -> None:
|
|
178
|
+
"""关闭浏览器"""
|
|
179
|
+
if not self._closed:
|
|
180
|
+
self._page.quit()
|
|
181
|
+
self._closed = True
|
rootbrowse/constants.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""常量配置"""
|
|
2
|
+
|
|
3
|
+
# 默认 URL
|
|
4
|
+
DEFAULT_URL = "https://www.bing.com"
|
|
5
|
+
|
|
6
|
+
# 可交互的 HTML 标签(可被点击、输入、选择的元素)
|
|
7
|
+
INTERACTIVE_TAGS = [
|
|
8
|
+
'a', # 链接
|
|
9
|
+
'button', # 按钮
|
|
10
|
+
'input', # 输入框
|
|
11
|
+
'select', # 下拉框
|
|
12
|
+
'textarea', # 文本域
|
|
13
|
+
'details', # 可折叠
|
|
14
|
+
'summary', # 可折叠标题
|
|
15
|
+
'menuitem', # 菜单项
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
# 可输入的 HTML 标签
|
|
19
|
+
INPUT_TAGS = [
|
|
20
|
+
'input',
|
|
21
|
+
'textarea',
|
|
22
|
+
'select',
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# 可点击的 HTML 标签
|
|
26
|
+
CLICKABLE_TAGS = [
|
|
27
|
+
'a',
|
|
28
|
+
'button',
|
|
29
|
+
'input',
|
|
30
|
+
'details',
|
|
31
|
+
'summary',
|
|
32
|
+
'menuitem',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# ARIA role 映射
|
|
36
|
+
ROLE_TAG_MAP = {
|
|
37
|
+
'link': 'a',
|
|
38
|
+
'button': 'button',
|
|
39
|
+
'textbox': 'input',
|
|
40
|
+
'searchbox': 'input',
|
|
41
|
+
'checkbox': 'input',
|
|
42
|
+
'radio': 'input',
|
|
43
|
+
'menuitem': 'menuitem',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# 区域 ID 命名
|
|
47
|
+
REGION_ID_HEADER = 'header'
|
|
48
|
+
REGION_ID_MAIN = 'main'
|
|
49
|
+
REGION_ID_SIDEBAR = 'sidebar'
|
|
50
|
+
REGION_ID_FOOTER = 'footer'
|
|
51
|
+
REGION_ID_NAV = 'nav'
|
|
52
|
+
|
|
53
|
+
# 默认配置
|
|
54
|
+
DEFAULT_LIMIT = 20
|
|
55
|
+
DEFAULT_OFFSET = 0
|
|
56
|
+
DEFAULT_TIMEOUT = 10
|
|
57
|
+
|
|
58
|
+
# ref 前缀
|
|
59
|
+
REF_PREFIX = 'r'
|
|
60
|
+
|
|
61
|
+
# 状态文件扩展名
|
|
62
|
+
STATE_FILE_EXTENSION = '.state.json'
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""元素操作器"""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .types import Element, OperationResult
|
|
6
|
+
from .exceptions import ElementNotFoundError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ElementOperator:
|
|
10
|
+
"""元素操作器,执行点击、输入、悬停等操作"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, page: Any, element_map: dict[str, Element]):
|
|
13
|
+
"""
|
|
14
|
+
初始化 ElementOperator
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
page: DrissionPage ChromiumPage 实例
|
|
18
|
+
element_map: ref -> Element 映射表
|
|
19
|
+
"""
|
|
20
|
+
self._page = page
|
|
21
|
+
self._element_map = element_map
|
|
22
|
+
|
|
23
|
+
def click(self, locator: str, locator_type: str | None = None) -> OperationResult:
|
|
24
|
+
"""
|
|
25
|
+
点击元素
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
locator: 定位符(ref / xpath / role / text)
|
|
29
|
+
locator_type: 定位类型,不指定则自动推断
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
OperationResult: {success, new_url?, error?}
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
xpath = self._resolve_locator(locator, locator_type)
|
|
36
|
+
self._page.ele(f'xpath={xpath}').click()
|
|
37
|
+
return OperationResult(success=True)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return OperationResult(success=False, error=str(e))
|
|
40
|
+
|
|
41
|
+
def input_by_ref(
|
|
42
|
+
self, locator: str, text: str, clear: bool = False, locator_type: str | None = None
|
|
43
|
+
) -> OperationResult:
|
|
44
|
+
"""
|
|
45
|
+
向输入框写入文字
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
locator: 定位符(ref / xpath / role / text)
|
|
49
|
+
text: 要输入的文字
|
|
50
|
+
clear: 是否先清空输入框
|
|
51
|
+
locator_type: 定位类型
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
OperationResult: {success, error?}
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
xpath = self._resolve_locator(locator, locator_type)
|
|
58
|
+
ele = self._page.ele(f'xpath={xpath}')
|
|
59
|
+
if clear:
|
|
60
|
+
ele.clear()
|
|
61
|
+
ele.input(text)
|
|
62
|
+
return OperationResult(success=True)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return OperationResult(success=False, error=str(e))
|
|
65
|
+
|
|
66
|
+
def hover(self, locator: str, locator_type: str | None = None) -> OperationResult:
|
|
67
|
+
"""
|
|
68
|
+
悬停在元素上
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
locator: 定位符(ref / xpath / role / text)
|
|
72
|
+
locator_type: 定位类型
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
OperationResult: {success, error?}
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
xpath = self._resolve_locator(locator, locator_type)
|
|
79
|
+
self._page.ele(f'xpath={xpath}').hover()
|
|
80
|
+
return OperationResult(success=True)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return OperationResult(success=False, error=str(e))
|
|
83
|
+
|
|
84
|
+
def double_click(self, locator: str, locator_type: str | None = None) -> OperationResult:
|
|
85
|
+
"""
|
|
86
|
+
双击元素
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
locator: 定位符(ref / xpath / role / text)
|
|
90
|
+
locator_type: 定位类型
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
OperationResult: {success, error?}
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
xpath = self._resolve_locator(locator, locator_type)
|
|
97
|
+
self._page.ele(f'xpath={xpath}').double_click()
|
|
98
|
+
return OperationResult(success=True)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return OperationResult(success=False, error=str(e))
|
|
101
|
+
|
|
102
|
+
def right_click(self, locator: str, locator_type: str | None = None) -> OperationResult:
|
|
103
|
+
"""
|
|
104
|
+
右键点击元素
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
locator: 定位符(ref / xpath / role / text)
|
|
108
|
+
locator_type: 定位类型
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
OperationResult: {success, error?}
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
xpath = self._resolve_locator(locator, locator_type)
|
|
115
|
+
self._page.ele(f'xpath={xpath}').right_click()
|
|
116
|
+
return OperationResult(success=True)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return OperationResult(success=False, error=str(e))
|
|
119
|
+
|
|
120
|
+
def submit(self, locator: str, locator_type: str | None = None) -> OperationResult:
|
|
121
|
+
"""
|
|
122
|
+
提交表单
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
locator: 定位符(ref / xpath / role / text)
|
|
126
|
+
locator_type: 定位类型
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
OperationResult: {success, error?}
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
xpath = self._resolve_locator(locator, locator_type)
|
|
133
|
+
self._page.ele(f'xpath={xpath}').submit()
|
|
134
|
+
return OperationResult(success=True)
|
|
135
|
+
except Exception as e:
|
|
136
|
+
return OperationResult(success=False, error=str(e))
|
|
137
|
+
|
|
138
|
+
def clear(self, locator: str, locator_type: str | None = None) -> OperationResult:
|
|
139
|
+
"""
|
|
140
|
+
清空输入框
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
locator: 定位符(ref / xpath / role / text)
|
|
144
|
+
locator_type: 定位类型
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
OperationResult: {success, error?}
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
xpath = self._resolve_locator(locator, locator_type)
|
|
151
|
+
self._page.ele(f'xpath={xpath}').clear()
|
|
152
|
+
return OperationResult(success=True)
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return OperationResult(success=False, error=str(e))
|
|
155
|
+
|
|
156
|
+
def send_enter(self) -> None:
|
|
157
|
+
"""发送回车键"""
|
|
158
|
+
self._page.run_js(
|
|
159
|
+
"document.activeElement.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', code: 'Enter', keyCode: 13}));"
|
|
160
|
+
)
|
|
161
|
+
self._page.wait(0.1)
|
|
162
|
+
|
|
163
|
+
def ref_to_xpath(self, ref: str) -> str:
|
|
164
|
+
"""
|
|
165
|
+
通过 ref 获取 xpath(内部方法,供外部调用)
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
ref: 元素引用 ID
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
xpath 路径
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
ElementNotFoundError: 元素不存在
|
|
175
|
+
"""
|
|
176
|
+
if ref not in self._element_map:
|
|
177
|
+
raise ElementNotFoundError(f"Element not found: {ref}")
|
|
178
|
+
return self._element_map[ref].xpath
|
|
179
|
+
|
|
180
|
+
def _resolve_locator(self, locator: str, locator_type: str | None = None) -> str:
|
|
181
|
+
"""
|
|
182
|
+
解析定位符,ref 需要查表转 xpath,其他直接返回
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
locator: 定位符
|
|
186
|
+
locator_type: 定位类型
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
xpath 字符串
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ElementNotFoundError: ref 不存在
|
|
193
|
+
"""
|
|
194
|
+
# 指定了类型
|
|
195
|
+
if locator_type == "ref":
|
|
196
|
+
return self.ref_to_xpath(locator)
|
|
197
|
+
elif locator_type:
|
|
198
|
+
return locator
|
|
199
|
+
|
|
200
|
+
# 自动推断:ref 查表,其他直接返回
|
|
201
|
+
if self._is_ref(locator):
|
|
202
|
+
return self.ref_to_xpath(locator)
|
|
203
|
+
return locator
|
|
204
|
+
|
|
205
|
+
def _is_ref(self, locator: str) -> bool:
|
|
206
|
+
"""
|
|
207
|
+
判断是否为 ref(格式:r + 数字)
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
locator: 定位符
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
是否为 ref
|
|
214
|
+
"""
|
|
215
|
+
if not locator:
|
|
216
|
+
return False
|
|
217
|
+
if locator.startswith("r") and locator[1:].isdigit():
|
|
218
|
+
return True
|
|
219
|
+
return False
|
rootbrowse/exceptions.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""自定义异常"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class RootBrowseError(Exception):
|
|
5
|
+
"""RootBrowse 基异常"""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BrowserError(RootBrowseError):
|
|
10
|
+
"""浏览器相关错误"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ElementNotFoundError(RootBrowseError):
|
|
15
|
+
"""元素未找到"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RegionNotFoundError(RootBrowseError):
|
|
20
|
+
"""区域未找到"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TabNotFoundError(RootBrowseError):
|
|
25
|
+
"""标签页未找到"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OperationError(RootBrowseError):
|
|
30
|
+
"""操作失败"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StateFileError(RootBrowseError):
|
|
35
|
+
"""状态文件相关错误"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PageLoadError(RootBrowseError):
|
|
40
|
+
"""页面加载失败"""
|
|
41
|
+
pass
|