- addons
Modern Execution Context Reference
execute() receives a context object as its second argument. Which fields are present depends on the platforms you declare in applicationType and the permissions you request. Requesting nothing gives you logging, a scratch directory, runtime variables, and an abort signal.
Context Fields
| Field | Available | Purpose |
|---|---|---|
| ctx.ui.browser | Platforms include WEB or MOBILE_WEB | Drive the page under test |
| ctx.ui.mobile | Platforms include ANDROID or IOS | Drive the app under test |
| ctx.runtime | Always | Read and write runtime variables by name |
| ctx.logger | Always | Structured logging: debug, info, warn, error |
| ctx.tmpDir | Always | Per-step scratch directory, auto-cleaned, always readable and writable |
| ctx.signal | Always | AbortSignal that fires if the run is cancelled |
| ctx.execution | Always | Read-only metadata: sessionId, applicationType, result identifiers |
| ctx.ai | With permissions.ai | Platform-brokered LLM generation |
| ctx.ocr | With permissions.ocr | Platform-brokered OCR and image finding |
| ctx.spawn | With permissions.spawn | Host-brokered subprocess, local agent only |
Honor ctx.signal in loops and long waits so cancelled runs stop promptly.
Browser Adapter
| Method | Notes |
|---|---|
| locate(elementRef | selector) | Returns a locator handle for a declared element or an ad-hoc selector |
| goto(url, opts?) | waitUntil: "load" | "domcontentloaded" | "networkidle" |
| screenshot(opts?) | { fullPage?: boolean }, returns a Buffer |
| title() / url() | Current page title and URL |
| evaluate(scriptString, args?) | Must be a string. Function values cannot be transferred. |
| waitForSelector(sel, opts?) | { state?: "attached" | "visible" | "hidden", timeout? } |
Locator handles support click(), fill(text), textContent(), getAttribute(name), isVisible(), count(), boundingBox(), and string-based evaluate().
Mobile Adapter
locate() accepts a declared ElementRef or an ad-hoc { strategy, value } literal. Supported strategies are accessibilityId, id, xpath, className, androidUiAutomator for Android, and iosPredicate or iosClassChain for iOS.
| Surface | Methods |
|---|---|
| Element handle | tap(), longPress({ms?}), fill(text), clear(), text(), getAttribute(name), isVisible(), boundingBox(), swipe(direction), count() |
| Device | screenshot(), getContexts(), getCurrentContext(), switchContext(name), getOrientation(), setOrientation(...), hideKeyboard() |
Using a platform-specific strategy on the other platform fails at run time. Coordinates from boundingBox() are device pixels, so divide by the device pixel ratio for density-independent units.
What Is Exposed
ctx.ui.browser and ctx.ui.browser.page are the same object at two levels of abstraction. The adapter above covers common cases; when it does not reach far enough, every session also exposes the real automation object and the real assertion library.
| Platform | Automation | Assertions |
|---|---|---|
| Web | ctx.ui.browser.page — Playwright Page | ctx.ui.browser.expect |
| Mobile | ctx.ui.mobile.driver — WebdriverIO driver | ctx.ui.mobile.expect |
No permission declaration is required for any of them. These are the genuine libraries rather than a curated subset, so every matcher, .not, .soft, and the generic value matchers work as documented.
Reach for these for frames, keyboard and mouse, dialogs, downloads, and accessibility snapshots on web, or raw Appium commands on mobile.
Three Rules That Differ from a Normal Test
Your addon runs in a worker while the browser lives in the host process. That has three consequences.
- Await everything, including calls that are synchronous in Playwright such as page.url(). Composing without awaiting is still free, so page.getByRole("form").locator("input") reaches the host only once, when you finally await it.
- Await every assertion. Generic matchers return a promise here. An un-awaited failing assertion becomes an unhandled rejection, and your step passes when it should not.
- No closures. Function arguments are rejected. Anything that evaluates in the browser or on the device takes a string instead, including evaluate("el => el.textContent"), waitForFunction, $$eval, and WebdriverIO's execute.
Structural Validation with Aria Snapshots
Asserting the shape of a form or menu in one call beats a pile of per-element checks. Matching is a subset check, so unrelated markup can change freely, and it auto-retries like any Playwright assertion.
const page = ctx.ui.browser.page;
const expect = ctx.ui.browser.expect;
await expect(page.getByRole("form", { name: "Filters" })).toMatchAriaSnapshot(`
- heading "Filters"
- textbox "Search"
- button "Apply"
`);
const snapshot = await page.getByRole("form", { name: "Filters" }).ariaSnapshot();Waiting on Mobile
WebdriverIO's waitUntil is unavailable, because its condition callback runs in Node and no string form can replace it. Use ctx.ui.mobile.waitUntil, which re-evaluates a driver expression on the device each interval.
await ctx.ui.mobile.waitUntil(driver.$("~ok").isDisplayed(), { timeout: 10_000 });Pass the expression without awaiting it. Awaiting first would check it once, and is refused with an explanation. Options are WebdriverIO's timeout, interval, and timeoutMsg.
What Is Refused
Your addon is one step inside someone else's session, so anything that outlives the step or hands over credentials is refused with a clear error.
| Group | Members |
|---|---|
| Persistent callbacks | on, once, route, exposeFunction, addInitScript |
| Session lifecycle | close, pause, setDefaultTimeout, deleteSession, reloadSession, addCommand |
| Escapes that outlive the step | newPage, newCDPSession, tracing, browser |
| Credentials, mobile | options, requestedCapabilities |
One-shot waitForEvent(...) is available and covers dialogs, downloads, and popups. capabilities stays available, so you can branch on platformVersion.
page.context() and page.request are available, so cookies, storageState(), and API calls all work. Only the dangerous members inside them are refused.
File paths passed to screenshot({ path }), Download.saveAs(), setInputFiles(), or WebdriverIO's savers must sit under ctx.tmpDir. Anywhere else is refused.
On mobile, prefer ctx.ui.mobile.locate(...) over driver.$(...) where it suffices. locate() applies UiAutomator resourceId wrapping, the iOS class-name predicate workaround, and xpath normalization that the raw driver does not.
Runtime Variables
const v = await ctx.runtime.get("orderId"); // undefined when unset
await ctx.runtime.set("orderId", "ORD-1042");
await ctx.runtime.set("token", jwt, { isEncrypted: true });AI
const answer = await ctx.ai.invoke("Summarize this receipt", {
files: ["receipt.png"],
});File paths are relative to ctx.tmpDir, or within your fs allowlist.
OCR
| Method | Returns |
|---|---|
| extractTextFromPage() | Recognized text spans with bounding boxes for the current page |
| extractTextFromImage(path) | The same, for an image file you supply |
| extractTextFromElement(elementRef) | The same, for one element. Web only. |
| findImage(refImage, opts?) | { isFound, x1, y1, x2, y2 } |
findImage accepts threshold, scale, and occurrence options.
