Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/workflows/infra.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ on:
branches:
- main
- release-*
# Disabled while iterating on tests_webview_gtk.yml; re-enable by reverting.
pull_request:
branches:
- main
- release-*
paths:
- '__webview_gtk_pr_disabled__'

env:
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
Expand Down
15 changes: 3 additions & 12 deletions .github/workflows/tests_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,10 @@ on:
branches:
- main
- release-*
# Disabled while iterating on tests_webview_gtk.yml; re-enable by reverting.
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
- 'packages/extension/**'
- 'packages/playwright-core/src/server/bidi/**'
- 'packages/playwright-core/src/tools/**'
- 'tests/bidi/**'
- 'tests/extension/**'
- 'tests/mcp/**'
branches:
- main
- release-*
paths:
- '__webview_gtk_pr_disabled__'

env:
FORCE_COLOR: 1
Expand Down
13 changes: 3 additions & 10 deletions .github/workflows/tests_mcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,10 @@ on:
branches:
- main
- release-*
# Disabled while iterating on tests_webview_gtk.yml; re-enable by reverting.
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
- 'packages/extension/**'
- 'packages/playwright-core/src/server/bidi/**'
- 'tests/bidi/**'
- 'tests/extension/**'
branches:
- main
- release-*
paths:
- '__webview_gtk_pr_disabled__'

concurrency:
# For pull requests, cancel all currently-running jobs for this workflow
Expand Down
15 changes: 3 additions & 12 deletions .github/workflows/tests_primary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,10 @@ on:
branches:
- main
- release-*
# Disabled while iterating on tests_webview_gtk.yml; re-enable by reverting.
pull_request:
paths-ignore:
- 'browser_patches/**'
- 'docs/**'
- 'packages/extension/**'
- 'packages/playwright-core/src/server/bidi/**'
- 'packages/playwright-core/src/tools/**'
- 'tests/bidi/**'
- 'tests/extension/**'
- 'tests/mcp/**'
branches:
- main
- release-*
paths:
- '__webview_gtk_pr_disabled__'

concurrency:
# For pull requests, cancel all currently-running jobs for this workflow
Expand Down
76 changes: 76 additions & 0 deletions .github/workflows/tests_webview_gtk.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: "tests WebView (WebKitGTK)"

on:
workflow_dispatch:
pull_request:
paths:
- 'tests/webview/**'
- 'packages/playwright-core/src/server/webkit/webview/**'
- '.github/workflows/tests_webview_gtk.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

env:
FORCE_COLOR: 1
ELECTRON_SKIP_BINARY_DOWNLOAD: 1
# The webview fixture launches this binary (provided by libwebkitgtk-6.0-4).
PW_WEBVIEW_PROXY_BASE: http://127.0.0.1:9222

jobs:
test_webview_gtk:
name: "WebView on WebKitGTK (${{ matrix.shard }}/4)"
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install WebKitGTK MiniBrowser
run: |
echo "::group::apt-get install libwebkitgtk-6.0-4"
sudo apt-get update
# libwebkitgtk-6.0-4 ships /usr/lib/x86_64-linux-gnu/webkitgtk-6.0/MiniBrowser
# and the engine; xvfb gives the GTK app a display, dbus-x11 a session bus.
sudo apt-get install -y libwebkitgtk-6.0-4 xvfb dbus-x11
ls -l /usr/lib/x86_64-linux-gnu/webkitgtk-6.0/MiniBrowser
echo "::endgroup::"

- name: npm ci
run: |
echo "::group::npm ci"
npm ci
echo "::endgroup::"

- name: npm run build
run: |
echo "::group::npm run build"
npm run build
echo "::endgroup::"

- name: Run WebView tests
run: |
echo "::group::Test run (shard ${{ matrix.shard }}/4)"
# GTK needs a display (xvfb) and a session bus (dbus-run-session). The
# fixture launches MiniBrowser itself and disables the WebKit sandbox.
# Scoped to a curated subset for now; widen to the full config later.
xvfb-run -a dbus-run-session -- \
npx playwright test page-check page-click page-goto page-keyboard \
--config tests/webview/playwright.config.ts --shard=${{ matrix.shard }}/4
echo "::endgroup::"

- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: webview-gtk-logs-${{ matrix.shard }}
path: |
${{ github.workspace }}/test-results/**
if-no-files-found: ignore
5 changes: 2 additions & 3 deletions .github/workflows/tests_webview_simulator.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ name: "tests WebView (iOS Simulator)"

on:
workflow_dispatch:
# Disabled while iterating on tests_webview_gtk.yml; re-enable by reverting.
pull_request:
paths:
- 'tests/webview/**'
- 'packages/playwright-core/src/server/webkit/webview/**'
- '.github/workflows/tests_webview_simulator.yml'
- '__webview_gtk_pr_disabled__'

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
Expand Down
65 changes: 57 additions & 8 deletions packages/playwright-core/src/server/webkit/webview/wvBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,65 @@ function deriveProxyBase(endpointURL: string): string {
}

function pageIdFromWsUrl(wsUrl: string): string {
const m = /\/devtools\/page\/([^/]+)/.exec(wsUrl);
return m ? m[1] : wsUrl;
// ios_webkit_debug_proxy exposes tabs as /devtools/page/<id>.
const cdp = /\/devtools\/page\/([^/]+)/.exec(wsUrl);
if (cdp)
return cdp[1];
// WebKitGTK/WPE remote inspector HTTP server exposes targets as
// /socket/<connectionID>/<targetID>/<type>.
const gtk = /\/socket\/(\d+)\/(\d+)\//.exec(wsUrl);
if (gtk)
return `${gtk[1]}/${gtk[2]}`;
return wsUrl;
}

function decodeHtmlEntities(text: string): string {
return text.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
.replace(/&quot;/g, '"').replace(/&#39;/g, '\'');
}

// WebKitGTK and WPE expose the remote inspector through an HTTP server (enabled
// with WEBKIT_INSPECTOR_HTTP_SERVER=host:port). Unlike ios_webkit_debug_proxy
// there is no /json endpoint: the target list is an HTML page at `/`, with each
// inspectable target carrying a `/socket/<connectionID>/<targetID>/<type>` path
// that upgrades to a WebSocket speaking the same protocol as the iOS tabs.
function parseGtkTargetListPage(html: string, proxyBase: string): ProxyTab[] {
const wsProto = new URL(proxyBase).protocol === 'https:' ? 'wss:' : 'ws:';
const host = new URL(proxyBase).host;
const tabs: ProxyTab[] = [];
const rowRegex = /<div class="targetname">([\s\S]*?)<\/div><div class="targeturl">([\s\S]*?)<\/div>[\s\S]*?\/socket\/(\d+)\/(\d+)\/(\w+)/g;
let match: RegExpExecArray | null;
while ((match = rowRegex.exec(html))) {
const [, name, url, connectionID, targetID, type] = match;
// Only page targets are driveable; skip ServiceWorker/JavaScript targets.
if (type !== 'WebPage')
continue;
tabs.push({
title: decodeHtmlEntities(name),
url: decodeHtmlEntities(url),
webSocketDebuggerUrl: `${wsProto}//${host}/socket/${connectionID}/${targetID}/${type}`,
});
}
return tabs;
}

async function listTabs(proxyBase: string, headers: { [key: string]: string }): Promise<ProxyTab[]> {
const res = await fetch(`${proxyBase}/json`, { headers });
if (!res.ok)
throw new Error(`ios_webkit_debug_proxy ${proxyBase}/json returned ${res.status}`);
const data = await res.json() as ProxyTab[];
return data.filter(t => !!t.webSocketDebuggerUrl);
// ios_webkit_debug_proxy exposes a CDP-style JSON listing.
const jsonRes = await fetch(`${proxyBase}/json`, { headers });
if (jsonRes.ok) {
const text = await jsonRes.text();
try {
const data = JSON.parse(text) as ProxyTab[];
if (Array.isArray(data))
return data.filter(t => !!t.webSocketDebuggerUrl);
} catch {
// Not a JSON listing — fall through to the WebKitGTK/WPE HTML listing.
}
}
const htmlRes = await fetch(`${proxyBase}/`, { headers });
if (!htmlRes.ok)
throw new Error(`Remote inspector at ${proxyBase} returned ${jsonRes.status} for /json and ${htmlRes.status} for /`);
return parseGtkTargetListPage(await htmlRes.text(), proxyBase);
}

// Local WebSocket-backed transport: defers opening the socket until `open()`
Expand Down Expand Up @@ -204,7 +253,7 @@ export class WVBrowser extends Browser {
} else {
await this._syncTabs();
if (!this._tabs.size)
throw new Error(`No Mobile Safari tabs found at ${this._proxyBase}/json — open Safari first.`);
throw new Error(`No inspectable tabs found at ${this._proxyBase} — open a page in the browser first.`);
}
this._page = this._firstTab().page;
}
Expand Down
Loading
Loading