-
Notifications
You must be signed in to change notification settings - Fork 40
feat: adding playwright webview support with getByWebView() locator #172
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 34 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 a2c68f3
feat(query-engine): add webview strategy kind
gmegidish 4ba901a
feat(core): add WebViewLocator, Page stub, and screen.getByWebView()
gmegidish 8ed5a83
feat(core): implement Page class and WebLocator stub
gmegidish afe7eaa
feat(core): Playwright-compatible DOM selector engine and WebLocator …
gmegidish 6029b75
feat(core): add PageAssertions and WebLocatorAssertions to expect()
gmegidish 09684e4
refactor(expect): WebLocatorAssertions extends LocatorAssertions
gmegidish 6ef547d
style(expect): restore multi-line inline functions for readability
gmegidish 1928a59
feat(tests): add unit tests for Page and WebLocator (step 7)
gmegidish 571a2e5
Merge origin/main into feat-adding-webview; thread stepFn through web…
gmegidish 35ebda1
feat(webview): mobilecli webView bridge, page goBack/goForward, iOS p…
gmegidish 71b4f4c
refactor(core): share step/eval helpers and drop dead WebViewSession …
gmegidish 0e08b8b
refactor(core): dedupe assertion helpers, share webview test fakes, t…
gmegidish a3983e3
fix test so it uses local mobilewright without npx
gmegidish 02bde8b
Merge remote-tracking branch 'origin/main' into feat-adding-webview
gmegidish 71c1c14
fix(core): resolve webview locators uniquely, reset regex state, hono…
gmegidish f3b1882
fix(core): scope chained web locators and reset regex state in waitFo…
gmegidish 7798f42
fix(core): assert element exists before web locator actions instead o…
gmegidish 19b32fb
test(core): add regression tests for webview resolution, regex state,…
gmegidish d876764
upgrade mobilecli
gmegidish 3b522c1
test(core): cover webview locator nth/first, chaining, and page() err…
gmegidish c95aa9f
fixed tests
gmegidish bfbf455
chore(deps): pin playwright, playwright-core and @playwright/test to …
gmegidish 182d5ec
feat(core): route webview locators and web-first assertions through p…
gmegidish becc706
docs: add webview playwright parity design specs and plans
gmegidish 7b29e69
feat(webview): run the injected engine on real devices (engine detect…
gmegidish 40fcf40
test(e2e): add on-device webview Playwright-parity conformance suite
gmegidish 5eaf547
docs: add webview conformance suite spec and plan
gmegidish 9bed6d4
feat(core): export Page and WebLocator from the umbrella package
gmegidish 0b416cd
chore: drop superpowers design docs from the branch
gmegidish 49f84ea
refactor(core): tidy webview locator and query-engine per clean-code …
gmegidish 79966c7
refactor(core): drop unsupported Page.screenshot() stub until a captu…
gmegidish 9ff58d1
fix(core): inject the webview engine after navigation settles so it s…
gmegidish 65ecb44
test(e2e): add a live-navigation webview conformance test against a r…
gmegidish 93a6dc6
chore: deleted old test
gmegidish 0eb57f6
test(e2e): run shared conformance specs under both mobilewright and p…
gmegidish cb0bfc3
fix(core): re-inject the webview engine when a page navigation drops it
gmegidish 029ef5d
test(e2e): split platform-specific tests into per-platform projects
gmegidish 1d57ef3
feat(core): make webview Page and Locator drop-in Playwright Page/Loc…
gmegidish bcb4a6d
test(e2e): drive conformance specs with Playwright's expect; skip foc…
gmegidish aa74609
feat(core): select webviews by testId and harden webview resolution
gmegidish bb2ce67
docs: add Web Views guide
gmegidish a695974
fix(core): recognize WebKit's engine-missing error so the webview eng…
gmegidish c831050
test(e2e): run e2e tests serially by dropping fullyParallel
gmegidish 685e33b
Merge remote-tracking branch 'origin/main' into fix-improving-webview
gmegidish File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)}`; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { test, expect } from '@mobilewright/test'; | ||
| import { openWebviewPage } from './harness.js'; | ||
|
|
||
| // A real, cross-origin HTTPS page (not a data: URL) so this exercises an actual | ||
| // network navigation plus engine re-injection on the fresh document. It covers | ||
| // the subset of the web API that stays stable against a live site; the | ||
| // deterministic matcher matrix lives in the other conformance files. | ||
| const PYTHAGOREAN_ARTICLE = 'https://en.wikipedia.org/wiki/Pythagorean_theorem'; | ||
|
|
||
| test('navigates to a live page and drives it like Playwright', async ({ device, screen }) => { | ||
| const page = await openWebviewPage({ device, screen }); | ||
|
|
||
| await page.goto(PYTHAGOREAN_ARTICLE); | ||
| await page.waitForLoadState('domcontentloaded'); | ||
|
|
||
| // Page-level state reflects the real navigation. | ||
| await expect(page).toHaveURL(/Pythagorean_theorem/); | ||
| await expect(page).toHaveTitle(/Pythagorean theorem/); | ||
|
|
||
| // The article heading resolves by id and by accessible role+name. | ||
| await expect(page.locator('#firstHeading')).toContainText('Pythagorean theorem'); | ||
| await expect(page.getByRole('heading', { name: /Pythagorean theorem/ }).first()).toBeVisible(); | ||
|
|
||
| // A real article has many links; assert a lower bound rather than an exact | ||
| // count, which would drift as the page is edited. | ||
| const linkCount = await page.getByRole('link').count(); | ||
| expect(linkCount).toBeGreaterThan(50); | ||
| await expect(page.getByRole('link').first()).toBeVisible(); | ||
|
|
||
| // Scroll the last link (near the page bottom) into view and confirm it lands | ||
| // in the viewport — a skin-independent target on a long article. | ||
| const lastLink = page.getByRole('link').last(); | ||
| await lastLink.scrollIntoViewIfNeeded(); | ||
| await expect(lastLink).toBeInViewport(); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'; | ||
|
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'); | ||
| }); | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make live-site navigation tests opt-in to avoid CI flakiness.
Line 13 onward depends on a third-party site and mutable content, so this can fail due to internet/geo/captcha/content drift rather than regressions in Mobilewright. Please gate this test behind an explicit env flag (or move it to a non-blocking nightly suite).
Proposed hardening diff
import { test, expect } from '`@mobilewright/test`'; import { openWebviewPage } from './harness.js'; +const RUN_LIVE_WEB = process.env.MW_RUN_LIVE_WEB === '1'; + // A real, cross-origin HTTPS page (not a data: URL) so this exercises an actual // network navigation plus engine re-injection on the fresh document. It covers // the subset of the web API that stays stable against a live site; the // deterministic matcher matrix lives in the other conformance files. const PYTHAGOREAN_ARTICLE = 'https://en.wikipedia.org/wiki/Pythagorean_theorem'; test('navigates to a live page and drives it like Playwright', async ({ device, screen }) => { + test.skip(!RUN_LIVE_WEB, 'Set MW_RUN_LIVE_WEB=1 to run external live-web conformance tests'); + const page = await openWebviewPage({ device, screen });🤖 Prompt for AI Agents