browser-toolkit 0.0.1a1__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.
@@ -0,0 +1,505 @@
1
+ from typing import Self
2
+
3
+ from playwright._impl._api_structures import SetCookieParam
4
+ from playwright.async_api import Page, Browser, TimeoutError, ElementHandle, Cookie as PlaywrightCookie
5
+
6
+ from browser_toolkit.base_toolkit import BaseBrowserToolkit, BaseWebElement
7
+ from browser_toolkit.types import Cookie
8
+ from browser_toolkit.utils import raise_not_implemented
9
+
10
+
11
+ class PlaywrightWebElement(BaseWebElement):
12
+ def __init__(self, web_element: ElementHandle):
13
+ self.web_element = web_element
14
+
15
+ async def click(self, selector: str, delay: int = 0) -> None:
16
+ """
17
+ Clicks the element
18
+
19
+ :param selector: string - CSS selector or XPath
20
+ :param delay: int - time to keep the mouse button pressed in seconds (default: 0)
21
+ :return:
22
+ """
23
+ await self.web_element.click(delay=delay)
24
+
25
+ async def click_js(self) -> None:
26
+ """
27
+ Clicks the element using JavaScript
28
+
29
+ :return:
30
+ """
31
+ await self.web_element.evaluate("element => element.click()")
32
+
33
+ async def type(self, text: str, interval: float | int, clear_before: bool) -> None:
34
+ """
35
+ Fills the element with the text
36
+
37
+ :param text: string - text to fill
38
+ :param interval: float or int - time to wait between each character in seconds
39
+ :param clear_before: bool - whether to clear the field before filling
40
+ :return:
41
+ """
42
+ if clear_before:
43
+ await self.clear()
44
+ await self.web_element.type(text, delay=interval * 1000)
45
+
46
+ async def clear(self) -> None:
47
+ """
48
+ Clears the element
49
+
50
+ :return:
51
+ """
52
+ await self.web_element.fill("")
53
+
54
+ async def selector(self, selector: str) -> Self | None:
55
+ """
56
+ Queries the web_element and returns the first element matching the selector.
57
+ :param selector:
58
+ :return:
59
+ """
60
+ element = await self.web_element.query_selector(selector=selector)
61
+ if element:
62
+ return PlaywrightWebElement(web_element=element)
63
+ return None
64
+
65
+ async def selector_all(self, selector: str) -> list[Self]:
66
+ """
67
+ Queries the web_element and returns all elements matching the selector.
68
+ :param selector:
69
+ :return:
70
+ """
71
+ elements = await self.web_element.query_selector_all(selector=selector)
72
+ return [PlaywrightWebElement(web_element=element) for element in elements]
73
+
74
+
75
+ class PlaywrightTollKit(BaseBrowserToolkit):
76
+ def __init__(self, browser: Browser, page: Page, *args, **kwargs):
77
+ self.browser: Browser = browser
78
+ self.page: Page = page
79
+
80
+ # --------------------------- START session management ---------------------------
81
+
82
+ async def close(self) -> None:
83
+ """
84
+ Closes the browser tab
85
+ :return:
86
+ """
87
+ await self.page.close()
88
+
89
+ # --------------------------- END session management ---------------------------
90
+
91
+ # --------------------------- START selectors ---------------------------
92
+
93
+ async def selector(self, selector: str, web_element: BaseWebElement | None = None) -> BaseWebElement | None:
94
+ """
95
+ Queries the page and returns the first element matching the selector.
96
+ If a web_element is provided, it queries within that element instead of the whole page.
97
+ :param selector:
98
+ :param web_element:
99
+ :return:
100
+ """
101
+ element = await self.page.query_selector(selector=selector)
102
+ if element:
103
+ return PlaywrightWebElement(web_element=element)
104
+ return None
105
+
106
+ async def selector_all(self, selector: str, web_element: BaseWebElement | None = None) -> list[BaseWebElement]:
107
+ """
108
+ Queries the page and returns all elements matching the selector.
109
+ If a web_element is provided, it queries within that element instead of the whole page.
110
+ :param selector:
111
+ :param web_element:
112
+ :return:
113
+ """
114
+ elements = await self.page.query_selector_all(selector=selector)
115
+ return [PlaywrightWebElement(web_element=element) for element in elements]
116
+
117
+ # --------------------------- END selectors ---------------------------
118
+
119
+ # --------------------------- START Actions ---------------------------
120
+
121
+ async def goto(self, url: str, timeout: int = 30) -> None:
122
+ """
123
+ Navigates to URL
124
+
125
+ :param url: url to navigate to
126
+ :param timeout: maximum time to wait for the page to load in seconds
127
+ :return:
128
+ """
129
+ await self.page.goto(url=url, timeout=timeout * 1000)
130
+
131
+ async def click(self, selector: str, delay: int = 0) -> None:
132
+ """
133
+ Clicks the element matching the selector
134
+
135
+ :param selector: string - CSS selector or XPath
136
+ :param delay: int - time to keep the mouse button pressed in seconds (default: 0)
137
+ :return:
138
+ """
139
+ await self.page.locator(selector=selector).click(delay=delay)
140
+
141
+ async def click_js(self, selector: str) -> None:
142
+ """
143
+ Clicks the element matching the selector using JavaScript
144
+
145
+ :param selector: string - CSS selector or XPath
146
+ :return:
147
+ """
148
+ command = f"document.querySelector('{selector}').click()"
149
+ await self.page.evaluate(expression=command)
150
+
151
+ async def type(self, text: str, selector: str, interval: float | int, clear_before: bool) -> None:
152
+ """
153
+ Fills the element matching the selector with the text
154
+
155
+ :param text: string - text to fill
156
+ :param selector: string - CSS selector or XPath
157
+ :param interval: float or int - time to wait between each character
158
+ :param clear_before: bool - whether to clear the field before filling
159
+ :return:
160
+ """
161
+ if clear_before:
162
+ await self.clear(selector=selector)
163
+ await self.page.locator(selector=selector).type(text, delay=interval * 1000)
164
+
165
+ async def clear(self, selector: str) -> None:
166
+ """
167
+ Clears the element matching the selector
168
+
169
+ :param selector: string - CSS selector or XPath
170
+ :return:
171
+ """
172
+ await self.page.locator(selector).fill("")
173
+
174
+ async def scroll_to_element(self, selector: str) -> None:
175
+ """
176
+ Scrolls to the element matching the selector
177
+
178
+ :param selector: string - CSS selector or XPath
179
+ :return:
180
+ """
181
+ command = f"document.querySelector('{selector}').scrollIntoView()"
182
+ await self.page.evaluate(expression=command)
183
+
184
+ async def scroll_to_top(self) -> None:
185
+ """
186
+ Scrolls to the top of the page
187
+
188
+ :return:
189
+ """
190
+ await self.page.evaluate("window.scrollTo(0, 0);")
191
+
192
+ async def scroll_to_bottom(self) -> None:
193
+ """
194
+ Scrolls to the bottom of the page
195
+
196
+ :return:
197
+ """
198
+ await self.page.evaluate("window.scrollTo(0, document.body.scrollHeight);")
199
+
200
+ async def reload(self) -> None:
201
+ """
202
+ Reloads the page
203
+
204
+ :return:
205
+ """
206
+ await self.page.reload()
207
+
208
+ async def hard_reload(self) -> None:
209
+ """
210
+ Hard reloads the page (ignoring cache)
211
+
212
+ :return:
213
+ """
214
+ cmd = "window.location.reload()"
215
+ await self.page.evaluate(expression=cmd)
216
+
217
+ # --------------------------- END Actions ---------------------------
218
+
219
+ # --------------------------- START page data ---------------------------
220
+ @property
221
+ async def current_url(self) -> str:
222
+ """
223
+ Gets the current URL
224
+
225
+ :return: str - current URL
226
+ """
227
+ return self.page.url
228
+
229
+ @property
230
+ async def title(self) -> str:
231
+ """
232
+ Gets the page title
233
+
234
+ :return: str - page title
235
+ """
236
+ return await self.page.title()
237
+
238
+ @property
239
+ async def page_source(self) -> str:
240
+ """
241
+ Gets the page source
242
+
243
+ :return: str - page source
244
+ """
245
+ return await self.page.content()
246
+
247
+ async def get_text(self, selector: str) -> str:
248
+ """
249
+ Gets the text from the element matching the selector
250
+
251
+ :param selector: string - CSS selector or XPath
252
+ :return: str - text of the element
253
+ """
254
+ return await self.page.locator(selector=selector).text_content()
255
+
256
+ async def get_attribute(self, selector: str, attribute: str) -> str:
257
+ """
258
+ Gets the attribute from the element matching the selector
259
+
260
+ :param selector: string - CSS selector or XPath
261
+ :param attribute: string - attribute name
262
+ :return: str - attribute of the element
263
+ """
264
+ return await self.page.locator(selector=selector).get_attribute(name=attribute)
265
+
266
+ async def save_screenshot(self, file_path: str) -> None:
267
+ """
268
+ Takes a screenshot of the current page and saves it to the exception directory if it is set
269
+ :param file_path:
270
+ :return:
271
+ """
272
+ await self.page.screenshot(path=file_path)
273
+
274
+ # --------------------------- END page data ---------------------------
275
+
276
+ # --------------------------- START network ---------------------------
277
+
278
+ async def get_network_requests(self) -> list[dict]:
279
+ """
280
+ Get all network Requests
281
+ :return:
282
+ """
283
+ raise_not_implemented()
284
+
285
+ async def get_network_response_body(self, request_id: str) -> str:
286
+ """
287
+ Gets the response body of the network request with the given request ID
288
+
289
+ :param request_id: string - network request ID
290
+ :return: str - response body of the network request
291
+ """
292
+ raise_not_implemented()
293
+
294
+ # --------------------------- END network ---------------------------
295
+
296
+ # --------------------------- START scripts ---------------------------
297
+ async def execute_script(self, script: str) -> any:
298
+ """
299
+ Executes the JavaScript script in the context of the current page
300
+
301
+ :param script: string - JavaScript code to execute
302
+ :return:
303
+ """
304
+ return await self.page.evaluate(expression=script)
305
+
306
+ async def execute_cdp_cmd(self, cmd: str, params: dict) -> any:
307
+ """
308
+ Executes the Chrome DevTools Protocol command in the context of the current page
309
+
310
+ :param cmd: string - CDP command to execute
311
+ :param params: dict - parameters for the CDP command
312
+ :return:
313
+ """
314
+ raise_not_implemented()
315
+
316
+ # --------------------------- END scripts ---------------------------
317
+
318
+ # --------------------------- START wait ---------------------------
319
+ async def element_is_present(self, selector: str, timeout: int) -> bool:
320
+ """
321
+ Checks if the element matching the selector is present
322
+
323
+ :param selector: string - CSS selector or XPath
324
+ :param timeout: int - seconds to wait for the element
325
+ :return: bool - whether the element is present
326
+ """
327
+ try:
328
+ await self.page.wait_for_selector(selector=selector, timeout=timeout * 1000, state="attached")
329
+ return True
330
+ except TimeoutError:
331
+ return False
332
+
333
+ async def element_is_visible(self, selector: str, timeout: int) -> bool:
334
+ """
335
+ Checks if the element matching the selector is visible
336
+
337
+ :param selector: string - CSS selector or XPath
338
+ :param timeout: int - seconds to wait for the element
339
+ :return: bool - whether the element is visible
340
+ """
341
+ try:
342
+ await self.page.wait_for_selector(selector=selector, timeout=timeout * 1000, state="visible")
343
+ return True
344
+ except TimeoutError:
345
+ return False
346
+
347
+ async def element_is_invisible(self, selector: str, timeout: int) -> bool:
348
+ """
349
+ Checks if the element matching the selector is invisible
350
+
351
+ :param selector: string - CSS selector or XPath
352
+ :param timeout: int - seconds to wait for the element
353
+ :return: bool - whether the element is invisible
354
+ """
355
+ try:
356
+ await self.page.wait_for_selector(selector=selector, timeout=timeout * 1000, state="hidden")
357
+ return True
358
+ except TimeoutError:
359
+ return False
360
+
361
+ async def element_is_clickable(self, selector: str, timeout: int) -> bool:
362
+ """
363
+ Checks if the element matching the selector is clickable
364
+
365
+ :param selector: string - CSS selector or XPath
366
+ :param timeout: int - seconds to wait for the element
367
+ :return: bool - whether the element is clickable
368
+ """
369
+ try:
370
+ await self.page.locator(selector=selector).click(trial=True)
371
+ return True
372
+ except:
373
+ return False
374
+
375
+ async def text_is_present(self, text: str, selector: str, timeout: int) -> bool:
376
+ """
377
+ Checks if the text is present in the element matching the selector
378
+
379
+ :param text: string - text to check
380
+ :param selector: string - CSS selector or XPath
381
+ :param timeout: int - seconds to wait for the element
382
+ :return: bool - whether the text is present in the element
383
+ """
384
+ try:
385
+ await self.page.wait_for_selector(selector=selector, timeout=timeout * 1000, state="visible")
386
+ element_text = await self.page.locator(selector=selector).text_content()
387
+ return text in element_text
388
+ except TimeoutError:
389
+ return False
390
+
391
+ async def alert_is_present(self, timeout: int, message: str) -> bool:
392
+ """
393
+ Checks if an alert is present
394
+
395
+ :param timeout: int - seconds to wait for the alert
396
+ :param message: str - alert message
397
+ :return: bool - whether an alert is present
398
+ """
399
+ try:
400
+ await self.page.wait_for_event("dialog", timeout=timeout * 1000)
401
+ return True
402
+ except TimeoutError:
403
+ return False
404
+
405
+ async def page_is_loading(self, timeout: int) -> bool:
406
+ """
407
+ Checks if the page is ready
408
+
409
+ :param timeout: int - seconds to wait for the page to be ready
410
+ :return: bool - whether the page is ready
411
+ """
412
+ try:
413
+ await self.page.wait_for_load_state(state="load", timeout=timeout * 1000)
414
+ return False
415
+ except TimeoutError:
416
+ return True
417
+
418
+ # --------------------------- END wait ---------------------------
419
+
420
+ # --------------------------- START session data ---------------------------
421
+ async def get_all_cookies(self) -> list[Cookie]:
422
+ """
423
+ Gets all cookies
424
+ :return: dict
425
+ """
426
+ raw_cookies: list[PlaywrightCookie] = await self.page.context.cookies()
427
+ transformed_cookies: list[Cookie] = []
428
+ for raw_cookie in raw_cookies:
429
+ transformed_cookie = Cookie(
430
+ name=raw_cookie.get("name"),
431
+ value=raw_cookie.get("value"),
432
+ url=raw_cookie.get("url"),
433
+ domain=raw_cookie.get("domain"),
434
+ path=raw_cookie.get("path"),
435
+ expires=raw_cookie.get("expires"),
436
+ httpOnly=raw_cookie.get("httpOnly"),
437
+ secure=raw_cookie.get("secure"),
438
+ sameSite=raw_cookie.get("sameSite"),
439
+ partitionKey=raw_cookie.get("partitionKey"),
440
+ )
441
+ transformed_cookies.append(transformed_cookie)
442
+
443
+ return transformed_cookies
444
+
445
+ async def add_cookie(self, cookie: Cookie) -> None:
446
+ """
447
+ Adds a cookie to the current session
448
+
449
+ :param cookie: Cookie - cookie to add
450
+ :return:
451
+ """
452
+ transformed_cookie: SetCookieParam = SetCookieParam(
453
+ name=cookie.name,
454
+ value=cookie.value,
455
+ url=cookie.url,
456
+ domain=cookie.domain,
457
+ path=cookie.path,
458
+ expires=cookie.expires,
459
+ httpOnly=cookie.httpOnly,
460
+ secure=cookie.secure,
461
+ sameSite=cookie.sameSite,
462
+ partitionKey=cookie.partitionKey,
463
+ )
464
+ await self.page.context.add_cookies(cookies=[transformed_cookie])
465
+
466
+ async def delete_all_cookies(self) -> None:
467
+ """
468
+ Deletes all cookies from the current session
469
+ :return:
470
+ """
471
+ await self.page.context.clear_cookies()
472
+
473
+ async def delete_cookie_by_name(self, name: str) -> None:
474
+ """
475
+ Deletes a cookie by name
476
+
477
+ :param name: string - name of the cookie to delete
478
+ :return:
479
+ """
480
+ await self.page.context.clear_cookies(name=name)
481
+
482
+ async def delete_cookie_filter(
483
+ self, name: str | None = None, domain: str | None = None, path: str | None = None
484
+ ) -> None:
485
+ """
486
+ Deletes cookies by name, domain, and path
487
+
488
+ :param name:
489
+ :param domain:
490
+ :param path:
491
+ :return:
492
+ """
493
+ await self.page.context.clear_cookies(name=name, domain=domain, path=path)
494
+
495
+ async def get_all_local_storage(self) -> dict:
496
+ """
497
+ Gets all local storage
498
+ :return: dict
499
+ """
500
+ local_storage = await self.page.evaluate("() => Object.fromEntries(Object.entries(localStorage))")
501
+ if not isinstance(local_storage, dict):
502
+ local_storage = {}
503
+ return local_storage
504
+
505
+ # --------------------------- END session data ---------------------------