Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
45 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
bfbf455
chore(deps): pin playwright, playwright-core and @playwright/test to …
gmegidish Jun 6, 2026
182d5ec
feat(core): route webview locators and web-first assertions through p…
gmegidish Jun 6, 2026
becc706
docs: add webview playwright parity design specs and plans
gmegidish Jun 6, 2026
7b29e69
feat(webview): run the injected engine on real devices (engine detect…
gmegidish Jun 7, 2026
40fcf40
test(e2e): add on-device webview Playwright-parity conformance suite
gmegidish Jun 7, 2026
5eaf547
docs: add webview conformance suite spec and plan
gmegidish Jun 7, 2026
9bed6d4
feat(core): export Page and WebLocator from the umbrella package
gmegidish Jun 7, 2026
0b416cd
chore: drop superpowers design docs from the branch
gmegidish Jun 7, 2026
49f84ea
refactor(core): tidy webview locator and query-engine per clean-code …
gmegidish Jun 7, 2026
79966c7
refactor(core): drop unsupported Page.screenshot() stub until a captu…
gmegidish Jun 7, 2026
9ff58d1
fix(core): inject the webview engine after navigation settles so it s…
gmegidish Jun 7, 2026
65ecb44
test(e2e): add a live-navigation webview conformance test against a r…
gmegidish Jun 7, 2026
93a6dc6
chore: deleted old test
gmegidish Jun 7, 2026
0eb57f6
test(e2e): run shared conformance specs under both mobilewright and p…
gmegidish Jun 7, 2026
cb0bfc3
fix(core): re-inject the webview engine when a page navigation drops it
gmegidish Jun 7, 2026
029ef5d
test(e2e): split platform-specific tests into per-platform projects
gmegidish Jun 7, 2026
1d57ef3
feat(core): make webview Page and Locator drop-in Playwright Page/Loc…
gmegidish Jun 8, 2026
bcb4a6d
test(e2e): drive conformance specs with Playwright's expect; skip foc…
gmegidish Jun 8, 2026
aa74609
feat(core): select webviews by testId and harden webview resolution
gmegidish Jun 8, 2026
bb2ce67
docs: add Web Views guide
gmegidish Jun 8, 2026
a695974
fix(core): recognize WebKit's engine-missing error so the webview eng…
gmegidish Jun 8, 2026
c831050
test(e2e): run e2e tests serially by dropping fullyParallel
gmegidish Jun 8, 2026
685e33b
Merge remote-tracking branch 'origin/main' into fix-improving-webview
gmegidish Jun 8, 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
46 changes: 46 additions & 0 deletions e2e/src/conformance/actions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { test, expect } from '@mobilewright/test';
import { openWebviewPage, pageWithBody } from './harness.js';

test('actions affect the DOM like Playwright', async ({ device, screen }) => {
const page = await openWebviewPage({ device, screen });

await page.goto(pageWithBody(`
<button id="b" onclick="this.textContent='clicked'">press me</button>
<input id="fill" type="text">
<input id="type" type="text">
<input id="key" type="text" onkeydown="this.value='key:'+event.key">
<input id="focusable" type="text">
<div id="hovered">idle</div>
<button id="hover" onmouseover="document.getElementById('hovered').textContent='hovered'">hover me</button>
<div style="height:2000px"></div>
<button id="bottom">bottom</button>
`));

// click
await page.locator('#b').click();
await expect(page.locator('#b')).toHaveText('clicked');

// fill
await page.locator('#fill').fill('hello@example.com');
await expect(page.locator('#fill')).toHaveValue('hello@example.com');

// type (appends)
await page.locator('#type').type('abc');
await expect(page.locator('#type')).toHaveValue('abc');

// press
await page.locator('#key').press('Enter');
await expect(page.locator('#key')).toHaveValue('key:Enter');

// focus
await page.locator('#focusable').focus();
await expect(page.locator('#focusable')).toBeFocused();

// hover
await page.locator('#hover').hover();
await expect(page.locator('#hovered')).toHaveText('hovered');

// scrollIntoViewIfNeeded — no throw, element becomes in viewport
await page.locator('#bottom').scrollIntoViewIfNeeded();
await expect(page.locator('#bottom')).toBeInViewport();
});
39 changes: 39 additions & 0 deletions e2e/src/conformance/assertions-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { test, expect } from '@mobilewright/test';
import { openWebviewPage, pageWithBody } from './harness.js';

test('state assertions match Playwright', async ({ device, screen }) => {
const page = await openWebviewPage({ device, screen });

await page.goto(pageWithBody(`
<div id="visible">shown</div>
<div id="hidden" style="display:none">gone</div>
<button id="enabled">ok</button>
<button id="disabled" disabled>no</button>
<input id="editable" type="text">
<input id="readonly" type="text" readonly>
<input id="checkbox" type="checkbox" checked>
<input id="empty" type="text" value="">
`));

await expect(page.locator('#visible')).toBeVisible();
await expect(page.locator('#hidden')).not.toBeVisible();
await expect(page.locator('#hidden')).toBeHidden();
await expect(page.locator('#visible')).not.toBeHidden();

await expect(page.locator('#enabled')).toBeEnabled();
await expect(page.locator('#disabled')).toBeDisabled();
await expect(page.locator('#disabled')).not.toBeEnabled();

await expect(page.locator('#editable')).toBeEditable();
await expect(page.locator('#readonly')).not.toBeEditable();

await expect(page.locator('#checkbox')).toBeChecked();

await expect(page.locator('#visible')).toBeAttached();
await expect(page.locator('#missing')).not.toBeAttached();

await expect(page.locator('#empty')).toBeEmpty();
await expect(page.locator('#visible')).not.toBeEmpty();

await expect(page.locator('#visible')).toBeInViewport();
});
29 changes: 29 additions & 0 deletions e2e/src/conformance/assertions-text.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test, expect } from '@mobilewright/test';
import { openWebviewPage, pageWithBody } from './harness.js';

test('text assertions match Playwright (incl. whitespace normalization)', async ({ device, screen }) => {
const page = await openWebviewPage({ device, screen });

await page.goto(pageWithBody(`
<p id="text"> Hello world </p>
<input id="value" type="text" value="john@example.com">
`));

// Playwright normalizes whitespace for STRING matches (so the multi-space,
// padded text equals 'Hello world')...
await expect(page.locator('#text')).toHaveText('Hello world');
await expect(page.locator('#text')).not.toHaveText('Goodbye');
// ...but NOT for REGEX matches — a regex is tested against the raw text, so it
// must account for the actual whitespace.
await expect(page.locator('#text')).toHaveText(/Hello\s+world/);
await expect(page.locator('#text')).not.toHaveText(/Hello world/);

// toContainText substring
await expect(page.locator('#text')).toContainText('world');
await expect(page.locator('#text')).not.toContainText('planet');

// toHaveValue exact + regex + negative
await expect(page.locator('#value')).toHaveValue('john@example.com');
await expect(page.locator('#value')).toHaveValue(/@example\.com$/);
await expect(page.locator('#value')).not.toHaveValue('other');
});
35 changes: 35 additions & 0 deletions e2e/src/conformance/assertions-web.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test, expect } from '@mobilewright/test';
import { openWebviewPage, pageWithBody } from './harness.js';

test('web-only assertions match Playwright', async ({ device, screen }) => {
const page = await openWebviewPage({ device, screen });

await page.goto(pageWithBody(`
<ul><li class="item">a</li><li class="item">b</li></ul>
<button id="btn" class="btn primary" data-variant="primary" style="color: rgb(255, 0, 0);">go</button>
<input id="check" type="checkbox" checked>
`));

// count
await expect(page.locator('.item')).toHaveCount(2);
await expect(page.locator('.item')).not.toHaveCount(3);

// attribute (exact + regex + negative)
await expect(page.locator('#btn')).toHaveAttribute('data-variant', 'primary');
await expect(page.locator('#btn')).toHaveAttribute('class', /primary/);
await expect(page.locator('#btn')).not.toHaveAttribute('data-variant', 'secondary');

// class (full token list) + contain (subset)
await expect(page.locator('#btn')).toHaveClass('btn primary');
await expect(page.locator('#btn')).toContainClass('primary');
await expect(page.locator('#btn')).not.toContainClass('danger');

// css
await expect(page.locator('#btn')).toHaveCSS('color', 'rgb(255, 0, 0)');

// id
await expect(page.locator('#btn')).toHaveId('btn');

// JS property
await expect(page.locator('#check')).toHaveJSProperty('checked', true);
});
21 changes: 21 additions & 0 deletions e2e/src/conformance/harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Device, Screen, Page } from 'mobilewright';

const PLAYGROUND_APP = 'com.mobilenext.playground';

// Launch the Playground app, open its WebView screen, and return the web Page.
// All conformance tests start from the Page this returns.
export async function openWebviewPage(ctx: { device: Device; screen: Screen }): Promise<Page> {
await ctx.device.terminateApp(PLAYGROUND_APP).catch(() => {});
await ctx.device.launchApp(PLAYGROUND_APP);
const webviewButton = ctx.screen.getByText('Web View');
await webviewButton.tap();
const page = await ctx.screen.getByWebView().page();
return page;
}

// Wrap a readable HTML body fragment into a self-contained data: URL document.
// Tests author legible HTML; the data-URL encoding stays hidden behind the name.
export function pageWithBody(bodyHtml: string): string {
const doc = `<!doctype html><meta charset="utf-8"><body>${bodyHtml}</body>`;
return `data:text/html,${encodeURIComponent(doc)}`;
}
41 changes: 41 additions & 0 deletions e2e/src/conformance/locators.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test, expect } from '@mobilewright/test';
import { openWebviewPage, pageWithBody } from './harness.js';

test('locator factories resolve like Playwright', async ({ device, screen }) => {
const page = await openWebviewPage({ device, screen });

await page.goto(pageWithBody(`
<button>Sign in</button>
<a href="#">Sign in</a>
<label>Email <input type="text" placeholder="you@example.com" data-testid="email"></label>
<img alt="Company logo" src="x">
<span title="Close dialog">x</span>
<p class="greeting">Hello world</p>
<ul><li>one</li><li>two</li><li>three</li></ul>
`));

// getByRole with accessible name, exact, and regex
await expect(page.getByRole('button', { name: 'Sign in' })).toHaveCount(1);
await expect(page.getByRole('button', { name: 'sign', exact: false })).toHaveCount(1);
await expect(page.getByRole('button', { name: /sign/i })).toHaveCount(1);
await expect(page.getByRole('link', { name: 'Sign in' })).toHaveCount(1);

// getByText exact vs substring vs regex
await expect(page.getByText('Hello world')).toHaveCount(1);
await expect(page.getByText('Hello', { exact: false })).toHaveCount(1);
await expect(page.getByText(/hello/i)).toHaveCount(1);

// label / placeholder / testid / alt / title
await expect(page.getByLabel('Email')).toHaveCount(1);
await expect(page.getByPlaceholder('you@example.com')).toHaveCount(1);
await expect(page.getByTestId('email')).toHaveCount(1);
await expect(page.getByAltText('Company logo')).toHaveCount(1);
await expect(page.getByTitle('Close dialog')).toHaveCount(1);

// raw css, count, nth/first/last, chaining
await expect(page.locator('li')).toHaveCount(3);
await expect(page.locator('li').first()).toHaveText('one');
await expect(page.locator('li').nth(1)).toHaveText('two');
await expect(page.locator('li').last()).toHaveText('three');
await expect(page.locator('ul').getByText('two')).toHaveCount(1);
});
19 changes: 19 additions & 0 deletions e2e/src/webview-injected-click.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test, expect } from '@mobilewright/test';

// Set to a webview-capable app installed on the target device.
const APP_ID = 'com.example.webviewdemo';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

test('clicks a real webview button via the injected Playwright engine', async ({ device, screen }) => {
await device.launchApp(APP_ID);

const page = await screen.getByWebView().page();

// Self-contained fixture: clicking the button mutates document.title, which
// we read back to confirm the click actually fired in the real webview.
await page.goto('data:text/html,<button onclick="document.title=\'clicked\'">Sign in</button>');

await page.getByRole('button', { name: 'Sign in' }).click();

const title = await page.title();
expect(title).toBe('clicked');
});
11 changes: 6 additions & 5 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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"devDependencies": {
"@mobilewright/test": "*",
"@playwright/test": "^1.58.2",
"@playwright/test": "1.58.2",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/node": "^22.19.15",
"@typescript-eslint/eslint-plugin": "^8.59.3",
Expand Down
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.78",
"ws": "^8.18.0"
},
"devDependencies": {
Expand Down
Loading