patchright-cli 0.2.0__py3-none-any.whl → 0.3.1__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.
- patchright_cli/__init__.py +1 -1
- patchright_cli/cli.py +55 -15
- patchright_cli/daemon.py +198 -58
- patchright_cli/snapshot.py +132 -28
- {patchright_cli-0.2.0.dist-info → patchright_cli-0.3.1.dist-info}/METADATA +30 -16
- patchright_cli-0.3.1.dist-info/RECORD +10 -0
- patchright_cli-0.2.0.dist-info/RECORD +0 -10
- {patchright_cli-0.2.0.dist-info → patchright_cli-0.3.1.dist-info}/WHEEL +0 -0
- {patchright_cli-0.2.0.dist-info → patchright_cli-0.3.1.dist-info}/entry_points.txt +0 -0
- {patchright_cli-0.2.0.dist-info → patchright_cli-0.3.1.dist-info}/licenses/LICENSE +0 -0
patchright_cli/__init__.py
CHANGED
patchright_cli/cli.py
CHANGED
|
@@ -64,14 +64,14 @@ COMMANDS_HELP = {
|
|
|
64
64
|
"goto": "goto <url> Navigate to URL",
|
|
65
65
|
"click": "click <ref> [button] Click element [--modifiers=Alt,Shift]",
|
|
66
66
|
"dblclick": "dblclick <ref> [btn] Double-click [--modifiers=Alt,Shift]",
|
|
67
|
-
"fill": "fill <ref> <value> Fill text into element",
|
|
68
|
-
"type": "type <text> Type text via keyboard",
|
|
67
|
+
"fill": "fill <ref> <value> Fill text into element [--submit]",
|
|
68
|
+
"type": "type <text> Type text via keyboard [--submit]",
|
|
69
69
|
"hover": "hover <ref> Hover over element",
|
|
70
70
|
"select": "select <ref> <value> Select dropdown option",
|
|
71
71
|
"check": "check <ref> Check checkbox/radio",
|
|
72
72
|
"uncheck": "uncheck <ref> Uncheck checkbox/radio",
|
|
73
|
-
"snapshot": "snapshot
|
|
74
|
-
"eval": "eval <expr> Evaluate JavaScript",
|
|
73
|
+
"snapshot": "snapshot [ref] Take accessibility snapshot",
|
|
74
|
+
"eval": "eval <expr> Evaluate JavaScript [--file=F or stdin]",
|
|
75
75
|
"screenshot": "screenshot [ref] Save screenshot [--full-page] [--filename=F]",
|
|
76
76
|
"drag": "drag <from> <to> Drag element to target",
|
|
77
77
|
"close": "close Close browser session",
|
|
@@ -120,22 +120,23 @@ COMMANDS_HELP = {
|
|
|
120
120
|
"sessionstorage-delete": "sessionstorage-delete <k> Delete sessionStorage item",
|
|
121
121
|
"sessionstorage-clear": "sessionstorage-clear Clear sessionStorage",
|
|
122
122
|
# Route
|
|
123
|
-
"route": "route <pattern> [--status=N] [--body=S] [--
|
|
123
|
+
"route": "route <pattern> [--status=N] [--body=S] [--content-type=T] [--header=K:V] Mock requests",
|
|
124
124
|
"route-list": "route-list List active routes",
|
|
125
125
|
"unroute": "unroute [pattern] Remove route(s)",
|
|
126
|
+
"network-state-set": "network-state-set <state> Set online/offline (online|offline)",
|
|
126
127
|
# Run code
|
|
127
|
-
"run-code": "run-code <code> Run raw JS in page context",
|
|
128
|
+
"run-code": "run-code <code> Run raw JS in page context [--file=F or stdin]",
|
|
128
129
|
# Tracing
|
|
129
130
|
"tracing-start": "tracing-start Start Playwright tracing",
|
|
130
131
|
"tracing-stop": "tracing-stop Stop tracing and save",
|
|
131
132
|
# Video
|
|
132
133
|
"video-start": "video-start Start video recording",
|
|
133
|
-
"video-stop": "video-stop
|
|
134
|
+
"video-stop": "video-stop Stop recording and save [--filename=F]",
|
|
134
135
|
# PDF
|
|
135
136
|
"pdf": "pdf [--filename=F] Save page as PDF",
|
|
136
137
|
# DevTools
|
|
137
|
-
"console": "console [level] Show console messages",
|
|
138
|
-
"network": "network Show network requests",
|
|
138
|
+
"console": "console [level] Show console messages [--clear]",
|
|
139
|
+
"network": "network Show network requests [--static] [--clear]",
|
|
139
140
|
# Session
|
|
140
141
|
"list": "list List sessions",
|
|
141
142
|
"close-all": "close-all Close all sessions",
|
|
@@ -153,6 +154,7 @@ def _print_help():
|
|
|
153
154
|
click.echo(" --headless Run headless (default: headed)")
|
|
154
155
|
click.echo(" --persistent Use persistent profile")
|
|
155
156
|
click.echo(" --profile=<path> Custom profile directory")
|
|
157
|
+
click.echo(" --proxy=<url> Proxy server (e.g. http://host:port, socks5://host:port)")
|
|
156
158
|
click.echo(" -s=<name> Named session (default: 'default')")
|
|
157
159
|
click.echo(" --port=<n> Daemon port (default: 9321)")
|
|
158
160
|
click.echo(" --version Show version")
|
|
@@ -207,7 +209,7 @@ def _print_help():
|
|
|
207
209
|
"sessionstorage-clear",
|
|
208
210
|
],
|
|
209
211
|
),
|
|
210
|
-
("Route", ["route", "route-list", "unroute"]),
|
|
212
|
+
("Route", ["route", "route-list", "unroute", "network-state-set"]),
|
|
211
213
|
("Code", ["run-code"]),
|
|
212
214
|
("Tracing", ["tracing-start", "tracing-stop"]),
|
|
213
215
|
("Video", ["video-start", "video-stop"]),
|
|
@@ -230,6 +232,7 @@ def main():
|
|
|
230
232
|
headless = False
|
|
231
233
|
persistent = False
|
|
232
234
|
profile = None
|
|
235
|
+
proxy = None
|
|
233
236
|
session_name = "default"
|
|
234
237
|
port = DEFAULT_PORT
|
|
235
238
|
|
|
@@ -244,19 +247,32 @@ def main():
|
|
|
244
247
|
persistent = True
|
|
245
248
|
elif arg.startswith("--profile="):
|
|
246
249
|
profile = arg.split("=", 1)[1]
|
|
247
|
-
elif arg
|
|
250
|
+
elif arg == "--profile" and i + 1 < len(argv):
|
|
248
251
|
i += 1
|
|
249
252
|
profile = argv[i]
|
|
253
|
+
elif arg.startswith("--proxy="):
|
|
254
|
+
proxy = arg.split("=", 1)[1]
|
|
255
|
+
elif arg == "--proxy" and i + 1 < len(argv):
|
|
256
|
+
i += 1
|
|
257
|
+
proxy = argv[i]
|
|
250
258
|
elif arg.startswith("-s="):
|
|
251
259
|
session_name = arg.split("=", 1)[1]
|
|
252
260
|
elif arg == "-s" and i + 1 < len(argv):
|
|
253
261
|
i += 1
|
|
254
262
|
session_name = argv[i]
|
|
255
263
|
elif arg.startswith("--port="):
|
|
256
|
-
|
|
264
|
+
try:
|
|
265
|
+
port = int(arg.split("=", 1)[1])
|
|
266
|
+
except ValueError:
|
|
267
|
+
click.echo(f"Invalid port value: {arg}", err=True)
|
|
268
|
+
sys.exit(1)
|
|
257
269
|
elif arg == "--port" and i + 1 < len(argv):
|
|
258
270
|
i += 1
|
|
259
|
-
|
|
271
|
+
try:
|
|
272
|
+
port = int(argv[i])
|
|
273
|
+
except ValueError:
|
|
274
|
+
click.echo(f"Invalid port value: {argv[i]}", err=True)
|
|
275
|
+
sys.exit(1)
|
|
260
276
|
elif arg in ("--version", "-v"):
|
|
261
277
|
click.echo(f"patchright-cli {__version__}")
|
|
262
278
|
sys.exit(0)
|
|
@@ -292,6 +308,27 @@ def main():
|
|
|
292
308
|
positional_args.append(a)
|
|
293
309
|
args = positional_args
|
|
294
310
|
|
|
311
|
+
# For eval/run-code: support --file=<path> and stdin via "-"
|
|
312
|
+
if command in ("eval", "run-code"):
|
|
313
|
+
file_opt = extra_opts.pop("file", None)
|
|
314
|
+
if file_opt:
|
|
315
|
+
# Read JS from file
|
|
316
|
+
try:
|
|
317
|
+
with open(file_opt, encoding="utf-8") as f:
|
|
318
|
+
args = [f.read()]
|
|
319
|
+
except FileNotFoundError:
|
|
320
|
+
click.echo(f"File not found: {file_opt}", err=True)
|
|
321
|
+
sys.exit(1)
|
|
322
|
+
elif args and args[0] == "-":
|
|
323
|
+
# Read JS from stdin
|
|
324
|
+
args = [sys.stdin.read()]
|
|
325
|
+
elif not args and not sys.stdin.isatty():
|
|
326
|
+
# Piped input with no positional arg
|
|
327
|
+
args = [sys.stdin.read()]
|
|
328
|
+
if not args:
|
|
329
|
+
click.echo(f"'{command}' requires a JS expression, --file=<path>, or piped stdin.", err=True)
|
|
330
|
+
sys.exit(1)
|
|
331
|
+
|
|
295
332
|
# Build options dict
|
|
296
333
|
options = {"session": session_name, **extra_opts}
|
|
297
334
|
if headless:
|
|
@@ -300,6 +337,8 @@ def main():
|
|
|
300
337
|
options["persistent"] = True
|
|
301
338
|
if profile:
|
|
302
339
|
options["profile"] = profile
|
|
340
|
+
if proxy:
|
|
341
|
+
options["proxy"] = proxy
|
|
303
342
|
|
|
304
343
|
# Ensure daemon is running (auto-start for 'open', require for others)
|
|
305
344
|
try:
|
|
@@ -309,17 +348,18 @@ def main():
|
|
|
309
348
|
# Try connecting first; if fails, tell user to open
|
|
310
349
|
import socket as _socket
|
|
311
350
|
|
|
351
|
+
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
312
352
|
try:
|
|
313
|
-
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
314
353
|
sock.settimeout(1)
|
|
315
354
|
sock.connect(("127.0.0.1", port))
|
|
316
|
-
sock.close()
|
|
317
355
|
except (ConnectionRefusedError, OSError, TimeoutError):
|
|
318
356
|
click.echo(
|
|
319
357
|
f"Daemon is not running on port {port}. Run 'patchright-cli open' first.",
|
|
320
358
|
err=True,
|
|
321
359
|
)
|
|
322
360
|
sys.exit(1)
|
|
361
|
+
finally:
|
|
362
|
+
sock.close()
|
|
323
363
|
except RuntimeError as e:
|
|
324
364
|
click.echo(str(e), err=True)
|
|
325
365
|
sys.exit(1)
|
patchright_cli/daemon.py
CHANGED
|
@@ -44,6 +44,9 @@ class Session:
|
|
|
44
44
|
self._pending_dialog_action: tuple | None = None
|
|
45
45
|
self._profile_dir: str | None = None
|
|
46
46
|
self._cdp_sessions: dict[int, object] = {}
|
|
47
|
+
self._video_cdp = None
|
|
48
|
+
self._video_frames: list[bytes] = []
|
|
49
|
+
self._video_recording: bool = False
|
|
47
50
|
|
|
48
51
|
# -- internal helpers ---------------------------------------------------
|
|
49
52
|
|
|
@@ -54,8 +57,9 @@ class Session:
|
|
|
54
57
|
self.context.on("page", lambda page: asyncio.ensure_future(self._on_new_page(page)))
|
|
55
58
|
|
|
56
59
|
async def _on_new_page(self, page):
|
|
57
|
-
self.pages
|
|
58
|
-
|
|
60
|
+
if page not in self.pages:
|
|
61
|
+
self.pages.append(page)
|
|
62
|
+
self.current_tab = len(self.pages) - 1
|
|
59
63
|
await self._attach_page_listeners(page)
|
|
60
64
|
|
|
61
65
|
async def _attach_page_listeners(self, page):
|
|
@@ -133,6 +137,7 @@ class DaemonState:
|
|
|
133
137
|
headless: bool | None = None,
|
|
134
138
|
persistent: bool = True,
|
|
135
139
|
profile: str | None = None,
|
|
140
|
+
proxy: str | None = None,
|
|
136
141
|
url: str | None = None,
|
|
137
142
|
) -> Session:
|
|
138
143
|
if name in self.sessions:
|
|
@@ -147,14 +152,18 @@ class DaemonState:
|
|
|
147
152
|
profile_dir = profile or str(Path.home() / ".patchright-cli" / "profiles" / name)
|
|
148
153
|
Path(profile_dir).mkdir(parents=True, exist_ok=True)
|
|
149
154
|
|
|
155
|
+
launch_kwargs = {
|
|
156
|
+
"channel": "chrome",
|
|
157
|
+
"headless": use_headless,
|
|
158
|
+
"no_viewport": True,
|
|
159
|
+
"args": ["--disable-blink-features=AutomationControlled"],
|
|
160
|
+
}
|
|
161
|
+
if proxy:
|
|
162
|
+
launch_kwargs["proxy"] = {"server": proxy}
|
|
163
|
+
|
|
150
164
|
context = await self.playwright.chromium.launch_persistent_context(
|
|
151
165
|
profile_dir,
|
|
152
|
-
|
|
153
|
-
headless=use_headless,
|
|
154
|
-
no_viewport=True,
|
|
155
|
-
args=[
|
|
156
|
-
"--disable-blink-features=AutomationControlled",
|
|
157
|
-
],
|
|
166
|
+
**launch_kwargs,
|
|
158
167
|
)
|
|
159
168
|
|
|
160
169
|
pages = context.pages or []
|
|
@@ -259,6 +268,7 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
259
268
|
headless=headless,
|
|
260
269
|
persistent=options.get("persistent", True),
|
|
261
270
|
profile=options.get("profile"),
|
|
271
|
+
proxy=options.get("proxy"),
|
|
262
272
|
url=url,
|
|
263
273
|
)
|
|
264
274
|
return await _page_info(session, cwd)
|
|
@@ -286,6 +296,50 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
286
296
|
|
|
287
297
|
page = session.page
|
|
288
298
|
|
|
299
|
+
# Commands that don't need a page
|
|
300
|
+
if cmd == "list":
|
|
301
|
+
lines = ["### Sessions"]
|
|
302
|
+
for sname, s in state.sessions.items():
|
|
303
|
+
marker = " *" if sname == session_name else ""
|
|
304
|
+
tab_count = len(s.pages)
|
|
305
|
+
lines.append(f" - {sname}{marker}: {tab_count} tab(s)")
|
|
306
|
+
return {"success": True, "output": "\n".join(lines)}
|
|
307
|
+
|
|
308
|
+
if cmd == "close":
|
|
309
|
+
closed = await state.close_session(session_name)
|
|
310
|
+
return {"success": True, "output": f"Session '{session_name}' closed." if closed else "Session not found."}
|
|
311
|
+
|
|
312
|
+
if cmd == "close-all":
|
|
313
|
+
names = list(state.sessions.keys())
|
|
314
|
+
for n in names:
|
|
315
|
+
await state.close_session(n)
|
|
316
|
+
return {"success": True, "output": f"Closed {len(names)} session(s)."}
|
|
317
|
+
|
|
318
|
+
if cmd == "kill-all":
|
|
319
|
+
names = list(state.sessions.keys())
|
|
320
|
+
for n in names:
|
|
321
|
+
s = state.sessions.get(n)
|
|
322
|
+
if s:
|
|
323
|
+
try:
|
|
324
|
+
for p in s.pages:
|
|
325
|
+
try:
|
|
326
|
+
await p.close()
|
|
327
|
+
except Exception:
|
|
328
|
+
pass
|
|
329
|
+
await s.context.close()
|
|
330
|
+
except Exception:
|
|
331
|
+
pass
|
|
332
|
+
state.sessions.pop(n, None)
|
|
333
|
+
if hasattr(state, "shutdown_event"):
|
|
334
|
+
state.shutdown_event.set()
|
|
335
|
+
return {"success": True, "output": f"Killed {len(names)} session(s) and stopping daemon."}
|
|
336
|
+
|
|
337
|
+
if page is None:
|
|
338
|
+
return {
|
|
339
|
+
"success": False,
|
|
340
|
+
"output": "No page open. Run 'tab-new' to create one, or 'close' and 'open' again.",
|
|
341
|
+
}
|
|
342
|
+
|
|
289
343
|
# -- Navigation -----------------------------------------------------
|
|
290
344
|
if cmd == "goto":
|
|
291
345
|
await page.goto(args[0])
|
|
@@ -321,11 +375,15 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
321
375
|
if cmd == "fill":
|
|
322
376
|
elem = await _resolve_ref(session, page, args[0])
|
|
323
377
|
await elem.fill(args[1])
|
|
378
|
+
if options.get("submit"):
|
|
379
|
+
await page.keyboard.press("Enter")
|
|
324
380
|
return await _page_info(session, cwd)
|
|
325
381
|
|
|
326
382
|
if cmd == "type":
|
|
327
383
|
text = args[0] if args else ""
|
|
328
384
|
await page.keyboard.type(text)
|
|
385
|
+
if options.get("submit"):
|
|
386
|
+
await page.keyboard.press("Enter")
|
|
329
387
|
return await _page_info(session, cwd)
|
|
330
388
|
|
|
331
389
|
if cmd == "hover":
|
|
@@ -356,7 +414,13 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
356
414
|
|
|
357
415
|
# -- Snapshot -------------------------------------------------------
|
|
358
416
|
if cmd == "snapshot":
|
|
359
|
-
|
|
417
|
+
element_ref = args[0] if args else None
|
|
418
|
+
if element_ref:
|
|
419
|
+
# Partial snapshot: scope JS to a specific element subtree
|
|
420
|
+
elem = await _resolve_ref(session, page, element_ref)
|
|
421
|
+
yaml_text, session.ref_map = await take_snapshot(page, root_element=elem)
|
|
422
|
+
else:
|
|
423
|
+
yaml_text, session.ref_map = await take_snapshot(page)
|
|
360
424
|
fn = options.get("filename")
|
|
361
425
|
if fn:
|
|
362
426
|
Path(fn).parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -402,11 +466,6 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
402
466
|
await page.screenshot(path=str(filepath), full_page=full_page)
|
|
403
467
|
return {"success": True, "output": f"Screenshot saved to {filepath}"}
|
|
404
468
|
|
|
405
|
-
# -- Close ----------------------------------------------------------
|
|
406
|
-
if cmd == "close":
|
|
407
|
-
closed = await state.close_session(session_name)
|
|
408
|
-
return {"success": True, "output": f"Session '{session_name}' closed." if closed else "Session not found."}
|
|
409
|
-
|
|
410
469
|
# -- Keyboard -------------------------------------------------------
|
|
411
470
|
if cmd == "press":
|
|
412
471
|
await page.keyboard.press(args[0])
|
|
@@ -463,6 +522,7 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
463
522
|
idx = int(args[0]) if args else session.current_tab
|
|
464
523
|
if 0 <= idx < len(session.pages):
|
|
465
524
|
p = session.pages.pop(idx)
|
|
525
|
+
session._cdp_sessions.pop(id(p), None)
|
|
466
526
|
await p.close()
|
|
467
527
|
session.current_tab = max(0, min(session.current_tab, len(session.pages) - 1))
|
|
468
528
|
return {"success": True, "output": f"Tab {idx} closed."}
|
|
@@ -501,9 +561,9 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
501
561
|
cookie["path"] = options.get("path", "/")
|
|
502
562
|
else:
|
|
503
563
|
cookie["url"] = page.url
|
|
504
|
-
if options.get("httpOnly")
|
|
564
|
+
if options.get("httpOnly"):
|
|
505
565
|
cookie["httpOnly"] = True
|
|
506
|
-
if options.get("secure")
|
|
566
|
+
if options.get("secure"):
|
|
507
567
|
cookie["secure"] = True
|
|
508
568
|
if options.get("sameSite"):
|
|
509
569
|
cookie["sameSite"] = options["sameSite"]
|
|
@@ -564,34 +624,38 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
564
624
|
if level_filter and m["type"] != level_filter:
|
|
565
625
|
continue
|
|
566
626
|
lines.append(f"[{m['type']}] {m['text']}")
|
|
627
|
+
if options.get("clear"):
|
|
628
|
+
session.console_messages.clear()
|
|
567
629
|
return {"success": True, "output": "\n".join(lines) or "(no console messages)"}
|
|
568
630
|
|
|
569
631
|
if cmd == "network":
|
|
632
|
+
include_static = bool(options.get("static"))
|
|
633
|
+
static_types = {"image", "font", "stylesheet", "script", "media"}
|
|
570
634
|
lines = []
|
|
571
635
|
for r in session.network_log[-50:]:
|
|
636
|
+
if not include_static and r["resource"] in static_types:
|
|
637
|
+
continue
|
|
572
638
|
lines.append(f"{r['method']} {r['url']} [{r['resource']}]")
|
|
639
|
+
if options.get("clear"):
|
|
640
|
+
session.network_log.clear()
|
|
573
641
|
return {"success": True, "output": "\n".join(lines) or "(no network requests)"}
|
|
574
642
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
names = list(state.sessions.keys())
|
|
592
|
-
for n in names:
|
|
593
|
-
await state.close_session(n)
|
|
594
|
-
return {"success": True, "output": f"Killed {len(names)} session(s)."}
|
|
643
|
+
if cmd == "network-state-set":
|
|
644
|
+
state_val = args[0] if args else ""
|
|
645
|
+
if state_val not in ("online", "offline"):
|
|
646
|
+
return {"success": False, "output": "Usage: network-state-set <online|offline>"}
|
|
647
|
+
cdp = await page.context.new_cdp_session(page)
|
|
648
|
+
if state_val == "offline":
|
|
649
|
+
await cdp.send(
|
|
650
|
+
"Network.emulateNetworkConditions",
|
|
651
|
+
{"offline": True, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1},
|
|
652
|
+
)
|
|
653
|
+
else:
|
|
654
|
+
await cdp.send(
|
|
655
|
+
"Network.emulateNetworkConditions",
|
|
656
|
+
{"offline": False, "latency": 0, "downloadThroughput": -1, "uploadThroughput": -1},
|
|
657
|
+
)
|
|
658
|
+
return {"success": True, "output": f"Network state set to {state_val}."}
|
|
595
659
|
|
|
596
660
|
# -- Dialog handling ------------------------------------------------
|
|
597
661
|
if cmd == "dialog-accept":
|
|
@@ -637,7 +701,9 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
637
701
|
# Apply cookies
|
|
638
702
|
if state_data.get("cookies"):
|
|
639
703
|
await session.context.add_cookies(state_data["cookies"])
|
|
640
|
-
# Apply localStorage via JS
|
|
704
|
+
# Apply localStorage via JS (only works if page is on the matching origin)
|
|
705
|
+
ls_applied = 0
|
|
706
|
+
ls_skipped = 0
|
|
641
707
|
for origin_data in state_data.get("origins", []):
|
|
642
708
|
origin = origin_data.get("origin", "")
|
|
643
709
|
ls = origin_data.get("localStorage", [])
|
|
@@ -646,7 +712,13 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
646
712
|
await page.evaluate(
|
|
647
713
|
f"() => localStorage.setItem({json.dumps(item['name'])}, {json.dumps(item['value'])})"
|
|
648
714
|
)
|
|
649
|
-
|
|
715
|
+
ls_applied += len(ls)
|
|
716
|
+
elif ls:
|
|
717
|
+
ls_skipped += len(ls)
|
|
718
|
+
msg = f"State loaded from {filepath}"
|
|
719
|
+
if ls_skipped:
|
|
720
|
+
msg += f" (note: {ls_skipped} localStorage item(s) skipped — navigate to the matching origin first)"
|
|
721
|
+
return {"success": True, "output": msg}
|
|
650
722
|
|
|
651
723
|
# -- Session storage ------------------------------------------------
|
|
652
724
|
if cmd == "sessionstorage-list":
|
|
@@ -728,8 +800,7 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
728
800
|
# -- Run code -------------------------------------------------------
|
|
729
801
|
if cmd == "run-code":
|
|
730
802
|
code = args[0] if args else ""
|
|
731
|
-
|
|
732
|
-
result = await page.evaluate(f"async () => {{ const page = window; {code} }}")
|
|
803
|
+
result = await page.evaluate(f"async () => {{ {code} }}")
|
|
733
804
|
return {
|
|
734
805
|
"success": True,
|
|
735
806
|
"output": json.dumps(result, indent=2, default=str) if result is not None else "Code executed.",
|
|
@@ -751,19 +822,88 @@ async def handle_command(state: DaemonState, msg: dict) -> dict:
|
|
|
751
822
|
|
|
752
823
|
# -- Video recording ------------------------------------------------
|
|
753
824
|
if cmd == "video-start":
|
|
754
|
-
session.
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
825
|
+
if session._video_recording:
|
|
826
|
+
return {"success": False, "output": "Video recording is already in progress."}
|
|
827
|
+
cdp = await page.context.new_cdp_session(page)
|
|
828
|
+
session._video_cdp = cdp
|
|
829
|
+
session._video_frames = []
|
|
830
|
+
session._video_recording = True
|
|
831
|
+
|
|
832
|
+
def _on_frame(event):
|
|
833
|
+
import base64
|
|
834
|
+
|
|
835
|
+
session._video_frames.append(base64.b64decode(event["data"]))
|
|
836
|
+
asyncio.ensure_future(cdp.send("Page.screencastFrameAck", {"sessionId": event["sessionId"]}))
|
|
837
|
+
|
|
838
|
+
cdp.on("Page.screencastFrame", _on_frame)
|
|
839
|
+
await cdp.send(
|
|
840
|
+
"Page.startScreencast", {"format": "jpeg", "quality": 80, "maxWidth": 1280, "maxHeight": 720}
|
|
841
|
+
)
|
|
842
|
+
return {"success": True, "output": "Video recording started."}
|
|
760
843
|
|
|
761
844
|
if cmd == "video-stop":
|
|
762
|
-
if
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
845
|
+
if not session._video_recording or not session._video_cdp:
|
|
846
|
+
return {"success": False, "output": "No video recording in progress."}
|
|
847
|
+
try:
|
|
848
|
+
await session._video_cdp.send("Page.stopScreencast")
|
|
849
|
+
except Exception:
|
|
850
|
+
pass
|
|
851
|
+
session._video_recording = False
|
|
852
|
+
frames = session._video_frames
|
|
853
|
+
session._video_frames = []
|
|
854
|
+
session._video_cdp = None
|
|
855
|
+
|
|
856
|
+
if not frames:
|
|
857
|
+
return {"success": False, "output": "No video frames were captured."}
|
|
858
|
+
|
|
859
|
+
base = Path(cwd) if cwd else Path.cwd()
|
|
860
|
+
snap_dir = base / ".patchright-cli"
|
|
861
|
+
snap_dir.mkdir(parents=True, exist_ok=True)
|
|
862
|
+
ts = int(time.time() * 1000)
|
|
863
|
+
|
|
864
|
+
fn = options.get("filename")
|
|
865
|
+
|
|
866
|
+
# Try ffmpeg for webm output, fall back to saving frames as images
|
|
867
|
+
video_path = snap_dir / (fn or f"video-{ts}.webm")
|
|
868
|
+
try:
|
|
869
|
+
import shutil
|
|
870
|
+
import tempfile
|
|
871
|
+
|
|
872
|
+
if not shutil.which("ffmpeg"):
|
|
873
|
+
raise FileNotFoundError("ffmpeg not found")
|
|
874
|
+
|
|
875
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
876
|
+
for i, frame_data in enumerate(frames):
|
|
877
|
+
Path(tmpdir, f"frame-{i:06d}.jpg").write_bytes(frame_data)
|
|
878
|
+
proc = await asyncio.create_subprocess_exec(
|
|
879
|
+
"ffmpeg",
|
|
880
|
+
"-y",
|
|
881
|
+
"-framerate",
|
|
882
|
+
"5",
|
|
883
|
+
"-i",
|
|
884
|
+
str(Path(tmpdir, "frame-%06d.jpg")),
|
|
885
|
+
"-c:v",
|
|
886
|
+
"libvpx",
|
|
887
|
+
"-b:v",
|
|
888
|
+
"1M",
|
|
889
|
+
str(video_path),
|
|
890
|
+
stdout=asyncio.subprocess.DEVNULL,
|
|
891
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
892
|
+
)
|
|
893
|
+
await proc.wait()
|
|
894
|
+
if proc.returncode != 0:
|
|
895
|
+
raise RuntimeError("ffmpeg failed")
|
|
896
|
+
return {"success": True, "output": f"Video saved to {video_path} ({len(frames)} frames)"}
|
|
897
|
+
except (FileNotFoundError, RuntimeError):
|
|
898
|
+
# Fallback: save frames as individual images
|
|
899
|
+
frames_dir = snap_dir / f"video-{ts}-frames"
|
|
900
|
+
frames_dir.mkdir(parents=True, exist_ok=True)
|
|
901
|
+
for i, frame_data in enumerate(frames):
|
|
902
|
+
(frames_dir / f"frame-{i:04d}.jpg").write_bytes(frame_data)
|
|
903
|
+
return {
|
|
904
|
+
"success": True,
|
|
905
|
+
"output": f"Saved {len(frames)} frames to {frames_dir}/ (install ffmpeg for video output)",
|
|
906
|
+
}
|
|
767
907
|
|
|
768
908
|
# -- PDF ------------------------------------------------------------
|
|
769
909
|
if cmd == "pdf":
|
|
@@ -840,6 +980,7 @@ async def run_daemon(port: int = DEFAULT_PORT, headless: bool = False):
|
|
|
840
980
|
"""Start the daemon TCP server."""
|
|
841
981
|
state = DaemonState()
|
|
842
982
|
state.default_headless = headless
|
|
983
|
+
state.shutdown_event = asyncio.Event()
|
|
843
984
|
|
|
844
985
|
async def client_handler(reader, writer):
|
|
845
986
|
await _handle_client(reader, writer, state)
|
|
@@ -850,7 +991,7 @@ async def run_daemon(port: int = DEFAULT_PORT, headless: bool = False):
|
|
|
850
991
|
print(f"patchright-cli daemon listening on {addr[0]}:{addr[1]}", flush=True)
|
|
851
992
|
|
|
852
993
|
# Handle graceful shutdown
|
|
853
|
-
shutdown_event =
|
|
994
|
+
shutdown_event = state.shutdown_event
|
|
854
995
|
|
|
855
996
|
def _signal_handler():
|
|
856
997
|
logger.info("Shutdown signal received")
|
|
@@ -862,12 +1003,11 @@ async def run_daemon(port: int = DEFAULT_PORT, headless: bool = False):
|
|
|
862
1003
|
loop.add_signal_handler(sig, _signal_handler)
|
|
863
1004
|
|
|
864
1005
|
try:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
await
|
|
869
|
-
|
|
870
|
-
async with server:
|
|
1006
|
+
async with server:
|
|
1007
|
+
if sys.platform == "win32":
|
|
1008
|
+
# On Windows, asyncio signal handlers don't work; use shutdown_event from kill-all
|
|
1009
|
+
await shutdown_event.wait()
|
|
1010
|
+
else:
|
|
871
1011
|
await shutdown_event.wait()
|
|
872
1012
|
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
873
1013
|
pass
|
patchright_cli/snapshot.py
CHANGED
|
@@ -48,7 +48,10 @@ _SNAPSHOT_JS = r"""
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
function isVis(el) {
|
|
51
|
-
if (!el.offsetParent && el.tagName !== "BODY" && el.tagName !== "HTML")
|
|
51
|
+
if (!el.offsetParent && el.tagName !== "BODY" && el.tagName !== "HTML") {
|
|
52
|
+
try { const pos = getComputedStyle(el).position; if (pos !== "fixed" && pos !== "sticky") return false; }
|
|
53
|
+
catch(e) { return false; }
|
|
54
|
+
}
|
|
52
55
|
try { const s = getComputedStyle(el); return s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0"; }
|
|
53
56
|
catch(e) { return false; }
|
|
54
57
|
}
|
|
@@ -115,6 +118,110 @@ _SNAPSHOT_JS = r"""
|
|
|
115
118
|
})()
|
|
116
119
|
"""
|
|
117
120
|
|
|
121
|
+
# Variant that takes a root element argument for partial snapshots
|
|
122
|
+
_SNAPSHOT_ELEMENT_JS = r"""
|
|
123
|
+
(rootEl) => {
|
|
124
|
+
const SKIP = new Set(["SCRIPT","STYLE","NOSCRIPT","SVG","LINK","META","BR","HR"]);
|
|
125
|
+
const INTERACTIVE = new Set(["A","BUTTON","INPUT","TEXTAREA","SELECT","DETAILS","SUMMARY","LABEL"]);
|
|
126
|
+
const SEMANTIC = new Set(["H1","H2","H3","H4","H5","H6","NAV","MAIN","HEADER","FOOTER","ARTICLE","SECTION","ASIDE","FORM","TABLE","UL","OL","LI","IMG","VIDEO","AUDIO","IFRAME"]);
|
|
127
|
+
|
|
128
|
+
rootEl.querySelectorAll("[data-patchright-ref]").forEach(el => el.removeAttribute("data-patchright-ref"));
|
|
129
|
+
|
|
130
|
+
function getRole(el) {
|
|
131
|
+
const ar = el.getAttribute && el.getAttribute("role");
|
|
132
|
+
if (ar) return ar;
|
|
133
|
+
const t = el.tagName;
|
|
134
|
+
const m = {A:"link",BUTTON:"button",TEXTAREA:"textbox",SELECT:"combobox",IMG:"img",TABLE:"table",FORM:"form",NAV:"navigation",MAIN:"main",HEADER:"banner",FOOTER:"contentinfo"};
|
|
135
|
+
if (m[t]) return m[t];
|
|
136
|
+
if (t === "INPUT") { const tp = (el.type||"text").toLowerCase(); return tp==="checkbox"?"checkbox":tp==="radio"?"radio":(tp==="submit"||tp==="button")?"button":"textbox"; }
|
|
137
|
+
if (t === "UL" || t === "OL") return "list";
|
|
138
|
+
if (t === "LI") return "listitem";
|
|
139
|
+
if (/^H[1-6]$/.test(t)) return "heading";
|
|
140
|
+
return t.toLowerCase();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getName(el) {
|
|
144
|
+
const t = el.tagName;
|
|
145
|
+
const ar = el.getAttribute && el.getAttribute("aria-label");
|
|
146
|
+
if (ar) return ar.substring(0, 120);
|
|
147
|
+
if (t === "IMG") return (el.getAttribute("alt") || "").substring(0, 120);
|
|
148
|
+
if (t === "INPUT" || t === "TEXTAREA") return (el.getAttribute("placeholder") || "").substring(0, 80);
|
|
149
|
+
if (t === "A" || t === "BUTTON" || /^H[1-6]$/.test(t) || t === "LABEL" || t === "LI" || t === "SUMMARY")
|
|
150
|
+
return (el.innerText || "").replace(/\s+/g, " ").trim().substring(0, 120);
|
|
151
|
+
const ti = el.getAttribute && el.getAttribute("title");
|
|
152
|
+
if (ti) return ti.substring(0, 80);
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isVis(el) {
|
|
157
|
+
if (!el.offsetParent && el.tagName !== "BODY" && el.tagName !== "HTML") {
|
|
158
|
+
try { const pos = getComputedStyle(el).position; if (pos !== "fixed" && pos !== "sticky") return false; }
|
|
159
|
+
catch(e) { return false; }
|
|
160
|
+
}
|
|
161
|
+
try { const s = getComputedStyle(el); return s.display !== "none" && s.visibility !== "hidden" && s.opacity !== "0"; }
|
|
162
|
+
catch(e) { return false; }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function shouldTag(el) {
|
|
166
|
+
return INTERACTIVE.has(el.tagName) || SEMANTIC.has(el.tagName) || !!el.getAttribute("role") || !!el.getAttribute("data-testid") || !!getName(el);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tw = document.createTreeWalker(rootEl, NodeFilter.SHOW_ELEMENT, {
|
|
170
|
+
acceptNode(n) {
|
|
171
|
+
if (SKIP.has(n.tagName)) return NodeFilter.FILTER_REJECT;
|
|
172
|
+
if (!isVis(n)) return NodeFilter.FILTER_REJECT;
|
|
173
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let counter = 0;
|
|
178
|
+
const flatList = [];
|
|
179
|
+
let node;
|
|
180
|
+
while (node = tw.nextNode()) {
|
|
181
|
+
if (shouldTag(node)) {
|
|
182
|
+
counter++;
|
|
183
|
+
const ref = "e" + counter;
|
|
184
|
+
node.setAttribute("data-patchright-ref", ref);
|
|
185
|
+
const role = getRole(node);
|
|
186
|
+
const name = getName(node);
|
|
187
|
+
const entry = { ref, role, name, tag: node.tagName };
|
|
188
|
+
if (node.tagName === "INPUT" || node.tagName === "TEXTAREA" || node.tagName === "SELECT") {
|
|
189
|
+
if (node.value) entry.value = node.value.substring(0, 200);
|
|
190
|
+
if (node.type === "checkbox" || node.type === "radio") entry.checked = node.checked;
|
|
191
|
+
if (node.disabled) entry.disabled = true;
|
|
192
|
+
}
|
|
193
|
+
if (node.tagName === "A" && node.href) entry.url = node.href.substring(0, 200);
|
|
194
|
+
if (/^H[1-6]$/.test(node.tagName)) entry.level = parseInt(node.tagName[1]);
|
|
195
|
+
flatList.push(entry);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildTree(el, depth) {
|
|
200
|
+
if (depth > 12 || !el || !el.tagName) return null;
|
|
201
|
+
if (SKIP.has(el.tagName)) return null;
|
|
202
|
+
const ref = el.getAttribute("data-patchright-ref");
|
|
203
|
+
const children = [];
|
|
204
|
+
for (const ch of el.children) {
|
|
205
|
+
if (!isVis(ch)) continue;
|
|
206
|
+
const c = buildTree(ch, depth + 1);
|
|
207
|
+
if (c) children.push(c);
|
|
208
|
+
}
|
|
209
|
+
if (!ref && !children.length) return null;
|
|
210
|
+
const out = {};
|
|
211
|
+
if (ref) {
|
|
212
|
+
const f = flatList.find(x => x.ref === ref);
|
|
213
|
+
if (f) Object.assign(out, f);
|
|
214
|
+
delete out.tag;
|
|
215
|
+
}
|
|
216
|
+
if (children.length) out.children = children;
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const tree = buildTree(rootEl, 0) || { role: "element", name: "", children: [] };
|
|
221
|
+
return { tree, flatList };
|
|
222
|
+
}
|
|
223
|
+
"""
|
|
224
|
+
|
|
118
225
|
|
|
119
226
|
def _walk_tree(node: dict, depth: int = 0) -> list[str]:
|
|
120
227
|
"""Recursively walk the parsed tree and produce YAML lines."""
|
|
@@ -158,11 +265,13 @@ def _yaml_escape(s: str) -> str:
|
|
|
158
265
|
if any(
|
|
159
266
|
c in s
|
|
160
267
|
for c in (
|
|
268
|
+
"\\",
|
|
161
269
|
":",
|
|
162
270
|
"#",
|
|
163
271
|
"'",
|
|
164
272
|
'"',
|
|
165
273
|
"\n",
|
|
274
|
+
"\r",
|
|
166
275
|
"[",
|
|
167
276
|
"]",
|
|
168
277
|
"{",
|
|
@@ -182,50 +291,45 @@ def _yaml_escape(s: str) -> str:
|
|
|
182
291
|
"`",
|
|
183
292
|
)
|
|
184
293
|
):
|
|
185
|
-
escaped = s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
|
294
|
+
escaped = s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r")
|
|
186
295
|
return f'"{escaped}"'
|
|
187
296
|
return s
|
|
188
297
|
|
|
189
298
|
|
|
190
|
-
async def take_snapshot(page) -> tuple[str, dict[str, dict]]:
|
|
191
|
-
"""Take a DOM snapshot. Returns (yaml_text, ref_map).
|
|
299
|
+
async def take_snapshot(page, root_element=None) -> tuple[str, dict[str, dict]]:
|
|
300
|
+
"""Take a DOM snapshot. Returns (yaml_text, ref_map).
|
|
301
|
+
|
|
302
|
+
If root_element is provided, only snapshot the subtree under that element.
|
|
303
|
+
"""
|
|
192
304
|
result = None
|
|
193
|
-
|
|
194
|
-
result = await page.evaluate(_SNAPSHOT_JS, isolated_context=False)
|
|
195
|
-
except TypeError:
|
|
305
|
+
if root_element:
|
|
196
306
|
try:
|
|
197
|
-
result = await
|
|
307
|
+
result = await root_element.evaluate(_SNAPSHOT_ELEMENT_JS)
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
else:
|
|
311
|
+
try:
|
|
312
|
+
result = await page.evaluate(_SNAPSHOT_JS, isolated_context=False)
|
|
313
|
+
except TypeError:
|
|
314
|
+
try:
|
|
315
|
+
result = await page.evaluate(_SNAPSHOT_JS)
|
|
316
|
+
except Exception:
|
|
317
|
+
pass
|
|
198
318
|
except Exception:
|
|
199
319
|
pass
|
|
200
|
-
except Exception:
|
|
201
|
-
pass
|
|
202
320
|
|
|
203
321
|
if not result or not result.get("tree"):
|
|
204
322
|
return "# Empty page - no accessible elements found\n", {}
|
|
205
323
|
|
|
206
324
|
flat_list = result.get("flatList", [])
|
|
325
|
+
tree = result.get("tree", {})
|
|
207
326
|
|
|
208
327
|
refs: dict[str, dict] = {}
|
|
209
|
-
lines: list[str] = []
|
|
210
328
|
for item in flat_list:
|
|
211
329
|
refs[item["ref"]] = item
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if name:
|
|
216
|
-
lines.append(f" name: {_yaml_escape(name)}")
|
|
217
|
-
for key in ("value", "url"):
|
|
218
|
-
val = item.get(key, "")
|
|
219
|
-
if val:
|
|
220
|
-
lines.append(f" {key}: {_yaml_escape(str(val))}")
|
|
221
|
-
if item.get("checked") is not None:
|
|
222
|
-
lines.append(f" checked: {str(item['checked']).lower()}")
|
|
223
|
-
if item.get("disabled"):
|
|
224
|
-
lines.append(" disabled: true")
|
|
225
|
-
if item.get("level") is not None:
|
|
226
|
-
lines.append(f" level: {item['level']}")
|
|
227
|
-
|
|
228
|
-
yaml_text = "\n".join(lines) + "\n"
|
|
330
|
+
|
|
331
|
+
lines = _walk_tree(tree)
|
|
332
|
+
yaml_text = "\n".join(lines) + "\n" if lines else "# Empty page - no accessible elements found\n"
|
|
229
333
|
return yaml_text, refs
|
|
230
334
|
|
|
231
335
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchright-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Anti-detect browser automation CLI using Patchright (undetected Playwright fork)
|
|
5
5
|
Project-URL: Homepage, https://github.com/AhaiMk01/patchright-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/AhaiMk01/patchright-cli
|
|
@@ -78,25 +78,22 @@ curl -s https://raw.githubusercontent.com/AhaiMk01/patchright-cli/main/docs/inst
|
|
|
78
78
|
> [!IMPORTANT]
|
|
79
79
|
> **Requirements:** Python 3.10+ and Google Chrome
|
|
80
80
|
|
|
81
|
-
<details>
|
|
82
|
-
<summary><b>Manual Install</b></summary>
|
|
83
|
-
|
|
84
81
|
```bash
|
|
85
|
-
#
|
|
86
|
-
|
|
82
|
+
# Recommended — always runs latest version, no install needed
|
|
83
|
+
uvx patchright-cli open https://example.com
|
|
84
|
+
```
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
<details>
|
|
87
|
+
<summary><b>Other install methods</b></summary>
|
|
90
88
|
|
|
91
|
-
|
|
89
|
+
```bash
|
|
90
|
+
# Via pip
|
|
91
|
+
pip install patchright-cli
|
|
92
92
|
patchright-cli open https://example.com
|
|
93
93
|
patchright-cli close
|
|
94
|
-
```
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
uvx patchright-cli open https://example.com
|
|
95
|
+
# Update
|
|
96
|
+
pip install --upgrade patchright-cli
|
|
100
97
|
```
|
|
101
98
|
|
|
102
99
|
**From source:**
|
|
@@ -167,16 +164,21 @@ patchright-cli click <ref> --modifiers=Alt,Shift
|
|
|
167
164
|
patchright-cli dblclick <ref> # Double-click
|
|
168
165
|
patchright-cli dblclick <ref> --modifiers=Shift
|
|
169
166
|
patchright-cli fill <ref> <value> # Fill text input
|
|
167
|
+
patchright-cli fill <ref> <value> --submit # Fill and press Enter
|
|
170
168
|
patchright-cli type <text> # Type via keyboard
|
|
169
|
+
patchright-cli type <text> --submit # Type and press Enter
|
|
171
170
|
patchright-cli hover <ref> # Hover over element
|
|
172
171
|
patchright-cli select <ref> <value> # Select dropdown option
|
|
173
172
|
patchright-cli check <ref> # Check checkbox
|
|
174
173
|
patchright-cli uncheck <ref> # Uncheck checkbox
|
|
175
174
|
patchright-cli drag <from> <to> # Drag and drop
|
|
176
175
|
patchright-cli snapshot # Accessibility snapshot
|
|
176
|
+
patchright-cli snapshot <ref> # Snapshot element subtree
|
|
177
177
|
patchright-cli snapshot --filename=f # Save to custom path
|
|
178
178
|
patchright-cli eval <expr> # Run JavaScript
|
|
179
|
+
patchright-cli eval --file=script.js # Run JS from file
|
|
179
180
|
patchright-cli run-code <code> # Run JS with return value
|
|
181
|
+
patchright-cli run-code --file=f.js # Run JS from file
|
|
180
182
|
patchright-cli screenshot # Page screenshot
|
|
181
183
|
patchright-cli screenshot --full-page # Full scrollable page
|
|
182
184
|
patchright-cli screenshot <ref> # Element screenshot
|
|
@@ -269,18 +271,30 @@ patchright-cli unroute "**/*.jpg"
|
|
|
269
271
|
patchright-cli unroute # Remove all routes
|
|
270
272
|
```
|
|
271
273
|
|
|
272
|
-
### Tracing / PDF
|
|
274
|
+
### Tracing / Video / PDF
|
|
273
275
|
```bash
|
|
274
276
|
patchright-cli tracing-start
|
|
275
277
|
patchright-cli tracing-stop # Saves .zip trace file
|
|
278
|
+
patchright-cli video-start # Start video recording (CDP screencast)
|
|
279
|
+
patchright-cli video-stop # Stop and save video (requires ffmpeg for .webm)
|
|
280
|
+
patchright-cli video-stop --filename=recording.webm
|
|
276
281
|
patchright-cli pdf --filename=page.pdf
|
|
277
282
|
```
|
|
278
283
|
|
|
284
|
+
### Network
|
|
285
|
+
```bash
|
|
286
|
+
patchright-cli network # Network request log
|
|
287
|
+
patchright-cli network --static # Include static resources
|
|
288
|
+
patchright-cli network --clear # Clear log after printing
|
|
289
|
+
patchright-cli network-state-set offline # Simulate offline mode
|
|
290
|
+
patchright-cli network-state-set online # Restore connectivity
|
|
291
|
+
```
|
|
292
|
+
|
|
279
293
|
### DevTools
|
|
280
294
|
```bash
|
|
281
295
|
patchright-cli console # All console messages
|
|
282
296
|
patchright-cli console warning # Filter by level
|
|
283
|
-
patchright-cli
|
|
297
|
+
patchright-cli console --clear # Clear after printing
|
|
284
298
|
```
|
|
285
299
|
|
|
286
300
|
### Sessions
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
patchright_cli/__init__.py,sha256=EPqY-eBKEojjl1y9dwsndFlrKpBqB0Wz6OBMfewzVWE,100
|
|
2
|
+
patchright_cli/__main__.py,sha256=iVuBuI_rPOENLhffOEHoS7JWCDBQciqT1kPXjRuhbZ4,127
|
|
3
|
+
patchright_cli/cli.py,sha256=pbwyLdszogGpgSiCEBW5lrsTssOEJaQzbbx7o1HduL4,14137
|
|
4
|
+
patchright_cli/daemon.py,sha256=AhGUiiuvAH197sDMhBsHvvDvB8YMQiYmX4XD4ie4oWM,44323
|
|
5
|
+
patchright_cli/snapshot.py,sha256=JuXO3dN6-JNSQ0-lLEXIkg3YqGXW6Ul4I6KskdPygtQ,13621
|
|
6
|
+
patchright_cli-0.3.1.dist-info/METADATA,sha256=Va7G26o7qU29yC0vcF3yZFsi6BVjxuchtviG5Nsdc0E,15498
|
|
7
|
+
patchright_cli-0.3.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
patchright_cli-0.3.1.dist-info/entry_points.txt,sha256=Syh904eOQA8ylakyqIvG4LmIHx3LMjgeNcnFap3O0NQ,59
|
|
9
|
+
patchright_cli-0.3.1.dist-info/licenses/LICENSE,sha256=adszqL4YR4pQUrc7LeAFxeg5Mc2cvljXp7QPlzKVWm8,10760
|
|
10
|
+
patchright_cli-0.3.1.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
patchright_cli/__init__.py,sha256=5FxYv6u1EbGSdJfoc1ZD217_KGIHbI5M8LjoKfv0M1w,100
|
|
2
|
-
patchright_cli/__main__.py,sha256=iVuBuI_rPOENLhffOEHoS7JWCDBQciqT1kPXjRuhbZ4,127
|
|
3
|
-
patchright_cli/cli.py,sha256=wR5v8OlXUBkHFW3rkBuEcG4wDq4wHHjMSifYL8l_Lfg,12415
|
|
4
|
-
patchright_cli/daemon.py,sha256=OtM3Gsm36F3tCDH6y5X8wEA2igp-cpsTmuvjl4ItDuI,38273
|
|
5
|
-
patchright_cli/snapshot.py,sha256=KTIcRyUl2kUFs3yfpPi3YVvz17zRjr4rcfqFgiyt1PM,8906
|
|
6
|
-
patchright_cli-0.2.0.dist-info/METADATA,sha256=VFBWzcV2Tzb3iIowYb5EFwudQjGoZehi2UWmAXy2nec,14710
|
|
7
|
-
patchright_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
-
patchright_cli-0.2.0.dist-info/entry_points.txt,sha256=Syh904eOQA8ylakyqIvG4LmIHx3LMjgeNcnFap3O0NQ,59
|
|
9
|
-
patchright_cli-0.2.0.dist-info/licenses/LICENSE,sha256=adszqL4YR4pQUrc7LeAFxeg5Mc2cvljXp7QPlzKVWm8,10760
|
|
10
|
-
patchright_cli-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|