Skip to content

Commit d9cf308

Browse files
committed
fix(webview): support iOS 26 site isolation and fix input/postData
Stock Mobile Safari on iOS 26 site-isolates frames, reporting a separate Target.targetCreated with type 'frame' for every page (even pages without iframes). wvPage asserted page-only targets and threw, breaking the entire WebView suite. Ignore non-page frame targets (resuming any paused ones) and drive the page through the top-level page target. Also fixes three input/network bugs uncovered once the suite ran again: - postData: stock WebKit reports Network.Request.postData as a plain string, not base64 like the patched fork — decode as utf8. - keyboard: synthetic key events never performed the default text insertion, so press/type left inputs unchanged — emulate insertion honoring selection. - mouse: move() only dispatched mousemove — fire the full out/leave -> over/enter -> move sequence (mouse + pointer) when the hovered element changes, so hover-driven UI reacts. Removes the 56 now-passing tests from the webkit-webview-page expectations and recategorizes the remaining genuine failures.
1 parent 37a5aa6 commit d9cf308

4 files changed

Lines changed: 107 additions & 70 deletions

File tree

packages/playwright-core/src/server/webkit/webview/wvInput.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,25 @@ export class RawKeyboardImpl implements input.RawKeyboard {
7777
const expr = `(() => {
7878
const t = document.activeElement || document.body;
7979
const init = { bubbles: true, cancelable: true, view: window, code: ${JSON.stringify(code)}, key: ${JSON.stringify(key)}, keyCode: ${keyCode}, which: ${keyCode}, location: ${location}, repeat: ${autoRepeat}, ctrlKey: ${mods.ctrlKey}, shiftKey: ${mods.shiftKey}, altKey: ${mods.altKey}, metaKey: ${mods.metaKey} };
80-
const dispatch = e => { Object.defineProperty(e, '__pwTrustedSynthetic', { value: true }); t.dispatchEvent(e); };
81-
dispatch(new KeyboardEvent('keydown', init));
82-
${text ? `dispatch(new KeyboardEvent('keypress', { ...init, charCode: ${charCode}, keyCode: ${charCode}, which: ${charCode} }));` : ''}
80+
const dispatch = e => { Object.defineProperty(e, '__pwTrustedSynthetic', { value: true }); return t.dispatchEvent(e); };
81+
const notPrevented = dispatch(new KeyboardEvent('keydown', init));
82+
${text ? `const charNotPrevented = dispatch(new KeyboardEvent('keypress', { ...init, charCode: ${charCode}, keyCode: ${charCode}, which: ${charCode} }));
83+
// Synthetic key events do not perform the browser's default text-insertion,
84+
// so emulate it here (honoring the current selection) unless the page
85+
// cancelled keydown/keypress.
86+
if (notPrevented && charNotPrevented) {
87+
const text = ${JSON.stringify(text)};
88+
if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) {
89+
const start = t.selectionStart ?? t.value.length;
90+
const end = t.selectionEnd ?? t.value.length;
91+
t.value = t.value.slice(0, start) + text + t.value.slice(end);
92+
const pos = start + text.length;
93+
try { t.setSelectionRange(pos, pos); } catch {}
94+
t.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: false, data: text, inputType: 'insertText' }));
95+
} else if (t && t.isContentEditable) {
96+
document.execCommand('insertText', false, text);
97+
}
98+
}` : ''}
8399
})()`;
84100
await evalInPage(progress, this._session, expr);
85101
}
@@ -123,7 +139,37 @@ export class RawMouseImpl implements input.RawMouse {
123139
}
124140

125141
async move(progress: Progress, x: number, y: number, button: types.MouseButton | 'none', buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, forClick: boolean): Promise<void> {
126-
await this._dispatchMouse(progress, 'mousemove', x, y, buttonToNumber(button), toButtonsMask(buttons), modifiers, 0);
142+
const mods = modifierFlags(modifiers);
143+
const buttonCode = buttonToNumber(button);
144+
const buttonsMask = toButtonsMask(buttons);
145+
// A real pointer move fires the full out/leave -> over/enter -> move sequence
146+
// (both mouse and pointer flavors) whenever the element under the pointer
147+
// changes. Synthetic dispatch must replicate it, otherwise hover-driven UI
148+
// (e.g. interstitials listening for mouseover/pointerover) never reacts.
149+
const expr = `(() => {
150+
const x = ${x}, y = ${y};
151+
const target = (${kDeepElementFromPointSrc})(x, y) || document.documentElement;
152+
const base = { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y, screenX: x, screenY: y, button: ${buttonCode}, buttons: ${buttonsMask}, ctrlKey: ${mods.ctrlKey}, shiftKey: ${mods.shiftKey}, altKey: ${mods.altKey}, metaKey: ${mods.metaKey} };
153+
const pointer = { ...base, pointerId: 1, pointerType: 'mouse', isPrimary: true };
154+
const fire = (Ctor, type, node, init) => { const e = new Ctor(type, init); Object.defineProperty(e, '__pwTrustedSynthetic', { value: true }); node.dispatchEvent(e); };
155+
const prev = window.__pwHoverTarget || null;
156+
if (prev !== target) {
157+
if (prev && prev.isConnected) {
158+
fire(PointerEvent, 'pointerout', prev, { ...pointer, relatedTarget: target });
159+
fire(MouseEvent, 'mouseout', prev, { ...base, relatedTarget: target });
160+
fire(PointerEvent, 'pointerleave', prev, { ...pointer, bubbles: false, cancelable: false, relatedTarget: target });
161+
fire(MouseEvent, 'mouseleave', prev, { ...base, bubbles: false, cancelable: false, relatedTarget: target });
162+
}
163+
fire(PointerEvent, 'pointerover', target, { ...pointer, relatedTarget: prev });
164+
fire(MouseEvent, 'mouseover', target, { ...base, relatedTarget: prev });
165+
fire(PointerEvent, 'pointerenter', target, { ...pointer, bubbles: false, cancelable: false, relatedTarget: prev });
166+
fire(MouseEvent, 'mouseenter', target, { ...base, bubbles: false, cancelable: false, relatedTarget: prev });
167+
window.__pwHoverTarget = target;
168+
}
169+
fire(PointerEvent, 'pointermove', target, pointer);
170+
fire(MouseEvent, 'mousemove', target, base);
171+
})()`;
172+
await evalInPage(progress, this._session, expr);
127173
}
128174

129175
async down(progress: Progress, x: number, y: number, button: types.MouseButton, buttons: Set<types.MouseButton>, modifiers: Set<types.KeyboardModifier>, clickCount: number): Promise<void> {

packages/playwright-core/src/server/webkit/webview/wvInterceptableRequest.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,11 @@ export class WVInterceptableRequest {
5656
let postDataBuffer = null;
5757
this._timestamp = event.timestamp;
5858
this._wallTime = event.walltime * 1000;
59-
if (event.request.postData)
60-
postDataBuffer = Buffer.from(event.request.postData, 'base64');
59+
if (event.request.postData) {
60+
// Stock WebKit reports Network.Request.postData as a plain string, unlike
61+
// the Playwright-patched WebKit build which base64-encodes it.
62+
postDataBuffer = Buffer.from(event.request.postData, 'utf8');
63+
}
6164
this.request = new network.Request(frame._page.browserContext, frame, null, redirectedFrom?.request || null, documentId, event.request.url,
6265
resourceType, event.request.method, postDataBuffer, headersObjectToArray(event.request.headers));
6366
}

packages/playwright-core/src/server/webkit/webview/wvPage.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,15 @@ export class WVPage implements PageDelegate {
210210

211211
private async _onTargetCreated(event: Protocol.Target.targetCreatedPayload) {
212212
const { targetInfo } = event;
213-
assert(targetInfo.type === 'page', 'Only page targets are expected in WebView, received: ' + targetInfo.type);
213+
if (targetInfo.type !== 'page') {
214+
// Site-isolated WebKit (iOS 26+) reports a separate target per frame. We
215+
// drive the whole page through the top-level page target, so out-of-process
216+
// frame targets are not attached. Resume any paused ones so navigation in
217+
// the owning page is not blocked waiting on them.
218+
if (targetInfo.isPaused)
219+
this._outerSession.sendMayFail('Target.resume', { targetId: targetInfo.targetId });
220+
return;
221+
}
214222
const session = this._createSession(targetInfo.targetId);
215223
if (!targetInfo.isProvisional) {
216224
let pageOrError: Page | Error;

tests/webview/expectations/webkit-webview-page.txt

Lines changed: 43 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -123,76 +123,56 @@ page/elementhandle-screenshot.spec.ts › element screenshot › should not issu
123123
page/locator-misc-1.spec.ts › should focus and blur a button [fail]
124124

125125
# ============================================================================
126-
# execution-context-destroyed (65 tests)
127-
# Cross-navigation handle invalidation is racy on stock RDP — execution
128-
# contexts are reported destroyed before our frame manager learns about the
129-
# navigation. Needs proper Runtime.executionContextsCleared wiring in wvPage.
130-
# ============================================================================
131-
page/elementhandle-convenience.spec.ts › innerHTML should work [fail]
132-
page/elementhandle-convenience.spec.ts › innerText should work [fail]
133-
page/elementhandle-convenience.spec.ts › textContent should work [fail]
134-
page/elementhandle-misc.spec.ts › should fill input when Node is removed [fail]
135-
page/elementhandle-misc.spec.ts › should select single option [fail]
136-
page/elementhandle-press.spec.ts › should not modify selection when focused [fail]
137-
page/elementhandle-query-selector.spec.ts › should query existing element [fail]
138-
page/elementhandle-select-text.spec.ts › should select input [fail]
139-
page/elementhandle-select-text.spec.ts › should select textarea [fail]
140-
page/elementhandle-type.spec.ts › should not modify selection when focused [fail]
141-
page/elementhandle-wait-for-element-state.spec.ts › should wait for stable position [fail]
126+
# service-worker (2 tests)
127+
# Service-worker request interception/reporting is unsupported on the stock
128+
# WebView backend; routing a request through a service worker tears down the
129+
# session ('Target closed').
130+
# ============================================================================
142131
page/interception.spec.ts › should intercept after a service worker [fail]
143-
page/locator-click.spec.ts › should double click the button [fail]
144-
page/locator-element-handle.spec.ts › xpath should query existing element [fail]
145-
page/locator-list.spec.ts › locator.all should work [fail]
146-
page/locator-misc-2.spec.ts › should select textarea [fail]
147-
page/network-post-data.spec.ts › should return post data for PUT requests [fail]
148-
page/network-post-data.spec.ts › should return post data w/o content-type @smoke [fail]
149-
page/page-add-init-script.spec.ts › should support multiple scripts [fail]
150-
page/page-add-locator-handler.spec.ts › should not work with force:true [fail]
151-
page/page-add-locator-handler.spec.ts › should work › mouseover 1 times [fail]
152-
page/page-add-script-tag.spec.ts › should work with a content and type=module [fail]
153-
page/page-add-style-tag.spec.ts › should include sourceURL when path is provided [fail]
154-
page/page-aria-snapshot.spec.ts › should include pseudo codepoints [fail]
132+
page/page-event-request.spec.ts › should report requests and responses handled by service worker with routing [fail]
133+
134+
# ============================================================================
135+
# out-of-process-iframe (1 test)
136+
# iOS 26 site-isolates frames into their own targets, which wvPage does not
137+
# attach — it drives the page through the top-level page target only. Actions
138+
# that must reach inside a cross-origin iframe therefore time out.
139+
# ============================================================================
155140
page/page-click-scroll.spec.ts › should scroll into view element in iframe [fail]
156-
page/page-click.spec.ts › should click on checkbox label and toggle [fail]
157-
page/page-click.spec.ts › should click the button after navigation [fail]
158-
page/page-click.spec.ts › should fail when element detaches after animation [fail]
159-
page/page-click.spec.ts › should not wait with force [fail]
160-
page/page-click.spec.ts › should report nice error when element is detached and force-clicked [fail]
161-
page/page-click.spec.ts › should waitFor display:none to be gone [fail]
162-
page/page-click.spec.ts › should waitFor visibility:hidden to be gone [fail]
163-
page/page-click.spec.ts › should waitFor visible when parent is hidden [fail]
141+
142+
# ============================================================================
143+
# native-drag-and-drop (1 test)
144+
# Synthetic mouse events cannot drive WebKit's native HTML5 drag controller, so
145+
# dragstart/dragenter/dragover/drop are never produced for draggable elements.
146+
# ============================================================================
164147
page/page-drag.spec.ts › Drag and drop › should send the right events [fail]
165-
page/page-evaluate.spec.ts › should work with overridden globalThis.Window/Document/Node › () => globalThis.Window = null [fail]
166-
page/page-evaluate.spec.ts › should work with overridden globalThis.Window/Document/Node › () => globalThis.Window = {} [fail]
167-
page/page-event-request.spec.ts › should report requests and responses handled by service worker with routing [fail]
168-
page/page-event-request.spec.ts › should return last requests [fail]
169-
page/page-fill.spec.ts › input event.composed should be true and cross shadow dom boundary - color [fail]
170-
page/page-fill.spec.ts › should fill different input types [fail]
171-
page/page-fill.spec.ts › should retry on disabled element [fail]
172-
page/page-fill.spec.ts › should retry on invisible element [fail]
173-
page/page-fill.spec.ts › should retry on readonly element [fail]
174-
page/page-fill.spec.ts › should throw on unsupported inputs [fail]
175-
page/page-history.spec.ts › page.reload should not resolve with same-document navigation [fail]
176-
page/page-keyboard.spec.ts › insertText should only emit input event [fail]
177-
page/page-keyboard.spec.ts › should have correct Keydown/Keyup order when pressing Escape key [fail]
178-
page/page-keyboard.spec.ts › should report multiple modifiers [fail]
179-
page/page-keyboard.spec.ts › should report shiftKey [fail]
180-
page/page-keyboard.spec.ts › should specify location [fail]
181-
page/page-mouse.spec.ts › should set modifier keys on click [fail]
182-
page/page-network-request.spec.ts › should parse the json post data [fail]
183-
page/page-route.spec.ts › should intercept when postData is more than 1MB [fail]
184-
page/page-route.spec.ts › should pause intercepted fetch request until continue [fail]
148+
149+
# ============================================================================
150+
# screenshot-animations (4 tests)
151+
# screenshot({ animations: 'disabled' }) cannot freeze or advance animations
152+
# over the stock RDP (no Playwright screenshot patches), so animation-sensitive
153+
# screenshot comparisons differ.
154+
# ============================================================================
185155
page/page-screenshot.spec.ts › page screenshot animations › should capture screenshots after layoutchanges in transitionend event › make sure transition is actually running [fail]
186156
page/page-screenshot.spec.ts › page screenshot animations › should fire transitionend for finite transitions › make sure transition is actually running [fail]
187157
page/page-screenshot.spec.ts › page screenshot animations › should not capture css animations in shadow DOM [fail]
188158
page/page-screenshot.spec.ts › page screenshot animations › should not capture pseudo element css animation [fail]
189-
page/page-screenshot.spec.ts › page screenshot animations › should not change animation with playbackRate equal to 0 [fail]
190-
page/page-screenshot.spec.ts › page screenshot animations › should resume infinite animations [fail]
191-
page/page-select-option.spec.ts › should wait for multiple options to be present [fail]
192-
page/page-select-option.spec.ts › should wait for option index to be present [fail]
193-
page/page-wait-for-selector-1.spec.ts › should report logs while waiting for hidden [fail]
194-
page/retarget.spec.ts › input value retargeting › "label" in "<label >Text <input id=target></label>" input value [fail]
195-
page/selectors-css.spec.ts › should work for open shadow roots [fail]
159+
160+
# ============================================================================
161+
# network-request-tracking (1 test)
162+
# Rapidly-issued requests are occasionally dropped from the page's request log
163+
# on the stock RDP, so the full ordered list comes back incomplete.
164+
# ============================================================================
165+
page/page-event-request.spec.ts › should return last requests [fail]
166+
167+
# ============================================================================
168+
# redirect-interception (3 tests)
169+
# Request interception across HTTP redirects is incomplete on the stock WebView
170+
# backend — redirected (and redirected subresource) requests are not surfaced
171+
# to the route handler, and the final navigation response comes back null.
172+
# ============================================================================
173+
page/page-route.spec.ts › should not work with redirects [fail]
174+
page/page-route.spec.ts › should chain fallback w/ dynamic URL [fail]
175+
page/page-route.spec.ts › should work with redirects for subresources [fail]
196176

197177
# ============================================================================
198178
# element-not-attached (11 tests)

0 commit comments

Comments
 (0)