dp-cli 0.1.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.
- dp_cli/__init__.py +1 -0
- dp_cli/commands/__init__.py +12 -0
- dp_cli/commands/_utils.py +107 -0
- dp_cli/commands/browser.py +159 -0
- dp_cli/commands/element.py +259 -0
- dp_cli/commands/keyboard.py +126 -0
- dp_cli/commands/misc.py +136 -0
- dp_cli/commands/network.py +169 -0
- dp_cli/commands/page.py +204 -0
- dp_cli/commands/snapshot_cmd.py +391 -0
- dp_cli/commands/storage.py +222 -0
- dp_cli/commands/tab.py +203 -0
- dp_cli/main.py +47 -0
- dp_cli/output.py +97 -0
- dp_cli/session.py +201 -0
- dp_cli/snapshot/__init__.py +23 -0
- dp_cli/snapshot/a11y.py +671 -0
- dp_cli/snapshot/extract.py +158 -0
- dp_cli/snapshot/js_scripts.py +155 -0
- dp_cli/snapshot/utils.py +43 -0
- dp_cli-0.1.0.dist-info/METADATA +103 -0
- dp_cli-0.1.0.dist-info/RECORD +25 -0
- dp_cli-0.1.0.dist-info/WHEEL +5 -0
- dp_cli-0.1.0.dist-info/entry_points.txt +2 -0
- dp_cli-0.1.0.dist-info/top_level.txt +1 -0
dp_cli/commands/misc.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""杂项命令: resize / maximize / state-save / state-load / config-set"""
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from dp_cli.output import ok, error
|
|
9
|
+
from dp_cli.commands._utils import session_option, _get_page
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(cli):
|
|
13
|
+
|
|
14
|
+
@cli.command('resize')
|
|
15
|
+
@click.argument('width', type=int)
|
|
16
|
+
@click.argument('height', type=int)
|
|
17
|
+
@session_option
|
|
18
|
+
def resize(width, height, session):
|
|
19
|
+
"""调整浏览器窗口大小。
|
|
20
|
+
|
|
21
|
+
\b
|
|
22
|
+
示例:
|
|
23
|
+
dp resize 1920 1080
|
|
24
|
+
dp resize 375 812
|
|
25
|
+
"""
|
|
26
|
+
page = _get_page(session)
|
|
27
|
+
try:
|
|
28
|
+
page.set.window.size(width, height)
|
|
29
|
+
ok({'width': width, 'height': height}, msg='窗口大小已调整')
|
|
30
|
+
except Exception as e:
|
|
31
|
+
error(f'调整窗口大小失败', code='RESIZE_FAILED', detail=str(e))
|
|
32
|
+
|
|
33
|
+
@cli.command('maximize')
|
|
34
|
+
@session_option
|
|
35
|
+
def maximize(session):
|
|
36
|
+
"""最大化浏览器窗口。"""
|
|
37
|
+
page = _get_page(session)
|
|
38
|
+
try:
|
|
39
|
+
page.set.window.max()
|
|
40
|
+
ok(msg='窗口已最大化')
|
|
41
|
+
except Exception as e:
|
|
42
|
+
error(f'最大化失败', code='WINDOW_FAILED', detail=str(e))
|
|
43
|
+
|
|
44
|
+
@cli.command('state-save')
|
|
45
|
+
@click.argument('filename', default='state.json')
|
|
46
|
+
@session_option
|
|
47
|
+
def state_save(filename, session):
|
|
48
|
+
"""保存浏览器状态(Cookie + localStorage)到文件。
|
|
49
|
+
|
|
50
|
+
\b
|
|
51
|
+
示例:
|
|
52
|
+
dp state-save
|
|
53
|
+
dp state-save auth.json
|
|
54
|
+
"""
|
|
55
|
+
page = _get_page(session)
|
|
56
|
+
try:
|
|
57
|
+
state = {
|
|
58
|
+
'cookies': page.cookies(all_domains=True).as_dict(),
|
|
59
|
+
'url': page.url,
|
|
60
|
+
}
|
|
61
|
+
try:
|
|
62
|
+
ls = page.run_js(
|
|
63
|
+
'return JSON.stringify(Object.fromEntries(Object.entries(localStorage)))',
|
|
64
|
+
as_expr=True)
|
|
65
|
+
state['localStorage'] = json.loads(ls) if isinstance(ls, str) else ls or {}
|
|
66
|
+
except Exception:
|
|
67
|
+
state['localStorage'] = {}
|
|
68
|
+
Path(filename).write_text(
|
|
69
|
+
json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
70
|
+
ok({'filename': str(Path(filename).absolute()),
|
|
71
|
+
'cookies_count': len(state['cookies'])}, msg='状态已保存')
|
|
72
|
+
except Exception as e:
|
|
73
|
+
error(f'保存状态失败', code='STATE_FAILED', detail=str(e))
|
|
74
|
+
|
|
75
|
+
@cli.command('state-load')
|
|
76
|
+
@click.argument('filename', default='state.json')
|
|
77
|
+
@session_option
|
|
78
|
+
def state_load(filename, session):
|
|
79
|
+
"""从文件加载浏览器状态(Cookie + localStorage)。
|
|
80
|
+
|
|
81
|
+
\b
|
|
82
|
+
示例:
|
|
83
|
+
dp state-load
|
|
84
|
+
dp state-load auth.json
|
|
85
|
+
"""
|
|
86
|
+
page = _get_page(session)
|
|
87
|
+
try:
|
|
88
|
+
state = json.loads(Path(filename).read_text(encoding='utf-8'))
|
|
89
|
+
if 'cookies' in state:
|
|
90
|
+
for name, value in state['cookies'].items():
|
|
91
|
+
try:
|
|
92
|
+
page.set.cookies({'name': name, 'value': value})
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
if 'localStorage' in state and state['localStorage']:
|
|
96
|
+
for k, v in state['localStorage'].items():
|
|
97
|
+
try:
|
|
98
|
+
page.run_js(f'localStorage.setItem({json.dumps(k)}, {json.dumps(v)})',
|
|
99
|
+
as_expr=True)
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
ok({'filename': filename,
|
|
103
|
+
'cookies_restored': len(state.get('cookies', {}))}, msg='状态已加载')
|
|
104
|
+
except FileNotFoundError:
|
|
105
|
+
error(f'状态文件不存在: {filename}', code='FILE_NOT_FOUND')
|
|
106
|
+
except Exception as e:
|
|
107
|
+
error(f'加载状态失败', code='STATE_FAILED', detail=str(e))
|
|
108
|
+
|
|
109
|
+
@cli.command('config-set')
|
|
110
|
+
@click.option('-p', '--browser-path', default=None, help='设置浏览器路径')
|
|
111
|
+
@click.option('-u', '--user-path', default=None, help='设置用户数据路径')
|
|
112
|
+
@click.option('-c', '--copy-config', is_flag=True, help='复制默认配置文件到当前目录')
|
|
113
|
+
def config_set(browser_path, user_path, copy_config):
|
|
114
|
+
"""修改 DrissionPage 配置文件。
|
|
115
|
+
|
|
116
|
+
\b
|
|
117
|
+
示例:
|
|
118
|
+
dp config-set --browser-path /usr/bin/google-chrome
|
|
119
|
+
dp config-set --user-path /home/user/.chrome-data
|
|
120
|
+
dp config-set --copy-config
|
|
121
|
+
"""
|
|
122
|
+
from DrissionPage._configs.chromium_options import ChromiumOptions
|
|
123
|
+
from DrissionPage._functions.tools import configs_to_here
|
|
124
|
+
|
|
125
|
+
if copy_config:
|
|
126
|
+
configs_to_here()
|
|
127
|
+
ok(msg='配置文件已复制到当前目录')
|
|
128
|
+
|
|
129
|
+
if browser_path or user_path:
|
|
130
|
+
co = ChromiumOptions()
|
|
131
|
+
if browser_path:
|
|
132
|
+
co.set_browser_path(browser_path)
|
|
133
|
+
if user_path:
|
|
134
|
+
co.set_user_data_path(user_path)
|
|
135
|
+
co.save()
|
|
136
|
+
ok(msg='配置已保存')
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""网络命令: listen / listen-stop / http-get / http-post"""
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
from dp_cli.output import ok, error
|
|
9
|
+
from dp_cli.commands._utils import session_option, _get_page
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(cli):
|
|
13
|
+
|
|
14
|
+
@cli.command('listen')
|
|
15
|
+
@session_option
|
|
16
|
+
@click.option('--filter', 'url_filter', default=None, help='URL 过滤关键字,如 "api/user"')
|
|
17
|
+
@click.option('--count', default=10, help='最多捕获请求数', show_default=True)
|
|
18
|
+
@click.option('--timeout', default=30, help='监听超时秒数', show_default=True)
|
|
19
|
+
@click.option('--method', default=None, help='过滤请求方法,如 POST')
|
|
20
|
+
def listen(session, url_filter, count, timeout, method):
|
|
21
|
+
"""监听网络请求(抓包)。先 listen,再执行触发操作,最后 listen-stop 读取结果。
|
|
22
|
+
|
|
23
|
+
\b
|
|
24
|
+
这是 DrissionPage 的核心独特能力,可精确捕获 XHR/Fetch/图片等任意请求。
|
|
25
|
+
|
|
26
|
+
\b
|
|
27
|
+
示例:
|
|
28
|
+
dp listen --filter "api/login"
|
|
29
|
+
dp listen --count 5 --timeout 10
|
|
30
|
+
"""
|
|
31
|
+
page = _get_page(session)
|
|
32
|
+
try:
|
|
33
|
+
targets = url_filter if url_filter else None
|
|
34
|
+
page.listen.start(targets=targets, method=method)
|
|
35
|
+
ok(msg=f'已开始监听,过滤: {url_filter or "全部"}')
|
|
36
|
+
except Exception as e:
|
|
37
|
+
error(f'启动监听失败', code='LISTEN_FAILED', detail=str(e))
|
|
38
|
+
|
|
39
|
+
@cli.command('listen-stop')
|
|
40
|
+
@session_option
|
|
41
|
+
@click.option('--count', default=1, help='等待数据包数量', show_default=True)
|
|
42
|
+
@click.option('--timeout', default=10, help='等待数据超时秒数', show_default=True)
|
|
43
|
+
def listen_stop(session, count, timeout):
|
|
44
|
+
"""停止监听并获取捕获的网络请求数据。"""
|
|
45
|
+
page = _get_page(session)
|
|
46
|
+
try:
|
|
47
|
+
packets = page.listen.wait(count=count, timeout=timeout, fit_count=False)
|
|
48
|
+
results = []
|
|
49
|
+
if packets:
|
|
50
|
+
pkts = packets if isinstance(packets, list) else [packets]
|
|
51
|
+
for pkt in pkts:
|
|
52
|
+
try:
|
|
53
|
+
item = {
|
|
54
|
+
'url': pkt.url,
|
|
55
|
+
'method': pkt.method,
|
|
56
|
+
'status': pkt.response.status if pkt.response else None,
|
|
57
|
+
'type': pkt.resourceType,
|
|
58
|
+
}
|
|
59
|
+
try:
|
|
60
|
+
item['response_body'] = pkt.response.body
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
results.append(item)
|
|
64
|
+
except Exception:
|
|
65
|
+
continue
|
|
66
|
+
page.listen.stop()
|
|
67
|
+
ok({'packets': results, 'count': len(results)})
|
|
68
|
+
except Exception as e:
|
|
69
|
+
error(f'获取监听数据失败', code='LISTEN_FAILED', detail=str(e))
|
|
70
|
+
|
|
71
|
+
@cli.command('http-get')
|
|
72
|
+
@click.argument('url')
|
|
73
|
+
@click.option('--headers', default=None, help='JSON 格式的 Headers')
|
|
74
|
+
@click.option('--proxy', default=None, help='代理地址')
|
|
75
|
+
@click.option('--timeout', default=30, help='超时秒数', show_default=True)
|
|
76
|
+
@click.option('--output', default=None, help='响应体保存路径')
|
|
77
|
+
def http_get(url, headers, proxy, timeout, output):
|
|
78
|
+
"""发送 HTTP GET 请求(不启动浏览器,高效爬虫模式)。
|
|
79
|
+
|
|
80
|
+
\b
|
|
81
|
+
示例:
|
|
82
|
+
dp http-get https://api.example.com/users
|
|
83
|
+
dp http-get https://example.com --output page.html
|
|
84
|
+
dp http-get https://api.example.com --headers '{"Authorization":"Bearer xxx"}'
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
from DrissionPage import SessionPage
|
|
88
|
+
from DrissionPage._configs.session_options import SessionOptions
|
|
89
|
+
|
|
90
|
+
so = SessionOptions()
|
|
91
|
+
if proxy:
|
|
92
|
+
so.set_proxies(http=proxy, https=proxy)
|
|
93
|
+
if headers:
|
|
94
|
+
so.set_headers(json.loads(headers))
|
|
95
|
+
|
|
96
|
+
page = SessionPage(session_or_options=so)
|
|
97
|
+
page.get(url, timeout=timeout)
|
|
98
|
+
|
|
99
|
+
result = {
|
|
100
|
+
'url': page.url,
|
|
101
|
+
'status_code': page.response.status_code if page.response else None,
|
|
102
|
+
}
|
|
103
|
+
try:
|
|
104
|
+
result['body'] = page.response.json()
|
|
105
|
+
except Exception:
|
|
106
|
+
try:
|
|
107
|
+
body_text = page.response.text[:3000]
|
|
108
|
+
except Exception:
|
|
109
|
+
body_text = '<binary>'
|
|
110
|
+
if output:
|
|
111
|
+
Path(output).write_text(page.response.text, encoding='utf-8')
|
|
112
|
+
result['saved_to'] = output
|
|
113
|
+
else:
|
|
114
|
+
result['body'] = body_text
|
|
115
|
+
|
|
116
|
+
ok(result)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
error(f'HTTP GET 失败', code='HTTP_FAILED', detail=str(e))
|
|
119
|
+
|
|
120
|
+
@cli.command('http-post')
|
|
121
|
+
@click.argument('url')
|
|
122
|
+
@click.option('--data', default=None, help='JSON 格式的请求体')
|
|
123
|
+
@click.option('--form', default=None, help='JSON 格式的表单数据')
|
|
124
|
+
@click.option('--headers', default=None, help='JSON 格式的 Headers')
|
|
125
|
+
@click.option('--proxy', default=None, help='代理地址')
|
|
126
|
+
@click.option('--timeout', default=30, help='超时秒数', show_default=True)
|
|
127
|
+
def http_post(url, data, form, headers, proxy, timeout):
|
|
128
|
+
"""发送 HTTP POST 请求(不启动浏览器)。
|
|
129
|
+
|
|
130
|
+
\b
|
|
131
|
+
示例:
|
|
132
|
+
dp http-post https://api.example.com/login --data '{"user":"admin","pass":"123"}'
|
|
133
|
+
dp http-post https://example.com/form --form '{"field":"value"}'
|
|
134
|
+
"""
|
|
135
|
+
try:
|
|
136
|
+
from DrissionPage import SessionPage
|
|
137
|
+
from DrissionPage._configs.session_options import SessionOptions
|
|
138
|
+
|
|
139
|
+
so = SessionOptions()
|
|
140
|
+
if proxy:
|
|
141
|
+
so.set_proxies(http=proxy, https=proxy)
|
|
142
|
+
if headers:
|
|
143
|
+
so.set_headers(json.loads(headers))
|
|
144
|
+
|
|
145
|
+
page = SessionPage(session_or_options=so)
|
|
146
|
+
|
|
147
|
+
kwargs = {'timeout': timeout}
|
|
148
|
+
if data:
|
|
149
|
+
kwargs['json'] = json.loads(data)
|
|
150
|
+
elif form:
|
|
151
|
+
kwargs['data'] = json.loads(form)
|
|
152
|
+
|
|
153
|
+
page.post(url, **kwargs)
|
|
154
|
+
|
|
155
|
+
result = {
|
|
156
|
+
'url': page.url,
|
|
157
|
+
'status_code': page.response.status_code if page.response else None,
|
|
158
|
+
}
|
|
159
|
+
try:
|
|
160
|
+
result['body'] = page.response.json()
|
|
161
|
+
except Exception:
|
|
162
|
+
try:
|
|
163
|
+
result['body'] = page.response.text[:3000]
|
|
164
|
+
except Exception:
|
|
165
|
+
result['body'] = '<binary>'
|
|
166
|
+
|
|
167
|
+
ok(result)
|
|
168
|
+
except Exception as e:
|
|
169
|
+
error(f'HTTP POST 失败', code='HTTP_FAILED', detail=str(e))
|
dp_cli/commands/page.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""页面操作命令: screenshot / pdf / eval / add-init-js / dialog-accept / dialog-dismiss / wait"""
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from time import perf_counter, sleep as _sleep
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
9
|
+
from dp_cli.output import ok, error, format_page_info
|
|
10
|
+
from dp_cli.commands._utils import session_option, _get_page
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def register(cli):
|
|
14
|
+
|
|
15
|
+
@cli.command('screenshot')
|
|
16
|
+
@session_option
|
|
17
|
+
@click.option('--locator', default=None, help='截图特定元素')
|
|
18
|
+
@click.option('--filename', default=None, help='保存路径')
|
|
19
|
+
@click.option('--full-page', is_flag=True, help='截取完整页面(包括视口外)')
|
|
20
|
+
@click.option('--format', 'fmt', type=click.Choice(['png', 'jpg', 'jpeg']),
|
|
21
|
+
default='png', show_default=True)
|
|
22
|
+
def cmd_screenshot(session, locator, filename, full_page, fmt):
|
|
23
|
+
"""截图。DrissionPage 支持完整页面截图(含视口外内容)。
|
|
24
|
+
|
|
25
|
+
\b
|
|
26
|
+
示例:
|
|
27
|
+
dp screenshot
|
|
28
|
+
dp screenshot --filename page.png
|
|
29
|
+
dp screenshot --full-page
|
|
30
|
+
dp screenshot --locator "#header"
|
|
31
|
+
"""
|
|
32
|
+
page = _get_page(session)
|
|
33
|
+
try:
|
|
34
|
+
if not filename:
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
37
|
+
filename = f'screenshot_{ts}.{fmt}'
|
|
38
|
+
if locator:
|
|
39
|
+
ele = page.ele(locator)
|
|
40
|
+
if not ele or ele.__class__.__name__ == 'NoneElement':
|
|
41
|
+
error(f'未找到元素: {locator}', code='ELEMENT_NOT_FOUND')
|
|
42
|
+
return
|
|
43
|
+
ele.get_screenshot(path=filename)
|
|
44
|
+
else:
|
|
45
|
+
page.get_screenshot(path=filename, full_page=full_page)
|
|
46
|
+
ok({'filename': str(Path(filename).absolute())}, msg='截图已保存')
|
|
47
|
+
except Exception as e:
|
|
48
|
+
error(f'截图失败', code='SCREENSHOT_FAILED', detail=str(e))
|
|
49
|
+
|
|
50
|
+
@cli.command('pdf')
|
|
51
|
+
@session_option
|
|
52
|
+
@click.option('--filename', default=None, help='保存路径')
|
|
53
|
+
def cmd_pdf(session, filename):
|
|
54
|
+
"""将当前页面保存为 PDF。
|
|
55
|
+
|
|
56
|
+
\b
|
|
57
|
+
示例:
|
|
58
|
+
dp pdf
|
|
59
|
+
dp pdf --filename output.pdf
|
|
60
|
+
"""
|
|
61
|
+
page = _get_page(session)
|
|
62
|
+
try:
|
|
63
|
+
if not filename:
|
|
64
|
+
from datetime import datetime
|
|
65
|
+
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
|
66
|
+
filename = f'page_{ts}.pdf'
|
|
67
|
+
result = page.run_cdp('Page.printToPDF', transferMode='ReturnAsBase64')
|
|
68
|
+
import base64
|
|
69
|
+
Path(filename).write_bytes(base64.b64decode(result['data']))
|
|
70
|
+
ok({'filename': str(Path(filename).absolute())}, msg='PDF 已保存')
|
|
71
|
+
except Exception as e:
|
|
72
|
+
error(f'保存 PDF 失败', code='PDF_FAILED', detail=str(e))
|
|
73
|
+
|
|
74
|
+
@cli.command('eval')
|
|
75
|
+
@click.argument('script')
|
|
76
|
+
@session_option
|
|
77
|
+
@click.option('--locator', default=None, help='在特定元素上执行(this 指向该元素)')
|
|
78
|
+
@click.option('--timeout', default=30, help='执行超时秒数', show_default=True)
|
|
79
|
+
def cmd_eval(script, session, locator, timeout):
|
|
80
|
+
"""执行 JavaScript 并返回结果。
|
|
81
|
+
|
|
82
|
+
\b
|
|
83
|
+
示例:
|
|
84
|
+
dp eval "document.title"
|
|
85
|
+
dp eval "window.innerWidth"
|
|
86
|
+
dp eval "el => el.textContent" --locator "#header"
|
|
87
|
+
dp eval "return Array.from(document.links).map(l=>l.href)"
|
|
88
|
+
"""
|
|
89
|
+
page = _get_page(session)
|
|
90
|
+
try:
|
|
91
|
+
if locator:
|
|
92
|
+
ele = page.ele(locator, timeout=timeout)
|
|
93
|
+
if not ele or ele.__class__.__name__ == 'NoneElement':
|
|
94
|
+
error(f'未找到元素: {locator}', code='ELEMENT_NOT_FOUND')
|
|
95
|
+
return
|
|
96
|
+
if script.strip().startswith(('el =>', 'el=>', 'function')):
|
|
97
|
+
result = ele.run_js(f'return ({script})(this)', timeout=timeout)
|
|
98
|
+
else:
|
|
99
|
+
result = ele.run_js(script, as_expr=True, timeout=timeout)
|
|
100
|
+
else:
|
|
101
|
+
if script.strip().startswith(('return ', 'function')):
|
|
102
|
+
result = page.run_js(script, timeout=timeout)
|
|
103
|
+
else:
|
|
104
|
+
result = page.run_js(script, as_expr=True, timeout=timeout)
|
|
105
|
+
ok({'result': result})
|
|
106
|
+
except Exception as e:
|
|
107
|
+
error(f'JavaScript 执行失败', code='JS_FAILED', detail=str(e))
|
|
108
|
+
|
|
109
|
+
@cli.command('add-init-js')
|
|
110
|
+
@click.argument('script')
|
|
111
|
+
@session_option
|
|
112
|
+
def add_init_js(script, session):
|
|
113
|
+
"""添加在每个新页面加载前执行的 JS 脚本(反检测/环境修改等)。
|
|
114
|
+
|
|
115
|
+
\b
|
|
116
|
+
示例:
|
|
117
|
+
dp add-init-js "Object.defineProperty(navigator,'webdriver',{get:()=>undefined})"
|
|
118
|
+
dp add-init-js "window.__dp_cli = true"
|
|
119
|
+
"""
|
|
120
|
+
page = _get_page(session)
|
|
121
|
+
try:
|
|
122
|
+
js_id = page.add_init_js(script)
|
|
123
|
+
ok({'id': js_id, 'script': script[:100]}, msg='初始化脚本已添加')
|
|
124
|
+
except Exception as e:
|
|
125
|
+
error(f'添加初始化脚本失败', code='INIT_JS_FAILED', detail=str(e))
|
|
126
|
+
|
|
127
|
+
@cli.command('dialog-accept')
|
|
128
|
+
@click.argument('text', required=False)
|
|
129
|
+
@session_option
|
|
130
|
+
def dialog_accept(text, session):
|
|
131
|
+
"""接受(确认)对话框。可选传入 prompt 输入内容。
|
|
132
|
+
|
|
133
|
+
\b
|
|
134
|
+
示例:
|
|
135
|
+
dp dialog-accept
|
|
136
|
+
dp dialog-accept "确认文本"
|
|
137
|
+
"""
|
|
138
|
+
page = _get_page(session)
|
|
139
|
+
try:
|
|
140
|
+
page.handle_alert(accept=True, send=text)
|
|
141
|
+
ok(msg='对话框已接受')
|
|
142
|
+
except Exception as e:
|
|
143
|
+
error(f'处理对话框失败', code='DIALOG_FAILED', detail=str(e))
|
|
144
|
+
|
|
145
|
+
@cli.command('dialog-dismiss')
|
|
146
|
+
@session_option
|
|
147
|
+
def dialog_dismiss(session):
|
|
148
|
+
"""取消对话框。"""
|
|
149
|
+
page = _get_page(session)
|
|
150
|
+
try:
|
|
151
|
+
page.handle_alert(accept=False)
|
|
152
|
+
ok(msg='对话框已取消')
|
|
153
|
+
except Exception as e:
|
|
154
|
+
error(f'处理对话框失败', code='DIALOG_FAILED', detail=str(e))
|
|
155
|
+
|
|
156
|
+
@cli.command('wait')
|
|
157
|
+
@session_option
|
|
158
|
+
@click.option('--url', default=None, help='等待 URL 变为此值(支持子串匹配)')
|
|
159
|
+
@click.option('--locator', default=None, help='等待元素出现')
|
|
160
|
+
@click.option('--locator-gone', default=None, help='等待元素消失')
|
|
161
|
+
@click.option('--text', default=None, help='等待页面包含指定文本')
|
|
162
|
+
@click.option('--loaded', is_flag=True, help='等待页面加载完成')
|
|
163
|
+
@click.option('--timeout', default=30, help='超时秒数', show_default=True)
|
|
164
|
+
def wait(session, url, locator, locator_gone, text, loaded, timeout):
|
|
165
|
+
"""等待条件满足。
|
|
166
|
+
|
|
167
|
+
\b
|
|
168
|
+
示例:
|
|
169
|
+
dp wait --loaded
|
|
170
|
+
dp wait --locator "#result"
|
|
171
|
+
dp wait --url "success"
|
|
172
|
+
dp wait --text "操作成功"
|
|
173
|
+
dp wait --locator-gone "css:.loading"
|
|
174
|
+
"""
|
|
175
|
+
page = _get_page(session)
|
|
176
|
+
try:
|
|
177
|
+
if loaded:
|
|
178
|
+
page.wait.doc_loaded()
|
|
179
|
+
ok(format_page_info(page), msg='页面已加载')
|
|
180
|
+
elif url:
|
|
181
|
+
page.wait.url_change(url, timeout=timeout)
|
|
182
|
+
ok(format_page_info(page), msg='URL 已变化')
|
|
183
|
+
elif locator:
|
|
184
|
+
ele = page.wait.ele_displayed(locator, timeout=timeout)
|
|
185
|
+
ok({'locator': locator, 'found': bool(ele)}, msg='元素已出现')
|
|
186
|
+
elif locator_gone:
|
|
187
|
+
page.wait.ele_hidden(locator_gone, timeout=timeout)
|
|
188
|
+
ok({'locator': locator_gone}, msg='元素已消失')
|
|
189
|
+
elif text:
|
|
190
|
+
end = perf_counter() + timeout
|
|
191
|
+
found = False
|
|
192
|
+
while perf_counter() < end:
|
|
193
|
+
if text in page.html:
|
|
194
|
+
found = True
|
|
195
|
+
break
|
|
196
|
+
_sleep(0.3)
|
|
197
|
+
if found:
|
|
198
|
+
ok({'text': text}, msg='文本已出现')
|
|
199
|
+
else:
|
|
200
|
+
error(f'等待超时:文本未出现 "{text}"', code='WAIT_TIMEOUT')
|
|
201
|
+
else:
|
|
202
|
+
error('请至少指定一个等待条件', code='INVALID_ARGS')
|
|
203
|
+
except Exception as e:
|
|
204
|
+
error(f'等待失败', code='WAIT_FAILED', detail=str(e))
|