Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ Example output for `agent status`:

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

On iOS, real devices and simulators alike, the same commands also drive the tabs of Safari when it is the foreground app, through the device's Web Inspector service. A real device needs **Settings > Apps > Safari > Advanced > Web Inspector** turned on. Safari does not keep the tabs it is not showing running: a command on such a tab fails after 2 seconds, so use the tab listed with `isVisible: true`.

```bash
# List embedded webviews in the foreground app
mobilecli webview list --device <device-id>
Expand Down
2 changes: 1 addition & 1 deletion cli/webview.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var webviewCmd = &cobra.Command{
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.`,
Long: `Returns all embedded webviews currently visible in the foreground app. When the foreground app is Safari on iOS, returns its tabs instead. Chrome is not included.`,
RunE: func(cmd *cobra.Command, args []string) error {
return runViaDaemon("cli.webview.list", commands.WebViewListRequest{
DeviceID: deviceId,
Expand Down
47 changes: 47 additions & 0 deletions devices/ios_device_webview.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,43 @@ package devices
import (
"encoding/json"
"fmt"
"io"
"time"

"github.com/mobile-next/mobilecli/utils"
)

// WebView feature for real iOS devices. These methods are thin JSON-RPC calls
// over the injected agent (see ios_device_agent.go for how the agent is
// injected and reached). They implement the WebViewable interface.
//
// Safari cannot take the agent, so when it is the foreground app its tabs are
// driven through the web inspector service instead (see ios_webinspector.go).

// safariInForeground returns what drives the tabs of Safari, when Safari is the
// app in front. Not knowing the foreground app leaves it to the injected agent,
// which reports why it cannot be reached.
func (d *IOSDevice) safariInForeground() (safariWebViews, bool) {
if err := d.StartAgent(StartAgentConfig{}); err != nil {
utils.Verbose("could not start the device agent to look up the foreground app: %v", err)
return safariWebViews{}, false
}
activeApp, err := d.deviceKitClient.GetActiveAppInfo()
if err != nil {
utils.Verbose("could not look up the foreground app: %v", err)
return safariWebViews{}, false
}
if activeApp.BundleID != safariBundleID {
return safariWebViews{}, false
}
inspector := webInspectorOf(d.Udid, func() (io.ReadWriteCloser, error) { return dialDeviceWebInspector(d.Udid) })
return safariWebViews{inspector: inspector}, true
}

func (d *IOSDevice) ListWebViews() ([]WebViewInfo, error) {
if safari, ok := d.safariInForeground(); ok {
return safari.ListWebViews()
}
result, err := d.agentCall("device.webview.list", nil)
if err != nil {
return nil, err
Expand All @@ -33,21 +62,33 @@ func (d *IOSDevice) ListWebViews() ([]WebViewInfo, error) {
}

func (d *IOSDevice) WebViewGoto(webviewID, url string) error {
if safari, ok := d.safariInForeground(); ok {
return safari.WebViewGoto(webviewID, url)
}
_, err := d.agentCall("device.webview.goto", map[string]any{"id": webviewID, "url": url})
return err
}

func (d *IOSDevice) WebViewReload(webviewID string) error {
if safari, ok := d.safariInForeground(); ok {
return safari.WebViewReload(webviewID)
}
_, err := d.agentCall("device.webview.reload", map[string]any{"id": webviewID})
return err
}

func (d *IOSDevice) WebViewGoBack(webviewID string) error {
if safari, ok := d.safariInForeground(); ok {
return safari.WebViewGoBack(webviewID)
}
_, err := d.agentCall("device.webview.goBack", map[string]any{"id": webviewID})
return err
}

func (d *IOSDevice) WebViewGoForward(webviewID string) error {
if safari, ok := d.safariInForeground(); ok {
return safari.WebViewGoForward(webviewID)
}
_, err := d.agentCall("device.webview.goForward", map[string]any{"id": webviewID})
return err
}
Expand All @@ -65,6 +106,9 @@ func (d *IOSDevice) WebViewContent(webviewID string) (string, error) {
}

func (d *IOSDevice) WebViewEvaluate(webviewID, expression string, args []any) (any, error) {
if safari, ok := d.safariInForeground(); ok {
return safari.WebViewEvaluate(webviewID, expression, args)
}
params := map[string]any{
"id": webviewID,
"expression": ensureReturnExpression(expression),
Expand All @@ -86,6 +130,9 @@ func (d *IOSDevice) WebViewEvaluate(webviewID, expression string, args []any) (a
}

func (d *IOSDevice) WebViewWaitForLoadState(webviewID, state string, timeoutMs int) error {
if safari, ok := d.safariInForeground(); ok {
return safari.WebViewWaitForLoadState(webviewID, state, timeoutMs)
}
if timeoutMs <= 0 {
timeoutMs = 30_000
}
Expand Down
Loading
Loading