playwright-browserstack 1.62.0__py3-none-win_amd64.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.
- playwright/__init__.py +19 -0
- playwright/__main__.py +33 -0
- playwright/_impl/__init__.py +0 -0
- playwright/_impl/__pyinstaller/__init__.py +20 -0
- playwright/_impl/__pyinstaller/hook-playwright.async_api.py +17 -0
- playwright/_impl/__pyinstaller/hook-playwright.sync_api.py +17 -0
- playwright/_impl/_android.py +704 -0
- playwright/_impl/_api_structures.py +365 -0
- playwright/_impl/_artifact.py +87 -0
- playwright/_impl/_assertions.py +1124 -0
- playwright/_impl/_async_base.py +105 -0
- playwright/_impl/_browser.py +293 -0
- playwright/_impl/_browser_context.py +754 -0
- playwright/_impl/_browser_type.py +368 -0
- playwright/_impl/_cdp_session.py +46 -0
- playwright/_impl/_clock.py +104 -0
- playwright/_impl/_connection.py +764 -0
- playwright/_impl/_console_message.py +98 -0
- playwright/_impl/_credentials.py +57 -0
- playwright/_impl/_debugger.py +54 -0
- playwright/_impl/_dialog.py +63 -0
- playwright/_impl/_disposable.py +96 -0
- playwright/_impl/_download.py +64 -0
- playwright/_impl/_driver.py +41 -0
- playwright/_impl/_element_handle.py +480 -0
- playwright/_impl/_errors.py +64 -0
- playwright/_impl/_event_context_manager.py +33 -0
- playwright/_impl/_fetch.py +626 -0
- playwright/_impl/_file_chooser.py +57 -0
- playwright/_impl/_form_data.py +34 -0
- playwright/_impl/_frame.py +985 -0
- playwright/_impl/_glob.py +65 -0
- playwright/_impl/_greenlets.py +49 -0
- playwright/_impl/_har_router.py +122 -0
- playwright/_impl/_helper.py +601 -0
- playwright/_impl/_impl_to_api_mapping.py +158 -0
- playwright/_impl/_input.py +111 -0
- playwright/_impl/_js_handle.py +336 -0
- playwright/_impl/_json_pipe.py +97 -0
- playwright/_impl/_local_utils.py +95 -0
- playwright/_impl/_locator.py +1024 -0
- playwright/_impl/_map.py +31 -0
- playwright/_impl/_network.py +1017 -0
- playwright/_impl/_object_factory.py +116 -0
- playwright/_impl/_page.py +1637 -0
- playwright/_impl/_path_utils.py +27 -0
- playwright/_impl/_playwright.py +68 -0
- playwright/_impl/_screencast.py +177 -0
- playwright/_impl/_selectors.py +67 -0
- playwright/_impl/_set_input_files_helpers.py +158 -0
- playwright/_impl/_str_utils.py +76 -0
- playwright/_impl/_stream.py +46 -0
- playwright/_impl/_sync_base.py +151 -0
- playwright/_impl/_tracing.py +281 -0
- playwright/_impl/_transport.py +184 -0
- playwright/_impl/_video.py +56 -0
- playwright/_impl/_waiter.py +197 -0
- playwright/_impl/_web_error.py +48 -0
- playwright/_impl/_web_storage.py +58 -0
- playwright/_impl/_writable_stream.py +42 -0
- playwright/_repo_version.py +24 -0
- playwright/async_api/__init__.py +275 -0
- playwright/async_api/_android_generated.py +462 -0
- playwright/async_api/_context_manager.py +57 -0
- playwright/async_api/_generated.py +23875 -0
- playwright/driver/LICENSE +2946 -0
- playwright/driver/README.md +1 -0
- playwright/driver/node.exe +0 -0
- playwright/driver/package/LICENSE +202 -0
- playwright/driver/package/NOTICE +5 -0
- playwright/driver/package/README.md +3 -0
- playwright/driver/package/ThirdPartyNotices.txt +13 -0
- playwright/driver/package/bin/install_media_pack.ps1 +5 -0
- playwright/driver/package/bin/install_webkit_wsl.ps1 +33 -0
- playwright/driver/package/bin/reinstall_chrome_beta_linux.sh +42 -0
- playwright/driver/package/bin/reinstall_chrome_beta_mac.sh +13 -0
- playwright/driver/package/bin/reinstall_chrome_beta_win.ps1 +24 -0
- playwright/driver/package/bin/reinstall_chrome_stable_linux.sh +42 -0
- playwright/driver/package/bin/reinstall_chrome_stable_mac.sh +12 -0
- playwright/driver/package/bin/reinstall_chrome_stable_win.ps1 +24 -0
- playwright/driver/package/bin/reinstall_msedge_beta_linux.sh +48 -0
- playwright/driver/package/bin/reinstall_msedge_beta_mac.sh +11 -0
- playwright/driver/package/bin/reinstall_msedge_beta_win.ps1 +23 -0
- playwright/driver/package/bin/reinstall_msedge_dev_linux.sh +48 -0
- playwright/driver/package/bin/reinstall_msedge_dev_mac.sh +11 -0
- playwright/driver/package/bin/reinstall_msedge_dev_win.ps1 +23 -0
- playwright/driver/package/bin/reinstall_msedge_stable_linux.sh +48 -0
- playwright/driver/package/bin/reinstall_msedge_stable_mac.sh +11 -0
- playwright/driver/package/bin/reinstall_msedge_stable_win.ps1 +24 -0
- playwright/driver/package/browsers.json +75 -0
- playwright/driver/package/cli.js +21 -0
- playwright/driver/package/index.d.ts +17 -0
- playwright/driver/package/index.js +17 -0
- playwright/driver/package/index.mjs +28 -0
- playwright/driver/package/lib/bootstrap.js +88 -0
- playwright/driver/package/lib/coreBundle.js +74830 -0
- playwright/driver/package/lib/entry/cliDaemon.js +5 -0
- playwright/driver/package/lib/entry/dashboardApp.js +3 -0
- playwright/driver/package/lib/entry/mcp.js +10 -0
- playwright/driver/package/lib/entry/oopBrowserDownload.js +3 -0
- playwright/driver/package/lib/package.js +50 -0
- playwright/driver/package/lib/server/chromium/appIcon.png +0 -0
- playwright/driver/package/lib/server/electron/loader.js +118 -0
- playwright/driver/package/lib/serverRegistry.js +7347 -0
- playwright/driver/package/lib/serverRegistry.js.LICENSE +354 -0
- playwright/driver/package/lib/tools/cli-client/channelSessions.js +141 -0
- playwright/driver/package/lib/tools/cli-client/cli.js +6 -0
- playwright/driver/package/lib/tools/cli-client/help.json +708 -0
- playwright/driver/package/lib/tools/cli-client/minimist.js +128 -0
- playwright/driver/package/lib/tools/cli-client/output.js +343 -0
- playwright/driver/package/lib/tools/cli-client/program.js +404 -0
- playwright/driver/package/lib/tools/cli-client/registry.js +176 -0
- playwright/driver/package/lib/tools/cli-client/session.js +258 -0
- playwright/driver/package/lib/tools/dashboard/appIcon.png +0 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/SKILL.md +420 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
- playwright/driver/package/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
- playwright/driver/package/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
- playwright/driver/package/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- playwright/driver/package/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
- playwright/driver/package/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
- playwright/driver/package/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
- playwright/driver/package/lib/tools/skills/playwright-trace/SKILL.md +171 -0
- playwright/driver/package/lib/tools/utils/extension.js +101 -0
- playwright/driver/package/lib/tools/utils/socketConnection.js +108 -0
- playwright/driver/package/lib/utilsBundle.js +90764 -0
- playwright/driver/package/lib/utilsBundle.js.LICENSE +2179 -0
- playwright/driver/package/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
- playwright/driver/package/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
- playwright/driver/package/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
- playwright/driver/package/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
- playwright/driver/package/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
- playwright/driver/package/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
- playwright/driver/package/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
- playwright/driver/package/lib/vite/dashboard/index.html +29 -0
- playwright/driver/package/lib/vite/dashboard/playwright-logo.svg +24 -0
- playwright/driver/package/lib/vite/htmlReport/index.html +16 -0
- playwright/driver/package/lib/vite/htmlReport/report.css +2 -0
- playwright/driver/package/lib/vite/htmlReport/report.js +32 -0
- playwright/driver/package/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- playwright/driver/package/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
- playwright/driver/package/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
- playwright/driver/package/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
- playwright/driver/package/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
- playwright/driver/package/lib/vite/recorder/index.html +29 -0
- playwright/driver/package/lib/vite/recorder/playwright-logo.svg +9 -0
- playwright/driver/package/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
- playwright/driver/package/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
- playwright/driver/package/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- playwright/driver/package/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
- playwright/driver/package/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- playwright/driver/package/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
- playwright/driver/package/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
- playwright/driver/package/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- playwright/driver/package/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
- playwright/driver/package/lib/vite/traceViewer/index.html +44 -0
- playwright/driver/package/lib/vite/traceViewer/manifest.webmanifest +16 -0
- playwright/driver/package/lib/vite/traceViewer/playwright-logo.svg +9 -0
- playwright/driver/package/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- playwright/driver/package/lib/vite/traceViewer/snapshot.html +10 -0
- playwright/driver/package/lib/vite/traceViewer/sw.bundle.js +4 -0
- playwright/driver/package/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
- playwright/driver/package/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
- playwright/driver/package/lib/vite/traceViewer/uiMode.html +18 -0
- playwright/driver/package/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- playwright/driver/package/lib/webp_codec.LICENSE +173 -0
- playwright/driver/package/lib/webp_codec.wasm +0 -0
- playwright/driver/package/lib/xdg-open +1267 -0
- playwright/driver/package/package.json +34 -0
- playwright/driver/package/types/protocol.d.ts +24842 -0
- playwright/driver/package/types/structs.d.ts +47 -0
- playwright/driver/package/types/types.d.ts +26188 -0
- playwright/py.typed +0 -0
- playwright/sync_api/__init__.py +275 -0
- playwright/sync_api/_android_generated.py +494 -0
- playwright/sync_api/_context_manager.py +98 -0
- playwright/sync_api/_generated.py +24045 -0
- playwright_browserstack-1.62.0.dist-info/METADATA +82 -0
- playwright_browserstack-1.62.0.dist-info/RECORD +192 -0
- playwright_browserstack-1.62.0.dist-info/WHEEL +5 -0
- playwright_browserstack-1.62.0.dist-info/entry_points.txt +5 -0
- playwright_browserstack-1.62.0.dist-info/licenses/LICENSE +202 -0
- playwright_browserstack-1.62.0.dist-info/licenses/LICENSE.txt +51 -0
- playwright_browserstack-1.62.0.dist-info/licenses/NOTICE +18 -0
- playwright_browserstack-1.62.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
# Copyright (c) 2025 BrowserStack. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# This software is the confidential and proprietary information of BrowserStack.
|
|
4
|
+
# You shall not disclose such Confidential Information and shall use it only in
|
|
5
|
+
# accordance with the terms of the license agreement you entered into with BrowserStack.
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import base64
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, cast
|
|
12
|
+
|
|
13
|
+
from pyee import EventEmitter
|
|
14
|
+
|
|
15
|
+
from playwright._impl._browser_context import BrowserContext
|
|
16
|
+
from playwright._impl._connection import ChannelOwner, Connection, from_channel
|
|
17
|
+
from playwright._impl._errors import Error, TargetClosedError, is_target_closed_error
|
|
18
|
+
from playwright._impl._helper import (
|
|
19
|
+
PLAYWRIGHT_MAX_DEADLINE,
|
|
20
|
+
TimeoutSettings,
|
|
21
|
+
async_writefile,
|
|
22
|
+
make_dirs_for_file,
|
|
23
|
+
)
|
|
24
|
+
from playwright._impl._json_pipe import JsonPipeTransport
|
|
25
|
+
from playwright._impl._waiter import Waiter, throw_on_timeout
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from playwright._impl._playwright import Playwright
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Android(ChannelOwner):
|
|
32
|
+
"""Root Android object — entry point for all Android device interactions."""
|
|
33
|
+
|
|
34
|
+
_playwright: "Playwright"
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
|
|
38
|
+
) -> None:
|
|
39
|
+
super().__init__(parent, type, guid, initializer)
|
|
40
|
+
self._timeout_settings = TimeoutSettings(None)
|
|
41
|
+
|
|
42
|
+
def set_default_timeout(self, timeout: float) -> None:
|
|
43
|
+
self._timeout_settings.set_default_timeout(timeout)
|
|
44
|
+
|
|
45
|
+
async def devices(self, port: int = None) -> List["AndroidDevice"]:
|
|
46
|
+
params: Dict = {}
|
|
47
|
+
if port is not None:
|
|
48
|
+
params["port"] = port
|
|
49
|
+
result = await self._channel.send_return_as_dict("devices", None, params)
|
|
50
|
+
return [from_channel(d) for d in result["devices"]]
|
|
51
|
+
|
|
52
|
+
async def connect(
|
|
53
|
+
self,
|
|
54
|
+
wsEndpoint: str,
|
|
55
|
+
headers: Dict[str, str] = None,
|
|
56
|
+
slowMo: float = None,
|
|
57
|
+
timeout: float = None,
|
|
58
|
+
) -> "AndroidDevice":
|
|
59
|
+
# Mirror Node.js: x-playwright-browser: 'android' header, pipe via LocalUtils.
|
|
60
|
+
if slowMo is None:
|
|
61
|
+
slowMo = 0
|
|
62
|
+
if timeout is None:
|
|
63
|
+
timeout = 0
|
|
64
|
+
# Marker FIRST, matching client/android.ts:
|
|
65
|
+
# { 'x-playwright-browser': 'android', ...options.headers }
|
|
66
|
+
# Note this is the opposite precedence from browserType.connect, which
|
|
67
|
+
# spreads the caller's headers first. That inconsistency is upstream's;
|
|
68
|
+
# we mirror the Android path deliberately.
|
|
69
|
+
req_headers = {"x-playwright-browser": "android", **(headers or {})}
|
|
70
|
+
local_utils = self._connection.local_utils
|
|
71
|
+
# Wire params must match the bundled driver's LocalUtilsConnectParams
|
|
72
|
+
# scheme. Upstream renamed `wsEndpoint` -> `endpoint` after 1.56, so
|
|
73
|
+
# this key is version-sensitive; see
|
|
74
|
+
# tests/test_android.py::test_connect_wire_params_match_bundled_driver.
|
|
75
|
+
pipe_channel = (
|
|
76
|
+
await local_utils._channel.send_return_as_dict(
|
|
77
|
+
"connect",
|
|
78
|
+
lambda t: t or 0,
|
|
79
|
+
{
|
|
80
|
+
"endpoint": wsEndpoint,
|
|
81
|
+
"headers": req_headers,
|
|
82
|
+
"slowMo": slowMo,
|
|
83
|
+
"timeout": timeout,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
)["pipe"]
|
|
87
|
+
transport = JsonPipeTransport(self._connection._loop, pipe_channel)
|
|
88
|
+
|
|
89
|
+
connection = Connection(
|
|
90
|
+
self._connection._dispatcher_fiber,
|
|
91
|
+
self._connection._object_factory,
|
|
92
|
+
transport,
|
|
93
|
+
self._connection._loop,
|
|
94
|
+
local_utils=self._connection.local_utils,
|
|
95
|
+
)
|
|
96
|
+
connection.mark_as_remote()
|
|
97
|
+
|
|
98
|
+
device: Optional["AndroidDevice"] = None
|
|
99
|
+
|
|
100
|
+
# Registered before create_task(connection.run()) — as
|
|
101
|
+
# _browser_type.connect does — so a pipe that closes *during* the
|
|
102
|
+
# handshake still runs cleanup(). Registering after the handshake
|
|
103
|
+
# leaked a connection on every failed connect: _init_task stayed
|
|
104
|
+
# pending forever and _closed_error was never set.
|
|
105
|
+
def handle_transport_close(reason: Optional[str]) -> None:
|
|
106
|
+
if device:
|
|
107
|
+
device._notify_remote_closed()
|
|
108
|
+
connection.cleanup(reason)
|
|
109
|
+
|
|
110
|
+
transport.once("close", handle_transport_close)
|
|
111
|
+
|
|
112
|
+
connection._is_sync = self._connection._is_sync
|
|
113
|
+
connection._loop.create_task(connection.run())
|
|
114
|
+
|
|
115
|
+
playwright_future = connection.playwright_future
|
|
116
|
+
# Truthiness, not "is not None": 0 means no timeout, and
|
|
117
|
+
# throw_on_timeout has no zero-guard (unlike Waiter.reject_on_timeout),
|
|
118
|
+
# so throw_on_timeout(0) would fire on the next loop tick.
|
|
119
|
+
timeout_future = throw_on_timeout(
|
|
120
|
+
timeout if timeout else PLAYWRIGHT_MAX_DEADLINE,
|
|
121
|
+
Error("Android.connect: timed out"),
|
|
122
|
+
)
|
|
123
|
+
done, _ = await asyncio.wait(
|
|
124
|
+
{transport.on_error_future, playwright_future, timeout_future},
|
|
125
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
126
|
+
)
|
|
127
|
+
if not playwright_future.done():
|
|
128
|
+
playwright_future.cancel()
|
|
129
|
+
if not timeout_future.done():
|
|
130
|
+
timeout_future.cancel()
|
|
131
|
+
|
|
132
|
+
playwright: "Playwright" = next(iter(done)).result()
|
|
133
|
+
# NB: no playwright._set_selectors(...) here. browserType.connect does
|
|
134
|
+
# that, but client/android.ts deliberately does not, so the child
|
|
135
|
+
# connection keeps its own Selectors — see the review notes on #16.
|
|
136
|
+
self._connection._child_ws_connections.append(connection)
|
|
137
|
+
|
|
138
|
+
pre_connected = playwright._initializer.get("preConnectedAndroidDevice")
|
|
139
|
+
if not pre_connected:
|
|
140
|
+
connection._transport.dispose()
|
|
141
|
+
raise Error("Malformed endpoint. Did you use Android.launchServer method?")
|
|
142
|
+
device = cast("AndroidDevice", from_channel(pre_connected))
|
|
143
|
+
device._should_close_connection_on_close = True
|
|
144
|
+
|
|
145
|
+
# android.ts closes the pipe with
|
|
146
|
+
# device.on(Events.AndroidDevice.Close, () => connection.close())
|
|
147
|
+
# because in JS Connection.close() also closes the transport. Python's
|
|
148
|
+
# Connection.cleanup() does not, so the equivalent has to hang off the
|
|
149
|
+
# connection's close event: AndroidDevice.close() calls
|
|
150
|
+
# self._connection.cleanup(), and without this bridge the pipe is never
|
|
151
|
+
# closed — BrowserStack then ends the session ABRUPT_CLOSE instead of
|
|
152
|
+
# CLIENT_STOPPED_SESSION.
|
|
153
|
+
def _on_connection_close() -> None:
|
|
154
|
+
try:
|
|
155
|
+
transport.request_stop()
|
|
156
|
+
except Exception:
|
|
157
|
+
pass # pipe already gone (e.g. server-initiated close)
|
|
158
|
+
|
|
159
|
+
connection.on("close", lambda *_: _on_connection_close())
|
|
160
|
+
|
|
161
|
+
return device
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class AndroidDevice(ChannelOwner):
|
|
165
|
+
"""Represents a connected Android device."""
|
|
166
|
+
|
|
167
|
+
class Events:
|
|
168
|
+
Close = "close"
|
|
169
|
+
WebView = "webview"
|
|
170
|
+
|
|
171
|
+
def __init__(
|
|
172
|
+
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
|
|
173
|
+
) -> None:
|
|
174
|
+
super().__init__(parent, type, guid, initializer)
|
|
175
|
+
self._android: Android = parent # type: ignore
|
|
176
|
+
self._timeout_settings = TimeoutSettings(
|
|
177
|
+
getattr(parent, "_timeout_settings", None)
|
|
178
|
+
)
|
|
179
|
+
self._webviews: Dict[str, "AndroidWebView"] = {}
|
|
180
|
+
self._should_close_connection_on_close = False
|
|
181
|
+
self._contexts: List[BrowserContext] = []
|
|
182
|
+
self._is_closed: bool = False
|
|
183
|
+
|
|
184
|
+
self.input = AndroidInput(self)
|
|
185
|
+
|
|
186
|
+
self._channel.on(
|
|
187
|
+
"webViewAdded",
|
|
188
|
+
lambda params: self._on_web_view_added(params["webView"]),
|
|
189
|
+
)
|
|
190
|
+
self._channel.on(
|
|
191
|
+
"webViewRemoved",
|
|
192
|
+
lambda params: self._on_web_view_removed(params["socketName"]),
|
|
193
|
+
)
|
|
194
|
+
self._channel.on("close", lambda _: self._did_close())
|
|
195
|
+
|
|
196
|
+
def _on_web_view_added(self, web_view_data: Dict) -> None:
|
|
197
|
+
view = AndroidWebView(self, web_view_data)
|
|
198
|
+
self._webviews[web_view_data["socketName"]] = view
|
|
199
|
+
self.emit(AndroidDevice.Events.WebView, view)
|
|
200
|
+
|
|
201
|
+
def _on_web_view_removed(self, socket_name: str) -> None:
|
|
202
|
+
view = self._webviews.pop(socket_name, None)
|
|
203
|
+
if view:
|
|
204
|
+
view.emit(AndroidWebView.Events.Close)
|
|
205
|
+
|
|
206
|
+
def _did_close(self) -> None:
|
|
207
|
+
if self._is_closed:
|
|
208
|
+
return
|
|
209
|
+
self._is_closed = True
|
|
210
|
+
self.emit(AndroidDevice.Events.Close, self)
|
|
211
|
+
|
|
212
|
+
def _notify_remote_closed(self) -> None:
|
|
213
|
+
for context in list(self._contexts):
|
|
214
|
+
for page in list(context.pages):
|
|
215
|
+
page._on_close()
|
|
216
|
+
context._on_close()
|
|
217
|
+
self._did_close()
|
|
218
|
+
|
|
219
|
+
def set_default_timeout(self, timeout: float) -> None:
|
|
220
|
+
self._timeout_settings.set_default_timeout(timeout)
|
|
221
|
+
|
|
222
|
+
def _timeout(self, timeout: Optional[float]) -> float:
|
|
223
|
+
"""Timeout calculator for channel calls, mirroring Frame._timeout.
|
|
224
|
+
|
|
225
|
+
Channel.send() pops "timeout" out of params and converts it into the
|
|
226
|
+
protocol message's own timeout field *via this calculator*. Passing None
|
|
227
|
+
there discards the value and sends timeout=0, which the driver treats as
|
|
228
|
+
"no timeout" (Node's kNoTimeout is literally {timeout: 0}). Every
|
|
229
|
+
timeout-bearing call must therefore pass this.
|
|
230
|
+
"""
|
|
231
|
+
return self._timeout_settings.timeout(timeout)
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def serial(self) -> str:
|
|
235
|
+
return self._initializer["serial"]
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def model(self) -> str:
|
|
239
|
+
return self._initializer["model"]
|
|
240
|
+
|
|
241
|
+
def web_views(self) -> List["AndroidWebView"]:
|
|
242
|
+
return list(self._webviews.values())
|
|
243
|
+
|
|
244
|
+
async def web_view(
|
|
245
|
+
self,
|
|
246
|
+
selector: Dict,
|
|
247
|
+
timeout: float = None,
|
|
248
|
+
) -> "AndroidWebView":
|
|
249
|
+
def _match(v: "AndroidWebView") -> bool:
|
|
250
|
+
if "pkg" in selector:
|
|
251
|
+
return v.pkg == selector["pkg"]
|
|
252
|
+
if "socketName" in selector:
|
|
253
|
+
return v._socket_name() == selector["socketName"]
|
|
254
|
+
return False
|
|
255
|
+
|
|
256
|
+
existing = next((v for v in self._webviews.values() if _match(v)), None)
|
|
257
|
+
if existing:
|
|
258
|
+
return existing
|
|
259
|
+
return await self.wait_for_event(
|
|
260
|
+
AndroidDevice.Events.WebView,
|
|
261
|
+
predicate=_match,
|
|
262
|
+
timeout=timeout,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
async def wait(
|
|
266
|
+
self,
|
|
267
|
+
selector: Dict,
|
|
268
|
+
state: str = None,
|
|
269
|
+
timeout: float = None,
|
|
270
|
+
) -> None:
|
|
271
|
+
params: Dict = {
|
|
272
|
+
"androidSelector": _to_selector_channel(selector),
|
|
273
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
274
|
+
}
|
|
275
|
+
if state is not None:
|
|
276
|
+
params["state"] = state
|
|
277
|
+
await self._channel.send("wait", self._timeout, params)
|
|
278
|
+
|
|
279
|
+
async def fill(self, selector: Dict, text: str, timeout: float = None) -> None:
|
|
280
|
+
await self._channel.send(
|
|
281
|
+
"fill",
|
|
282
|
+
self._timeout,
|
|
283
|
+
{
|
|
284
|
+
"androidSelector": _to_selector_channel(selector),
|
|
285
|
+
"text": text,
|
|
286
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def tap(
|
|
291
|
+
self, selector: Dict, duration: float = None, timeout: float = None
|
|
292
|
+
) -> None:
|
|
293
|
+
params: Dict = {
|
|
294
|
+
"androidSelector": _to_selector_channel(selector),
|
|
295
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
296
|
+
}
|
|
297
|
+
if duration is not None:
|
|
298
|
+
params["duration"] = duration
|
|
299
|
+
await self._channel.send("tap", self._timeout, params)
|
|
300
|
+
|
|
301
|
+
async def long_tap(self, selector: Dict, timeout: float = None) -> None:
|
|
302
|
+
await self._channel.send(
|
|
303
|
+
"longTap",
|
|
304
|
+
self._timeout,
|
|
305
|
+
{
|
|
306
|
+
"androidSelector": _to_selector_channel(selector),
|
|
307
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
async def drag(
|
|
312
|
+
self,
|
|
313
|
+
selector: Dict,
|
|
314
|
+
dest: Dict,
|
|
315
|
+
speed: float = None,
|
|
316
|
+
timeout: float = None,
|
|
317
|
+
) -> None:
|
|
318
|
+
params: Dict = {
|
|
319
|
+
"androidSelector": _to_selector_channel(selector),
|
|
320
|
+
"dest": dest,
|
|
321
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
322
|
+
}
|
|
323
|
+
if speed is not None:
|
|
324
|
+
params["speed"] = speed
|
|
325
|
+
await self._channel.send("drag", self._timeout, params)
|
|
326
|
+
|
|
327
|
+
async def fling(
|
|
328
|
+
self,
|
|
329
|
+
selector: Dict,
|
|
330
|
+
direction: str,
|
|
331
|
+
speed: float = None,
|
|
332
|
+
timeout: float = None,
|
|
333
|
+
) -> None:
|
|
334
|
+
params: Dict = {
|
|
335
|
+
"androidSelector": _to_selector_channel(selector),
|
|
336
|
+
"direction": direction,
|
|
337
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
338
|
+
}
|
|
339
|
+
if speed is not None:
|
|
340
|
+
params["speed"] = speed
|
|
341
|
+
await self._channel.send("fling", self._timeout, params)
|
|
342
|
+
|
|
343
|
+
async def pinch_close(
|
|
344
|
+
self,
|
|
345
|
+
selector: Dict,
|
|
346
|
+
percent: float,
|
|
347
|
+
speed: float = None,
|
|
348
|
+
timeout: float = None,
|
|
349
|
+
) -> None:
|
|
350
|
+
params: Dict = {
|
|
351
|
+
"androidSelector": _to_selector_channel(selector),
|
|
352
|
+
"percent": percent,
|
|
353
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
354
|
+
}
|
|
355
|
+
if speed is not None:
|
|
356
|
+
params["speed"] = speed
|
|
357
|
+
await self._channel.send("pinchClose", self._timeout, params)
|
|
358
|
+
|
|
359
|
+
async def pinch_open(
|
|
360
|
+
self,
|
|
361
|
+
selector: Dict,
|
|
362
|
+
percent: float,
|
|
363
|
+
speed: float = None,
|
|
364
|
+
timeout: float = None,
|
|
365
|
+
) -> None:
|
|
366
|
+
params: Dict = {
|
|
367
|
+
"androidSelector": _to_selector_channel(selector),
|
|
368
|
+
"percent": percent,
|
|
369
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
370
|
+
}
|
|
371
|
+
if speed is not None:
|
|
372
|
+
params["speed"] = speed
|
|
373
|
+
await self._channel.send("pinchOpen", self._timeout, params)
|
|
374
|
+
|
|
375
|
+
async def scroll(
|
|
376
|
+
self,
|
|
377
|
+
selector: Dict,
|
|
378
|
+
direction: str,
|
|
379
|
+
percent: float,
|
|
380
|
+
speed: float = None,
|
|
381
|
+
timeout: float = None,
|
|
382
|
+
) -> None:
|
|
383
|
+
params: Dict = {
|
|
384
|
+
"androidSelector": _to_selector_channel(selector),
|
|
385
|
+
"direction": direction,
|
|
386
|
+
"percent": percent,
|
|
387
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
388
|
+
}
|
|
389
|
+
if speed is not None:
|
|
390
|
+
params["speed"] = speed
|
|
391
|
+
await self._channel.send("scroll", self._timeout, params)
|
|
392
|
+
|
|
393
|
+
async def swipe(
|
|
394
|
+
self,
|
|
395
|
+
selector: Dict,
|
|
396
|
+
direction: str,
|
|
397
|
+
percent: float,
|
|
398
|
+
speed: float = None,
|
|
399
|
+
timeout: float = None,
|
|
400
|
+
) -> None:
|
|
401
|
+
params: Dict = {
|
|
402
|
+
"androidSelector": _to_selector_channel(selector),
|
|
403
|
+
"direction": direction,
|
|
404
|
+
"percent": percent,
|
|
405
|
+
"timeout": self._timeout_settings.timeout(timeout),
|
|
406
|
+
}
|
|
407
|
+
if speed is not None:
|
|
408
|
+
params["speed"] = speed
|
|
409
|
+
await self._channel.send("swipe", self._timeout, params)
|
|
410
|
+
|
|
411
|
+
async def press(self, selector: Dict, key: str, timeout: float = None) -> None:
|
|
412
|
+
await self.tap(selector, timeout=timeout)
|
|
413
|
+
await self.input.press(key)
|
|
414
|
+
|
|
415
|
+
async def info(self, selector: Dict) -> Dict:
|
|
416
|
+
result = await self._channel.send_return_as_dict(
|
|
417
|
+
"info", None, {"androidSelector": _to_selector_channel(selector)}
|
|
418
|
+
)
|
|
419
|
+
return result["info"]
|
|
420
|
+
|
|
421
|
+
async def screenshot(self, path: Union[str, Path] = None) -> bytes:
|
|
422
|
+
result = await self._channel.send_return_as_dict("screenshot", None, {})
|
|
423
|
+
data: bytes = base64.b64decode(result["binary"])
|
|
424
|
+
if path:
|
|
425
|
+
make_dirs_for_file(path)
|
|
426
|
+
await async_writefile(path, data)
|
|
427
|
+
return data
|
|
428
|
+
|
|
429
|
+
async def shell(self, command: str) -> bytes:
|
|
430
|
+
result = await self._channel.send_return_as_dict(
|
|
431
|
+
"shell", None, {"command": command}
|
|
432
|
+
)
|
|
433
|
+
return base64.b64decode(result["result"])
|
|
434
|
+
|
|
435
|
+
async def open(self, command: str) -> "AndroidSocket":
|
|
436
|
+
result = await self._channel.send_return_as_dict(
|
|
437
|
+
"open", None, {"command": command}
|
|
438
|
+
)
|
|
439
|
+
return from_channel(result["socket"])
|
|
440
|
+
|
|
441
|
+
async def install_apk(
|
|
442
|
+
self, file: Union[str, Path, bytes], args: List[str] = None
|
|
443
|
+
) -> None:
|
|
444
|
+
params: Dict = {"file": base64.b64encode(await _load_file(file)).decode()}
|
|
445
|
+
if args is not None:
|
|
446
|
+
params["args"] = args
|
|
447
|
+
await self._channel.send("installApk", None, params)
|
|
448
|
+
|
|
449
|
+
async def push(
|
|
450
|
+
self,
|
|
451
|
+
file: Union[str, Path, bytes],
|
|
452
|
+
path: str,
|
|
453
|
+
mode: int = None,
|
|
454
|
+
) -> None:
|
|
455
|
+
params: Dict = {
|
|
456
|
+
"file": base64.b64encode(await _load_file(file)).decode(),
|
|
457
|
+
"path": path,
|
|
458
|
+
}
|
|
459
|
+
if mode is not None:
|
|
460
|
+
params["mode"] = mode
|
|
461
|
+
await self._channel.send("push", None, params)
|
|
462
|
+
|
|
463
|
+
async def launch_browser(self, pkg: str = None, **kwargs: Any) -> BrowserContext:
|
|
464
|
+
params: Dict = {k: v for k, v in kwargs.items() if v is not None}
|
|
465
|
+
if pkg is not None:
|
|
466
|
+
params["pkg"] = pkg
|
|
467
|
+
await self._android._playwright.chromium._prepare_browser_context_params(params)
|
|
468
|
+
result = await self._channel.send_return_as_dict("launchBrowser", None, params)
|
|
469
|
+
context = from_channel(result["context"])
|
|
470
|
+
assert isinstance(context, BrowserContext)
|
|
471
|
+
self._contexts.append(context)
|
|
472
|
+
|
|
473
|
+
# android.ts:
|
|
474
|
+
# selectors._contextsForSelectors.add(context)
|
|
475
|
+
# context.once(Close, () => selectors._contextsForSelectors.delete(context))
|
|
476
|
+
# BrowserContext._on_close only deregisters contexts that have a
|
|
477
|
+
# ._browser, which Android contexts do not, so we must remove it here
|
|
478
|
+
# or the set leaks a closed context.
|
|
479
|
+
selectors = self._android._playwright.selectors
|
|
480
|
+
selectors._contexts_for_selectors.add(context)
|
|
481
|
+
|
|
482
|
+
def _on_context_close(_: Any = None) -> None:
|
|
483
|
+
if context in self._contexts:
|
|
484
|
+
self._contexts.remove(context)
|
|
485
|
+
selectors._contexts_for_selectors.discard(context)
|
|
486
|
+
|
|
487
|
+
context.on(BrowserContext.Events.Close, _on_context_close)
|
|
488
|
+
|
|
489
|
+
# android.ts: await context._initializeHarFromOptions(options.recordHar)
|
|
490
|
+
# HAR capture is started client-side via tracing; without this the
|
|
491
|
+
# record_har option is silently a no-op.
|
|
492
|
+
record_har = kwargs.get("recordHar") or kwargs.get("record_har")
|
|
493
|
+
if record_har:
|
|
494
|
+
await context._initialize_har_from_options(
|
|
495
|
+
record_har_path=record_har.get("path"),
|
|
496
|
+
record_har_content=record_har.get("content"),
|
|
497
|
+
record_har_omit_content=record_har.get("omitContent"),
|
|
498
|
+
record_har_url_filter=record_har.get("urlFilter"),
|
|
499
|
+
record_har_mode=record_har.get("mode"),
|
|
500
|
+
)
|
|
501
|
+
return context
|
|
502
|
+
|
|
503
|
+
async def wait_for_event(
|
|
504
|
+
self,
|
|
505
|
+
event: str,
|
|
506
|
+
predicate: Callable = None,
|
|
507
|
+
timeout: float = None,
|
|
508
|
+
) -> Any:
|
|
509
|
+
t = self._timeout_settings.timeout(timeout)
|
|
510
|
+
waiter = Waiter(self, event)
|
|
511
|
+
waiter.reject_on_timeout(
|
|
512
|
+
t, f'Timeout {t}ms exceeded while waiting for event "{event}"'
|
|
513
|
+
)
|
|
514
|
+
if event != AndroidDevice.Events.Close:
|
|
515
|
+
waiter.reject_on_event(
|
|
516
|
+
self,
|
|
517
|
+
AndroidDevice.Events.Close,
|
|
518
|
+
lambda: TargetClosedError(),
|
|
519
|
+
)
|
|
520
|
+
waiter.wait_for_event(self, event, predicate)
|
|
521
|
+
return await waiter.result()
|
|
522
|
+
|
|
523
|
+
async def close(self) -> None:
|
|
524
|
+
try:
|
|
525
|
+
if self._should_close_connection_on_close:
|
|
526
|
+
# Aligned with Node.js: this._connection.close().
|
|
527
|
+
# Cleanup rejects pending callbacks, emits 'close', and the
|
|
528
|
+
# _on_connection_close listener then tears down pages/contexts
|
|
529
|
+
# and calls transport.request_stop() to close the pipe to BrowserStack.
|
|
530
|
+
self._connection.cleanup()
|
|
531
|
+
else:
|
|
532
|
+
await self._channel.send("close", None, {})
|
|
533
|
+
except Exception as e:
|
|
534
|
+
if is_target_closed_error(e):
|
|
535
|
+
return
|
|
536
|
+
raise
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class AndroidSocket(ChannelOwner):
|
|
540
|
+
"""Represents an open socket connection to an Android process."""
|
|
541
|
+
|
|
542
|
+
class Events:
|
|
543
|
+
Close = "close"
|
|
544
|
+
Data = "data"
|
|
545
|
+
|
|
546
|
+
def __init__(
|
|
547
|
+
self, parent: ChannelOwner, type: str, guid: str, initializer: Dict
|
|
548
|
+
) -> None:
|
|
549
|
+
super().__init__(parent, type, guid, initializer)
|
|
550
|
+
# `data` is protocol `binary`, i.e. base64 text on the wire. JS gets
|
|
551
|
+
# bytes because its transport converts Binary -> Buffer before the
|
|
552
|
+
# handler runs; Python has no such step, so decode here (same as
|
|
553
|
+
# _network.py does for WebSocket frames).
|
|
554
|
+
self._channel.on(
|
|
555
|
+
"data",
|
|
556
|
+
lambda params: self.emit(
|
|
557
|
+
AndroidSocket.Events.Data, base64.b64decode(params["data"])
|
|
558
|
+
),
|
|
559
|
+
)
|
|
560
|
+
self._channel.on("close", lambda _: self.emit(AndroidSocket.Events.Close))
|
|
561
|
+
|
|
562
|
+
async def write(self, data: bytes) -> None:
|
|
563
|
+
await self._channel.send(
|
|
564
|
+
"write", None, {"data": base64.b64encode(data).decode()}
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
async def close(self) -> None:
|
|
568
|
+
try:
|
|
569
|
+
await self._channel.send("close", None, {})
|
|
570
|
+
except Exception as e:
|
|
571
|
+
if is_target_closed_error(e):
|
|
572
|
+
return
|
|
573
|
+
raise
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
class AndroidWebView(EventEmitter):
|
|
577
|
+
"""Represents a WebView running inside an Android app."""
|
|
578
|
+
|
|
579
|
+
class Events:
|
|
580
|
+
Close = "close"
|
|
581
|
+
|
|
582
|
+
def __init__(self, device: AndroidDevice, data: Dict) -> None:
|
|
583
|
+
super().__init__()
|
|
584
|
+
self._device = device
|
|
585
|
+
self._data = data
|
|
586
|
+
self._page_promise: Optional[Any] = None
|
|
587
|
+
|
|
588
|
+
@property
|
|
589
|
+
def pid(self) -> int:
|
|
590
|
+
return self._data["pid"]
|
|
591
|
+
|
|
592
|
+
@property
|
|
593
|
+
def pkg(self) -> str:
|
|
594
|
+
return self._data["pkg"]
|
|
595
|
+
|
|
596
|
+
def _socket_name(self) -> str:
|
|
597
|
+
return self._data["socketName"]
|
|
598
|
+
|
|
599
|
+
async def page(self) -> Any:
|
|
600
|
+
if self._page_promise is None:
|
|
601
|
+
self._page_promise = asyncio.ensure_future(self._fetch_page())
|
|
602
|
+
return await self._page_promise
|
|
603
|
+
|
|
604
|
+
async def _fetch_page(self) -> Any:
|
|
605
|
+
result = await self._device._channel.send_return_as_dict(
|
|
606
|
+
"connectToWebView", None, {"socketName": self._data["socketName"]}
|
|
607
|
+
)
|
|
608
|
+
context = from_channel(result["context"])
|
|
609
|
+
return context.pages[0]
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
class AndroidInput:
|
|
613
|
+
"""Low-level input controls for an Android device (touch, keyboard)."""
|
|
614
|
+
|
|
615
|
+
def __init__(self, device: AndroidDevice) -> None:
|
|
616
|
+
self._device = device
|
|
617
|
+
|
|
618
|
+
async def type(self, text: str) -> None:
|
|
619
|
+
await self._device._channel.send("inputType", None, {"text": text})
|
|
620
|
+
|
|
621
|
+
async def press(self, key: str) -> None:
|
|
622
|
+
await self._device._channel.send("inputPress", None, {"key": key})
|
|
623
|
+
|
|
624
|
+
async def tap(self, point: Dict) -> None:
|
|
625
|
+
await self._device._channel.send("inputTap", None, {"point": point})
|
|
626
|
+
|
|
627
|
+
async def swipe(self, start: Dict, segments: List[Dict], steps: int) -> None:
|
|
628
|
+
# Node accepts `from` for signature parity but does not send it, and
|
|
629
|
+
# AndroidInputSwipeParams lists only segments + steps.
|
|
630
|
+
await self._device._channel.send(
|
|
631
|
+
"inputSwipe", None, {"segments": segments, "steps": steps}
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
async def drag(self, start: Dict, end: Dict, steps: int) -> None:
|
|
635
|
+
await self._device._channel.send(
|
|
636
|
+
"inputDrag", None, {"from": start, "to": end, "steps": steps}
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# ---------------------------------------------------------------------------
|
|
641
|
+
# Helpers
|
|
642
|
+
# ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _to_selector_channel(selector: Dict) -> Dict:
|
|
646
|
+
"""Convert a Python AndroidSelector dict to the wire protocol format.
|
|
647
|
+
|
|
648
|
+
Mirrors Node.js toSelectorChannel() in android.ts — string fields are
|
|
649
|
+
wrapped in a full-match regex; boolean/int fields pass through unchanged.
|
|
650
|
+
"""
|
|
651
|
+
|
|
652
|
+
def _to_regex(value: Optional[Union[str, "re.Pattern[str]"]]) -> Optional[str]:
|
|
653
|
+
if value is None:
|
|
654
|
+
return None
|
|
655
|
+
if isinstance(value, re.Pattern): # compiled regex — use source directly
|
|
656
|
+
return value.pattern
|
|
657
|
+
# Match Node's toSelectorChannel() escaping exactly rather than using
|
|
658
|
+
# re.escape(): the driver compiles these with java.util.regex, whose
|
|
659
|
+
# escape rules differ from Python's, and re.escape() additionally
|
|
660
|
+
# escapes space, #, &, ~ and whitespace characters. The output is
|
|
661
|
+
# semantically equivalent but not textually identical, so mirror Node
|
|
662
|
+
# to keep one regex dialect across all language bindings.
|
|
663
|
+
# '^' + v.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d') + '$'
|
|
664
|
+
escaped = re.sub(r"([|\\{}()\[\]^$+*?.])", r"\\\1", str(value))
|
|
665
|
+
escaped = escaped.replace("-", r"\x2d")
|
|
666
|
+
return f"^{escaped}$"
|
|
667
|
+
|
|
668
|
+
result: Dict = {}
|
|
669
|
+
for key in (
|
|
670
|
+
"checkable",
|
|
671
|
+
"checked",
|
|
672
|
+
"clickable",
|
|
673
|
+
"depth",
|
|
674
|
+
"enabled",
|
|
675
|
+
"focusable",
|
|
676
|
+
"focused",
|
|
677
|
+
"longClickable",
|
|
678
|
+
"scrollable",
|
|
679
|
+
"selected",
|
|
680
|
+
):
|
|
681
|
+
if key in selector:
|
|
682
|
+
result[key] = selector[key]
|
|
683
|
+
for key in ("clazz", "pkg", "desc", "res", "text"):
|
|
684
|
+
if key in selector:
|
|
685
|
+
result[key] = _to_regex(selector[key])
|
|
686
|
+
if "hasChild" in selector:
|
|
687
|
+
result["hasChild"] = {
|
|
688
|
+
"androidSelector": _to_selector_channel(selector["hasChild"]["selector"])
|
|
689
|
+
}
|
|
690
|
+
if "hasDescendant" in selector:
|
|
691
|
+
d = selector["hasDescendant"]
|
|
692
|
+
entry: Dict = {"androidSelector": _to_selector_channel(d["selector"])}
|
|
693
|
+
if "maxDepth" in d:
|
|
694
|
+
entry["maxDepth"] = d["maxDepth"]
|
|
695
|
+
result["hasDescendant"] = entry
|
|
696
|
+
return result
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
async def _load_file(file: Union[str, Path, bytes]) -> bytes:
|
|
700
|
+
"""Read a file path into bytes, or return the bytes as-is."""
|
|
701
|
+
if isinstance(file, bytes):
|
|
702
|
+
return file
|
|
703
|
+
loop = asyncio.get_event_loop()
|
|
704
|
+
return await loop.run_in_executor(None, lambda: Path(file).read_bytes())
|