From 1e8051792d9d7b90192cba670854e821cb4e62d5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 19 Jun 2026 11:46:00 -0700 Subject: [PATCH 1/2] feat(webview): implement cookie support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wire up doGetCookies/addCookies/doClearCookies on WVBrowserContext using the stock Web Inspector Page.getCookies/setCookie/deleteCookie commands. These operate on the cookie store visible to the current page, so cookie access is scoped to the current page's domain rather than the whole context — a limitation of the iOS RDP protocol. Also clear cookies in the webview test fixture teardown so the single shared Mobile Safari cookie store doesn't leak state across tests. Fixes the cookie API ("navigation response when URL has cookies") and the cross-test cookie-leak failure ("should not override cookie header"). --- .../src/server/webkit/webview/wvBrowser.ts | 58 ++++++++++++++++++- .../src/server/webkit/webview/wvPage.ts | 18 ++++++ .../expectations/webkit-webview-page.txt | 2 - tests/webview/webviewTest.ts | 4 ++ 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts index 313f3ab0ae8a9..3006ba1a902f2 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts @@ -25,6 +25,7 @@ import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from '@utils/happyEye import { headersArrayToObject } from '@isomorphic/headers'; import { Browser } from '../../browser'; import { helper } from '../../helper'; +import * as network from '../../network'; import { perMessageDeflate } from '../../transport'; import { getUserAgent } from '../../userAgent'; import { BrowserContext } from '../../browserContext'; @@ -35,6 +36,7 @@ import { WVPage } from './wvPage'; import type { BrowserOptions, BrowserProcess } from '../../browser'; import type { SdkObject } from '../../instrumentation'; import type { InitScript, Page } from '../../page'; +import type { Protocol } from './protocol'; import type { ProtocolRequest, ProtocolResponse } from '../../transport'; import type * as types from '../../types'; import type * as channels from '../../channels'; @@ -305,16 +307,66 @@ export class WVBrowserContext extends BrowserContext { throw new Error('Not supported'); } + // Cookies are read/written through the current page's `Page` cookie commands, + // so they only cover the cookie store visible to that page (its domain), not + // the whole browser context. Without an attached page there is nothing to + // operate on. + private _cookiePage(): WVPage | undefined { + const page = this.pages()[0]; + return page ? page.delegate as WVPage : undefined; + } + async doGetCookies(urls: string[]): Promise { - return []; + const page = this._cookiePage(); + if (!page) + return []; + const cookies = await page.getCookies(); + return network.filterCookies(cookies.map(c => { + const copy: channels.NetworkCookie = { + name: c.name, + value: c.value, + domain: c.domain, + path: c.path, + expires: c.session ? -1 : c.expires / 1000, + httpOnly: c.httpOnly, + secure: c.secure, + sameSite: c.sameSite, + }; + return copy; + }), urls); } async addCookies(cookies: channels.SetNetworkCookie[]) { - throw new Error('Method not implemented.'); + const page = this._cookiePage(); + if (!page) + throw new Error('Cannot set cookies without an open page'); + const protocolCookies = network.rewriteCookies(cookies).map(c => { + const session = c.expires === undefined || c.expires === -1; + const cookie: Protocol.Page.Cookie = { + name: c.name, + value: c.value, + domain: c.domain!, + path: c.path!, + expires: session ? 0 : c.expires! * 1000, + session, + httpOnly: !!c.httpOnly, + secure: !!c.secure, + sameSite: c.sameSite ?? 'Lax', + }; + return cookie; + }); + await page.setCookies(protocolCookies); } async doClearCookies() { - throw new Error('Method not implemented.'); + const page = this._cookiePage(); + if (!page) + return; + const cookies = await page.getCookies(); + await page.deleteCookies(cookies.map(c => ({ + cookieName: c.name, + url: `${c.secure ? 'https' : 'http'}://${c.domain.replace(/^\./, '')}${c.path}`, + }))); } async doGrantPermissions(origin: string, permissions: string[]) { diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index 4d249e705ae4f..cf1d4a31ff77c 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -586,6 +586,24 @@ export class WVPage implements PageDelegate { await this._session.send('Page.reload'); } + // Cookie support is limited to the stock Web Inspector `Page` cookie commands, + // which operate on the cookie store visible to the inspected page — i.e. the + // current page's domain — rather than the whole context. + async getCookies(): Promise { + const { cookies } = await this._session.send('Page.getCookies'); + return cookies; + } + + async setCookies(cookies: Protocol.Page.Cookie[]): Promise { + for (const cookie of cookies) + await this._session.send('Page.setCookie', { cookie }); + } + + async deleteCookies(cookies: { cookieName: string, url: string }[]): Promise { + for (const { cookieName, url } of cookies) + await this._session.send('Page.deleteCookie', { cookieName, url }); + } + async addInitScript(initScript: InitScript): Promise { await this._updateBootstrapScript(); } diff --git a/tests/webview/expectations/webkit-webview-page.txt b/tests/webview/expectations/webkit-webview-page.txt index bd3c95dcdf560..0db259d0d6ef5 100644 --- a/tests/webview/expectations/webkit-webview-page.txt +++ b/tests/webview/expectations/webkit-webview-page.txt @@ -336,7 +336,6 @@ page/page-request-fulfill.spec.ts › should fulfill with multiple set-cookie [f page/page-request-intercept.spec.ts › should support timeout option in route.fetch [fail] page/page-route.spec.ts › should fail navigation when aborting main resource [fail] page/page-route.spec.ts › should not auto-intercept non-preflight OPTIONS with network interception [fail] -page/page-route.spec.ts › should not override cookie header [fail] page/page-route.spec.ts › should send referer [fail] page/page-screenshot.spec.ts › page screenshot animations › should capture screenshots after layoutchanges in transitionend event › make sure transition is actually running [fail] page/page-screenshot.spec.ts › page screenshot animations › should fire transitionend for finite transitions › make sure transition is actually running [fail] @@ -492,7 +491,6 @@ page/page-request-gc.spec.ts › should work [fail] page/page-request-intercept.spec.ts › should fulfill intercepted response using alias [fail] page/page-request-intercept.spec.ts › should intercept with url override [fail] page/page-request-intercept.spec.ts › should not follow redirects when maxRedirects is set to 0 in route.fetch [fail] -page/page-route.spec.ts › should properly return navigation response when URL has cookies [fail] page/page-set-input-files.spec.ts › should detect mime type [fail] page/page-set-input-files.spec.ts › should emit input and change events [fail] page/page-set-input-files.spec.ts › should upload a folder [fail] diff --git a/tests/webview/webviewTest.ts b/tests/webview/webviewTest.ts index 2fc530c00e670..5649edc5a520e 100644 --- a/tests/webview/webviewTest.ts +++ b/tests/webview/webviewTest.ts @@ -148,6 +148,10 @@ export const webviewTest = baseTest.extend {}); await run(page); + // The single Mobile Safari instance keeps one cookie store across tests, so + // wipe cookies for the page's current domain before disconnecting — webview + // cookies are scoped to the current page's domain (see WVBrowserContext). + await page.context().clearCookies().catch(() => {}); await browser.close(); }, From b72057ac0295ee52a118496ec5927576acfdced5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 19 Jun 2026 15:46:58 -0700 Subject: [PATCH 2/2] chore(webview): trim cookie comments Keep only the non-obvious notes: the Page cookie commands are domain-scoped, and the test fixture clears cookies while still on the test's domain. --- .../playwright-core/src/server/webkit/webview/wvBrowser.ts | 6 ++---- .../playwright-core/src/server/webkit/webview/wvPage.ts | 3 --- tests/webview/webviewTest.ts | 5 ++--- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts index 3006ba1a902f2..9309873ef9d22 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvBrowser.ts @@ -307,10 +307,8 @@ export class WVBrowserContext extends BrowserContext { throw new Error('Not supported'); } - // Cookies are read/written through the current page's `Page` cookie commands, - // so they only cover the cookie store visible to that page (its domain), not - // the whole browser context. Without an attached page there is nothing to - // operate on. + // The Page cookie commands only see cookies for the current page's domain, so + // cookie access is scoped to that page rather than the whole context. private _cookiePage(): WVPage | undefined { const page = this.pages()[0]; return page ? page.delegate as WVPage : undefined; diff --git a/packages/playwright-core/src/server/webkit/webview/wvPage.ts b/packages/playwright-core/src/server/webkit/webview/wvPage.ts index cf1d4a31ff77c..812f867aa4ace 100644 --- a/packages/playwright-core/src/server/webkit/webview/wvPage.ts +++ b/packages/playwright-core/src/server/webkit/webview/wvPage.ts @@ -586,9 +586,6 @@ export class WVPage implements PageDelegate { await this._session.send('Page.reload'); } - // Cookie support is limited to the stock Web Inspector `Page` cookie commands, - // which operate on the cookie store visible to the inspected page — i.e. the - // current page's domain — rather than the whole context. async getCookies(): Promise { const { cookies } = await this._session.send('Page.getCookies'); return cookies; diff --git a/tests/webview/webviewTest.ts b/tests/webview/webviewTest.ts index 5649edc5a520e..b5e0e1c21d939 100644 --- a/tests/webview/webviewTest.ts +++ b/tests/webview/webviewTest.ts @@ -148,9 +148,8 @@ export const webviewTest = baseTest.extend {}); await run(page); - // The single Mobile Safari instance keeps one cookie store across tests, so - // wipe cookies for the page's current domain before disconnecting — webview - // cookies are scoped to the current page's domain (see WVBrowserContext). + // The shared Mobile Safari cookie store persists across tests; clear it + // while still on the test's domain (webview cookies are domain-scoped). await page.context().clearCookies().catch(() => {}); await browser.close(); },