Script¶
The script module lets you execute JavaScript in a browsing context. There
are two ways to run code: evaluate (run an expression) and call_function
(run a function with arguments). Both return a RemoteValue — a typed
wrapper around the JavaScript value.
evaluate¶
evaluate runs a JavaScript expression in the page's main realm and
returns the result as a RemoteValue:
Pattern matching with RemoteValue¶
The protocol serializes JavaScript values into JSON with a type field.
bidiwave parses this into specific Python classes. Use match for
type-safe narrowing:
from bidiwave import StringValue, NumberValue, NullValue
result = await client.script.evaluate(ctx, "document.title")
match result:
case StringValue(value=title):
print(f"Title: {title}")
case NullValue():
print("No title")
case _:
print(f"Unexpected type: {result.type}")
Awaiting Promises¶
By default, evaluate returns immediately even if the expression returns a
Promise. Set await_promise=True to wait for the Promise to resolve:
result = await client.script.evaluate(
ctx,
"new Promise(r => setTimeout(() => r('done'), 1000))",
await_promise=True,
)
match result:
case StringValue(value=text):
print(f"Promise resolved: {text}") # "done"
This is essential for waiting on fetch() calls, animations, or any
async JavaScript:
result = await client.script.evaluate(
ctx,
"fetch('/api/data').then(r => r.json()).then(d => d.result)",
await_promise=True,
)
All RemoteValue types¶
| JS type | Python class | Key field | Example |
|---|---|---|---|
"string" |
StringValue |
.value: str |
"hello" |
"number" |
NumberValue |
.value: int \| float |
42, 3.14 |
"boolean" |
BooleanValue |
.value: bool |
true |
"null" |
NullValue |
(none) | null |
"undefined" |
UndefinedValue |
(none) | undefined |
"bigint" |
BigIntValue |
.value: str |
9007199254740993n |
"object" |
ObjectValue |
.value: dict |
{a: 1} |
"array" |
ArrayValue |
.value: list |
[1, 2, 3] |
"function" |
HandleValue |
.handle: str |
() => {} |
"symbol" |
HandleValue |
.handle: str |
Symbol() |
Working with objects and arrays¶
When JavaScript returns an object or array, bidiwave parses it into
ObjectValue or ArrayValue. Nested values are preserved as dicts and
lists:
result = await client.script.evaluate(ctx, "({name: 'Alice', age: 30})")
match result:
case ObjectValue(value=data):
print(data["name"]) # "Alice"
print(data["age"]) # 30
result = await client.script.evaluate(ctx, "[1, 2, 3, 4, 5]")
match result:
case ArrayValue(value=items):
print(items) # [1, 2, 3, 4, 5]
print(sum(items)) # 15
Handles and disown¶
When JavaScript returns a function, symbol, or an object with circular
references, the protocol returns a handle instead of the value. Handles
are string references to objects living in the browser. Use disown to
release them and free memory:
result = await client.script.evaluate(ctx, "document.querySelector('button')")
match result:
case ObjectValue(handle=h) if h:
# h is a handle string like "element-1234"
# Use it as an argument in call_function
await client.script.call_function(
ctx,
"(el) => el.click()",
args=[{"handle": h}],
)
# Release the handle when done
await client.script.disown(ctx, handles=[h])
Memory leaks
Handles keep objects alive in the browser's memory. Always call
disown when you're done with a handle to avoid memory leaks,
especially in long-running scripts.
call_function¶
call_function executes a JavaScript function with arguments. The
function declaration is a string (not an expression), and arguments are
passed as a list of BiDi value descriptors:
result = await client.script.call_function(
ctx,
"(a, b) => a + b",
args=[
{"type": "number", "value": 2},
{"type": "number", "value": 3},
],
)
match result:
case NumberValue(value=n):
print(f"2 + 3 = {n}") # 5
Argument format¶
Arguments are dictionaries with type and value keys:
| JS type | Argument format |
|---|---|
| string | {"type": "string", "value": "hello"} |
| number | {"type": "number", "value": 42} |
| boolean | {"type": "boolean", "value": true} |
| null | {"type": "null"} |
| undefined | {"type": "undefined"} |
| handle | {"handle": "handle-id"} |
Passing handles¶
Handles from evaluate can be passed to call_function:
# Get a reference to an element
element = await client.script.evaluate(ctx, "document.querySelector('#btn')")
match element:
case ObjectValue(handle=h) if h:
# Pass the handle as an argument
result = await client.script.call_function(
ctx,
"(el) => el.textContent",
args=[{"handle": h}],
)
await client.script.disown(ctx, handles=[h])
ChromeDriver/EdgeDriver bug
Some versions of ChromeDriver and EdgeDriver have a bug where
callFunction returns NaN when passed primitive number arguments.
If you encounter this, use evaluate with an inline expression
instead:
Preload scripts¶
Preload scripts run before the page's own scripts. They're useful for injecting polyfills, overriding globals, or monitoring page behavior from the very start.
script.addPreloadScript (with channels)¶
Unlike preload.addPreloadScript, script.addPreloadScript supports
channels for bidirectional communication between the preload script
and the client via script.message events:
result = await client.script.add_preload_script(
function_declaration="(channel) => { channel('hello from page'); }",
arguments=[{"type": "channel", "value": {"channel": "my-channel"}}],
contexts=["context-id-1"],
)
print(result.script) # preload script ID
print(result.channel) # channel name for communication
Receiving messages from preload scripts¶
async def on_message(event):
print(f"Message from preload: {event.data}")
client.on("script.message", on_message)
await client.session.subscribe(["script.message"])
preload.addPreloadScript (without channels)¶
For simpler use cases where you don't need bidirectional communication:
result = await client.preload.add_preload_script(
function_declaration="() => { window.myPolyfill = true; }",
contexts=["context-id-1"],
)
# result.script = "preload-script-id"
# Remove when done
await client.preload.remove_preload_script(result.script)
add_preload_script parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
function_declaration |
str |
(required) | JS function to execute |
arguments |
list[dict] \| None |
None |
Arguments (supports channel type) |
contexts |
list[str] \| None |
None |
Context IDs to scope the script |
user_contexts |
list[str] \| None |
None |
User context IDs to scope |
Realms¶
Realms are JavaScript execution environments. Each browsing context has at least one realm (the main world). Iframes and service workers have their own realms.
Get all realms¶
realms = await client.script.get_realms()
for realm in realms:
print(f"Realm: {realm.realm} (type={realm.type}, context={realm.context})")
Filter by context¶
Filter by realm type¶
# Only main realms
realms = await client.script.get_realms(realm_type="window")
# Service worker realms
realms = await client.script.get_realms(realm_type="service-worker")
| Realm type | Description |
|---|---|
"window" |
Main page realm (one per browsing context) |
"dedicated-worker" |
Web Worker realm |
"shared-worker" |
SharedWorker realm |
"service-worker" |
Service Worker realm |
"worker" |
Generic worker realm |
Script events¶
| Event | Fired when | Handler receives |
|---|---|---|
script.realmCreated |
A new JavaScript realm is created | ScriptRealmCreatedEvent |
script.realmDestroyed |
A realm is destroyed (context closed, worker terminated) | ScriptRealmDestroyedEvent |
script.message |
A preload script sends a message via channel | ScriptMessageEvent |
async def on_realm_created(event):
print(f"Realm created: {event.realm} (type={event.type})")
async def on_realm_destroyed(event):
print(f"Realm destroyed: {event.realm}")
client.on_realm_created(on_realm_created)
client.on_realm_destroyed(on_realm_destroyed)
await client.session.subscribe(["script.realmCreated", "script.realmDestroyed"])
Page convenience methods¶
The Page object provides shortcuts for evaluate and call_function:
async with await client.browsing.open("https://example.com") as page:
# Same as client.script.evaluate(page._context, expr)
result = await page.evaluate("document.title")
# Same as client.script.call_function(page._context, fn, args)
result = await page.call("(a, b) => a + b", args=[...])
API reference¶
See Script API for the complete ScriptModule reference,
Preload API for the PreloadModule reference,
and RemoteValue API for all RemoteValue subtypes.