Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9ce4f38
feat: add device.webview.* method stubs
gmegidish May 11, 2026
8dabdb1
feat: align webview methods with trimmed OpenRPC spec
gmegidish May 12, 2026
3945a67
feat: add webview CLI commands
gmegidish May 12, 2026
36729b8
feat: implement WebViewListCommand for Android
gmegidish May 12, 2026
cef8b30
fix: remove hardcoded DEX_PATH from jvmti_agent.c
gmegidish May 12, 2026
559d39d
feat: implement webview goto, evaluate (url + title) for Android
gmegidish May 12, 2026
a6e7d1e
fix: webview url/title/eval returning empty result
gmegidish May 12, 2026
37687ee
feat: implement webview back, forward, content
gmegidish May 12, 2026
d8109a6
fix: ensure bare expressions are wrapped with return in WebViewEvaluate
gmegidish May 12, 2026
46f452c
feat: implement webview reload
gmegidish May 12, 2026
e6a0c33
fix: handle null evalJs result and ClassCastException in evaluateExpr…
gmegidish May 12, 2026
98d936d
build: wire agents/android into root Makefile
gmegidish May 12, 2026
7808755
feat: implement webview waitForLoadState
gmegidish May 12, 2026
31386a7
refactor: replace map[string]any with typed result structs
gmegidish May 12, 2026
4abe35b
chore: add agent sources, Makefile, and adb client
gmegidish May 12, 2026
f42b53c
chore: remove built agent binaries from git, add to gitignore
gmegidish May 12, 2026
581330f
fix: cross-platform agent Makefile and CI agent build job
gmegidish May 12, 2026
b1cfde6
feat: iOS simulator webview agent (list, url, goto)
gmegidish May 12, 2026
c6d1eea
no dylib in git
gmegidish May 12, 2026
49d63ee
feat: complete iOS simulator webview support + WebViewable interface
gmegidish May 12, 2026
c74d7b7
feat: iOS real device webview list (inject via lldb, forward via go-ios)
gmegidish May 12, 2026
c0e2d01
fix: iOS 26 SDK compat for real device webview injection via LLDB
gmegidish May 13, 2026
0391621
performance
gmegidish May 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ A universal command-line tool for managing iOS and Android devices, simulators,
- **Device Control**: Reboot devices, tap screen coordinates, press hardware buttons
- **App Management**: Launch, terminate, install, uninstall, list, and get foreground apps
- **Crash Reports**: List and fetch crash reports from iOS and Android devices
- **Webview Inspection**: List, navigate, query DOM, and evaluate JavaScript in embedded webviews

### 🎯 Platform Support

Expand Down Expand Up @@ -255,6 +256,66 @@ Example output for `agent status`:
}
```

### Webview Inspection 🌐

Inspect and interact with embedded webviews (`WKWebView` on iOS, `android.webkit.WebView` on Android) running inside native apps.

```bash
# List embedded webviews in the foreground app
mobilecli webview list --device <device-id>

# Navigate a webview to a URL
mobilecli webview goto <id> https://example.com --device <device-id>

# Reload, go back or forward
mobilecli webview reload <id> --device <device-id>
mobilecli webview back <id> --device <device-id>
mobilecli webview forward <id> --device <device-id>

# Get current URL and page title
mobilecli webview url <id> --device <device-id>
mobilecli webview title <id> --device <device-id>

# Dump the full HTML content of the page
mobilecli webview content <id> --device <device-id>

# Query DOM elements by CSS selector
mobilecli webview query <id> "button" --device <device-id>
mobilecli webview query <id> "[data-testid='submit']" --device <device-id>

# Evaluate arbitrary JavaScript
mobilecli webview eval <id> "document.querySelectorAll('a').length" --device <device-id>

# Wait for the page to finish loading
mobilecli webview wait <id> --state load --device <device-id>
mobilecli webview wait <id> --state domcontentloaded --timeout 5000 --device <device-id>
```

Example output for `webview list`:
```json
{
"status": "ok",
"data": [
{
"id": "1",
"url": "https://example.com",
"title": "Example Domain"
}
]
}
```

Example output for `webview query <id> "button"`:
```json
{
"status": "ok",
"data": [
{ "tag": "button", "text": "Sign In", "id": "login-btn", "class": "btn-primary", "value": null, "href": null },
{ "tag": "button", "text": "Cancel", "id": null, "class": "btn-secondary", "value": null, "href": null }
]
}
```

### Crash Reports 💥

```bash
Expand Down
4 changes: 4 additions & 0 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ var (
fleetNames []string
fleetWait bool
fleetTimeout int

// for webview wait command
webviewWaitState string
webviewWaitTimeout int
)
29 changes: 29 additions & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,35 @@ INPUT/OUTPUT:
# Send text input
mobilecli io text --device <device-id> "Hello World"

WEBVIEW:
# List embedded webviews in the foreground app
mobilecli webview list --device <device-id>

# Navigate a webview to a URL
mobilecli webview goto <id> https://example.com --device <device-id>

# Reload, go back or forward
mobilecli webview reload <id> --device <device-id>
mobilecli webview back <id> --device <device-id>
mobilecli webview forward <id> --device <device-id>

# Get current URL and page title
mobilecli webview url <id> --device <device-id>
mobilecli webview title <id> --device <device-id>

# Dump full HTML content
mobilecli webview content <id> --device <device-id>

# Query DOM elements by CSS selector
mobilecli webview query <id> "button" --device <device-id>
mobilecli webview query <id> "[data-testid='submit']" --device <device-id>

# Evaluate arbitrary JavaScript
mobilecli webview eval <id> "document.querySelectorAll('a').length" --device <device-id>

# Wait for page load
mobilecli webview wait <id> --state load --device <device-id>

CRASH REPORTS:
# List crash reports from a device
mobilecli device crashes list --device <device-id>
Expand Down
262 changes: 262 additions & 0 deletions cli/webview.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package cli

import (
"fmt"

"github.com/mobile-next/mobilecli/commands"
"github.com/spf13/cobra"
)

var webviewCmd = &cobra.Command{
Use: "webview",
Short: "Inspect and interact with embedded webviews",
Long: `List, navigate, evaluate JavaScript, and inspect DOM content inside embedded webviews on a device.`,
}

var webviewListCmd = &cobra.Command{
Use: "list",
Short: "List embedded webviews on a device",
Long: `Returns all embedded webviews currently visible in the foreground app. Browser apps (Safari, Chrome) are not included.`,
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewListCommand(commands.WebViewListRequest{
DeviceID: deviceId,
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewGotoCmd = &cobra.Command{
Use: "goto <id> <url>",
Short: "Navigate a webview to a URL",
Long: `Navigates the specified webview to the given URL. The webview id comes from 'webview list'.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewGotoCommand(commands.WebViewGotoRequest{
DeviceID: deviceId,
WebViewID: args[0],
URL: args[1],
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewReloadCmd = &cobra.Command{
Use: "reload <id>",
Short: "Reload a webview",
Long: `Reloads the page currently loaded in the specified webview.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewReloadCommand(commands.WebViewReloadRequest{
DeviceID: deviceId,
WebViewID: args[0],
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewBackCmd = &cobra.Command{
Use: "back <id>",
Short: "Navigate a webview back",
Long: `Navigates the webview back in its history, equivalent to pressing the browser back button.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewGoBackCommand(commands.WebViewRequest{
DeviceID: deviceId,
WebViewID: args[0],
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewForwardCmd = &cobra.Command{
Use: "forward <id>",
Short: "Navigate a webview forward",
Long: `Navigates the webview forward in its history, equivalent to pressing the browser forward button.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewGoForwardCommand(commands.WebViewRequest{
DeviceID: deviceId,
WebViewID: args[0],
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewEvalCmd = &cobra.Command{
Use: "eval <id> <expression>",
Short: "Evaluate JavaScript in a webview",
Long: `Evaluates a JavaScript expression in the context of the specified webview and returns the result.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewEvaluateCommand(commands.WebViewEvaluateRequest{
DeviceID: deviceId,
WebViewID: args[0],
Expression: args[1],
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewWaitCmd = &cobra.Command{
Use: "wait <id>",
Short: "Wait for a webview to finish loading",
Long: `Waits for the webview to reach the specified load state before returning.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewWaitForLoadStateCommand(commands.WebViewWaitForLoadStateRequest{
DeviceID: deviceId,
WebViewID: args[0],
State: webviewWaitState,
Timeout: webviewWaitTimeout,
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
Comment on lines +124 to +141
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate --state and --timeout in CLI before dispatching.

Right now invalid values are sent downstream; fail fast in CLI to provide immediate feedback.

🛡️ Suggested fix
 RunE: func(cmd *cobra.Command, args []string) error {
+		switch webviewWaitState {
+		case "load", "domcontentloaded":
+		default:
+			return fmt.Errorf("invalid --state %q (allowed: load, domcontentloaded)", webviewWaitState)
+		}
+		if webviewWaitTimeout < 0 {
+			return fmt.Errorf("--timeout must be >= 0")
+		}
+
 		response := commands.WebViewWaitForLoadStateCommand(commands.WebViewWaitForLoadStateRequest{
 			DeviceID:  deviceId,
 			WebViewID: args[0],
 			State:     webviewWaitState,
 			Timeout:   webviewWaitTimeout,
 		})
-		printJson(response)
-		if response.Status == "error" {
-			return fmt.Errorf("%s", response.Error)
-		}
-		return nil
+		return printAndReturnError(response)
 	},

Also applies to: 256-257

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/webview.go` around lines 124 - 141, Validate CLI flag values before
calling commands: in webviewWaitCmd's RunE, check that webviewWaitState is one
of the allowed load states (e.g., "load", "domcontentloaded", "networkidle" or
whatever enum your backend expects) and that webviewWaitTimeout is > 0 (or
within acceptable bounds); if invalid, return a descriptive error from RunE
instead of calling commands.WebViewWaitForLoadStateCommand with bad data.
Perform the same pre-dispatch validation for the other webview-related
command(s) that use webviewWaitState/webviewWaitTimeout (the same pattern as
WebViewWaitForLoadStateRequest) so invalid flags fail fast in the CLI.

}

// ─── Convenience commands built on evaluate ───────────────────

var webviewURLCmd = &cobra.Command{
Use: "url <id>",
Short: "Print the current URL of a webview",
Long: `Prints the current URL loaded in the specified webview.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewEvaluateCommand(commands.WebViewEvaluateRequest{
DeviceID: deviceId,
WebViewID: args[0],
Expression: "location.href",
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
Comment on lines +146 to +162
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

url, title, and content are thin wrappers around evaluate with no dedicated server handlers. That means their response shape is {"status":"ok","data":"https://..."} — a raw JS result string in the data field, indistinguishable from webview eval <id> "location.href".

Fine for now, but worth documenting this in the Long description for each command. When implementation lands, consider first-class server handlers returning typed shapes ({"url":"..."}, {"title":"..."}) — this also simplifies the mobilewright driver, which would need to know the response type to extract the value.

}

var webviewTitleCmd = &cobra.Command{
Use: "title <id>",
Short: "Print the title of a webview",
Long: `Prints the document title of the page currently loaded in the specified webview.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewEvaluateCommand(commands.WebViewEvaluateRequest{
DeviceID: deviceId,
WebViewID: args[0],
Expression: "document.title",
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewContentCmd = &cobra.Command{
Use: "content <id>",
Short: "Dump the HTML content of a webview",
Long: `Returns the full outer HTML of the page currently loaded in the specified webview.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
response := commands.WebViewEvaluateCommand(commands.WebViewEvaluateRequest{
DeviceID: deviceId,
WebViewID: args[0],
Expression: "document.documentElement.outerHTML",
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

var webviewQueryCmd = &cobra.Command{
Use: "query <id> <selector>",
Short: "Query DOM elements in a webview",
Long: `Finds elements matching a CSS selector and returns their tag, text, id, and value. Useful for inspecting webview content.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
selector := args[1]
expression := fmt.Sprintf(
`Array.from(document.querySelectorAll(%q)).map(el => ({`+
`tag: el.tagName.toLowerCase(),`+
`text: (el.textContent || "").trim().slice(0, 200),`+
`id: el.id || null,`+
`class: el.className || null,`+
`value: el.value || null,`+
`href: el.href || null`+
`}))`,
selector,
)
response := commands.WebViewEvaluateCommand(commands.WebViewEvaluateRequest{
DeviceID: deviceId,
WebViewID: args[0],
Expression: expression,
})
printJson(response)
if response.Status == "error" {
return fmt.Errorf("%s", response.Error)
}
return nil
},
}

func init() {
rootCmd.AddCommand(webviewCmd)

webviewCmd.AddCommand(webviewListCmd)
webviewCmd.AddCommand(webviewGotoCmd)
webviewCmd.AddCommand(webviewReloadCmd)
webviewCmd.AddCommand(webviewBackCmd)
webviewCmd.AddCommand(webviewForwardCmd)
webviewCmd.AddCommand(webviewEvalCmd)
webviewCmd.AddCommand(webviewWaitCmd)
webviewCmd.AddCommand(webviewURLCmd)
webviewCmd.AddCommand(webviewTitleCmd)
webviewCmd.AddCommand(webviewContentCmd)
webviewCmd.AddCommand(webviewQueryCmd)

webviewListCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewGotoCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewReloadCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewBackCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewForwardCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewEvalCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewWaitCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewWaitCmd.Flags().StringVar(&webviewWaitState, "state", "load", `load state to wait for: "load" or "domcontentloaded"`)
webviewWaitCmd.Flags().IntVar(&webviewWaitTimeout, "timeout", 0, "maximum time to wait in milliseconds (0 = default)")
webviewURLCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewTitleCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewContentCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
webviewQueryCmd.Flags().StringVar(&deviceId, "device", "", "ID of the device")
}
Loading
Loading