diff --git a/docs/src/api/class-browsercontext.md b/docs/src/api/class-browsercontext.md index 87f6369b5c..c47a1892cb 100644 --- a/docs/src/api/class-browsercontext.md +++ b/docs/src/api/class-browsercontext.md @@ -1010,6 +1010,27 @@ Creates a new page in the browser context. Returns all open pages in the context. +## async method: BrowserContext.removeCookies +* since: v1.43 + +Removes cookies from context. At least one of the removal criteria should be provided. + +**Usage** + +```js +await browserContext.removeCookies({ name: 'session-id' }); +await browserContext.removeCookies({ domain: 'my-origin.com' }); +await browserContext.removeCookies({ path: '/api/v1' }); +await browserContext.removeCookies({ name: 'session-id', domain: 'my-origin.com' }); +``` + +### param: BrowserContext.removeCookies.filter +* since: v1.43 +- `filter` <[Object]> + - `name` ?<[string]> + - `domain` ?<[string]> + - `path` ?<[string]> + ## property: BrowserContext.request * since: v1.16 * langs: diff --git a/packages/playwright-core/src/client/browserContext.ts b/packages/playwright-core/src/client/browserContext.ts index 39140f904c..37d6b6744f 100644 --- a/packages/playwright-core/src/client/browserContext.ts +++ b/packages/playwright-core/src/client/browserContext.ts @@ -269,6 +269,10 @@ export class BrowserContext extends ChannelOwner await this._channel.clearCookies(); } + async removeCookies(filter: network.RemoveNetworkCookieParam): Promise { + await this._channel.removeCookies({ filter }); + } + async grantPermissions(permissions: string[], options?: { origin?: string }): Promise { await this._channel.grantPermissions({ permissions, ...options }); } diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 6f35fdbc70..75cda8d207 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -58,6 +58,12 @@ export type SetNetworkCookieParam = { sameSite?: 'Strict' | 'Lax' | 'None' }; +export type RemoveNetworkCookieParam = { + name?: string, + domain?: string, + path?: string, +}; + type SerializedFallbackOverrides = { url?: string; method?: string; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f33a3891b6..4a8f7fb3d1 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -828,6 +828,14 @@ scheme.BrowserContextAddInitScriptParams = tObject({ scheme.BrowserContextAddInitScriptResult = tOptional(tObject({})); scheme.BrowserContextClearCookiesParams = tOptional(tObject({})); scheme.BrowserContextClearCookiesResult = tOptional(tObject({})); +scheme.BrowserContextRemoveCookiesParams = tObject({ + filter: tObject({ + name: tOptional(tString), + domain: tOptional(tString), + path: tOptional(tString), + }), +}); +scheme.BrowserContextRemoveCookiesResult = tOptional(tObject({})); scheme.BrowserContextClearPermissionsParams = tOptional(tObject({})); scheme.BrowserContextClearPermissionsResult = tOptional(tObject({})); scheme.BrowserContextCloseParams = tObject({ diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 0bc14f45e1..51add38e0e 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -276,6 +276,22 @@ export abstract class BrowserContext extends SdkObject { return await this.doGetCookies(urls as string[]); } + async removeCookies(filter: {name?: string, domain?: string, path?: string}): Promise { + if (!filter.name && !filter.domain && !filter.path) + throw new Error(`Either name, domain or path are required`); + + const currentCookies = await this.cookies(); + + const cookiesToKeep = currentCookies.filter(cookie => { + return !((!filter.name || filter.name === cookie.name) && + (!filter.domain || filter.domain === cookie.domain) && + (!filter.path || filter.path === cookie.path)); + }); + + await this.clearCookies(); + await this.addCookies(cookiesToKeep); + } + setHTTPCredentials(httpCredentials?: types.Credentials): Promise { return this.doSetHTTPCredentials(httpCredentials); } diff --git a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts index b4a06a67b5..d04418866a 100644 --- a/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserContextDispatcher.ts @@ -224,6 +224,10 @@ export class BrowserContextDispatcher extends Dispatcher { + await this._context.removeCookies(params.filter); + } + async grantPermissions(params: channels.BrowserContextGrantPermissionsParams): Promise { await this._context.grantPermissions(params.permissions, params.origin); } diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index e8f1bb9c20..bbebc7dfda 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -8441,6 +8441,28 @@ export interface BrowserContext { */ pages(): Array; + /** + * Removes cookies from context. The method will throw an error if either name, domain or path has not been passed. + * + * **Usage** + * + * ```js + * await browserContext.removeCookies({ name: 'session-id' }); + * await browserContext.removeCookies({ domain: 'my-origin.com' }); + * await browserContext.removeCookies({ path: '/api/v1' }); + * await browserContext.removeCookies({ name: 'session-id', domain: 'my-origin.com' }); + * ``` + * + * @param filter + */ + removeCookies(filter: { + name?: string; + + domain?: string; + + path?: string; + }): Promise; + /** * Routing provides the capability to modify network requests that are made by any page in the browser context. Once * route is enabled, every request matching the url pattern will stall unless it's continued, fulfilled or aborted. diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 379b5a48e8..c0af7ac678 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1427,6 +1427,7 @@ export interface BrowserContextChannel extends BrowserContextEventTarget, EventT addCookies(params: BrowserContextAddCookiesParams, metadata?: CallMetadata): Promise; addInitScript(params: BrowserContextAddInitScriptParams, metadata?: CallMetadata): Promise; clearCookies(params?: BrowserContextClearCookiesParams, metadata?: CallMetadata): Promise; + removeCookies(params: BrowserContextRemoveCookiesParams, metadata?: CallMetadata): Promise; clearPermissions(params?: BrowserContextClearPermissionsParams, metadata?: CallMetadata): Promise; close(params: BrowserContextCloseParams, metadata?: CallMetadata): Promise; cookies(params: BrowserContextCookiesParams, metadata?: CallMetadata): Promise; @@ -1523,6 +1524,17 @@ export type BrowserContextAddInitScriptResult = void; export type BrowserContextClearCookiesParams = {}; export type BrowserContextClearCookiesOptions = {}; export type BrowserContextClearCookiesResult = void; +export type BrowserContextRemoveCookiesParams = { + filter: { + name?: string, + domain?: string, + path?: string, + }, +}; +export type BrowserContextRemoveCookiesOptions = { + +}; +export type BrowserContextRemoveCookiesResult = void; export type BrowserContextClearPermissionsParams = {}; export type BrowserContextClearPermissionsOptions = {}; export type BrowserContextClearPermissionsResult = void; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index c21cd006e9..acb847b08b 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1032,6 +1032,15 @@ BrowserContext: clearCookies: + removeCookies: + parameters: + filter: + type: object + properties: + name: string? + domain: string? + path: string? + clearPermissions: close: @@ -3222,7 +3231,7 @@ ElectronApplication: events: close: console: - parameters: + parameters: $mixin: ConsoleMessage Android: diff --git a/tests/library/browsercontext-remove-cookies.spec.ts b/tests/library/browsercontext-remove-cookies.spec.ts new file mode 100644 index 0000000000..e7a3cdaad3 --- /dev/null +++ b/tests/library/browsercontext-remove-cookies.spec.ts @@ -0,0 +1,231 @@ +/** + * Copyright 2018 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { contextTest as it, expect } from '../config/browserTest'; + +it('should remove cookies by name', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/', + }, + { + name: 'cookie2', + value: '2', + domain: 'www.example.com', + path: '/', + } + ]); + await page.goto('https://www.example.com'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2'); + await context.removeCookies({ name: 'cookie1' }); + expect(await page.evaluate('document.cookie')).toBe('cookie2=2'); +}); + +it('should remove cookies by domain', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/', + }, + { + name: 'cookie2', + value: '2', + domain: 'www.example.org', + path: '/', + } + ]); + await page.goto('https://www.example.com'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + await page.goto('https://www.example.org'); + expect(await page.evaluate('document.cookie')).toBe('cookie2=2'); + await context.removeCookies({ domain: 'www.example.org' }); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto('https://www.example.com'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); +}); + +it('should remove cookies by path', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/api/v1', + }, + { + name: 'cookie2', + value: '2', + domain: 'www.example.com', + path: '/api/v2', + }, + { + name: 'cookie3', + value: '3', + domain: 'www.example.com', + path: '/', + } + ]); + await page.goto('https://www.example.com/api/v1'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie3=3'); + await context.removeCookies({ path: '/api/v1' }); + expect(await page.evaluate('document.cookie')).toBe('cookie3=3'); + await page.goto('https://www.example.com/api/v2'); + expect(await page.evaluate('document.cookie')).toBe('cookie2=2; cookie3=3'); + await page.goto('https://www.example.com/'); + expect(await page.evaluate('document.cookie')).toBe('cookie3=3'); +}); + +it('should remove cookies by name and domain', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/', + }, + { + name: 'cookie1', + value: '1', + domain: 'www.example.org', + path: '/', + } + ]); + await page.goto('https://www.example.com'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + await context.removeCookies({ name: 'cookie1', domain: 'www.example.com' }); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto('https://www.example.org'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); +}); + +it('should remove cookies by name and path', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/api/v1', + }, + { + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/api/v2', + }, + { + name: 'cookie3', + value: '3', + domain: 'www.example.com', + path: '/', + } + ]); + await page.goto('https://www.example.com/api/v1'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie3=3'); + await context.removeCookies({ name: 'cookie1', path: '/api/v1' }); + expect(await page.evaluate('document.cookie')).toBe('cookie3=3'); + await page.goto('https://www.example.com/api/v2'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie3=3'); + await page.goto('https://www.example.com/'); + expect(await page.evaluate('document.cookie')).toBe('cookie3=3'); +}); + +it('should remove cookies by domain and path', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/api/v1', + }, + { + name: 'cookie2', + value: '2', + domain: 'www.example.com', + path: '/api/v2', + }, + { + name: 'cookie3', + value: '3', + domain: 'www.example.org', + path: '/api/v1', + }, + { + name: 'cookie4', + value: '4', + domain: 'www.example.org', + path: '/api/v2', + } + ]); + await page.goto('https://www.example.com/api/v1'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + await context.removeCookies({ domain: 'www.example.com', path: '/api/v1' }); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto('https://www.example.com/api/v2'); + expect(await page.evaluate('document.cookie')).toBe('cookie2=2'); + await page.goto('https://www.example.org/api/v2'); + expect(await page.evaluate('document.cookie')).toBe('cookie4=4'); +}); + +it('should remove cookies by name, domain and path', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/api/v1', + }, + { + name: 'cookie2', + value: '2', + domain: 'www.example.com', + path: '/api/v2', + }, + { + name: 'cookie1', + value: '1', + domain: 'www.example.org', + path: '/api/v1', + }, + ]); + await page.goto('https://www.example.com/api/v1'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); + await context.removeCookies({ name: 'cookie1', domain: 'www.example.com', path: '/api/v1' }); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto('https://www.example.com/api/v2'); + expect(await page.evaluate('document.cookie')).toBe('cookie2=2'); + await page.goto('https://www.example.org/api/v1'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1'); +}); + +it('should throw if empty object is passed', async ({ context, page, server }) => { + await context.addCookies([{ + name: 'cookie1', + value: '1', + domain: 'www.example.com', + path: '/', + }, + { + name: 'cookie2', + value: '2', + domain: 'www.example.com', + path: '/', + }, + ]); + await page.goto('https://www.example.com/'); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2'); + const error = await context.removeCookies({ }).catch(e => e); + expect(error.message).toContain(`Either name, domain or path are required`); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2'); +});