Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
de0f07c
feat(protocol): add WebViewInfo, WebViewBridge, and WebViewSession types
gmegidish May 10, 2026
a2c68f3
feat(query-engine): add webview strategy kind
gmegidish May 10, 2026
4ba901a
feat(core): add WebViewLocator, Page stub, and screen.getByWebView()
gmegidish May 10, 2026
8ed5a83
feat(core): implement Page class and WebLocator stub
gmegidish May 10, 2026
afe7eaa
feat(core): Playwright-compatible DOM selector engine and WebLocator …
gmegidish May 10, 2026
6029b75
feat(core): add PageAssertions and WebLocatorAssertions to expect()
gmegidish May 10, 2026
09684e4
refactor(expect): WebLocatorAssertions extends LocatorAssertions
gmegidish May 10, 2026
6ef547d
style(expect): restore multi-line inline functions for readability
gmegidish May 10, 2026
1928a59
feat(tests): add unit tests for Page and WebLocator (step 7)
gmegidish May 10, 2026
571a2e5
Merge origin/main into feat-adding-webview; thread stepFn through web…
gmegidish Jun 3, 2026
35ebda1
feat(webview): mobilecli webView bridge, page goBack/goForward, iOS p…
gmegidish Jun 3, 2026
71b4f4c
refactor(core): share step/eval helpers and drop dead WebViewSession …
gmegidish Jun 3, 2026
0e08b8b
refactor(core): dedupe assertion helpers, share webview test fakes, t…
gmegidish Jun 3, 2026
a3983e3
fix test so it uses local mobilewright without npx
gmegidish Jun 4, 2026
02bde8b
Merge remote-tracking branch 'origin/main' into feat-adding-webview
gmegidish Jun 4, 2026
71c1c14
fix(core): resolve webview locators uniquely, reset regex state, hono…
gmegidish Jun 4, 2026
f3b1882
fix(core): scope chained web locators and reset regex state in waitFo…
gmegidish Jun 4, 2026
7798f42
fix(core): assert element exists before web locator actions instead o…
gmegidish Jun 4, 2026
19b32fb
test(core): add regression tests for webview resolution, regex state,…
gmegidish Jun 4, 2026
d876764
upgrade mobilecli
gmegidish Jun 4, 2026
3b522c1
test(core): cover webview locator nth/first, chaining, and page() err…
gmegidish Jun 5, 2026
c95aa9f
fixed tests
gmegidish Jun 5, 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
4 changes: 2 additions & 2 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"description": "End-to-end tests for mobilewright drivers",
"type": "module",
"scripts": {
"test:mobilecli": "MOBILEWRIGHT_DRIVER=mobilecli npx mobilewright test",
"test:mobilenext": "MOBILEWRIGHT_DRIVER=mobilenext npx mobilewright test"
"test:mobilecli": "MOBILEWRIGHT_DRIVER=mobilecli node ../packages/mobilewright/dist/cli.js test",
"test:mobilenext": "MOBILEWRIGHT_DRIVER=mobilenext node ../packages/mobilewright/dist/cli.js test"
},
"dependencies": {
"mobilewright": "^0.0.1",
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/driver-mobilecli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"dependencies": {
"@mobilewright/protocol": "^0.0.1",
"debug": "^4.4.3",
"mobilecli": "0.3.75",
"mobilecli": "0.3.77",
"ws": "^8.18.0"
},
"devDependencies": {
Expand Down
93 changes: 93 additions & 0 deletions packages/driver-mobilecli/src/driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,25 @@ function allowRpc(driver: MobilecliDriver): void {
(driver as any).session.rpc.call = async () => ({});
}

interface RecordedCall {
method: string;
params: Record<string, unknown>;
}

// Replace the session's RPC transport with a recorder that returns canned
// responses keyed by method name, capturing every (method, params) pair.
function recordRpc(
driver: MobilecliDriver,
responses: Record<string, unknown>,
): RecordedCall[] {
const calls: RecordedCall[] = [];
(driver as any).session.rpc.call = async (method: string, params: Record<string, unknown>) => {
calls.push({ method, params });
return responses[method];
};
return calls;
}

test.describe('MobilecliDriver.installApp()', () => {
test.describe('iOS simulator', () => {
test('accepts a .zip file', async () => {
Expand Down Expand Up @@ -199,3 +218,77 @@ test.describe('MobilecliDriver.installApp()', () => {
});
});
});

test.describe('MobilecliDriver.webViewBridge', () => {
test('listWebViews maps device.webview.list entries to WebViewInfo', async () => {
const driver = createDriverWithSession();
recordRpc(driver, {
'device.webview.list': [
{
id: 'wv-1',
url: 'https://example.com/',
title: 'Example',
bundleId: 'com.example.app',
bounds: { x: 0, y: 100, width: 390, height: 700 },
isVisible: true,
},
],
});

const webviews = await driver.webViewBridge.listWebViews();
expect(webviews).toEqual([
{
id: 'wv-1',
url: 'https://example.com/',
title: 'Example',
nativeBounds: { x: 0, y: 100, width: 390, height: 700 },
},
]);
});

test('navigation methods call the matching RPC with the device and webview ids', async () => {
const driver = createDriverWithSession();
const calls = recordRpc(driver, {});
const session = await driver.webViewBridge.attachWebView('wv-1');

await session.goto('https://example.com/');
await session.goBack();
await session.goForward();
await session.reload();

expect(calls.map((c) => c.method)).toEqual([
'device.webview.goto',
'device.webview.goBack',
'device.webview.goForward',
'device.webview.reload',
]);
// deviceId is injected by the driver, id by the session.
expect(calls[0].params).toEqual({ deviceId: SIMULATOR_DEVICE_ID, id: 'wv-1', url: 'https://example.com/' });
expect(calls[1].params).toEqual({ deviceId: SIMULATOR_DEVICE_ID, id: 'wv-1' });
});

test('evaluate forwards the expression and returns the value directly', async () => {
const driver = createDriverWithSession();
const calls = recordRpc(driver, { 'device.webview.evaluate': 42 });
const session = await driver.webViewBridge.attachWebView('wv-1');

const value = await session.evaluate<number>('6 * 7');
expect(value).toBe(42);
expect(calls[0]).toEqual({
method: 'device.webview.evaluate',
params: { deviceId: SIMULATOR_DEVICE_ID, id: 'wv-1', expression: '6 * 7' },
});
});

test('url and title return the raw RPC string results', async () => {
const driver = createDriverWithSession();
recordRpc(driver, {
'device.webview.url': 'https://example.com/page',
'device.webview.title': 'Page Title',
});
const session = await driver.webViewBridge.attachWebView('wv-1');

expect(await session.url()).toBe('https://example.com/page');
expect(await session.title()).toBe('Page Title');
});
});
95 changes: 95 additions & 0 deletions packages/driver-mobilecli/src/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { execFileSync } from 'node:child_process';
import { openSync, readSync, closeSync } from 'node:fs';
import type {
AppInfo,
Bounds,
ConnectionConfig,
DeviceInfo,
DeviceState,
Expand All @@ -22,6 +23,9 @@ import type {
SwipeDirection,
SwipeOptions,
ViewNode,
WebViewBridge,
WebViewInfo,
WebViewSession,
} from '@mobilewright/protocol';
import { RpcClient } from './rpc-client.js';
import { resolveMobilecliBinary } from './resolve-binary.js';
Expand Down Expand Up @@ -99,6 +103,20 @@ interface MobilecliAgentStatusResponse {
};
}

/** A single embedded webview as returned by device.webview.list */
interface MobilecliWebViewEntry {
id: string;
url: string;
title: string;
bundleId?: string;
processName?: string;
bounds?: Bounds;
isVisible?: boolean;
}

/** RPC caller bound to a device, used by the webview session. */
type WebViewRpcCall = <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>;

const VALID_PLATFORMS = new Set<string>(['ios', 'android']);
const VALID_DEVICE_TYPES = new Set<string>(['real', 'simulator', 'emulator']);
const VALID_DEVICE_STATES = new Set<string>(['online', 'offline']);
Expand Down Expand Up @@ -149,6 +167,63 @@ function assertValidZipFile(path: string): void {

const debug = createDebug('mw:driver-mobilecli');

/**
* A WebViewSession backed by mobilecli's device.webview.* RPC methods.
* Bound to a single webview `id`; the deviceId is injected by the caller.
* The core's WebLocator drives the page through evaluate() and the injected
* DOM selector engine.
*/
class MobilecliWebViewSession implements WebViewSession {
constructor(
private readonly call: WebViewRpcCall,
private readonly id: string,
) {}

async evaluate<T = unknown>(expr: string): Promise<T> {
// mobilecli evaluates the expression and returns the value directly
// (not wrapped in a { result } envelope).
return this.call<T>('device.webview.evaluate', {
id: this.id,
expression: expr,
});
}

async goto(url: string): Promise<void> {
await this.call('device.webview.goto', { id: this.id, url });
}

async goBack(): Promise<void> {
await this.call('device.webview.goBack', { id: this.id });
}

async goForward(): Promise<void> {
await this.call('device.webview.goForward', { id: this.id });
}

async reload(): Promise<void> {
await this.call('device.webview.reload', { id: this.id });
}

async url(): Promise<string> {
return this.call<string>('device.webview.url', { id: this.id });
}

async title(): Promise<string> {
return this.call<string>('device.webview.title', { id: this.id });
}

async waitForLoadState(state?: 'load' | 'domcontentloaded'): Promise<void> {
await this.call('device.webview.waitForLoadState', {
id: this.id,
...(state !== undefined && { state }),
});
}

async close(): Promise<void> {
// mobilecli has no webview-close RPC; webviews are owned by the host app.
}
}

export class MobilecliDriver implements MobilewrightDriver {
private session: { deviceId: string; deviceName: string; platform: Platform; deviceType: DeviceType; rpc: RpcClient } | null = null;
private readonly serverUrl: string;
Expand Down Expand Up @@ -491,6 +566,26 @@ export class MobilecliDriver implements MobilewrightDriver {
await this.call('device.url', { url });
}

// ─── WebView ─────────────────────────────────────────────────

get webViewBridge(): WebViewBridge {
const call: WebViewRpcCall = (method, params) => this.call(method, params);
return {
listWebViews: async (): Promise<WebViewInfo[]> => {
const entries = await this.call<MobilecliWebViewEntry[]>('device.webview.list');
return entries.map((e) => ({
id: e.id,
url: e.url,
title: e.title,
...(e.bounds && { nativeBounds: e.bounds }),
}));
},
attachWebView: async (id: string): Promise<WebViewSession> => {
return new MobilecliWebViewSession(call, id);
},
};
}

// ─── Helpers ─────────────────────────────────────────────────

/** RPC call on the active session, auto-injecting deviceId. */
Expand Down
Loading