from fastcore.test import test_eqSolveitCDP API
len(_cdp_domains), [d['domain'] for d in _cdp_domains[:5]](55, ['Accessibility', 'Animation', 'Audits', 'Autofill', 'BackgroundService'])
cdp_search
def cdp_search(
q:str
):
Search CDP domains and commands by name or description
cdp_search('target')[:100]'Audits.checkFormsIssues: Runs the form issues check for the target page. Found issues are reported\nu'
solvecdp relies on the solveit-chrome extension being available - let’s check it:
test_eq((await event_get_a('ext-ping')).result, 'pong')Let’s try using the extension cdp directly:
r = await event_get_a('cdp-new-tab', url='https://example.com')
tid = r.result['tabId']
await asyncio.sleep(0.5)
tid895201191
We can add a little convenience for it:
js_cdp
async def js_cdp(
method, tabId:NoneType=None, params:VAR_KEYWORD
):
Call self as a function.
await js_cdp('Runtime.evaluate', tabId=tid, expression='document.title'){'type': 'string', 'value': '6. Example Domain'}JsCDP
def JsCDP(
tid
):
Chrome DevTools Protocol via JS bridge with event support
jc = await JsCDP.new()We can subscribe to events thanks to the solveit-chrome extension bridge:
JsCDP.on
def on(
events:VAR_POSITIONAL
):
Call self as a function.
JsCDP.unsubscribe
async def unsubscribe(
sub_id
):
Call self as a function.
JsSub
def JsSub(
cdp, events
):
Initialize self. See help(type(self)) for accurate signature.
async with jc.on('Page.loadEventFired') as sub:
await jc('Page.navigate', url='https://example.com')
evt = await sub()await jc.close()JsCDPMethod
def JsCDPMethod(
cdp, domain, method
):
Initialize self. See help(type(self)) for accurate signature.
JsCDPDomain
def JsCDPDomain(
cdp, domain
):
Initialize self. See help(type(self)) for accurate signature.
JsCDP.eval
async def eval(
expr
):
Call self as a function.
JsCDP.wait_for_selector
async def wait_for_selector(
sel, timeout:int=10
):
Wait for CSS selector to match an element
JsCDP.wait_for
async def wait_for(
expr, timeout:int=10
):
Wait for JS expression to be truthy, return its value
jc = await JsCDP.new()await jc.page.navigate(url='https://example.com')
await jc.wait_for('document.title')'Example Domain'
await jc.close()JsCDP.goto
async def goto(
url, kwargs:VAR_KEYWORD
):
Navigate to url and wait for load+idle
JsCDP.wait_for_ready
async def wait_for_ready(
timeout:int=10, idle_ms:int=500
):
Wait until network is idle for idle_ms
jc = await JsCDP.new()await jc.goto('https://example.com')
await jc.eval('document.title')'6. Example Domain'
JsCDP.pages
async def pages(
):
Call self as a function.
pages = await jc.pages
for p in pages[:3]: print(f"{p.get('tabId', '?'):>10} {p.get('title', '')[:60]}")
pg = first(p for p in pages if 'Example' in p['title'])
pg['url'] 895199784 8. Database Transactions — PlanetScale
895200613 2. l:SolveIT - nbs◀️dialoghelper◀️aai-ws
895199539 2. [AnswerDotAI/safepyrun] Run failed: Deploy to GitHub Page
'https://example.com/'
jc2 = await JsCDP.new(pg['tabId'])
await jc2.eval('document.title')'5. Example Domain'
JsCDP.screenshot
async def screenshot(
):
Call self as a function.
# await jc2.screenshot()await jc2.close()LLMs and accessibility
page = await JsCDP.new(url='https://httpbin.org/forms/post')
await page.eval('document.title')'5. httpbin.org/forms/post'
await page.accessibility.enable()
tree = await page.accessibility.getFullAXTree()
len(tree)90
tree[0]{ 'backendDOMNodeId': 14,
'childIds': ['15'],
'chromeRole': {'type': 'internalRole', 'value': 144},
'frameId': '92354B6BB2A591B3DDB0267C24E21365',
'ignored': False,
'name': { 'sources': [{'attribute': 'aria-labelledby', 'type': 'relatedElement'}, {'attribute': 'aria-label', 'type': 'attribute'}, {'attribute': 'aria-label', 'superseded': True, 'type': 'attribute'}, {'nativeSource': 'title', 'type': 'relatedElement', 'value': {'type': 'computedString', 'value': '5. httpbin.org/forms/post'}}],
'type': 'computedString',
'value': '5. httpbin.org/forms/post'},
'nodeId': '14',
'properties': [{'name': 'focusable', 'value': {'type': 'booleanOrUndefined', 'value': True}}, {'name': 'url', 'value': {'type': 'string', 'value': 'https://httpbin.org/forms/post'}}],
'role': {'type': 'internalRole', 'value': 'RootWebArea'}}await page.close()AXTree
def AXTree(
raw
):
Chrome accessibility tree node with compact repr
AXNode
def AXNode(
raw
):
Chrome accessibility tree node with compact repr
build_ax_tree
def build_ax_tree(
nodes
):
Build AXNode tree from flat CDP accessibility node list
JsCDP.ax_tree
async def ax_tree(
sid:NoneType=None
):
Get accessibility tree for session
page = await JsCDP.new(url='https://httpbin.org/forms/post')root = await page.ax_tree()
root- RootWebArea “5. httpbin.org/forms/post”
focusable=Trueurl=https://httpbin.org/forms/post[#14]- LabelText “” [#20]
- StaticText “Customer name:” [#62]
- InlineTextBox “Customer name:”
- textbox “Customer name:”
focusable=Trueeditable=plaintextsettable=True[#2]
- StaticText “Customer name:” [#62]
- LabelText “” [#23]
- StaticText “Telephone:” [#63]
- InlineTextBox “Telephone:”
- textbox “Telephone:”
focusable=Trueeditable=plaintextsettable=True[#3]
- StaticText “Telephone:” [#63]
- LabelText “” [#26]
- StaticText “E-mail address:” [#64]
- InlineTextBox “E-mail address:”
- textbox “E-mail address:”
focusable=Trueeditable=plaintextsettable=True[#4]
- StaticText “E-mail address:” [#64]
- group “Pizza Size” [#28]
- Legend “” [#29]
- StaticText “Pizza Size” [#65]
- InlineTextBox “Pizza Size”
- StaticText “Pizza Size” [#65]
- radio ” Small”
focusable=True[#6] - radio ” Medium”
focusable=True[#7] - radio ” Large”
focusable=True[#8]
- Legend “” [#29]
- group “Pizza Toppings” [#36]
- Legend “” [#37]
- StaticText “Pizza Toppings” [#69]
- InlineTextBox “Pizza Toppings”
- StaticText “Pizza Toppings” [#69]
- checkbox ” Bacon”
focusable=True[#9] - checkbox ” Extra Cheese”
focusable=True[#10] - checkbox ” Onion”
focusable=True[#11] - checkbox ” Mushroom”
focusable=True[#12]
- Legend “” [#37]
- LabelText “” [#47]
- StaticText “Preferred delivery time:” [#74]
- InlineTextBox “Preferred delivery time:”
- InputTime “Preferred delivery time:”
focusable=Truesettable=True[#13]- spinbutton “Hours Hours”
focusable=Truesettable=Truevaluemin=1valuemax=12[#51]- StaticText “–” [#75]
- InlineTextBox “–”
- StaticText “–” [#75]
- StaticText “:” [#76]
- InlineTextBox “:”
- spinbutton “Minutes Minutes”
focusable=Truesettable=Truevaluemax=59[#53]- StaticText “–” [#77]
- InlineTextBox “–”
- StaticText “–” [#77]
- StaticText ” ” [#78]
- InlineTextBox ” ”
- spinbutton “AM/PM AM/PM”
focusable=Truesettable=Truevaluemin=1valuemax=2[#55]- StaticText “–” [#79]
- InlineTextBox “–”
- StaticText “–” [#79]
- button “Show time picker”
focusable=TruehasPopup=menu[#56]
- spinbutton “Hours Hours”
- StaticText “Preferred delivery time:” [#74]
- LabelText “” [#58]
- StaticText “Delivery instructions:” [#80]
- InlineTextBox “Delivery instructions:”
- textbox “Delivery instructions:”
focusable=Trueeditable=plaintextsettable=Truemultiline=True[#5]
- StaticText “Delivery instructions:” [#80]
- button “Submit order”
focusable=True[#61]- StaticText “Submit order” [#81]
- InlineTextBox “Submit order”
- StaticText “Submit order” [#81]
- LabelText “” [#20]
AXNode.find_all
def find_all(
role:NoneType=None, name:NoneType=None
):
Find all descendants matching role and/or name substring
AXNode.find_id
def find_id(
role:NoneType=None, name:NoneType=None
):
Find first descendant matching role and/or name substring
AXNode.find
def find(
role:NoneType=None, name:NoneType=None
):
Find first descendant matching role and/or name substring
nmid = root.find_id('textbox', 'Customer name')
phid = root.find_id('textbox', 'Telephone')
nmid2
JsCDP.click
async def click(
backendNodeId, sid:NoneType=None
):
Call self as a function.
JsCDP.js_node_run
async def js_node_run(
code, backendNodeId, sid:NoneType=None
):
Call self as a function.
JsCDP.js_node
async def js_node(
fn, backendNodeId, sid:NoneType=None
):
Call self as a function.
await page.DOM.focus(backendNodeId=nmid)
await page.input.insertText(text='Jeremy Howard')
await page.DOM.focus(backendNodeId=phid)
await page.input.insertText(text='555-1234'){}await page.click(root.find_id('radio', 'Large'))
await page.click(root.find_id('checkbox', 'Extra Cheese'))JsCDP.fill_text
async def fill_text(
backendNodeId, text, sid:NoneType=None
):
Call self as a function.
await page.fill_text(root.find_id('textbox', 'Delivery'), 'Ring the doorbell twice')await page.js_node_run('this.value = "18:30"', root.find_id('InputTime', 'delivery time'));JsCDP.click_and_wait
async def click_and_wait(
backendNodeId, kwargs:VAR_KEYWORD
):
Click element and wait for load+idle
await page.click_and_wait(root.find_id('button', 'Submit order'))cdp_yolo
def cdp_yolo(
):
Allow all CDP classes in safepyrun