drissionpage-cli 0.1.2__tar.gz → 0.1.4__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.
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/PKG-INFO +1 -1
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/__init__.py +37 -16
- drissionpage_cli-0.1.4/drissionpage_cli/__main__.py +3 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/PKG-INFO +1 -1
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/SOURCES.txt +1 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/pyproject.toml +1 -1
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/tests/test_integration.py +31 -17
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/tests/test_unit.py +51 -5
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/LICENSE +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/README.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/SKILL.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/references/dual-mode.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/references/element-locators.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/references/network-listening.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/references/running-code.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/references/screenshots-pdf.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/references/session-management.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/references/storage-state.md +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/dependency_links.txt +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/entry_points.txt +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/requires.txt +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/top_level.txt +0 -0
- {drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/setup.cfg +0 -0
|
@@ -15,7 +15,7 @@ import time
|
|
|
15
15
|
import traceback
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
|
-
__version__ = "0.1.
|
|
18
|
+
__version__ = "0.1.4"
|
|
19
19
|
|
|
20
20
|
# Session storage directory
|
|
21
21
|
CLI_DIR = Path(os.environ.get("DRISSIONPAGE_CLI_DIR", ".drissionpage-cli"))
|
|
@@ -72,27 +72,45 @@ def _get_page(session_name, create=False, options=None):
|
|
|
72
72
|
so that multiple named sessions can coexist.
|
|
73
73
|
"""
|
|
74
74
|
from DrissionPage import ChromiumPage, ChromiumOptions
|
|
75
|
+
from DrissionPage._functions.tools import port_is_using
|
|
75
76
|
|
|
76
77
|
sessions = _load_sessions()
|
|
77
78
|
info = sessions.get(session_name)
|
|
78
79
|
|
|
79
80
|
if info:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
#
|
|
81
|
+
address = info["address"]
|
|
82
|
+
ip, port_str = address.split(":")
|
|
83
|
+
if port_is_using(ip, port_str):
|
|
84
|
+
# Browser is alive — connect to it.
|
|
85
|
+
# We must restore the headless state that was used when the session was
|
|
86
|
+
# created. If we don't, DrissionPage detects a headless/headed mismatch
|
|
87
|
+
# and calls Browser.close() to kill Chrome before relaunching it —
|
|
88
|
+
# which then fails for a reconnect-only scenario.
|
|
89
|
+
co = ChromiumOptions()
|
|
90
|
+
co.set_address(address)
|
|
91
|
+
co.headless(info.get("headless", True))
|
|
88
92
|
try:
|
|
89
|
-
page
|
|
93
|
+
page = ChromiumPage(addr_or_opts=co)
|
|
94
|
+
if not create:
|
|
95
|
+
return page
|
|
96
|
+
# create=True but session is alive — close old one first
|
|
97
|
+
try:
|
|
98
|
+
page.quit()
|
|
99
|
+
except Exception:
|
|
100
|
+
pass
|
|
101
|
+
_kill_session(sessions, session_name)
|
|
102
|
+
sessions = _load_sessions()
|
|
90
103
|
except Exception:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
_kill_session(sessions, session_name)
|
|
105
|
+
sessions = _load_sessions()
|
|
106
|
+
if not create:
|
|
107
|
+
raise RuntimeError(
|
|
108
|
+
f"Session '{session_name}' is no longer running. "
|
|
109
|
+
f"Use 'open' to start a new one."
|
|
110
|
+
)
|
|
111
|
+
else:
|
|
112
|
+
# Port not in use — browser is dead, clean up without trying to connect
|
|
113
|
+
# (avoids DrissionPage launching a phantom Chrome on the stale port).
|
|
96
114
|
_kill_session(sessions, session_name)
|
|
97
115
|
sessions = _load_sessions()
|
|
98
116
|
if not create:
|
|
@@ -111,7 +129,9 @@ def _get_page(session_name, create=False, options=None):
|
|
|
111
129
|
|
|
112
130
|
use_system_user_path = options and options.get("system_user_path")
|
|
113
131
|
if use_system_user_path:
|
|
114
|
-
#
|
|
132
|
+
# auto_port picks a free port; use_system_user_path then causes DrissionPage
|
|
133
|
+
# to strip the --user-data-dir arg at launch, so Chrome uses the system profile.
|
|
134
|
+
co.auto_port()
|
|
115
135
|
co.use_system_user_path(True)
|
|
116
136
|
else:
|
|
117
137
|
co.auto_port() # pick a free port to avoid conflicts with stale browsers
|
|
@@ -140,6 +160,7 @@ def _get_page(session_name, create=False, options=None):
|
|
|
140
160
|
"address": page.address,
|
|
141
161
|
"pid": page.process_id,
|
|
142
162
|
"started": time.time(),
|
|
163
|
+
"headless": options.get("headless", True) if options else True,
|
|
143
164
|
}
|
|
144
165
|
_save_sessions(sessions)
|
|
145
166
|
return page
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "drissionpage-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "DrissionPage CLI - Token-efficient browser automation for coding agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -21,7 +21,7 @@ from pathlib import Path
|
|
|
21
21
|
|
|
22
22
|
import pytest
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
CLI_ROOT = Path(__file__).resolve().parent.parent
|
|
25
25
|
|
|
26
26
|
# Skip all integration tests if no browser or explicitly skipped
|
|
27
27
|
SKIP_INTEGRATION = os.environ.get("SKIP_INTEGRATION", "0") == "1"
|
|
@@ -33,10 +33,11 @@ pytestmark = pytest.mark.skipif(
|
|
|
33
33
|
def run_cli(*args, timeout=60, cwd=None, env_extra=None):
|
|
34
34
|
"""Run drissionpage-cli and return (returncode, stdout, stderr)."""
|
|
35
35
|
env = os.environ.copy()
|
|
36
|
+
env["PYTHONPATH"] = str(CLI_ROOT) + os.pathsep + env.get("PYTHONPATH", "")
|
|
36
37
|
if env_extra:
|
|
37
38
|
env.update(env_extra)
|
|
38
39
|
|
|
39
|
-
cmd = [sys.executable,
|
|
40
|
+
cmd = [sys.executable, "-m", "drissionpage_cli"] + list(args)
|
|
40
41
|
proc = subprocess.run(
|
|
41
42
|
cmd, capture_output=True, text=True, timeout=timeout, env=env, cwd=cwd
|
|
42
43
|
)
|
|
@@ -53,6 +54,15 @@ def isolated_env(tmp_path):
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
|
|
57
|
+
@pytest.fixture
|
|
58
|
+
def storage_url(tmp_path):
|
|
59
|
+
"""A file:// URL that supports localStorage and sessionStorage.
|
|
60
|
+
data: URLs are null-origin in Chrome and block storage access."""
|
|
61
|
+
html_file = tmp_path / "storage_test.html"
|
|
62
|
+
html_file.write_text("<p>Storage Test</p>")
|
|
63
|
+
return html_file.as_uri()
|
|
64
|
+
|
|
65
|
+
|
|
56
66
|
@pytest.fixture(autouse=True)
|
|
57
67
|
def cleanup_after_test(isolated_env):
|
|
58
68
|
"""Ensure browser is closed after each test."""
|
|
@@ -73,7 +83,7 @@ class TestIntegrationBasic:
|
|
|
73
83
|
def test_version(self):
|
|
74
84
|
rc, stdout, stderr = run_cli("--version")
|
|
75
85
|
assert rc == 0
|
|
76
|
-
assert "0.1.
|
|
86
|
+
assert "0.1.4" in stdout
|
|
77
87
|
|
|
78
88
|
def test_help(self):
|
|
79
89
|
rc, stdout, stderr = run_cli("--help")
|
|
@@ -215,10 +225,10 @@ class TestIntegrationBrowser:
|
|
|
215
225
|
rc, stdout, stderr = run_cli("reload", env_extra=isolated_env)
|
|
216
226
|
assert rc == 0
|
|
217
227
|
|
|
218
|
-
def test_localstorage_operations(self, isolated_env):
|
|
228
|
+
def test_localstorage_operations(self, isolated_env, storage_url):
|
|
219
229
|
"""Test localStorage set, get, list, delete, clear."""
|
|
220
|
-
|
|
221
|
-
run_cli("open",
|
|
230
|
+
# data: URLs are null-origin and block localStorage; use file:// instead.
|
|
231
|
+
run_cli("open", storage_url, env_extra=isolated_env)
|
|
222
232
|
|
|
223
233
|
# Set
|
|
224
234
|
rc, stdout, _ = run_cli(
|
|
@@ -250,10 +260,10 @@ class TestIntegrationBrowser:
|
|
|
250
260
|
rc, stdout, _ = run_cli("localstorage-clear", env_extra=isolated_env)
|
|
251
261
|
assert rc == 0
|
|
252
262
|
|
|
253
|
-
def test_sessionstorage_operations(self, isolated_env):
|
|
263
|
+
def test_sessionstorage_operations(self, isolated_env, storage_url):
|
|
254
264
|
"""Test sessionStorage set, get, clear."""
|
|
255
|
-
|
|
256
|
-
run_cli("open",
|
|
265
|
+
# data: URLs are null-origin and block sessionStorage; use file:// instead.
|
|
266
|
+
run_cli("open", storage_url, env_extra=isolated_env)
|
|
257
267
|
|
|
258
268
|
rc, stdout, _ = run_cli(
|
|
259
269
|
"sessionstorage-set", "step", "5", env_extra=isolated_env
|
|
@@ -271,10 +281,10 @@ class TestIntegrationBrowser:
|
|
|
271
281
|
)
|
|
272
282
|
assert rc == 0
|
|
273
283
|
|
|
274
|
-
def test_state_save_and_load(self, isolated_env, tmp_path):
|
|
284
|
+
def test_state_save_and_load(self, isolated_env, storage_url, tmp_path):
|
|
275
285
|
"""Save and load browser state."""
|
|
276
|
-
|
|
277
|
-
run_cli("open",
|
|
286
|
+
# data: URLs are null-origin and block localStorage; use file:// instead.
|
|
287
|
+
run_cli("open", storage_url, env_extra=isolated_env)
|
|
278
288
|
|
|
279
289
|
# Set some storage
|
|
280
290
|
run_cli("localstorage-set", "saved_key", "saved_val", env_extra=isolated_env)
|
|
@@ -371,14 +381,18 @@ class TestIntegrationBrowser:
|
|
|
371
381
|
assert "resized" in stdout.lower()
|
|
372
382
|
|
|
373
383
|
def test_dialog_handling(self, isolated_env):
|
|
374
|
-
"""Test dialog accept/dismiss."""
|
|
384
|
+
"""Test dialog accept/dismiss in a single subprocess to avoid losing dialog state."""
|
|
375
385
|
html = "data:text/html,<button id='a' onclick='alert(\"hi\")'>Alert</button>"
|
|
376
386
|
run_cli("open", html, env_extra=isolated_env)
|
|
377
387
|
|
|
378
|
-
# Click
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
# Click and dismiss must happen in the same subprocess — the alert dialog is
|
|
389
|
+
# lost between separate CLI invocations because headless Chrome doesn't persist
|
|
390
|
+
# pending dialog state across CDP reconnections.
|
|
391
|
+
rc, stdout, _ = run_cli(
|
|
392
|
+
"run-code",
|
|
393
|
+
"import time; page.ele('@id=a').click(); time.sleep(0.3); page.handle_alert(accept=False)",
|
|
394
|
+
env_extra=isolated_env,
|
|
395
|
+
)
|
|
382
396
|
assert rc == 0
|
|
383
397
|
|
|
384
398
|
def test_hover(self, isolated_env):
|
|
@@ -26,7 +26,7 @@ import drissionpage_cli as cli
|
|
|
26
26
|
|
|
27
27
|
class TestVersionAndHelp:
|
|
28
28
|
def test_version_string(self):
|
|
29
|
-
assert cli.__version__ == "0.1.
|
|
29
|
+
assert cli.__version__ == "0.1.4"
|
|
30
30
|
|
|
31
31
|
def test_build_parser(self):
|
|
32
32
|
parser = cli.build_parser()
|
|
@@ -473,7 +473,7 @@ class TestCliInvocation:
|
|
|
473
473
|
|
|
474
474
|
result = run_cli("--version")
|
|
475
475
|
assert result.exit_code == 0
|
|
476
|
-
assert "0.1.
|
|
476
|
+
assert "0.1.4" in result.output
|
|
477
477
|
|
|
478
478
|
def test_no_command_shows_help(self):
|
|
479
479
|
"""No command shows help text."""
|
|
@@ -619,11 +619,11 @@ class TestGetPageOptions:
|
|
|
619
619
|
self._run_get_page({"system_user_path": True}, tmp_path, mock_co, mock_page)
|
|
620
620
|
mock_co.use_system_user_path.assert_called_once_with(True)
|
|
621
621
|
|
|
622
|
-
def
|
|
622
|
+
def test_system_user_path_also_calls_auto_port(self, tmp_path):
|
|
623
623
|
mock_co = self._make_mock_co()
|
|
624
624
|
mock_page = MagicMock()
|
|
625
625
|
self._run_get_page({"system_user_path": True}, tmp_path, mock_co, mock_page)
|
|
626
|
-
mock_co.auto_port.
|
|
626
|
+
mock_co.auto_port.assert_called_once()
|
|
627
627
|
|
|
628
628
|
def test_no_system_user_path_calls_auto_port(self, tmp_path):
|
|
629
629
|
mock_co = self._make_mock_co()
|
|
@@ -639,6 +639,52 @@ class TestGetPageOptions:
|
|
|
639
639
|
mock_co.use_system_user_path.assert_called_once_with(True)
|
|
640
640
|
mock_co.headless.assert_called_once_with(False)
|
|
641
641
|
|
|
642
|
+
def test_stale_session_dead_port_cleaned_without_connect(self, tmp_path):
|
|
643
|
+
"""When session port is not in use, we must clean up WITHOUT calling
|
|
644
|
+
ChromiumPage (which would launch a phantom Chrome on the stale port)."""
|
|
645
|
+
sessions_file = tmp_path / "sessions.json"
|
|
646
|
+
cli_dir_path = tmp_path / ".drissionpage-cli"
|
|
647
|
+
cli_dir_path.mkdir()
|
|
648
|
+
sessions_file.write_text(
|
|
649
|
+
'{"default": {"address": "127.0.0.1:9222", "pid": 99999, "started": 0}}'
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
mock_page = MagicMock()
|
|
653
|
+
with patch.object(cli, "SESSIONS_FILE", sessions_file), \
|
|
654
|
+
patch.object(cli, "CLI_DIR", cli_dir_path), \
|
|
655
|
+
patch("DrissionPage._functions.tools.port_is_using", return_value=False), \
|
|
656
|
+
patch("DrissionPage.ChromiumPage") as mock_chromium_page:
|
|
657
|
+
try:
|
|
658
|
+
cli._get_page("default", create=False, options=None)
|
|
659
|
+
except RuntimeError:
|
|
660
|
+
pass
|
|
661
|
+
# ChromiumPage must NOT be called — no phantom Chrome launch
|
|
662
|
+
mock_chromium_page.assert_not_called()
|
|
663
|
+
|
|
664
|
+
def test_reconnect_restores_headless_state(self, tmp_path):
|
|
665
|
+
"""When reconnecting to a live session, headless state from session info
|
|
666
|
+
must be passed to ChromiumOptions to prevent DrissionPage restarting Chrome."""
|
|
667
|
+
sessions_file = tmp_path / "sessions.json"
|
|
668
|
+
cli_dir_path = tmp_path / ".drissionpage-cli"
|
|
669
|
+
cli_dir_path.mkdir()
|
|
670
|
+
sessions_file.write_text(
|
|
671
|
+
'{"default": {"address": "127.0.0.1:9333", "pid": 111, "started": 0, "headless": true}}'
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
mock_co = MagicMock()
|
|
675
|
+
mock_page = MagicMock()
|
|
676
|
+
mock_page.address = "127.0.0.1:9333"
|
|
677
|
+
mock_page.process_id = 111
|
|
678
|
+
|
|
679
|
+
with patch.object(cli, "SESSIONS_FILE", sessions_file), \
|
|
680
|
+
patch.object(cli, "CLI_DIR", cli_dir_path), \
|
|
681
|
+
patch("DrissionPage._functions.tools.port_is_using", return_value=True), \
|
|
682
|
+
patch("DrissionPage.ChromiumOptions", return_value=mock_co), \
|
|
683
|
+
patch("DrissionPage.ChromiumPage", return_value=mock_page):
|
|
684
|
+
cli._get_page("default", create=False, options=None)
|
|
685
|
+
|
|
686
|
+
mock_co.headless.assert_called_once_with(True)
|
|
687
|
+
|
|
642
688
|
|
|
643
689
|
# ---------------------------------------------------------------------------
|
|
644
690
|
# Command handlers (mocked browser)
|
|
@@ -942,7 +988,7 @@ class TestCommandHandlersMocked:
|
|
|
942
988
|
args.session = None
|
|
943
989
|
args.domain = None
|
|
944
990
|
cli.cmd_cookie_list(args)
|
|
945
|
-
mock_page.cookies.assert_called_once_with(
|
|
991
|
+
mock_page.cookies.assert_called_once_with(all_info=True)
|
|
946
992
|
|
|
947
993
|
def test_cmd_cookie_list_filtered(self, tmp_path):
|
|
948
994
|
mock_page = self._make_mock_page()
|
|
File without changes
|
|
File without changes
|
{drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli/skills/drissionpage-cli/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{drissionpage_cli-0.1.2 → drissionpage_cli-0.1.4}/drissionpage_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|