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.
Files changed (192) hide show
  1. playwright/__init__.py +19 -0
  2. playwright/__main__.py +33 -0
  3. playwright/_impl/__init__.py +0 -0
  4. playwright/_impl/__pyinstaller/__init__.py +20 -0
  5. playwright/_impl/__pyinstaller/hook-playwright.async_api.py +17 -0
  6. playwright/_impl/__pyinstaller/hook-playwright.sync_api.py +17 -0
  7. playwright/_impl/_android.py +704 -0
  8. playwright/_impl/_api_structures.py +365 -0
  9. playwright/_impl/_artifact.py +87 -0
  10. playwright/_impl/_assertions.py +1124 -0
  11. playwright/_impl/_async_base.py +105 -0
  12. playwright/_impl/_browser.py +293 -0
  13. playwright/_impl/_browser_context.py +754 -0
  14. playwright/_impl/_browser_type.py +368 -0
  15. playwright/_impl/_cdp_session.py +46 -0
  16. playwright/_impl/_clock.py +104 -0
  17. playwright/_impl/_connection.py +764 -0
  18. playwright/_impl/_console_message.py +98 -0
  19. playwright/_impl/_credentials.py +57 -0
  20. playwright/_impl/_debugger.py +54 -0
  21. playwright/_impl/_dialog.py +63 -0
  22. playwright/_impl/_disposable.py +96 -0
  23. playwright/_impl/_download.py +64 -0
  24. playwright/_impl/_driver.py +41 -0
  25. playwright/_impl/_element_handle.py +480 -0
  26. playwright/_impl/_errors.py +64 -0
  27. playwright/_impl/_event_context_manager.py +33 -0
  28. playwright/_impl/_fetch.py +626 -0
  29. playwright/_impl/_file_chooser.py +57 -0
  30. playwright/_impl/_form_data.py +34 -0
  31. playwright/_impl/_frame.py +985 -0
  32. playwright/_impl/_glob.py +65 -0
  33. playwright/_impl/_greenlets.py +49 -0
  34. playwright/_impl/_har_router.py +122 -0
  35. playwright/_impl/_helper.py +601 -0
  36. playwright/_impl/_impl_to_api_mapping.py +158 -0
  37. playwright/_impl/_input.py +111 -0
  38. playwright/_impl/_js_handle.py +336 -0
  39. playwright/_impl/_json_pipe.py +97 -0
  40. playwright/_impl/_local_utils.py +95 -0
  41. playwright/_impl/_locator.py +1024 -0
  42. playwright/_impl/_map.py +31 -0
  43. playwright/_impl/_network.py +1017 -0
  44. playwright/_impl/_object_factory.py +116 -0
  45. playwright/_impl/_page.py +1637 -0
  46. playwright/_impl/_path_utils.py +27 -0
  47. playwright/_impl/_playwright.py +68 -0
  48. playwright/_impl/_screencast.py +177 -0
  49. playwright/_impl/_selectors.py +67 -0
  50. playwright/_impl/_set_input_files_helpers.py +158 -0
  51. playwright/_impl/_str_utils.py +76 -0
  52. playwright/_impl/_stream.py +46 -0
  53. playwright/_impl/_sync_base.py +151 -0
  54. playwright/_impl/_tracing.py +281 -0
  55. playwright/_impl/_transport.py +184 -0
  56. playwright/_impl/_video.py +56 -0
  57. playwright/_impl/_waiter.py +197 -0
  58. playwright/_impl/_web_error.py +48 -0
  59. playwright/_impl/_web_storage.py +58 -0
  60. playwright/_impl/_writable_stream.py +42 -0
  61. playwright/_repo_version.py +24 -0
  62. playwright/async_api/__init__.py +275 -0
  63. playwright/async_api/_android_generated.py +462 -0
  64. playwright/async_api/_context_manager.py +57 -0
  65. playwright/async_api/_generated.py +23875 -0
  66. playwright/driver/LICENSE +2946 -0
  67. playwright/driver/README.md +1 -0
  68. playwright/driver/node.exe +0 -0
  69. playwright/driver/package/LICENSE +202 -0
  70. playwright/driver/package/NOTICE +5 -0
  71. playwright/driver/package/README.md +3 -0
  72. playwright/driver/package/ThirdPartyNotices.txt +13 -0
  73. playwright/driver/package/bin/install_media_pack.ps1 +5 -0
  74. playwright/driver/package/bin/install_webkit_wsl.ps1 +33 -0
  75. playwright/driver/package/bin/reinstall_chrome_beta_linux.sh +42 -0
  76. playwright/driver/package/bin/reinstall_chrome_beta_mac.sh +13 -0
  77. playwright/driver/package/bin/reinstall_chrome_beta_win.ps1 +24 -0
  78. playwright/driver/package/bin/reinstall_chrome_stable_linux.sh +42 -0
  79. playwright/driver/package/bin/reinstall_chrome_stable_mac.sh +12 -0
  80. playwright/driver/package/bin/reinstall_chrome_stable_win.ps1 +24 -0
  81. playwright/driver/package/bin/reinstall_msedge_beta_linux.sh +48 -0
  82. playwright/driver/package/bin/reinstall_msedge_beta_mac.sh +11 -0
  83. playwright/driver/package/bin/reinstall_msedge_beta_win.ps1 +23 -0
  84. playwright/driver/package/bin/reinstall_msedge_dev_linux.sh +48 -0
  85. playwright/driver/package/bin/reinstall_msedge_dev_mac.sh +11 -0
  86. playwright/driver/package/bin/reinstall_msedge_dev_win.ps1 +23 -0
  87. playwright/driver/package/bin/reinstall_msedge_stable_linux.sh +48 -0
  88. playwright/driver/package/bin/reinstall_msedge_stable_mac.sh +11 -0
  89. playwright/driver/package/bin/reinstall_msedge_stable_win.ps1 +24 -0
  90. playwright/driver/package/browsers.json +75 -0
  91. playwright/driver/package/cli.js +21 -0
  92. playwright/driver/package/index.d.ts +17 -0
  93. playwright/driver/package/index.js +17 -0
  94. playwright/driver/package/index.mjs +28 -0
  95. playwright/driver/package/lib/bootstrap.js +88 -0
  96. playwright/driver/package/lib/coreBundle.js +74830 -0
  97. playwright/driver/package/lib/entry/cliDaemon.js +5 -0
  98. playwright/driver/package/lib/entry/dashboardApp.js +3 -0
  99. playwright/driver/package/lib/entry/mcp.js +10 -0
  100. playwright/driver/package/lib/entry/oopBrowserDownload.js +3 -0
  101. playwright/driver/package/lib/package.js +50 -0
  102. playwright/driver/package/lib/server/chromium/appIcon.png +0 -0
  103. playwright/driver/package/lib/server/electron/loader.js +118 -0
  104. playwright/driver/package/lib/serverRegistry.js +7347 -0
  105. playwright/driver/package/lib/serverRegistry.js.LICENSE +354 -0
  106. playwright/driver/package/lib/tools/cli-client/channelSessions.js +141 -0
  107. playwright/driver/package/lib/tools/cli-client/cli.js +6 -0
  108. playwright/driver/package/lib/tools/cli-client/help.json +708 -0
  109. playwright/driver/package/lib/tools/cli-client/minimist.js +128 -0
  110. playwright/driver/package/lib/tools/cli-client/output.js +343 -0
  111. playwright/driver/package/lib/tools/cli-client/program.js +404 -0
  112. playwright/driver/package/lib/tools/cli-client/registry.js +176 -0
  113. playwright/driver/package/lib/tools/cli-client/session.js +258 -0
  114. playwright/driver/package/lib/tools/dashboard/appIcon.png +0 -0
  115. playwright/driver/package/lib/tools/skills/playwright-cli/SKILL.md +420 -0
  116. playwright/driver/package/lib/tools/skills/playwright-cli/references/element-attributes.md +23 -0
  117. playwright/driver/package/lib/tools/skills/playwright-cli/references/playwright-tests.md +39 -0
  118. playwright/driver/package/lib/tools/skills/playwright-cli/references/request-mocking.md +87 -0
  119. playwright/driver/package/lib/tools/skills/playwright-cli/references/running-code.md +241 -0
  120. playwright/driver/package/lib/tools/skills/playwright-cli/references/session-management.md +225 -0
  121. playwright/driver/package/lib/tools/skills/playwright-cli/references/storage-state.md +275 -0
  122. playwright/driver/package/lib/tools/skills/playwright-cli/references/test-generation.md +433 -0
  123. playwright/driver/package/lib/tools/skills/playwright-cli/references/tracing.md +139 -0
  124. playwright/driver/package/lib/tools/skills/playwright-cli/references/video-recording.md +143 -0
  125. playwright/driver/package/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
  126. playwright/driver/package/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
  127. playwright/driver/package/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
  128. playwright/driver/package/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
  129. playwright/driver/package/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
  130. playwright/driver/package/lib/tools/skills/playwright-trace/SKILL.md +171 -0
  131. playwright/driver/package/lib/tools/utils/extension.js +101 -0
  132. playwright/driver/package/lib/tools/utils/socketConnection.js +108 -0
  133. playwright/driver/package/lib/utilsBundle.js +90764 -0
  134. playwright/driver/package/lib/utilsBundle.js.LICENSE +2179 -0
  135. playwright/driver/package/lib/vite/dashboard/assets/codicon-DCmgc-ay.ttf +0 -0
  136. playwright/driver/package/lib/vite/dashboard/assets/firefox-1bWoP6pv.svg +1 -0
  137. playwright/driver/package/lib/vite/dashboard/assets/firefox-beta-k3eOH_eK.svg +1 -0
  138. playwright/driver/package/lib/vite/dashboard/assets/firefox-nightly-Cp5nfeDT.svg +1 -0
  139. playwright/driver/package/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
  140. playwright/driver/package/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
  141. playwright/driver/package/lib/vite/dashboard/assets/safari-na3_-uQk.svg +1 -0
  142. playwright/driver/package/lib/vite/dashboard/index.html +29 -0
  143. playwright/driver/package/lib/vite/dashboard/playwright-logo.svg +24 -0
  144. playwright/driver/package/lib/vite/htmlReport/index.html +16 -0
  145. playwright/driver/package/lib/vite/htmlReport/report.css +2 -0
  146. playwright/driver/package/lib/vite/htmlReport/report.js +32 -0
  147. playwright/driver/package/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
  148. playwright/driver/package/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
  149. playwright/driver/package/lib/vite/recorder/assets/codicon-DCmgc-ay.ttf +0 -0
  150. playwright/driver/package/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
  151. playwright/driver/package/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
  152. playwright/driver/package/lib/vite/recorder/index.html +29 -0
  153. playwright/driver/package/lib/vite/recorder/playwright-logo.svg +9 -0
  154. playwright/driver/package/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
  155. playwright/driver/package/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
  156. playwright/driver/package/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
  157. playwright/driver/package/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
  158. playwright/driver/package/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
  159. playwright/driver/package/lib/vite/traceViewer/codicon.DCmgc-ay.ttf +0 -0
  160. playwright/driver/package/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
  161. playwright/driver/package/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
  162. playwright/driver/package/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
  163. playwright/driver/package/lib/vite/traceViewer/index.html +44 -0
  164. playwright/driver/package/lib/vite/traceViewer/manifest.webmanifest +16 -0
  165. playwright/driver/package/lib/vite/traceViewer/playwright-logo.svg +9 -0
  166. playwright/driver/package/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
  167. playwright/driver/package/lib/vite/traceViewer/snapshot.html +10 -0
  168. playwright/driver/package/lib/vite/traceViewer/sw.bundle.js +4 -0
  169. playwright/driver/package/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
  170. playwright/driver/package/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
  171. playwright/driver/package/lib/vite/traceViewer/uiMode.html +18 -0
  172. playwright/driver/package/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
  173. playwright/driver/package/lib/webp_codec.LICENSE +173 -0
  174. playwright/driver/package/lib/webp_codec.wasm +0 -0
  175. playwright/driver/package/lib/xdg-open +1267 -0
  176. playwright/driver/package/package.json +34 -0
  177. playwright/driver/package/types/protocol.d.ts +24842 -0
  178. playwright/driver/package/types/structs.d.ts +47 -0
  179. playwright/driver/package/types/types.d.ts +26188 -0
  180. playwright/py.typed +0 -0
  181. playwright/sync_api/__init__.py +275 -0
  182. playwright/sync_api/_android_generated.py +494 -0
  183. playwright/sync_api/_context_manager.py +98 -0
  184. playwright/sync_api/_generated.py +24045 -0
  185. playwright_browserstack-1.62.0.dist-info/METADATA +82 -0
  186. playwright_browserstack-1.62.0.dist-info/RECORD +192 -0
  187. playwright_browserstack-1.62.0.dist-info/WHEEL +5 -0
  188. playwright_browserstack-1.62.0.dist-info/entry_points.txt +5 -0
  189. playwright_browserstack-1.62.0.dist-info/licenses/LICENSE +202 -0
  190. playwright_browserstack-1.62.0.dist-info/licenses/LICENSE.txt +51 -0
  191. playwright_browserstack-1.62.0.dist-info/licenses/NOTICE +18 -0
  192. 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())