From 4faf6c5154850aab2dc5f566a8e24268189ce92d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 30 May 2025 16:23:40 -0700 Subject: [PATCH 01/11] feat(cookie): export/import chips cookies Export/import partitionKey in Chromium. It is used to partition cookies. Not exposed in the public API yet. Reference: https://github.com/microsoft/playwright/issues/35598 --- .../playwright-core/src/protocol/validator.ts | 8 + packages/protocol/src/channels.d.ts | 8 + packages/protocol/src/protocol.yml | 10 + ...browsercontext-cookies-third-party.spec.ts | 211 +++++++++++++++--- 4 files changed, 209 insertions(+), 28 deletions(-) diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f3de08b065e4f..24beb701a3a68 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -136,6 +136,10 @@ scheme.SetNetworkCookie = tObject({ httpOnly: tOptional(tBoolean), secure: tOptional(tBoolean), sameSite: tOptional(tEnum(['Strict', 'Lax', 'None'])), + partitionKey: tOptional(tObject({ + topLevelSite: tString, + hasCrossSiteAncestor: tBoolean, + })), }); scheme.NetworkCookie = tObject({ name: tString, @@ -146,6 +150,10 @@ scheme.NetworkCookie = tObject({ httpOnly: tBoolean, secure: tBoolean, sameSite: tEnum(['Strict', 'Lax', 'None']), + partitionKey: tOptional(tObject({ + topLevelSite: tString, + hasCrossSiteAncestor: tBoolean, + })), }); scheme.NameValue = tObject({ name: tString, diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 7831b4e26eea5..8c2ceb43d80c8 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -260,6 +260,10 @@ export type SetNetworkCookie = { httpOnly?: boolean, secure?: boolean, sameSite?: 'Strict' | 'Lax' | 'None', + partitionKey?: { + topLevelSite: string, + hasCrossSiteAncestor: boolean, + }, }; export type NetworkCookie = { @@ -271,6 +275,10 @@ export type NetworkCookie = { httpOnly: boolean, secure: boolean, sameSite: 'Strict' | 'Lax' | 'None', + partitionKey?: { + topLevelSite: string, + hasCrossSiteAncestor: boolean, + }, }; export type NameValue = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 805c0396d6334..fe0e85d4aa12d 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -223,6 +223,11 @@ SetNetworkCookie: - Strict - Lax - None + partitionKey: + type: object? + properties: + topLevelSite: string + hasCrossSiteAncestor: boolean NetworkCookie: @@ -241,6 +246,11 @@ NetworkCookie: - Strict - Lax - None + partitionKey: + type: object? + properties: + topLevelSite: string + hasCrossSiteAncestor: boolean NameValue: diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 0fbce763edf13..39d62c9ef89c8 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -23,64 +23,129 @@ test.use({ ignoreHTTPSErrors: true, }); -test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac }) => { - httpsServer.setRoute('/empty.html', (req, res) => { - res.setHeader('Set-Cookie', `name=value; SameSite=None; Path=/; Secure;`); +function addCommonCookieHandlers(httpsServer: TestServer) { + // '/set-cookie.html' handlers are added in the tests. + httpsServer.setRoute('/read-cookie.html', (req, res) => { res.setHeader('Content-Type', 'text/html'); const cookies = req.headers.cookie?.split(';').map(c => c.trim()).sort().join('; '); res.end(`Received cookie: ${cookies}`); }); - httpsServer.setRoute('/with-frame.html', (req, res) => { + httpsServer.setRoute('/frame-set-cookie.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); + }); + httpsServer.setRoute('/frame-read-cookie.html', (req, res) => { res.setHeader('Content-Type', 'text/html'); - res.end(``); + res.end(``); }); +} - await page.goto(httpsServer.EMPTY_PAGE); - await page.goto(httpsServer.EMPTY_PAGE); - expect(await page.locator('body').textContent()).toBe('Received cookie: name=value'); +async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) { + addCommonCookieHandlers(httpsServer); + httpsServer.setRoute('/set-cookie.html', (req, res) => { + res.setHeader('Set-Cookie', `name=value${req.headers.referer ? '-third-party' : '-top-level'}; SameSite=None; Path=/; Secure;`); + res.setHeader('Content-Type', 'text/html'); + res.end(); + }); - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/with-frame.html'); + await page.goto(httpsServer.PREFIX + '/set-cookie.html'); + await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + expect(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level'); + + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('body'); // WebKit does not support third-party cookies without a 'Partition' attribute. if (browserName === 'webkit' && isMac) await expect(frameBody).toHaveText('Received cookie: undefined'); else - await expect(frameBody).toHaveText('Received cookie: name=value'); + await expect(frameBody).toHaveText('Received cookie: name=value-top-level'); + + // Set cookie and do second navigation. + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-set-cookie.html'); + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + const expectedThirdParty = browserName === 'webkit' ? + 'Received cookie: undefined' : + 'Received cookie: name=value-third-party'; + await expect(frameBody).toHaveText(expectedThirdParty); + + // Check again the top-level cookie. + await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + const expectedTopLevel = browserName === 'webkit' && isMac ? + 'Received cookie: name=value-top-level' : + 'Received cookie: name=value-third-party'; + expect(await page.locator('body').textContent()).toBe(expectedTopLevel); + + return { + expectedTopLevel, + expectedThirdParty, + }; +} + +test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { + await runNonPartitionedTest(page, httpsServer, browserName, isMac); }); -test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac }) => { - httpsServer.setRoute('/empty.html', (req, res) => { +test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { + // Run the test to populate the cookies. + const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac); + + async function checkCookies(page: Page) { + // Check top-level cookie first. + await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + expect.soft(await page.locator('body').textContent()).toBe(expectedTopLevel); + + // Check third-party cookie. + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + const frameBody = page.locator('iframe').contentFrame().locator('body'); + await expect.soft(frameBody).toHaveText(expectedThirdParty); + } + + await checkCookies(page); + + await test.step('export via cookies/addCookies', async () => { + const cookies = await page.context().cookies(); + const context2 = await browser.newContext(); + await context2.addCookies(cookies); + const page2 = await context2.newPage(); + await checkCookies(page2); + }); + + await test.step('export via storageState', async () => { + const storageState = await page.context().storageState(); + const context3 = await browser.newContext({ storageState }); + const page3 = await context3.newPage(); + await checkCookies(page3); + }); +}); + +async function runPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) { + addCommonCookieHandlers(httpsServer); + httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', [ - `name=value; SameSite=None; Path=/; Secure; Partitioned;`, + `name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`, `nonPartitionedName=value; SameSite=None; Path=/; Secure;` ]); - res.setHeader('Content-Type', 'text/html'); - const cookies = req.headers.cookie?.split(';').map(c => c.trim()).sort().join('; '); - res.end(`Received cookie: ${cookies}`); - }); - httpsServer.setRoute('/with-frame.html', (req, res) => { - res.setHeader('Content-Type', 'text/html'); - res.end(``); + res.end(); }); - await page.goto(httpsServer.EMPTY_PAGE); - await page.goto(httpsServer.EMPTY_PAGE); - expect(await page.locator('body').textContent()).toBe('Received cookie: name=value; nonPartitionedName=value'); + await page.goto(httpsServer.PREFIX + '/set-cookie.html'); + await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + expect(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value'); - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/with-frame.html'); + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('body'); // Firefox cookie partitioning is disabled in Firefox. // TODO: reenable cookie partitioning? if (browserName === 'firefox') { - await expect(frameBody).toHaveText('Received cookie: name=value; nonPartitionedName=value'); + await expect(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value'); return; } // Linux and Windows WebKit builds do not partition third-party cookies at all. if (browserName === 'webkit' && !isMac) { - await expect(frameBody).toHaveText('Received cookie: name=value; nonPartitionedName=value'); + await expect(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value'); return; } @@ -98,11 +163,101 @@ test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServ // - sets the third-party cookie for the top-level context // Second navigation: // - sends the cookie as it was just set for the (top-level site, iframe url) partition. - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/with-frame.html'); + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-set-cookie.html'); + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); if (browserName === 'webkit') await expect(frameBody).toHaveText('Received cookie: undefined'); else - await expect(frameBody).toHaveText('Received cookie: name=value; nonPartitionedName=value'); + await expect(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); +} + +test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { + await runPartitionedTest(page, httpsServer, browserName, isMac); +}); + +test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { + test.fixme(browserName === 'firefox', 'Firefox cookie partitioning is disabled in Firefox.'); + test.fixme(browserName === 'webkit' && !isMac, 'Linux and Windows WebKit builds do not partition third-party cookies at all.'); + + await runPartitionedTest(page, httpsServer, browserName, isMac); + + async function checkCookies(page: Page) { + // Check top-level cookie first. + await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + expect.soft(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value'); + + // Check third-party cookie. + await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + const frameBody = page.locator('iframe').contentFrame().locator('body'); + if (browserName === 'webkit') + await expect.soft(frameBody).toHaveText('Received cookie: undefined'); + else + await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); + } + + await checkCookies(page); + + await test.step('export via cookies/addCookies', async () => { + const cookies = await page.context().cookies(); + const context2 = await browser.newContext(); + await context2.addCookies(cookies); + const page2 = await context2.newPage(); + await checkCookies(page2); + }); + + await test.step('export via storageState', async () => { + const storageState = await page.context().storageState(); + const context3 = await browser.newContext({ storageState }); + const page3 = await context3.newPage(); + await checkCookies(page3); + }); +}); + +test(`same origin third party 'Partitioned;' cookie with different origin intermediate iframe`, async ({ page, browserName, httpsServer, isMac, browser }) => { + addCommonCookieHandlers(httpsServer); + httpsServer.setRoute('/set-cookie.html', (req, res) => { + res.setHeader('Set-Cookie', [ + `name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`, + `nonPartitionedName=value; SameSite=None; Path=/; Secure;` + ]); + res.end(); + }); + // main frame: origin1 -> iframe1: origin2 -> iframe2: origin1 + // In this case the cookie in iframe2 will have hasCrossSiteAncestor=true even though + // the innermost frame is in origin1. + httpsServer.setRoute('/top-frame-set-cookie.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); + }); + httpsServer.setRoute('/top-frame-read-cookie.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); + }); + + await page.goto(httpsServer.PREFIX + '/top-frame-set-cookie.html'); + + async function checkCookies(page: Page) { + await page.goto(httpsServer.PREFIX + '/top-frame-read-cookie.html'); + const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); + await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); + } + + await checkCookies(page); + + await test.step('export via cookies/addCookies', async () => { + const cookies = await page.context().cookies(); + const context2 = await browser.newContext(); + await context2.addCookies(cookies); + const page2 = await context2.newPage(); + await checkCookies(page2); + }); + + await test.step('export via storageState', async () => { + const storageState = await page.context().storageState(); + const context3 = await browser.newContext({ storageState }); + const page3 = await context3.newPage(); + await checkCookies(page3); + }); }); test('should be able to send third party cookies via an iframe', async ({ browser, httpsServer, browserName, isMac }) => { From 2ddc0cf717dc14bc48a7efc9667f58fe2cd8f3f5 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Jun 2025 17:16:44 -0700 Subject: [PATCH 02/11] more tests --- ...browsercontext-cookies-third-party.spec.ts | 81 ++++++++++++++++--- 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 39d62c9ef89c8..3ba7e87761010 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -38,6 +38,16 @@ function addCommonCookieHandlers(httpsServer: TestServer) { res.setHeader('Content-Type', 'text/html'); res.end(``); }); + // Nested cross-origin iframe: + // main frame: (origin1 or origin2) -> iframe1: origin2 -> iframe2: origin1 + httpsServer.setRoute('/nested-frame-set-cookie.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); + }); + httpsServer.setRoute('/nested-frame-read-cookie.html', (req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(``); + }); } async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) { @@ -223,23 +233,70 @@ test(`same origin third party 'Partitioned;' cookie with different origin interm res.end(); }); // main frame: origin1 -> iframe1: origin2 -> iframe2: origin1 - // In this case the cookie in iframe2 will have hasCrossSiteAncestor=true even though - // the innermost frame is in origin1. - httpsServer.setRoute('/top-frame-set-cookie.html', (req, res) => { - res.setHeader('Content-Type', 'text/html'); - res.end(``); + // In this case the cookie in iframe2 is a third-party partitioned cookie, even though + // it's the same origin as the main frame. + await page.goto(httpsServer.PREFIX + '/nested-frame-set-cookie.html'); + + async function checkCookies(page: Page) { + await page.goto(httpsServer.PREFIX + '/nested-frame-read-cookie.html'); + const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); + await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); + } + + await checkCookies(page); + + await test.step('export via cookies/addCookies', async () => { + const cookies = await page.context().cookies(); + const context2 = await browser.newContext(); + await context2.addCookies(cookies); + const page2 = await context2.newPage(); + await checkCookies(page2); }); - httpsServer.setRoute('/top-frame-read-cookie.html', (req, res) => { - res.setHeader('Content-Type', 'text/html'); - res.end(``); + + await test.step('export via storageState', async () => { + const storageState = await page.context().storageState(); + const context3 = await browser.newContext({ storageState }); + const page3 = await context3.newPage(); + await checkCookies(page3); }); +}); - await page.goto(httpsServer.PREFIX + '/top-frame-set-cookie.html'); +test(`top level 'Partitioned;' cookie and same origin iframe`, async ({ page, browserName, httpsServer, isMac, browser }) => { + addCommonCookieHandlers(httpsServer); + httpsServer.setRoute('/set-cookie.html', (req, res) => { + res.setHeader('Set-Cookie', [ + `name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`, + `nonPartitionedName=value; SameSite=None; Path=/; Secure;` + ]); + res.end(); + }); + + // Same origin iframe cookies are partitioned the same way as top-level cookies. + await page.goto(httpsServer.PREFIX + '/set-cookie.html'); + await page.context().storageState({ path: '/tmp/state2.json' }); async function checkCookies(page: Page) { - await page.goto(httpsServer.PREFIX + '/top-frame-read-cookie.html'); - const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); - await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); + { + // Check top-level cookie first. + await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + expect.soft(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value'); + } + { + // Same origin iframe. + await page.goto(httpsServer.PREFIX + '/frame-read-cookie.html'); + const frameBody = page.locator('iframe').contentFrame().locator('body'); + await expect.soft(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value', { timeout: 1000 }); + } + { + // Check third-party cookie. + // main frame: origin1 -> iframe1: origin2 -> iframe2: origin1 + await page.goto(httpsServer.PREFIX + '/nested-frame-read-cookie.html'); + const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); + const expectedThirdParty = browserName === 'chromium' + ? 'Received cookie: nonPartitionedName=value' + : 'Received cookie: name=value-top-level; nonPartitionedName=value' + await expect.soft(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); + } } await checkCookies(page); From 56b4133c0a276eecd6f58e0cb993f23a75eb49fd Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 4 Jun 2025 13:59:42 -0700 Subject: [PATCH 03/11] fix --- docs/src/api/class-browsercontext.md | 5 ++++- packages/playwright-client/types/types.d.ts | 13 ++++++++++++- packages/playwright-core/types/types.d.ts | 13 ++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index afd73a414221f..325ff27bc09dd 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -363,6 +363,7 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 }); - `httpOnly` ?<[boolean]> Optional. - `secure` ?<[boolean]> Optional. - `sameSite` ?<[SameSiteAttribute]<"Strict"|"Lax"|"None">> Optional. + - `topLevelSite` ?<[string]> For partitioned third-party cookies (aka [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the top level site (scheme and host) used as the partition key. Optional. ## async method: BrowserContext.addInitScript * since: v1.8 @@ -602,9 +603,10 @@ The default browser context cannot be closed. - `httpOnly` <[boolean]> - `secure` <[boolean]> - `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> + - `topLevelSite` ?<[string]> If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs -are returned. +are returned. Note that cookies are matched based on the URLs regardless of their `topLevelSite` value. ### param: BrowserContext.cookies.urls * since: v1.8 @@ -1504,6 +1506,7 @@ Whether to emulate network being offline for the browser context. - `httpOnly` <[boolean]> - `secure` <[boolean]> - `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> + - `topLevelSite` ?<[string]> - `origins` <[Array]<[Object]>> - `origin` <[string]> - `localStorage` <[Array]<[Object]>> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index efe7626b2391d..20bb3cc56dc58 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -8830,6 +8830,13 @@ export interface BrowserContext { * Optional. */ sameSite?: "Strict"|"Lax"|"None"; + + /** + * For partitioned third-party cookies (aka + * [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the top + * level site (scheme and host) used as the partition key. Optional. + */ + topLevelSite?: string; }>): Promise; /** @@ -8908,7 +8915,7 @@ export interface BrowserContext { /** * If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those - * URLs are returned. + * URLs are returned. Note that cookies are matched based on the URLs regardless of their `topLevelSite` value. * @param urls Optional list of URLs. */ cookies(urls?: string|ReadonlyArray): Promise>; @@ -9304,6 +9311,8 @@ export interface BrowserContext { secure: boolean; sameSite: "Strict"|"Lax"|"None"; + + topLevelSite?: string; }>; origins: Array<{ @@ -22501,6 +22510,8 @@ export interface Cookie { secure: boolean; sameSite: "Strict"|"Lax"|"None"; + + topLevelSite?: string; } interface PageWaitForSelectorOptions { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index efe7626b2391d..20bb3cc56dc58 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8830,6 +8830,13 @@ export interface BrowserContext { * Optional. */ sameSite?: "Strict"|"Lax"|"None"; + + /** + * For partitioned third-party cookies (aka + * [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the top + * level site (scheme and host) used as the partition key. Optional. + */ + topLevelSite?: string; }>): Promise; /** @@ -8908,7 +8915,7 @@ export interface BrowserContext { /** * If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those - * URLs are returned. + * URLs are returned. Note that cookies are matched based on the URLs regardless of their `topLevelSite` value. * @param urls Optional list of URLs. */ cookies(urls?: string|ReadonlyArray): Promise>; @@ -9304,6 +9311,8 @@ export interface BrowserContext { secure: boolean; sameSite: "Strict"|"Lax"|"None"; + + topLevelSite?: string; }>; origins: Array<{ @@ -22501,6 +22510,8 @@ export interface Cookie { secure: boolean; sameSite: "Strict"|"Lax"|"None"; + + topLevelSite?: string; } interface PageWaitForSelectorOptions { From d65a9cc100319758ce1110c46a1833927fa43116 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 4 Jun 2025 14:44:14 -0700 Subject: [PATCH 04/11] update names --- ...browsercontext-cookies-third-party.spec.ts | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 3ba7e87761010..149b5841a3ddf 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -53,14 +53,14 @@ function addCommonCookieHandlers(httpsServer: TestServer) { async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) { addCommonCookieHandlers(httpsServer); httpsServer.setRoute('/set-cookie.html', (req, res) => { - res.setHeader('Set-Cookie', `name=value${req.headers.referer ? '-third-party' : '-top-level'}; SameSite=None; Path=/; Secure;`); + res.setHeader('Set-Cookie', `${req.headers.referer ? 'frame' : 'top-level'}=value; SameSite=None; Path=/; Secure;`); res.setHeader('Content-Type', 'text/html'); res.end(); }); await page.goto(httpsServer.PREFIX + '/set-cookie.html'); await page.goto(httpsServer.PREFIX + '/read-cookie.html'); - expect(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level'); + expect(await page.locator('body').textContent()).toBe('Received cookie: top-level=value'); await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('body'); @@ -69,21 +69,21 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse if (browserName === 'webkit' && isMac) await expect(frameBody).toHaveText('Received cookie: undefined'); else - await expect(frameBody).toHaveText('Received cookie: name=value-top-level'); + await expect(frameBody).toHaveText('Received cookie: top-level=value'); // Set cookie and do second navigation. await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-set-cookie.html'); await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); const expectedThirdParty = browserName === 'webkit' ? 'Received cookie: undefined' : - 'Received cookie: name=value-third-party'; + 'Received cookie: frame=value; top-level=value'; await expect(frameBody).toHaveText(expectedThirdParty); // Check again the top-level cookie. await page.goto(httpsServer.PREFIX + '/read-cookie.html'); const expectedTopLevel = browserName === 'webkit' && isMac ? - 'Received cookie: name=value-top-level' : - 'Received cookie: name=value-third-party'; + 'Received cookie: top-level=value' : + 'Received cookie: frame=value; top-level=value'; expect(await page.locator('body').textContent()).toBe(expectedTopLevel); return { @@ -133,15 +133,15 @@ async function runPartitionedTest(page: Page, httpsServer: TestServer, browserNa addCommonCookieHandlers(httpsServer); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', [ - `name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`, - `nonPartitionedName=value; SameSite=None; Path=/; Secure;` + `${req.headers.referer ? 'frame' : 'top-level'}-partitioned=value; SameSite=None; Path=/; Secure; Partitioned;`, + `${req.headers.referer ? 'frame' : 'top-level'}-non-partitioned=value; SameSite=None; Path=/; Secure;` ]); res.end(); }); await page.goto(httpsServer.PREFIX + '/set-cookie.html'); await page.goto(httpsServer.PREFIX + '/read-cookie.html'); - expect(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value'); + expect(await page.locator('body').textContent()).toBe('Received cookie: top-level-non-partitioned=value; top-level-partitioned=value'); await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('body'); @@ -149,13 +149,13 @@ async function runPartitionedTest(page: Page, httpsServer: TestServer, browserNa // Firefox cookie partitioning is disabled in Firefox. // TODO: reenable cookie partitioning? if (browserName === 'firefox') { - await expect(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value'); + await expect(frameBody).toHaveText('Received cookie: top-level-non-partitioned=value; top-level-partitioned=value'); return; } // Linux and Windows WebKit builds do not partition third-party cookies at all. if (browserName === 'webkit' && !isMac) { - await expect(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value'); + await expect(frameBody).toHaveText('Received cookie: top-level-non-partitioned=value; top-level-partitioned=value'); return; } @@ -165,7 +165,7 @@ async function runPartitionedTest(page: Page, httpsServer: TestServer, browserNa } else { // For non-partitioned cookies, the cookie is sent to the iframe right away, // if third-party cookies are supported by the browser. - await expect(frameBody).toHaveText('Received cookie: nonPartitionedName=value'); + await expect(frameBody).toHaveText('Received cookie: top-level-non-partitioned=value'); } // First navigation: @@ -178,7 +178,7 @@ async function runPartitionedTest(page: Page, httpsServer: TestServer, browserNa if (browserName === 'webkit') await expect(frameBody).toHaveText('Received cookie: undefined'); else - await expect(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); + await expect(frameBody).toHaveText('Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value'); } test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { @@ -194,15 +194,18 @@ test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, async function checkCookies(page: Page) { // Check top-level cookie first. await page.goto(httpsServer.PREFIX + '/read-cookie.html'); - expect.soft(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value'); + const expectedTopLevel = browserName === 'webkit' && isMac ? + 'Received cookie: top-level-non-partitioned=value; top-level-partitioned=value' : + 'Received cookie: frame-non-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value'; + expect.soft(await page.locator('body').textContent()).toBe(expectedTopLevel); // Check third-party cookie. await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('body'); - if (browserName === 'webkit') - await expect.soft(frameBody).toHaveText('Received cookie: undefined'); - else - await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); + const expectedThirdParty = browserName === 'webkit' ? + 'Received cookie: undefined' : + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value'; + await expect.soft(frameBody).toHaveText(expectedThirdParty); } await checkCookies(page); @@ -227,8 +230,8 @@ test(`same origin third party 'Partitioned;' cookie with different origin interm addCommonCookieHandlers(httpsServer); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', [ - `name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`, - `nonPartitionedName=value; SameSite=None; Path=/; Secure;` + `${req.headers.referer ? 'frame' : 'top-level'}-partitioned=value; SameSite=None; Path=/; Secure; Partitioned;`, + `${req.headers.referer ? 'frame' : 'top-level'}-non-partitioned=value; SameSite=None; Path=/; Secure;` ]); res.end(); }); @@ -240,7 +243,7 @@ test(`same origin third party 'Partitioned;' cookie with different origin interm async function checkCookies(page: Page) { await page.goto(httpsServer.PREFIX + '/nested-frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); - await expect.soft(frameBody).toHaveText('Received cookie: name=value-partitioned; nonPartitionedName=value'); + await expect.soft(frameBody).toHaveText('Received cookie: frame-non-partitioned=value; frame-partitioned=value'); } await checkCookies(page); @@ -265,8 +268,8 @@ test(`top level 'Partitioned;' cookie and same origin iframe`, async ({ page, br addCommonCookieHandlers(httpsServer); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', [ - `name=value${req.headers.referer ? '-partitioned' : '-top-level'}; SameSite=None; Path=/; Secure; Partitioned;`, - `nonPartitionedName=value; SameSite=None; Path=/; Secure;` + `${req.headers.referer ? 'frame' : 'top-level'}=value; SameSite=None; Path=/; Secure; Partitioned;`, + `${req.headers.referer ? 'frame' : 'top-level'}-non-partitioned=value; SameSite=None; Path=/; Secure;` ]); res.end(); }); @@ -279,13 +282,13 @@ test(`top level 'Partitioned;' cookie and same origin iframe`, async ({ page, br { // Check top-level cookie first. await page.goto(httpsServer.PREFIX + '/read-cookie.html'); - expect.soft(await page.locator('body').textContent()).toBe('Received cookie: name=value-top-level; nonPartitionedName=value'); + expect.soft(await page.locator('body').textContent()).toBe('Received cookie: top-level-non-partitioned=value; top-level=value'); } { // Same origin iframe. await page.goto(httpsServer.PREFIX + '/frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('body'); - await expect.soft(frameBody).toHaveText('Received cookie: name=value-top-level; nonPartitionedName=value', { timeout: 1000 }); + await expect.soft(frameBody).toHaveText('Received cookie: top-level-non-partitioned=value; top-level=value', { timeout: 1000 }); } { // Check third-party cookie. @@ -293,8 +296,8 @@ test(`top level 'Partitioned;' cookie and same origin iframe`, async ({ page, br await page.goto(httpsServer.PREFIX + '/nested-frame-read-cookie.html'); const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); const expectedThirdParty = browserName === 'chromium' - ? 'Received cookie: nonPartitionedName=value' - : 'Received cookie: name=value-top-level; nonPartitionedName=value' + ? 'Received cookie: top-level-non-partitioned=value' + : 'Received cookie: top-level-non-partitioned=value; top-level=value'; await expect.soft(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); } } From dc7e15cd896886283b6a70623e83ce5c7fe3bf68 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 4 Jun 2025 16:28:20 -0700 Subject: [PATCH 05/11] origin-aliases --- ...browsercontext-cookies-third-party.spec.ts | 179 ++++++++++++------ 1 file changed, 124 insertions(+), 55 deletions(-) diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 149b5841a3ddf..427e19655a8c1 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -14,16 +14,59 @@ * limitations under the License. */ -import { contextTest as test, expect } from '../config/browserTest'; +import { contextTest, expect } from '../config/browserTest'; import type { Page, BrowserContext } from 'playwright'; import type { TestServer } from '../config/testserver'; +type TestUrls = { + origin1: string; + origin2: string; + read_origin1: string; + read_origin2_origin1: string; + read_origin1_origin1: string; + read_origin1_origin2_origin1: string; + set_origin1: string; + set_origin2_origin1: string; + set_origin1_origin2_origin1: string; +}; + +const test = contextTest.extend<{ urls: TestUrls }>({ + urls: async ({ httpsServer }, run) => { + const origin1 = httpsServer.PREFIX; + const origin2 = httpsServer.CROSS_PROCESS_PREFIX; + await run({ + origin1, + origin2, + read_origin1: origin1 + '/read-cookie.html', + read_origin2_origin1: origin2 + '/frame-read-cookie.html', + read_origin1_origin1: origin1 + '/frame-read-cookie.html', + read_origin1_origin2_origin1: origin1 + '/nested-frame-read-cookie.html', + set_origin1: origin1 + '/set-cookie.html', + set_origin2_origin1: origin2 + '/frame-set-cookie.html', + set_origin1_origin2_origin1: origin1 + '/nested-frame-set-cookie.html', + }); + }, +}); + test.use({ ignoreHTTPSErrors: true, }); -function addCommonCookieHandlers(httpsServer: TestServer) { +/** + * origin1: + * top-level-partitioned=value + * top-level-non-partitioned=value + * + * origin2: + * origin1: + * frame-partitioned=value + * frame-non-partitioned=value + * + * origin1 = httpsServer.PREFIX + * origin2 = httpsServer.CROSS_PROCESS_PREFIX + */ +function addCommonCookieHandlers(httpsServer: TestServer, urls: TestUrls) { // '/set-cookie.html' handlers are added in the tests. httpsServer.setRoute('/read-cookie.html', (req, res) => { res.setHeader('Content-Type', 'text/html'); @@ -32,37 +75,37 @@ function addCommonCookieHandlers(httpsServer: TestServer) { }); httpsServer.setRoute('/frame-set-cookie.html', (req, res) => { res.setHeader('Content-Type', 'text/html'); - res.end(``); + res.end(``); }); httpsServer.setRoute('/frame-read-cookie.html', (req, res) => { res.setHeader('Content-Type', 'text/html'); - res.end(``); + res.end(``); }); // Nested cross-origin iframe: // main frame: (origin1 or origin2) -> iframe1: origin2 -> iframe2: origin1 httpsServer.setRoute('/nested-frame-set-cookie.html', (req, res) => { res.setHeader('Content-Type', 'text/html'); - res.end(``); + res.end(``); }); httpsServer.setRoute('/nested-frame-read-cookie.html', (req, res) => { res.setHeader('Content-Type', 'text/html'); - res.end(``); + res.end(``); }); } -async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) { - addCommonCookieHandlers(httpsServer); +async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, urls: TestUrls) { + addCommonCookieHandlers(httpsServer, urls); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', `${req.headers.referer ? 'frame' : 'top-level'}=value; SameSite=None; Path=/; Secure;`); res.setHeader('Content-Type', 'text/html'); res.end(); }); - await page.goto(httpsServer.PREFIX + '/set-cookie.html'); - await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + await page.goto(urls.set_origin1); + await page.goto(urls.read_origin1); expect(await page.locator('body').textContent()).toBe('Received cookie: top-level=value'); - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + await page.goto(urls.read_origin2_origin1); const frameBody = page.locator('iframe').contentFrame().locator('body'); // WebKit does not support third-party cookies without a 'Partition' attribute. @@ -72,15 +115,15 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse await expect(frameBody).toHaveText('Received cookie: top-level=value'); // Set cookie and do second navigation. - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-set-cookie.html'); - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + await page.goto(urls.set_origin2_origin1); + await page.goto(urls.read_origin2_origin1); const expectedThirdParty = browserName === 'webkit' ? 'Received cookie: undefined' : 'Received cookie: frame=value; top-level=value'; await expect(frameBody).toHaveText(expectedThirdParty); // Check again the top-level cookie. - await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + await page.goto(urls.read_origin1); const expectedTopLevel = browserName === 'webkit' && isMac ? 'Received cookie: top-level=value' : 'Received cookie: frame=value; top-level=value'; @@ -92,21 +135,21 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse }; } -test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { - await runNonPartitionedTest(page, httpsServer, browserName, isMac); +test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, urls }) => { + await runNonPartitionedTest(page, httpsServer, browserName, isMac, urls); }); -test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { +test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser, urls }) => { // Run the test to populate the cookies. - const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac); + const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac, urls); async function checkCookies(page: Page) { // Check top-level cookie first. - await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + await page.goto(urls.read_origin1); expect.soft(await page.locator('body').textContent()).toBe(expectedTopLevel); // Check third-party cookie. - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + await page.goto(urls.read_origin2_origin1); const frameBody = page.locator('iframe').contentFrame().locator('body'); await expect.soft(frameBody).toHaveText(expectedThirdParty); } @@ -129,8 +172,8 @@ test(`save/load third party non-partitioned cookies`, async ({ page, browserName }); }); -async function runPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean) { - addCommonCookieHandlers(httpsServer); +async function runPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, urls: TestUrls) { + addCommonCookieHandlers(httpsServer, urls); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', [ `${req.headers.referer ? 'frame' : 'top-level'}-partitioned=value; SameSite=None; Path=/; Secure; Partitioned;`, @@ -139,11 +182,11 @@ async function runPartitionedTest(page: Page, httpsServer: TestServer, browserNa res.end(); }); - await page.goto(httpsServer.PREFIX + '/set-cookie.html'); - await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + await page.goto(urls.set_origin1); + await page.goto(urls.read_origin1); expect(await page.locator('body').textContent()).toBe('Received cookie: top-level-non-partitioned=value; top-level-partitioned=value'); - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + await page.goto(urls.read_origin2_origin1); const frameBody = page.locator('iframe').contentFrame().locator('body'); // Firefox cookie partitioning is disabled in Firefox. @@ -173,39 +216,50 @@ async function runPartitionedTest(page: Page, httpsServer: TestServer, browserNa // - sets the third-party cookie for the top-level context // Second navigation: // - sends the cookie as it was just set for the (top-level site, iframe url) partition. - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-set-cookie.html'); - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); + await page.goto(urls.set_origin2_origin1); + await page.goto(urls.read_origin2_origin1); if (browserName === 'webkit') await expect(frameBody).toHaveText('Received cookie: undefined'); else await expect(frameBody).toHaveText('Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value'); } -test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { - await runPartitionedTest(page, httpsServer, browserName, isMac); +test(`third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, urls }) => { + await runPartitionedTest(page, httpsServer, browserName, isMac, urls); }); -test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser }) => { +test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, httpsServer, isMac, browser, urls }) => { test.fixme(browserName === 'firefox', 'Firefox cookie partitioning is disabled in Firefox.'); test.fixme(browserName === 'webkit' && !isMac, 'Linux and Windows WebKit builds do not partition third-party cookies at all.'); - await runPartitionedTest(page, httpsServer, browserName, isMac); + await runPartitionedTest(page, httpsServer, browserName, isMac, urls); async function checkCookies(page: Page) { - // Check top-level cookie first. - await page.goto(httpsServer.PREFIX + '/read-cookie.html'); - const expectedTopLevel = browserName === 'webkit' && isMac ? - 'Received cookie: top-level-non-partitioned=value; top-level-partitioned=value' : - 'Received cookie: frame-non-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value'; - expect.soft(await page.locator('body').textContent()).toBe(expectedTopLevel); - - // Check third-party cookie. - await page.goto(httpsServer.CROSS_PROCESS_PREFIX + '/frame-read-cookie.html'); - const frameBody = page.locator('iframe').contentFrame().locator('body'); - const expectedThirdParty = browserName === 'webkit' ? - 'Received cookie: undefined' : - 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value'; - await expect.soft(frameBody).toHaveText(expectedThirdParty); + { + // Check top-level cookie first. + await page.goto(urls.read_origin1); + const expectedTopLevel = browserName === 'webkit' && isMac ? + 'Received cookie: top-level-non-partitioned=value; top-level-partitioned=value' : + 'Received cookie: frame-non-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value'; + expect.soft(await page.locator('body').textContent()).toBe(expectedTopLevel); + } + { + // Check third-party cookie. + await page.goto(urls.read_origin2_origin1); + const frameBody = page.locator('iframe').contentFrame().locator('body'); + const expectedThirdParty = browserName === 'webkit' ? + 'Received cookie: undefined' : + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value'; + await expect.soft(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); + } + { + await page.goto(urls.read_origin1_origin2_origin1); // read-origin1-origin2-origin1.html + const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); + const expectedThirdParty = browserName === 'webkit' ? + 'Received cookie: top-level-non-partitioned=value; top-level-partitioned=value' : + 'Received cookie: frame-non-partitioned=value; top-level-non-partitioned=value'; + await expect.soft(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); + } } await checkCookies(page); @@ -226,8 +280,23 @@ test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, }); }); -test(`same origin third party 'Partitioned;' cookie with different origin intermediate iframe`, async ({ page, browserName, httpsServer, isMac, browser }) => { - addCommonCookieHandlers(httpsServer); +/** + * origin1: + * top-level-partitioned=value + * top-level-non-partitioned=value + * + * origin1: + * origin2: + * origin1: + * frame-partitioned=value + * frame-non-partitioned=value + * + * origin1 = httpsServer.PREFIX + * origin2 = httpsServer.CROSS_PROCESS_PREFIX + */ + +test(`same origin third party 'Partitioned;' cookie with different origin intermediate iframe`, async ({ page, httpsServer, browser, urls }) => { + addCommonCookieHandlers(httpsServer, urls); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', [ `${req.headers.referer ? 'frame' : 'top-level'}-partitioned=value; SameSite=None; Path=/; Secure; Partitioned;`, @@ -238,10 +307,10 @@ test(`same origin third party 'Partitioned;' cookie with different origin interm // main frame: origin1 -> iframe1: origin2 -> iframe2: origin1 // In this case the cookie in iframe2 is a third-party partitioned cookie, even though // it's the same origin as the main frame. - await page.goto(httpsServer.PREFIX + '/nested-frame-set-cookie.html'); + await page.goto(urls.set_origin1_origin2_origin1); async function checkCookies(page: Page) { - await page.goto(httpsServer.PREFIX + '/nested-frame-read-cookie.html'); + await page.goto(urls.read_origin1_origin2_origin1); const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); await expect.soft(frameBody).toHaveText('Received cookie: frame-non-partitioned=value; frame-partitioned=value'); } @@ -264,8 +333,8 @@ test(`same origin third party 'Partitioned;' cookie with different origin interm }); }); -test(`top level 'Partitioned;' cookie and same origin iframe`, async ({ page, browserName, httpsServer, isMac, browser }) => { - addCommonCookieHandlers(httpsServer); +test(`top level 'Partitioned;' cookie and same origin iframe`, async ({ page, browserName, httpsServer, browser, urls }) => { + addCommonCookieHandlers(httpsServer, urls); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', [ `${req.headers.referer ? 'frame' : 'top-level'}=value; SameSite=None; Path=/; Secure; Partitioned;`, @@ -275,25 +344,25 @@ test(`top level 'Partitioned;' cookie and same origin iframe`, async ({ page, br }); // Same origin iframe cookies are partitioned the same way as top-level cookies. - await page.goto(httpsServer.PREFIX + '/set-cookie.html'); + await page.goto(urls.set_origin1); await page.context().storageState({ path: '/tmp/state2.json' }); async function checkCookies(page: Page) { { // Check top-level cookie first. - await page.goto(httpsServer.PREFIX + '/read-cookie.html'); + await page.goto(urls.read_origin1); expect.soft(await page.locator('body').textContent()).toBe('Received cookie: top-level-non-partitioned=value; top-level=value'); } { // Same origin iframe. - await page.goto(httpsServer.PREFIX + '/frame-read-cookie.html'); + await page.goto(urls.read_origin1_origin1); const frameBody = page.locator('iframe').contentFrame().locator('body'); await expect.soft(frameBody).toHaveText('Received cookie: top-level-non-partitioned=value; top-level=value', { timeout: 1000 }); } { // Check third-party cookie. // main frame: origin1 -> iframe1: origin2 -> iframe2: origin1 - await page.goto(httpsServer.PREFIX + '/nested-frame-read-cookie.html'); + await page.goto(urls.read_origin1_origin2_origin1); const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); const expectedThirdParty = browserName === 'chromium' ? 'Received cookie: top-level-non-partitioned=value' From 6fc3aeb3ead1d26677a03897c751ba31e521fcfa Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 4 Jun 2025 20:26:04 -0700 Subject: [PATCH 06/11] add cookie test --- .../playwright-core/src/protocol/validator.ts | 12 +- .../src/server/chromium/crBrowser.ts | 27 +++- .../src/server/firefox/ffBrowser.ts | 12 +- packages/protocol/src/channels.d.ts | 12 +- packages/protocol/src/protocol.yml | 14 +- ...browsercontext-cookies-third-party.spec.ts | 134 ++++++++++++++++-- 6 files changed, 165 insertions(+), 46 deletions(-) diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 24beb701a3a68..409f08a336ed2 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -136,10 +136,8 @@ scheme.SetNetworkCookie = tObject({ httpOnly: tOptional(tBoolean), secure: tOptional(tBoolean), sameSite: tOptional(tEnum(['Strict', 'Lax', 'None'])), - partitionKey: tOptional(tObject({ - topLevelSite: tString, - hasCrossSiteAncestor: tBoolean, - })), + topLevelSite: tOptional(tString), + _chromiumHasCrossSiteAncestor: tOptional(tBoolean), }); scheme.NetworkCookie = tObject({ name: tString, @@ -150,10 +148,8 @@ scheme.NetworkCookie = tObject({ httpOnly: tBoolean, secure: tBoolean, sameSite: tEnum(['Strict', 'Lax', 'None']), - partitionKey: tOptional(tObject({ - topLevelSite: tString, - hasCrossSiteAncestor: tBoolean, - })), + topLevelSite: tOptional(tString), + _chromiumHasCrossSiteAncestor: tOptional(tBoolean), }); scheme.NameValue = tObject({ name: tString, diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 3cb4b7bd744f5..ede6c4a51cc40 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -388,12 +388,37 @@ export class CRBrowserContext extends BrowserContext { delete copy.sameParty; delete copy.sourceScheme; delete copy.sourcePort; + delete copy.partitionKey; + // If hasCrossSiteAncestor is false, the cookie is a partitioned first party cookie, + // this is Chromium specific, see https://chromestatus.com/feature/5144832583663616 + // and https://github.com/explainers-by-googlers/CHIPS-spec. + if (c.partitionKey) { + copy._chromiumHasCrossSiteAncestor = c.partitionKey.hasCrossSiteAncestor; + copy.topLevelSite = c.partitionKey.topLevelSite; + } return copy as channels.NetworkCookie; }), urls); } async addCookies(cookies: channels.SetNetworkCookie[]) { - await this._browser._session.send('Storage.setCookies', { cookies: network.rewriteCookies(cookies), browserContextId: this._browserContextId }); + function toChromiumCookie(cookie: channels.SetNetworkCookie) { + const { topLevelSite, _chromiumHasCrossSiteAncestor, ...rest } = cookie; + if (!topLevelSite) + return cookie; + return { + ...rest, + partitionKey: { + topLevelSite, + // _chromiumHasCrossSiteAncestor is non-standard, set it true by default if the cookie is partitioned. + hasCrossSiteAncestor: _chromiumHasCrossSiteAncestor ?? true, + }, + }; + } + + await this._browser._session.send('Storage.setCookies', { + cookies: network.rewriteCookies(cookies.map(toChromiumCookie)), + browserContextId: this._browserContextId + }); } async doClearCookies() { diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 6acfa8aa4edd3..cf99d2035dbed 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -305,10 +305,14 @@ export class FFBrowserContext extends BrowserContext { } async addCookies(cookies: channels.SetNetworkCookie[]) { - const cc = network.rewriteCookies(cookies).map(c => ({ - ...c, - expires: c.expires === -1 ? undefined : c.expires, - })); + const cc = network.rewriteCookies(cookies).map(c => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { _chromiumHasCrossSiteAncestor, topLevelSite, ...rest } = c; + return { + ...rest, + expires: c.expires === -1 ? undefined : c.expires, + }; + }); await this._browser.session.send('Browser.setCookies', { browserContextId: this._browserContextId, cookies: cc }); } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 8c2ceb43d80c8..e6590c072e135 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -260,10 +260,8 @@ export type SetNetworkCookie = { httpOnly?: boolean, secure?: boolean, sameSite?: 'Strict' | 'Lax' | 'None', - partitionKey?: { - topLevelSite: string, - hasCrossSiteAncestor: boolean, - }, + topLevelSite?: string, + _chromiumHasCrossSiteAncestor?: boolean, }; export type NetworkCookie = { @@ -275,10 +273,8 @@ export type NetworkCookie = { httpOnly: boolean, secure: boolean, sameSite: 'Strict' | 'Lax' | 'None', - partitionKey?: { - topLevelSite: string, - hasCrossSiteAncestor: boolean, - }, + topLevelSite?: string, + _chromiumHasCrossSiteAncestor?: boolean, }; export type NameValue = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index fe0e85d4aa12d..7ce448604c33d 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -223,11 +223,8 @@ SetNetworkCookie: - Strict - Lax - None - partitionKey: - type: object? - properties: - topLevelSite: string - hasCrossSiteAncestor: boolean + topLevelSite: string? + _chromiumHasCrossSiteAncestor: boolean? NetworkCookie: @@ -246,11 +243,8 @@ NetworkCookie: - Strict - Lax - None - partitionKey: - type: object? - properties: - topLevelSite: string - hasCrossSiteAncestor: boolean + topLevelSite: string? + _chromiumHasCrossSiteAncestor: boolean? NameValue: diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 427e19655a8c1..8fe081e65c0c0 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -16,7 +16,7 @@ import { contextTest, expect } from '../config/browserTest'; -import type { Page, BrowserContext } from 'playwright'; +import type { Page, BrowserContext, Cookie } from 'playwright'; import type { TestServer } from '../config/testserver'; type TestUrls = { @@ -63,6 +63,12 @@ test.use({ * frame-partitioned=value * frame-non-partitioned=value * + * origin1: + * origin2: + * origin1: + * frame-partitioned=value + * frame-non-partitioned=value + * * origin1 = httpsServer.PREFIX * origin2 = httpsServer.CROSS_PROCESS_PREFIX */ @@ -93,6 +99,17 @@ function addCommonCookieHandlers(httpsServer: TestServer, urls: TestUrls) { }); } +function findCookie(cookies: Cookie[], name: string) { + const result = cookies.find(cookie => cookie.name === name); + expect(result, `Cookie ${name} not found in ${JSON.stringify(cookies, null, 2)}`).toBeTruthy(); + return result; +} + +function expectTopLevelSite(cookies: Cookie[], name: string, topLevelSite: string) { + const cookie = findCookie(cookies, name); + expect(cookie.topLevelSite, `Cookie ${name}`).toBe(topLevelSite); +} + async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, urls: TestUrls) { addCommonCookieHandlers(httpsServer, urls); httpsServer.setRoute('/set-cookie.html', (req, res) => { @@ -264,6 +281,23 @@ test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, await checkCookies(page); + function checkStorageCookies(cookies: Cookie[]) { + const expectedTopLevelPartitioned = browserName === 'webkit' && isMac ? + undefined : + 'https://localhost'; + expectTopLevelSite(cookies, 'top-level-partitioned', expectedTopLevelPartitioned); + expectTopLevelSite(cookies, 'top-level-non-partitioned', undefined); + if (browserName === 'webkit' && isMac) { + expect(cookies.find(cookie => cookie.name === 'frame-partitioned')).toBeUndefined(); + expect(cookies.find(cookie => cookie.name === 'frame-non-partitioned')).toBeUndefined(); + } else { + expectTopLevelSite(cookies, 'frame-partitioned', 'https://127.0.0.1'); + expectTopLevelSite(cookies, 'frame-non-partitioned', undefined); + } + } + checkStorageCookies(await page.context().cookies()); + checkStorageCookies((await page.context().storageState()).cookies); + await test.step('export via cookies/addCookies', async () => { const cookies = await page.context().cookies(); const context2 = await browser.newContext(); @@ -280,20 +314,90 @@ test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, }); }); -/** - * origin1: - * top-level-partitioned=value - * top-level-non-partitioned=value - * - * origin1: - * origin2: - * origin1: - * frame-partitioned=value - * frame-non-partitioned=value - * - * origin1 = httpsServer.PREFIX - * origin2 = httpsServer.CROSS_PROCESS_PREFIX - */ +test(`add 'Partitioned;' cookie via API`, async ({ page, context, browserName, httpsServer, isMac, urls }) => { + // test.fixme(browserName === 'firefox', 'Firefox cookie partitioning is disabled in Firefox.'); + // test.fixme(browserName === 'webkit' && !isMac, 'Linux and Windows WebKit builds do not partition third-party cookies at all.'); + addCommonCookieHandlers(httpsServer, urls); + + await context.addCookies([ + { + name: 'top-level-partitioned', + value: 'value', + domain: 'localhost', + path: '/', + expires: -1, + httpOnly: false, + secure: true, + sameSite: 'None', + topLevelSite: 'https://localhost', + _chromiumHasCrossSiteAncestor: false + } as any, + { + name: 'top-level-non-partitioned', + value: 'value', + domain: 'localhost', + path: '/', + expires: -1, + httpOnly: false, + secure: true, + sameSite: 'None' + }, + { + name: 'frame-partitioned', + value: 'value', + domain: 'localhost', + path: '/', + expires: -1, + httpOnly: false, + secure: true, + sameSite: 'None', + topLevelSite: 'https://127.0.0.1', + _chromiumHasCrossSiteAncestor: true + } as any, + { + name: 'frame-non-partitioned', + value: 'value', + domain: 'localhost', + path: '/', + expires: -1, + httpOnly: false, + secure: true, + sameSite: 'None' + } + ]); + + async function checkCookies(page: Page) { + { + // Check top-level cookie first. + await page.goto(urls.read_origin1); + const expectedTopLevel = browserName === 'webkit' || browserName === 'firefox' ? + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value' : + 'Received cookie: frame-non-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value'; + expect.soft(await page.locator('body').textContent()).toBe(expectedTopLevel); + } + { + // Check third-party cookie. + await page.goto(urls.read_origin2_origin1); + const frameBody = page.locator('iframe').contentFrame().locator('body'); + const expectedThirdParty = browserName === 'webkit' ? + 'Received cookie: undefined' : browserName === 'firefox' ? + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value' : + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value'; + await expect.soft(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); + } + { + await page.goto(urls.read_origin1_origin2_origin1); // read-origin1-origin2-origin1.html + const frameBody = page.locator('iframe').contentFrame().locator('iframe').contentFrame().locator('body'); + const expectedThirdParty = browserName === 'webkit' || browserName === 'firefox' ? + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value' : + 'Received cookie: frame-non-partitioned=value; top-level-non-partitioned=value'; + await expect.soft(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); + } + } + + await checkCookies(page); +}); + test(`same origin third party 'Partitioned;' cookie with different origin intermediate iframe`, async ({ page, httpsServer, browser, urls }) => { addCommonCookieHandlers(httpsServer, urls); From 28e46e2f6e09d8722dfa9327bdd6aeae74f924e6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 5 Jun 2025 10:57:45 -0700 Subject: [PATCH 07/11] Update tests for linux --- ...browsercontext-cookies-third-party.spec.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 8fe081e65c0c0..63c0f47dcf7a0 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -134,14 +134,15 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse // Set cookie and do second navigation. await page.goto(urls.set_origin2_origin1); await page.goto(urls.read_origin2_origin1); - const expectedThirdParty = browserName === 'webkit' ? - 'Received cookie: undefined' : - 'Received cookie: frame=value; top-level=value'; - await expect(frameBody).toHaveText(expectedThirdParty); + const expectedThirdParty = browserName === 'webkit' && isMac ? + 'Received cookie: undefined' : browserName === 'webkit' ? + 'Received cookie: top-level=value' : + 'Received cookie: frame=value; top-level=value'; + await expect(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); // Check again the top-level cookie. await page.goto(urls.read_origin1); - const expectedTopLevel = browserName === 'webkit' && isMac ? + const expectedTopLevel = browserName === 'webkit' ? 'Received cookie: top-level=value' : 'Received cookie: frame=value; top-level=value'; expect(await page.locator('body').textContent()).toBe(expectedTopLevel); @@ -315,8 +316,6 @@ test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, }); test(`add 'Partitioned;' cookie via API`, async ({ page, context, browserName, httpsServer, isMac, urls }) => { - // test.fixme(browserName === 'firefox', 'Firefox cookie partitioning is disabled in Firefox.'); - // test.fixme(browserName === 'webkit' && !isMac, 'Linux and Windows WebKit builds do not partition third-party cookies at all.'); addCommonCookieHandlers(httpsServer, urls); await context.addCookies([ @@ -379,10 +378,11 @@ test(`add 'Partitioned;' cookie via API`, async ({ page, context, browserName, h // Check third-party cookie. await page.goto(urls.read_origin2_origin1); const frameBody = page.locator('iframe').contentFrame().locator('body'); - const expectedThirdParty = browserName === 'webkit' ? - 'Received cookie: undefined' : browserName === 'firefox' ? - 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value' : - 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value'; + const expectedThirdParty = browserName === 'webkit' && isMac ? + 'Received cookie: undefined' : browserName === 'chromium' ? + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value' : + // Firefox and WebKit on Linux/Windows do not partition third-party cookies. + 'Received cookie: frame-non-partitioned=value; frame-partitioned=value; top-level-non-partitioned=value; top-level-partitioned=value'; await expect.soft(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); } { From 5f51f8717aaa45ccbe99f41ccf778f91bedef360 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 5 Jun 2025 11:29:25 -0700 Subject: [PATCH 08/11] destructure --- docs/src/api/class-browsercontext.md | 8 +-- packages/playwright-client/types/types.d.ts | 12 ++--- .../playwright-core/src/protocol/validator.ts | 4 +- .../src/server/chromium/crBrowser.ts | 52 ++++++++++++------- .../src/server/firefox/ffBrowser.ts | 29 ++++++++--- .../src/server/webkit/wkBrowser.ts | 36 +++++++++---- packages/playwright-core/types/types.d.ts | 12 ++--- packages/protocol/src/channels.d.ts | 4 +- packages/protocol/src/protocol.yml | 4 +- ...browsercontext-cookies-third-party.spec.ts | 17 +++--- 10 files changed, 111 insertions(+), 67 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 325ff27bc09dd..d6744aebb4c68 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -363,7 +363,7 @@ await context.AddCookiesAsync(new[] { cookie1, cookie2 }); - `httpOnly` ?<[boolean]> Optional. - `secure` ?<[boolean]> Optional. - `sameSite` ?<[SameSiteAttribute]<"Strict"|"Lax"|"None">> Optional. - - `topLevelSite` ?<[string]> For partitioned third-party cookies (aka [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the top level site (scheme and host) used as the partition key. Optional. + - `partitionKey` ?<[string]> For partitioned third-party cookies (aka [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the partition key. Optional. ## async method: BrowserContext.addInitScript * since: v1.8 @@ -603,10 +603,10 @@ The default browser context cannot be closed. - `httpOnly` <[boolean]> - `secure` <[boolean]> - `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> - - `topLevelSite` ?<[string]> + - `partitionKey` ?<[string]> If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs -are returned. Note that cookies are matched based on the URLs regardless of their `topLevelSite` value. +are returned. Note that cookies are matched based on the URLs regardless of their `partitionKey` value. ### param: BrowserContext.cookies.urls * since: v1.8 @@ -1506,7 +1506,7 @@ Whether to emulate network being offline for the browser context. - `httpOnly` <[boolean]> - `secure` <[boolean]> - `sameSite` <[SameSiteAttribute]<"Strict"|"Lax"|"None">> - - `topLevelSite` ?<[string]> + - `partitionKey` ?<[string]> - `origins` <[Array]<[Object]>> - `origin` <[string]> - `localStorage` <[Array]<[Object]>> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 20bb3cc56dc58..d6e24ef506f47 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -8833,10 +8833,10 @@ export interface BrowserContext { /** * For partitioned third-party cookies (aka - * [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the top - * level site (scheme and host) used as the partition key. Optional. + * [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the + * partition key. Optional. */ - topLevelSite?: string; + partitionKey?: string; }>): Promise; /** @@ -8915,7 +8915,7 @@ export interface BrowserContext { /** * If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those - * URLs are returned. Note that cookies are matched based on the URLs regardless of their `topLevelSite` value. + * URLs are returned. Note that cookies are matched based on the URLs regardless of their `partitionKey` value. * @param urls Optional list of URLs. */ cookies(urls?: string|ReadonlyArray): Promise>; @@ -9312,7 +9312,7 @@ export interface BrowserContext { sameSite: "Strict"|"Lax"|"None"; - topLevelSite?: string; + partitionKey?: string; }>; origins: Array<{ @@ -22511,7 +22511,7 @@ export interface Cookie { sameSite: "Strict"|"Lax"|"None"; - topLevelSite?: string; + partitionKey?: string; } interface PageWaitForSelectorOptions { diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 409f08a336ed2..8a61b7a086275 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -136,7 +136,7 @@ scheme.SetNetworkCookie = tObject({ httpOnly: tOptional(tBoolean), secure: tOptional(tBoolean), sameSite: tOptional(tEnum(['Strict', 'Lax', 'None'])), - topLevelSite: tOptional(tString), + partitionKey: tOptional(tString), _chromiumHasCrossSiteAncestor: tOptional(tBoolean), }); scheme.NetworkCookie = tObject({ @@ -148,7 +148,7 @@ scheme.NetworkCookie = tObject({ httpOnly: tBoolean, secure: tBoolean, sameSite: tEnum(['Strict', 'Lax', 'None']), - topLevelSite: tOptional(tString), + partitionKey: tOptional(tString), _chromiumHasCrossSiteAncestor: tOptional(tBoolean), }); scheme.NameValue = tObject({ diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index ede6c4a51cc40..472e4d4e5887f 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -381,42 +381,54 @@ export class CRBrowserContext extends BrowserContext { async doGetCookies(urls: string[]): Promise { const { cookies } = await this._browser._session.send('Storage.getCookies', { browserContextId: this._browserContextId }); return network.filterCookies(cookies.map(c => { - const copy: any = { sameSite: 'Lax', ...c }; - delete copy.size; - delete copy.priority; - delete copy.session; - delete copy.sameParty; - delete copy.sourceScheme; - delete copy.sourcePort; - delete copy.partitionKey; + const { name, value, domain, path, expires, httpOnly, secure, sameSite } = c; + const copy: channels.NetworkCookie = { + name, + value, + domain, + path, + expires, + httpOnly, + secure, + sameSite: sameSite ?? 'Lax', + }; // If hasCrossSiteAncestor is false, the cookie is a partitioned first party cookie, // this is Chromium specific, see https://chromestatus.com/feature/5144832583663616 // and https://github.com/explainers-by-googlers/CHIPS-spec. if (c.partitionKey) { copy._chromiumHasCrossSiteAncestor = c.partitionKey.hasCrossSiteAncestor; - copy.topLevelSite = c.partitionKey.topLevelSite; + copy.partitionKey = c.partitionKey.topLevelSite; } - return copy as channels.NetworkCookie; + return copy; }), urls); } async addCookies(cookies: channels.SetNetworkCookie[]) { function toChromiumCookie(cookie: channels.SetNetworkCookie) { - const { topLevelSite, _chromiumHasCrossSiteAncestor, ...rest } = cookie; - if (!topLevelSite) - return cookie; - return { - ...rest, - partitionKey: { - topLevelSite, + const { name, value, url, domain, path, expires, httpOnly, secure, sameSite, partitionKey, _chromiumHasCrossSiteAncestor } = cookie; + const copy: Protocol.Network.CookieParam = { + name, + value, + url, + domain, + path, + expires, + httpOnly, + secure, + sameSite + }; + if (partitionKey) { + copy.partitionKey = { + topLevelSite: partitionKey, // _chromiumHasCrossSiteAncestor is non-standard, set it true by default if the cookie is partitioned. hasCrossSiteAncestor: _chromiumHasCrossSiteAncestor ?? true, - }, - }; + }; + } + return copy; } await this._browser._session.send('Storage.setCookies', { - cookies: network.rewriteCookies(cookies.map(toChromiumCookie)), + cookies: network.rewriteCookies(cookies).map(toChromiumCookie), browserContextId: this._browserContextId }); } diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index cf99d2035dbed..4b529d0ad8987 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -297,20 +297,33 @@ export class FFBrowserContext extends BrowserContext { async doGetCookies(urls: string[]): Promise { const { cookies } = await this._browser.session.send('Browser.getCookies', { browserContextId: this._browserContextId }); return network.filterCookies(cookies.map(c => { - const copy: any = { ... c }; - delete copy.size; - delete copy.session; - return copy as channels.NetworkCookie; + const { name, value, domain, path, expires, httpOnly, secure, sameSite } = c; + return { + name, + value, + domain, + path, + expires, + httpOnly, + secure, + sameSite, + }; }), urls); } async addCookies(cookies: channels.SetNetworkCookie[]) { const cc = network.rewriteCookies(cookies).map(c => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { _chromiumHasCrossSiteAncestor, topLevelSite, ...rest } = c; + const { name, value, url, domain, path, expires, httpOnly, secure, sameSite } = c; return { - ...rest, - expires: c.expires === -1 ? undefined : c.expires, + name, + value, + url, + domain, + path, + expires: expires === -1 ? undefined : expires, + httpOnly, + secure, + sameSite }; }); await this._browser.session.send('Browser.setCookies', { browserContextId: this._browserContextId, cookies: cc }); diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 4cdeb3a353b2f..aaf4c197a4659 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -255,19 +255,37 @@ export class WKBrowserContext extends BrowserContext { async doGetCookies(urls: string[]): Promise { const { cookies } = await this._browser._browserSession.send('Playwright.getAllCookies', { browserContextId: this._browserContextId }); return network.filterCookies(cookies.map((c: channels.NetworkCookie) => { - const copy: any = { ... c }; - copy.expires = c.expires === -1 ? -1 : c.expires / 1000; - delete copy.session; - return copy as channels.NetworkCookie; + const { name, value, domain, path, expires, httpOnly, secure, sameSite } = c; + const copy: channels.NetworkCookie = { + name, + value, + domain, + path, + expires: expires === -1 ? -1 : expires / 1000, + httpOnly, + secure, + sameSite, + }; + return copy; }), urls); } async addCookies(cookies: channels.SetNetworkCookie[]) { - const cc = network.rewriteCookies(cookies).map(c => ({ - ...c, - session: c.expires === -1 || c.expires === undefined, - expires: c.expires && c.expires !== -1 ? c.expires * 1000 : c.expires, - })) as Protocol.Playwright.SetCookieParam[]; + const cc = network.rewriteCookies(cookies).map(c => { + const { name, value, domain, path, expires, httpOnly, secure, sameSite } = c; + const copy: Protocol.Playwright.SetCookieParam = { + name, + value, + domain: domain!, + path: path!, + expires: expires && expires !== -1 ? expires * 1000 : expires, + httpOnly, + secure, + sameSite, + session: expires === -1 || expires === undefined, + }; + return copy; + }); await this._browser._browserSession.send('Playwright.setCookies', { cookies: cc, browserContextId: this._browserContextId }); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 20bb3cc56dc58..d6e24ef506f47 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8833,10 +8833,10 @@ export interface BrowserContext { /** * For partitioned third-party cookies (aka - * [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the top - * level site (scheme and host) used as the partition key. Optional. + * [CHIPS](https://developer.mozilla.org/en-US/docs/Web/Privacy/Guides/Privacy_sandbox/Partitioned_cookies)), the + * partition key. Optional. */ - topLevelSite?: string; + partitionKey?: string; }>): Promise; /** @@ -8915,7 +8915,7 @@ export interface BrowserContext { /** * If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those - * URLs are returned. Note that cookies are matched based on the URLs regardless of their `topLevelSite` value. + * URLs are returned. Note that cookies are matched based on the URLs regardless of their `partitionKey` value. * @param urls Optional list of URLs. */ cookies(urls?: string|ReadonlyArray): Promise>; @@ -9312,7 +9312,7 @@ export interface BrowserContext { sameSite: "Strict"|"Lax"|"None"; - topLevelSite?: string; + partitionKey?: string; }>; origins: Array<{ @@ -22511,7 +22511,7 @@ export interface Cookie { sameSite: "Strict"|"Lax"|"None"; - topLevelSite?: string; + partitionKey?: string; } interface PageWaitForSelectorOptions { diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index e6590c072e135..8fb21b7cf3def 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -260,7 +260,7 @@ export type SetNetworkCookie = { httpOnly?: boolean, secure?: boolean, sameSite?: 'Strict' | 'Lax' | 'None', - topLevelSite?: string, + partitionKey?: string, _chromiumHasCrossSiteAncestor?: boolean, }; @@ -273,7 +273,7 @@ export type NetworkCookie = { httpOnly: boolean, secure: boolean, sameSite: 'Strict' | 'Lax' | 'None', - topLevelSite?: string, + partitionKey?: string, _chromiumHasCrossSiteAncestor?: boolean, }; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 7ce448604c33d..338ef34a25df3 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -223,7 +223,7 @@ SetNetworkCookie: - Strict - Lax - None - topLevelSite: string? + partitionKey: string? _chromiumHasCrossSiteAncestor: boolean? @@ -243,7 +243,7 @@ NetworkCookie: - Strict - Lax - None - topLevelSite: string? + partitionKey: string? _chromiumHasCrossSiteAncestor: boolean? diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 63c0f47dcf7a0..195ce528e9929 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -105,9 +105,10 @@ function findCookie(cookies: Cookie[], name: string) { return result; } -function expectTopLevelSite(cookies: Cookie[], name: string, topLevelSite: string) { +function expectPartitionKey(cookies: Cookie[], name: string, partitionKey: string) { const cookie = findCookie(cookies, name); - expect(cookie.topLevelSite, `Cookie ${name}`).toBe(topLevelSite); + if (partitionKey !== cookie.partitionKey) + throw new Error(`Cookie ${name} has partitionKey ${cookie.partitionKey} but expected ${partitionKey}.`); } async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, urls: TestUrls) { @@ -286,14 +287,14 @@ test(`save/load third party 'Partitioned;' cookies`, async ({ page, browserName, const expectedTopLevelPartitioned = browserName === 'webkit' && isMac ? undefined : 'https://localhost'; - expectTopLevelSite(cookies, 'top-level-partitioned', expectedTopLevelPartitioned); - expectTopLevelSite(cookies, 'top-level-non-partitioned', undefined); + expectPartitionKey(cookies, 'top-level-partitioned', expectedTopLevelPartitioned); + expectPartitionKey(cookies, 'top-level-non-partitioned', undefined); if (browserName === 'webkit' && isMac) { expect(cookies.find(cookie => cookie.name === 'frame-partitioned')).toBeUndefined(); expect(cookies.find(cookie => cookie.name === 'frame-non-partitioned')).toBeUndefined(); } else { - expectTopLevelSite(cookies, 'frame-partitioned', 'https://127.0.0.1'); - expectTopLevelSite(cookies, 'frame-non-partitioned', undefined); + expectPartitionKey(cookies, 'frame-partitioned', 'https://127.0.0.1'); + expectPartitionKey(cookies, 'frame-non-partitioned', undefined); } } checkStorageCookies(await page.context().cookies()); @@ -328,7 +329,7 @@ test(`add 'Partitioned;' cookie via API`, async ({ page, context, browserName, h httpOnly: false, secure: true, sameSite: 'None', - topLevelSite: 'https://localhost', + partitionKey: 'https://localhost', _chromiumHasCrossSiteAncestor: false } as any, { @@ -350,7 +351,7 @@ test(`add 'Partitioned;' cookie via API`, async ({ page, context, browserName, h httpOnly: false, secure: true, sameSite: 'None', - topLevelSite: 'https://127.0.0.1', + partitionKey: 'https://127.0.0.1', _chromiumHasCrossSiteAncestor: true } as any, { From 36b716c0422bab47d2a5240726db9796b783e639 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 5 Jun 2025 11:34:37 -0700 Subject: [PATCH 09/11] rename --- packages/playwright-core/src/protocol/validator.ts | 4 ++-- packages/playwright-core/src/server/chromium/crBrowser.ts | 8 ++++---- packages/protocol/src/channels.d.ts | 4 ++-- packages/protocol/src/protocol.yml | 4 ++-- tests/library/browsercontext-cookies-third-party.spec.ts | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 8a61b7a086275..71903879cec9f 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -137,7 +137,7 @@ scheme.SetNetworkCookie = tObject({ secure: tOptional(tBoolean), sameSite: tOptional(tEnum(['Strict', 'Lax', 'None'])), partitionKey: tOptional(tString), - _chromiumHasCrossSiteAncestor: tOptional(tBoolean), + _crHasCrossSiteAncestor: tOptional(tBoolean), }); scheme.NetworkCookie = tObject({ name: tString, @@ -149,7 +149,7 @@ scheme.NetworkCookie = tObject({ secure: tBoolean, sameSite: tEnum(['Strict', 'Lax', 'None']), partitionKey: tOptional(tString), - _chromiumHasCrossSiteAncestor: tOptional(tBoolean), + _crHasCrossSiteAncestor: tOptional(tBoolean), }); scheme.NameValue = tObject({ name: tString, diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 472e4d4e5887f..0557ea11cf977 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -396,7 +396,7 @@ export class CRBrowserContext extends BrowserContext { // this is Chromium specific, see https://chromestatus.com/feature/5144832583663616 // and https://github.com/explainers-by-googlers/CHIPS-spec. if (c.partitionKey) { - copy._chromiumHasCrossSiteAncestor = c.partitionKey.hasCrossSiteAncestor; + copy._crHasCrossSiteAncestor = c.partitionKey.hasCrossSiteAncestor; copy.partitionKey = c.partitionKey.topLevelSite; } return copy; @@ -405,7 +405,7 @@ export class CRBrowserContext extends BrowserContext { async addCookies(cookies: channels.SetNetworkCookie[]) { function toChromiumCookie(cookie: channels.SetNetworkCookie) { - const { name, value, url, domain, path, expires, httpOnly, secure, sameSite, partitionKey, _chromiumHasCrossSiteAncestor } = cookie; + const { name, value, url, domain, path, expires, httpOnly, secure, sameSite, partitionKey, _crHasCrossSiteAncestor } = cookie; const copy: Protocol.Network.CookieParam = { name, value, @@ -420,8 +420,8 @@ export class CRBrowserContext extends BrowserContext { if (partitionKey) { copy.partitionKey = { topLevelSite: partitionKey, - // _chromiumHasCrossSiteAncestor is non-standard, set it true by default if the cookie is partitioned. - hasCrossSiteAncestor: _chromiumHasCrossSiteAncestor ?? true, + // _crHasCrossSiteAncestor is non-standard, set it true by default if the cookie is partitioned. + hasCrossSiteAncestor: _crHasCrossSiteAncestor ?? true, }; } return copy; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 8fb21b7cf3def..90ab1680d9ddf 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -261,7 +261,7 @@ export type SetNetworkCookie = { secure?: boolean, sameSite?: 'Strict' | 'Lax' | 'None', partitionKey?: string, - _chromiumHasCrossSiteAncestor?: boolean, + _crHasCrossSiteAncestor?: boolean, }; export type NetworkCookie = { @@ -274,7 +274,7 @@ export type NetworkCookie = { secure: boolean, sameSite: 'Strict' | 'Lax' | 'None', partitionKey?: string, - _chromiumHasCrossSiteAncestor?: boolean, + _crHasCrossSiteAncestor?: boolean, }; export type NameValue = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 338ef34a25df3..8e85b73422594 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -224,7 +224,7 @@ SetNetworkCookie: - Lax - None partitionKey: string? - _chromiumHasCrossSiteAncestor: boolean? + _crHasCrossSiteAncestor: boolean? NetworkCookie: @@ -244,7 +244,7 @@ NetworkCookie: - Lax - None partitionKey: string? - _chromiumHasCrossSiteAncestor: boolean? + _crHasCrossSiteAncestor: boolean? NameValue: diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 195ce528e9929..83a26ee67ff45 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -330,7 +330,7 @@ test(`add 'Partitioned;' cookie via API`, async ({ page, context, browserName, h secure: true, sameSite: 'None', partitionKey: 'https://localhost', - _chromiumHasCrossSiteAncestor: false + _crHasCrossSiteAncestor: false } as any, { name: 'top-level-non-partitioned', @@ -352,7 +352,7 @@ test(`add 'Partitioned;' cookie via API`, async ({ page, context, browserName, h secure: true, sameSite: 'None', partitionKey: 'https://127.0.0.1', - _chromiumHasCrossSiteAncestor: true + _crHasCrossSiteAncestor: true } as any, { name: 'frame-non-partitioned', From 5de59e746f163f7d83bacd41989e3abc8b324ce4 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 5 Jun 2025 11:42:47 -0700 Subject: [PATCH 10/11] Revert docs change --- docs/src/api/class-browsercontext.md | 2 +- packages/playwright-client/types/types.d.ts | 2 +- packages/playwright-core/types/types.d.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index d6744aebb4c68..48eb96543e2b6 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -606,7 +606,7 @@ The default browser context cannot be closed. - `partitionKey` ?<[string]> If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs -are returned. Note that cookies are matched based on the URLs regardless of their `partitionKey` value. +are returned. ### param: BrowserContext.cookies.urls * since: v1.8 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index d6e24ef506f47..e390595a1c3c7 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -8915,7 +8915,7 @@ export interface BrowserContext { /** * If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those - * URLs are returned. Note that cookies are matched based on the URLs regardless of their `partitionKey` value. + * URLs are returned. * @param urls Optional list of URLs. */ cookies(urls?: string|ReadonlyArray): Promise>; diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index d6e24ef506f47..e390595a1c3c7 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8915,7 +8915,7 @@ export interface BrowserContext { /** * If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those - * URLs are returned. Note that cookies are matched based on the URLs regardless of their `partitionKey` value. + * URLs are returned. * @param urls Optional list of URLs. */ cookies(urls?: string|ReadonlyArray): Promise>; From d45851def52c15fd14e84e2bf9f3c1ff87fa60e6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 5 Jun 2025 11:59:27 -0700 Subject: [PATCH 11/11] update windows expectations --- .../browsercontext-cookies-third-party.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/library/browsercontext-cookies-third-party.spec.ts b/tests/library/browsercontext-cookies-third-party.spec.ts index 83a26ee67ff45..fcb8083057d0c 100644 --- a/tests/library/browsercontext-cookies-third-party.spec.ts +++ b/tests/library/browsercontext-cookies-third-party.spec.ts @@ -111,7 +111,7 @@ function expectPartitionKey(cookies: Cookie[], name: string, partitionKey: strin throw new Error(`Cookie ${name} has partitionKey ${cookie.partitionKey} but expected ${partitionKey}.`); } -async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, urls: TestUrls) { +async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browserName: string, isMac: boolean, isLinux: boolean, urls: TestUrls) { addCommonCookieHandlers(httpsServer, urls); httpsServer.setRoute('/set-cookie.html', (req, res) => { res.setHeader('Set-Cookie', `${req.headers.referer ? 'frame' : 'top-level'}=value; SameSite=None; Path=/; Secure;`); @@ -136,14 +136,14 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse await page.goto(urls.set_origin2_origin1); await page.goto(urls.read_origin2_origin1); const expectedThirdParty = browserName === 'webkit' && isMac ? - 'Received cookie: undefined' : browserName === 'webkit' ? + 'Received cookie: undefined' : browserName === 'webkit' && isLinux ? 'Received cookie: top-level=value' : 'Received cookie: frame=value; top-level=value'; await expect(frameBody).toHaveText(expectedThirdParty, { timeout: 1000 }); // Check again the top-level cookie. await page.goto(urls.read_origin1); - const expectedTopLevel = browserName === 'webkit' ? + const expectedTopLevel = browserName === 'webkit' && (isMac || isLinux) ? 'Received cookie: top-level=value' : 'Received cookie: frame=value; top-level=value'; expect(await page.locator('body').textContent()).toBe(expectedTopLevel); @@ -154,13 +154,13 @@ async function runNonPartitionedTest(page: Page, httpsServer: TestServer, browse }; } -test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, urls }) => { - await runNonPartitionedTest(page, httpsServer, browserName, isMac, urls); +test(`third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, isLinux, urls }) => { + await runNonPartitionedTest(page, httpsServer, browserName, isMac, isLinux, urls); }); -test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, browser, urls }) => { +test(`save/load third party non-partitioned cookies`, async ({ page, browserName, httpsServer, isMac, isLinux, browser, urls }) => { // Run the test to populate the cookies. - const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac, urls); + const { expectedTopLevel, expectedThirdParty } = await runNonPartitionedTest(page, httpsServer, browserName, isMac, isLinux, urls); async function checkCookies(page: Page) { // Check top-level cookie first.