browsix 1.0.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.
- browsix/__init__.py +3 -0
- browsix/__main__.py +6 -0
- browsix/actions/__init__.py +1 -0
- browsix/actions/accessibility.py +53 -0
- browsix/actions/animation.py +59 -0
- browsix/actions/base.py +26 -0
- browsix/actions/bluetooth.py +68 -0
- browsix/actions/browser.py +39 -0
- browsix/actions/cast.py +67 -0
- browsix/actions/console.py +57 -0
- browsix/actions/css.py +96 -0
- browsix/actions/debug.py +134 -0
- browsix/actions/dialog.py +50 -0
- browsix/actions/dom.py +70 -0
- browsix/actions/dom_snapshot.py +45 -0
- browsix/actions/download.py +38 -0
- browsix/actions/emulation.py +45 -0
- browsix/actions/eval.py +36 -0
- browsix/actions/har.py +27 -0
- browsix/actions/input.py +62 -0
- browsix/actions/media.py +62 -0
- browsix/actions/multi.py +29 -0
- browsix/actions/navigate.py +98 -0
- browsix/actions/network.py +70 -0
- browsix/actions/overlay.py +61 -0
- browsix/actions/pdf.py +32 -0
- browsix/actions/performance.py +69 -0
- browsix/actions/permissions.py +50 -0
- browsix/actions/scrape.py +50 -0
- browsix/actions/screencast.py +44 -0
- browsix/actions/screenshot.py +37 -0
- browsix/actions/security.py +50 -0
- browsix/actions/service_worker.py +69 -0
- browsix/actions/storage.py +92 -0
- browsix/actions/tabs.py +53 -0
- browsix/actions/webaudio.py +62 -0
- browsix/actions/webauthn.py +96 -0
- browsix/auth.py +86 -0
- browsix/backend/__init__.py +1 -0
- browsix/backend/base.py +829 -0
- browsix/backend/bidi.py +953 -0
- browsix/backend/cdp.py +2339 -0
- browsix/backend/manager.py +114 -0
- browsix/cli/__init__.py +8 -0
- browsix/cli/app.py +2409 -0
- browsix/config.py +589 -0
- browsix/exceptions.py +61 -0
- browsix/multi.py +156 -0
- browsix/output.py +142 -0
- browsix/plugins.py +101 -0
- browsix/record.py +105 -0
- browsix/serve.py +343 -0
- browsix-1.0.0.dist-info/METADATA +128 -0
- browsix-1.0.0.dist-info/RECORD +57 -0
- browsix-1.0.0.dist-info/WHEEL +4 -0
- browsix-1.0.0.dist-info/entry_points.txt +2 -0
- browsix-1.0.0.dist-info/licenses/LICENSE +21 -0
browsix/__init__.py
ADDED
browsix/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Actions layer for browsix."""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Accessibility action for retrieving a11y tree, node, and ancestors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from browsix.actions.base import BaseAction
|
|
8
|
+
from browsix.backend.base import AbstractBackend
|
|
9
|
+
from browsix.config import WaitStrategy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AccessibilityAction(BaseAction[Any, Any]):
|
|
13
|
+
"""Action for accessibility tree operations."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
params: Any,
|
|
18
|
+
action: str = "tree",
|
|
19
|
+
node_id: str = "",
|
|
20
|
+
url: str = "",
|
|
21
|
+
wait: WaitStrategy | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.params = params
|
|
24
|
+
self._action = action
|
|
25
|
+
self._node_id = node_id
|
|
26
|
+
self._url = url
|
|
27
|
+
self._wait = wait or WaitStrategy(strategy="load")
|
|
28
|
+
|
|
29
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
30
|
+
"""Execute the accessibility action on the backend.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
backend: The browser backend to use.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Accessibility tree dict, node dict, or list of ancestor dicts.
|
|
37
|
+
"""
|
|
38
|
+
from browsix.config import BrowserOptions
|
|
39
|
+
|
|
40
|
+
await backend.launch(BrowserOptions())
|
|
41
|
+
try:
|
|
42
|
+
if self._url:
|
|
43
|
+
await backend.navigate(self._url, self._wait)
|
|
44
|
+
if self._action == "tree":
|
|
45
|
+
return await backend.a11y_tree()
|
|
46
|
+
elif self._action == "node":
|
|
47
|
+
return await backend.a11y_node(self._node_id)
|
|
48
|
+
elif self._action == "ancestors":
|
|
49
|
+
return await backend.a11y_ancestors(self._node_id)
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError(f"Unknown a11y action: {self._action}")
|
|
52
|
+
finally:
|
|
53
|
+
await backend.close()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Animation action for listing, pausing, playing, and seeking animations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from browsix.actions.base import BaseAction
|
|
8
|
+
from browsix.backend.base import AbstractBackend
|
|
9
|
+
from browsix.config import AnimationParams
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AnimationAction(BaseAction[AnimationParams, Any]):
|
|
13
|
+
"""Action for animation operations."""
|
|
14
|
+
|
|
15
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
16
|
+
"""Execute the animation action on the backend.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
backend: An AbstractBackend instance.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Result of the animation operation.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
await backend.launch(self.params.browser)
|
|
26
|
+
if self.params.url:
|
|
27
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
28
|
+
|
|
29
|
+
action = self.params.action
|
|
30
|
+
|
|
31
|
+
if action == "list":
|
|
32
|
+
return await backend.animation_list()
|
|
33
|
+
|
|
34
|
+
if action == "pause":
|
|
35
|
+
if not self.params.animation_id:
|
|
36
|
+
raise ValueError("animation_id is required for pause action")
|
|
37
|
+
await backend.animation_pause(self.params.animation_id)
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if action == "play":
|
|
41
|
+
if not self.params.animation_id:
|
|
42
|
+
raise ValueError("animation_id is required for play action")
|
|
43
|
+
await backend.animation_play(self.params.animation_id)
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
if action == "seek":
|
|
47
|
+
if not self.params.animation_id or self.params.time_ms is None:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
"animation_id and time_ms are required for seek action"
|
|
50
|
+
)
|
|
51
|
+
await backend.animation_seek(
|
|
52
|
+
self.params.animation_id, self.params.time_ms
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
raise ValueError(f"Unknown animation action: {action}")
|
|
57
|
+
|
|
58
|
+
finally:
|
|
59
|
+
await backend.close()
|
browsix/actions/base.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Base action class for browsix actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
from browsix.backend.base import AbstractBackend
|
|
9
|
+
|
|
10
|
+
P = TypeVar("P")
|
|
11
|
+
R = TypeVar("R")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseAction(ABC, Generic[P, R]):
|
|
15
|
+
"""Abstract base class for all browsix actions.
|
|
16
|
+
|
|
17
|
+
An action encapsulates a single operation (e.g. screenshot, eval, pdf)
|
|
18
|
+
that is executed against a backend.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, params: P) -> None:
|
|
22
|
+
self.params = params
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def execute(self, backend: AbstractBackend) -> R:
|
|
26
|
+
"""Execute the action against the given backend."""
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Bluetooth action for BLE emulation (experimental)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from browsix.actions.base import BaseAction
|
|
9
|
+
from browsix.backend.base import AbstractBackend
|
|
10
|
+
from browsix.config import BrowserOptions, WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class BluetoothParams:
|
|
15
|
+
"""Parameters for Bluetooth emulation operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before Bluetooth operations.
|
|
19
|
+
action: Bluetooth action — "emulate", "stop".
|
|
20
|
+
name: Device name for "emulate" action.
|
|
21
|
+
address: Device MAC address for "emulate" action.
|
|
22
|
+
wait: Wait strategy after navigation.
|
|
23
|
+
browser: Browser launch options.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
url: str = ""
|
|
27
|
+
action: str = "emulate"
|
|
28
|
+
name: str | None = None
|
|
29
|
+
address: str = "00:00:00:00:00:01"
|
|
30
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
31
|
+
browser: BrowserOptions = field(default_factory=BrowserOptions)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BluetoothAction(BaseAction[BluetoothParams, Any]):
|
|
35
|
+
"""Action for Bluetooth emulation operations (experimental)."""
|
|
36
|
+
|
|
37
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
38
|
+
"""Execute the Bluetooth action on the backend.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
backend: An AbstractBackend instance.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Result of the Bluetooth operation.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
await backend.launch(self.params.browser)
|
|
48
|
+
if self.params.url:
|
|
49
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
50
|
+
|
|
51
|
+
action = self.params.action
|
|
52
|
+
|
|
53
|
+
if action == "emulate":
|
|
54
|
+
if not self.params.name:
|
|
55
|
+
raise ValueError("name is required for emulate action")
|
|
56
|
+
await backend.bluetooth_emulate(
|
|
57
|
+
self.params.name, self.params.address
|
|
58
|
+
)
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
if action == "stop":
|
|
62
|
+
await backend.bluetooth_stop()
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
raise ValueError(f"Unknown Bluetooth action: {action}")
|
|
66
|
+
|
|
67
|
+
finally:
|
|
68
|
+
await backend.close()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Browser action for context and window management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from browsix.actions.base import BaseAction
|
|
8
|
+
from browsix.backend.base import AbstractBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BrowserAction(BaseAction[str, Any]):
|
|
12
|
+
"""Action for browser management operations.
|
|
13
|
+
|
|
14
|
+
Supports contexts (new/list/close), window bounds (get/set), and version.
|
|
15
|
+
The params string specifies the action: "new_context", "list_contexts",
|
|
16
|
+
"close_context", "get_window", "set_window", or "version".
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
20
|
+
"""Execute the browser action.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
backend: The browser backend to use.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Action-dependent result (str, list, dict, or None).
|
|
27
|
+
"""
|
|
28
|
+
action = self.params
|
|
29
|
+
|
|
30
|
+
if action == "version":
|
|
31
|
+
return await backend.browser_version()
|
|
32
|
+
|
|
33
|
+
if action == "new_context":
|
|
34
|
+
return await backend.new_context()
|
|
35
|
+
|
|
36
|
+
if action == "list_contexts":
|
|
37
|
+
return await backend.list_contexts()
|
|
38
|
+
|
|
39
|
+
return None
|
browsix/actions/cast.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Cast action for listing sinks and controlling tab mirroring (experimental)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from browsix.actions.base import BaseAction
|
|
9
|
+
from browsix.backend.base import AbstractBackend
|
|
10
|
+
from browsix.config import BrowserOptions, WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CastParams:
|
|
15
|
+
"""Parameters for Cast operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before Cast operations.
|
|
19
|
+
action: Cast action — "list", "start-tab", "stop".
|
|
20
|
+
sink_name: Cast sink name for "start-tab" action.
|
|
21
|
+
wait: Wait strategy after navigation.
|
|
22
|
+
browser: Browser launch options.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
url: str = ""
|
|
26
|
+
action: str = "list"
|
|
27
|
+
sink_name: str | None = None
|
|
28
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
29
|
+
browser: BrowserOptions = field(default_factory=BrowserOptions)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CastAction(BaseAction[CastParams, Any]):
|
|
33
|
+
"""Action for Cast operations (experimental)."""
|
|
34
|
+
|
|
35
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
36
|
+
"""Execute the Cast action on the backend.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
backend: An AbstractBackend instance.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Result of the Cast operation.
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
await backend.launch(self.params.browser)
|
|
46
|
+
if self.params.url:
|
|
47
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
48
|
+
|
|
49
|
+
action = self.params.action
|
|
50
|
+
|
|
51
|
+
if action == "list":
|
|
52
|
+
return await backend.cast_list()
|
|
53
|
+
|
|
54
|
+
if action == "start-tab":
|
|
55
|
+
if not self.params.sink_name:
|
|
56
|
+
raise ValueError("sink_name is required for start-tab action")
|
|
57
|
+
await backend.cast_start_tab(self.params.sink_name)
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
if action == "stop":
|
|
61
|
+
await backend.cast_stop()
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
raise ValueError(f"Unknown Cast action: {action}")
|
|
65
|
+
|
|
66
|
+
finally:
|
|
67
|
+
await backend.close()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Console action for capturing console messages and browser logs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from browsix.actions.base import BaseAction
|
|
9
|
+
from browsix.backend.base import AbstractBackend
|
|
10
|
+
from browsix.config import WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ConsoleParams:
|
|
15
|
+
"""Parameters for console capture.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before capturing.
|
|
19
|
+
level: Minimum log level — "all", "error", "warning", "info".
|
|
20
|
+
wait: Wait strategy after navigation.
|
|
21
|
+
capture: What to capture — "console", "logs", or "both".
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
url: str = ""
|
|
25
|
+
level: str = "all"
|
|
26
|
+
wait: WaitStrategy | None = None
|
|
27
|
+
capture: str = "console"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ConsoleAction(BaseAction[ConsoleParams, dict[str, Any]]):
|
|
31
|
+
"""Action for capturing console messages and browser logs.
|
|
32
|
+
|
|
33
|
+
Navigates to the URL, then captures console messages and/or log entries.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
async def execute(self, backend: AbstractBackend) -> dict[str, Any]:
|
|
37
|
+
"""Execute the console action.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
backend: The browser backend to use.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dict with "console" and/or "logs" keys containing entry lists.
|
|
44
|
+
"""
|
|
45
|
+
params = self.params
|
|
46
|
+
if params.url:
|
|
47
|
+
await backend.navigate(params.url, params.wait)
|
|
48
|
+
|
|
49
|
+
result: dict[str, Any] = {}
|
|
50
|
+
|
|
51
|
+
if params.capture in ("console", "both"):
|
|
52
|
+
result["console"] = await backend.capture_console(level=params.level)
|
|
53
|
+
|
|
54
|
+
if params.capture in ("logs", "both"):
|
|
55
|
+
result["logs"] = await backend.capture_logs()
|
|
56
|
+
|
|
57
|
+
return result
|
browsix/actions/css.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""CSS action for inspecting styles, stylesheets, rules, and computed styles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from browsix.actions.base import BaseAction
|
|
9
|
+
from browsix.backend.base import AbstractBackend
|
|
10
|
+
from browsix.config import BrowserOptions, CSSParams, WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CSSActionParams:
|
|
15
|
+
"""Parameters for CSS inspection operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before CSS inspection.
|
|
19
|
+
selector: CSS selector for the target element.
|
|
20
|
+
stylesheet_id: Stylesheet ID for rules action.
|
|
21
|
+
action: CSS action — "styles", "stylesheets", "rules", "computed".
|
|
22
|
+
wait: Wait strategy after navigation.
|
|
23
|
+
browser: Browser launch options.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
url: str = ""
|
|
27
|
+
selector: str | None = None
|
|
28
|
+
stylesheet_id: str | None = None
|
|
29
|
+
action: str = "styles"
|
|
30
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
31
|
+
browser: BrowserOptions = field(default_factory=BrowserOptions)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CSSAction(BaseAction[CSSActionParams, dict[str, Any] | list[dict[str, Any]]]):
|
|
35
|
+
"""Action for CSS inspection operations."""
|
|
36
|
+
|
|
37
|
+
async def execute(
|
|
38
|
+
self, backend: AbstractBackend
|
|
39
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
40
|
+
"""Execute the CSS action on the backend.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
backend: The browser backend to use.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Dict or list of dicts containing CSS data.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If the action is not recognized or required params missing.
|
|
50
|
+
"""
|
|
51
|
+
await backend.launch(self.params.browser)
|
|
52
|
+
try:
|
|
53
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
54
|
+
return await self._run_action(backend)
|
|
55
|
+
finally:
|
|
56
|
+
await backend.close()
|
|
57
|
+
|
|
58
|
+
async def _run_action(
|
|
59
|
+
self, backend: AbstractBackend
|
|
60
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
61
|
+
action = self.params.action
|
|
62
|
+
if action == "styles":
|
|
63
|
+
if not self.params.selector:
|
|
64
|
+
raise ValueError("selector is required for styles action")
|
|
65
|
+
return await backend.css_get_styles(self.params.selector)
|
|
66
|
+
if action == "stylesheets":
|
|
67
|
+
return await backend.css_get_stylesheets()
|
|
68
|
+
if action == "rules":
|
|
69
|
+
if not self.params.stylesheet_id:
|
|
70
|
+
raise ValueError("stylesheet_id is required for rules action")
|
|
71
|
+
return await backend.css_get_rules(self.params.stylesheet_id)
|
|
72
|
+
if action == "computed":
|
|
73
|
+
if not self.params.selector:
|
|
74
|
+
raise ValueError("selector is required for computed action")
|
|
75
|
+
return await backend.css_get_computed(self.params.selector)
|
|
76
|
+
raise ValueError(f"Unknown CSS action: {action}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def css_action_from_config(params: CSSParams) -> CSSAction:
|
|
80
|
+
"""Create a CSSAction from CSSParams config dataclass.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
params: CSSParams from browsix.config.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
CSSAction instance with mapped parameters.
|
|
87
|
+
"""
|
|
88
|
+
action_params = CSSActionParams(
|
|
89
|
+
url=params.url,
|
|
90
|
+
selector=params.selector,
|
|
91
|
+
stylesheet_id=params.stylesheet_id,
|
|
92
|
+
action=params.action,
|
|
93
|
+
wait=params.wait,
|
|
94
|
+
browser=params.browser,
|
|
95
|
+
)
|
|
96
|
+
return CSSAction(action_params)
|
browsix/actions/debug.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Debug action for breakpoints, stepping, pause/resume, and listeners."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from browsix.actions.base import BaseAction
|
|
9
|
+
from browsix.backend.base import AbstractBackend
|
|
10
|
+
from browsix.config import BrowserOptions, DebugParams, WaitStrategy
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class DebugActionParams:
|
|
15
|
+
"""Parameters for debugging operations.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
url: URL to navigate to before debugging (optional for step/pause/resume).
|
|
19
|
+
line: Line number for breakpoint (0-based).
|
|
20
|
+
function_name: Function name for function breakpoint.
|
|
21
|
+
condition: Optional condition expression for breakpoint.
|
|
22
|
+
action: Debug action — "breakpoint", "function_breakpoint",
|
|
23
|
+
"remove_breakpoint", "step_over", "step_into", "step_out",
|
|
24
|
+
"pause", "resume", "listeners".
|
|
25
|
+
breakpoint_id: Breakpoint ID for remove_breakpoint.
|
|
26
|
+
selector: CSS selector for listeners action.
|
|
27
|
+
script_url: URL of the script for breakpoint (distinct from page url).
|
|
28
|
+
wait: Wait strategy after navigation.
|
|
29
|
+
browser: Browser launch options.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
url: str | None = None
|
|
33
|
+
line: int | None = None
|
|
34
|
+
function_name: str | None = None
|
|
35
|
+
condition: str | None = None
|
|
36
|
+
action: str = "breakpoint"
|
|
37
|
+
breakpoint_id: str | None = None
|
|
38
|
+
selector: str | None = None
|
|
39
|
+
script_url: str | None = None
|
|
40
|
+
wait: WaitStrategy = field(default_factory=WaitStrategy)
|
|
41
|
+
browser: BrowserOptions = field(default_factory=BrowserOptions)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class DebugAction(BaseAction[DebugActionParams, Any]):
|
|
45
|
+
"""Action for debugging operations."""
|
|
46
|
+
|
|
47
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
48
|
+
"""Execute the debug action on the backend.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
backend: The browser backend to use.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Result of the debug action (breakpoint ID, listener list, or None).
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
ValueError: If the action is not recognized or required params missing.
|
|
58
|
+
"""
|
|
59
|
+
await backend.launch(self.params.browser)
|
|
60
|
+
try:
|
|
61
|
+
if self.params.url:
|
|
62
|
+
await backend.navigate(self.params.url, self.params.wait)
|
|
63
|
+
return await self._run_action(backend)
|
|
64
|
+
finally:
|
|
65
|
+
await backend.close()
|
|
66
|
+
|
|
67
|
+
async def _run_action(self, backend: AbstractBackend) -> Any:
|
|
68
|
+
action = self.params.action
|
|
69
|
+
if action == "breakpoint":
|
|
70
|
+
if self.params.script_url is None or self.params.line is None:
|
|
71
|
+
raise ValueError(
|
|
72
|
+
"script_url and line are required for breakpoint action"
|
|
73
|
+
)
|
|
74
|
+
return await backend.debug_set_breakpoint(
|
|
75
|
+
self.params.script_url, self.params.line, self.params.condition
|
|
76
|
+
)
|
|
77
|
+
if action == "function_breakpoint":
|
|
78
|
+
if not self.params.function_name:
|
|
79
|
+
raise ValueError(
|
|
80
|
+
"function_name is required for function_breakpoint action"
|
|
81
|
+
)
|
|
82
|
+
return await backend.debug_set_breakpoint_function(
|
|
83
|
+
self.params.function_name
|
|
84
|
+
)
|
|
85
|
+
if action == "remove_breakpoint":
|
|
86
|
+
if not self.params.breakpoint_id:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"breakpoint_id is required for remove_breakpoint action"
|
|
89
|
+
)
|
|
90
|
+
await backend.debug_remove_breakpoint(self.params.breakpoint_id)
|
|
91
|
+
return None
|
|
92
|
+
if action == "step_over":
|
|
93
|
+
await backend.debug_step_over()
|
|
94
|
+
return None
|
|
95
|
+
if action == "step_into":
|
|
96
|
+
await backend.debug_step_into()
|
|
97
|
+
return None
|
|
98
|
+
if action == "step_out":
|
|
99
|
+
await backend.debug_step_out()
|
|
100
|
+
return None
|
|
101
|
+
if action == "pause":
|
|
102
|
+
await backend.debug_pause()
|
|
103
|
+
return None
|
|
104
|
+
if action == "resume":
|
|
105
|
+
await backend.debug_resume()
|
|
106
|
+
return None
|
|
107
|
+
if action == "listeners":
|
|
108
|
+
if not self.params.selector:
|
|
109
|
+
raise ValueError("selector is required for listeners action")
|
|
110
|
+
return await backend.debug_get_listeners(self.params.selector)
|
|
111
|
+
raise ValueError(f"Unknown debug action: {action}")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def debug_action_from_config(params: DebugParams) -> DebugAction:
|
|
115
|
+
"""Create a DebugAction from DebugParams config dataclass.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
params: DebugParams from browsix.config.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
DebugAction instance with mapped parameters.
|
|
122
|
+
"""
|
|
123
|
+
action_params = DebugActionParams(
|
|
124
|
+
url=params.url,
|
|
125
|
+
line=params.line,
|
|
126
|
+
function_name=params.function_name,
|
|
127
|
+
condition=params.condition,
|
|
128
|
+
action=params.action,
|
|
129
|
+
breakpoint_id=params.breakpoint_id,
|
|
130
|
+
selector=params.selector,
|
|
131
|
+
wait=params.wait,
|
|
132
|
+
browser=params.browser,
|
|
133
|
+
)
|
|
134
|
+
return DebugAction(action_params)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Dialog action for accepting or dismissing JavaScript dialogs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from browsix.actions.base import BaseAction
|
|
8
|
+
from browsix.backend.base import AbstractBackend
|
|
9
|
+
from browsix.config import BrowserOptions, WaitStrategy
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DialogAction(BaseAction[str, None]):
|
|
13
|
+
"""Action for handling JavaScript dialogs (alert, confirm, prompt)."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
params: str,
|
|
18
|
+
action: str = "accept",
|
|
19
|
+
prompt_text: str | None = None,
|
|
20
|
+
url: str = "",
|
|
21
|
+
wait: WaitStrategy | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
self.params = params
|
|
24
|
+
self._action = action
|
|
25
|
+
self._prompt_text = prompt_text
|
|
26
|
+
self._url = url
|
|
27
|
+
self._wait = wait or WaitStrategy(strategy="load")
|
|
28
|
+
|
|
29
|
+
async def execute(self, backend: AbstractBackend) -> Any:
|
|
30
|
+
"""Execute the dialog action on the backend.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
backend: The browser backend to use.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
None.
|
|
37
|
+
"""
|
|
38
|
+
await backend.launch(BrowserOptions())
|
|
39
|
+
try:
|
|
40
|
+
if self._url:
|
|
41
|
+
await backend.navigate(self._url, self._wait)
|
|
42
|
+
if self._action == "accept":
|
|
43
|
+
await backend.dialog_accept(self._prompt_text)
|
|
44
|
+
elif self._action == "dismiss":
|
|
45
|
+
await backend.dialog_dismiss()
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError(f"Unknown dialog action: {self._action}")
|
|
48
|
+
finally:
|
|
49
|
+
await backend.close()
|
|
50
|
+
return None
|