diff --git a/docs/api.md b/docs/api.md index 66f80185..12e0ed9f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -34,6 +34,8 @@ + [page.click(selector[, options])](#pageclickselector-options) + [page.close()](#pageclose) + [page.content()](#pagecontent) + + [page.cookies(...urls)](#pagecookiesurls) + + [page.deleteCookie(...cookies)](#pagedeletecookiecookies) + [page.emulate(options)](#pageemulateoptions) + [page.emulateMedia(mediaType)](#pageemulatemediamediatype) + [page.evaluate(pageFunction, ...args)](#pageevaluatepagefunction-args) @@ -55,6 +57,7 @@ + [page.reload(options)](#pagereloadoptions) + [page.screenshot([options])](#pagescreenshotoptions) + [page.setContent(html)](#pagesetcontenthtml) + + [page.setCookie(...cookies)](#pagesetcookiecookies) + [page.setExtraHTTPHeaders(headers)](#pagesetextrahttpheadersheaders) + [page.setJavaScriptEnabled(enabled)](#pagesetjavascriptenabledenabled) + [page.setRequestInterceptionEnabled(value)](#pagesetrequestinterceptionenabledvalue) @@ -338,6 +341,30 @@ If there's no element matching `selector`, the method throws an error. Gets the full HTML contents of the page, including the doctype. +#### page.cookies(...urls) +- `...urls` <...[string]> +- returns: <[Promise]<[Array]<[Object]>>> + - `name` <[string]> + - `value` <[string]> + - `domain` <[string]> + - `path` <[string]> + - `expires` <[number]> Unix time in seconds. + - `httpOnly` <[boolean]> + - `secure` <[boolean]> + - `sameSite` <[string]> `"Strict"` or `"Lax"`. + +If no URLs are specified, this method returns cookies for the current page URL. +If URLs are specified, only cookies for those URLs are returned. + +#### page.deleteCookie(...cookies) +- `...cookies` <...[Object]> + - `name` <[string]> **required** + - `url` <[string]> + - `domain` <[string]> + - `path` <[string]> + - `secure` <[boolean]> +- returns: <[Promise]> + #### page.emulate(options) - `options` <[Object]> - `viewport` <[Object]> @@ -652,6 +679,19 @@ Shortcut for [`keyboard.down`](#keyboarddownkey-options) and [`keyboard.up`](#ke - `html` <[string]> HTML markup to assign to the page. - returns: <[Promise]> +#### page.setCookie(...cookies) +- `...cookies` <...[Object]> + - `name` <[string]> **required** + - `value` <[string]> **required** + - `url` <[string]> + - `domain` <[string]> + - `path` <[string]> + - `expires` <[number]> Unix time in seconds. + - `httpOnly` <[boolean]> + - `secure` <[boolean]> + - `sameSite` <[string]> `"Strict"` or `"Lax"`. +- returns: <[Promise]> + #### page.setExtraHTTPHeaders(headers) - `headers` <[Map]> A map of additional http headers to be sent with every request. - returns: <[Promise]> diff --git a/lib/Page.js b/lib/Page.js index 1ae29623..55093415 100644 --- a/lib/Page.js +++ b/lib/Page.js @@ -154,6 +154,45 @@ class Page extends EventEmitter { return this.mainFrame().$$(selector); } + /** + * @param {!Array} urls + * @return {!Promise>} + */ + async cookies(...urls) { + return (await this._client.send('Network.getCookies', { + urls: urls.length ? urls : [this.url()] + })).cookies; + } + + /** + * @param {Array} cookies + */ + async deleteCookie(...cookies) { + const pageURL = this.url(); + for (const cookie of cookies) { + const item = Object.assign({}, cookie); + if (!cookie.url && pageURL.startsWith('http')) + item.url = pageURL; + await this._client.send('Network.deleteCookies', item); + } + } + + /** + * @param {Array} cookies + */ + async setCookie(...cookies) { + const items = cookies.map(cookie => { + const item = Object.assign({}, cookie); + const pageURL = this.url(); + if (!item.url && pageURL.startsWith('http')) + item.url = this.url(); + return item; + }); + await this.deleteCookie(...items); + if (items.length) + await this._client.send('Network.setCookies', { cookies: items }); + } + /** * @param {string} url * @return {!Promise} @@ -758,5 +797,34 @@ Page.Events = { /** @typedef {{width: number, height: number, deviceScaleFactor: number|undefined, isMobile: boolean|undefined, isLandscape: boolean, hasTouch: boolean|undefined}} */ Page.Viewport; +/** + * @typedef {Object} Network.Cookie + * @property {string} name + * @property {string} value + * @property {string} domain + * @property {string} path + * @property {number} expires + * @property {number} size + * @property {boolean} httpOnly + * @property {boolean} secure + * @property {boolean} session + * @property {("Strict"|"Lax")=} sameSite + */ + + +/** + * @typedef {Object} Network.CookieParam + * @property {string} name + * @property {string=} value + * @property {string=} url + * @property {string=} domain + * @property {string=} path + * @property {number=} expires + * @property {boolean=} httpOnly + * @property {boolean=} secure + * @property {("Strict"|"Lax")=} sameSite + */ + + module.exports = Page; helper.tracePublicAPI(Page); diff --git a/phantom_shim/WebPage.js b/phantom_shim/WebPage.js index af61a881..6b74937c 100644 --- a/phantom_shim/WebPage.js +++ b/phantom_shim/WebPage.js @@ -207,6 +207,25 @@ class WebPage { this._currentFrame = this._page.mainFrame(); } + get cookies() { + return await(this._page.cookies()); + } + + set cookies(cookies) { + const cookies2 = await(this._page.cookies()); + await(this._page.deleteCookie(...cookies2)); + await(this._page.setCookie(...cookies)); + + } + + addCookie(cookie) { + await(this._page.setCookie(cookie)); + } + + deleteCookie(cookieName) { + await(this._page.deleteCookie({name: cookieName})); + } + get onInitialized() { return this._onInitialized; } diff --git a/test/test.js b/test/test.js index f5e57ab3..454138d5 100644 --- a/test/test.js +++ b/test/test.js @@ -1920,6 +1920,162 @@ describe('Page', function() { await page.tracing.stop(); })); }); + + describe('Cookies', function() { + afterEach(SX(async function(){ + const cookies = await page.cookies(PREFIX + '/grid.html', CROSS_PROCESS_PREFIX); + for (const cookie of cookies) + await page.deleteCookie(cookie); + })); + it('should set and get cookies', SX(async function(){ + await page.goto(PREFIX + '/grid.html'); + expect(await page.cookies()).toEqual([]); + await page.evaluate(() => { + document.cookie = 'username=John Doe'; + }); + expect(await page.cookies()).toEqual([{ + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: 0, + size: 16, + httpOnly: false, + secure: false, + session: true } + ]); + await page.setCookie({ + name: 'password', + value: '123456' + }); + expect(await page.evaluate('document.cookie')).toBe('username=John Doe; password=123456'); + expect(await page.cookies()).toEqual([{ + name: 'password', + value: '123456', + domain: 'localhost', + path: '/', + expires: 0, + size: 14, + httpOnly: false, + secure: false, + session: true + }, { + name: 'username', + value: 'John Doe', + domain: 'localhost', + path: '/', + expires: 0, + size: 16, + httpOnly: false, + secure: false, + session: true + }]); + })); + + it('should set a cookie with a path', SX(async function(){ + await page.goto(PREFIX + '/grid.html'); + await page.setCookie({ + name: 'gridcookie', + value: 'GRID', + path: '/grid.html' + }); + expect(await page.cookies()).toEqual([{ + name: 'gridcookie', + value: 'GRID', + domain: 'localhost', + path: '/grid.html', + expires: 0, + size: 14, + httpOnly: false, + secure: false, + session: true + }]); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + await page.goto(PREFIX + '/empty.html'); + expect(await page.cookies()).toEqual([]); + expect(await page.evaluate('document.cookie')).toBe(''); + await page.goto(PREFIX + '/grid.html'); + expect(await page.evaluate('document.cookie')).toBe('gridcookie=GRID'); + })); + + + it('should delete a cookie', SX(async function(){ + await page.goto(PREFIX + '/grid.html'); + await page.setCookie({ + name: 'cookie1', + value: '1' + }, { + name: 'cookie2', + value: '2' + }, { + name: 'cookie3', + value: '3' + }); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie2=2; cookie3=3'); + await page.deleteCookie({name: 'cookie2'}); + expect(await page.evaluate('document.cookie')).toBe('cookie1=1; cookie3=3'); + })); + + it('should set a cookie on a different domain', SX(async function() { + await page.goto(PREFIX + '/grid.html'); + await page.setCookie({name: 'example-cookie', value: 'best', url: 'https://www.example.com'}); + expect(await page.evaluate('document.cookie')).toBe(''); + expect(await page.cookies()).toEqual([]); + expect(await page.cookies('https://www.example.com')).toEqual([{ + name: 'example-cookie', + value: 'best', + domain: 'www.example.com', + path: '/', + expires: 0, + size: 18, + httpOnly: false, + secure: true, + session: true + }]); + })); + + it('should set cookies from a frame', SX(async function() { + await page.goto(PREFIX + '/grid.html'); + await page.setCookie({name: 'localhost-cookie', value: 'best'}); + await page.evaluate(src => { + let fulfill; + const promise = new Promise(x => fulfill = x); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.onload = fulfill; + iframe.src = src; + return promise; + }, CROSS_PROCESS_PREFIX); + await page.setCookie({name: '127-cookie', value: 'worst', url: CROSS_PROCESS_PREFIX}); + expect(await page.evaluate('document.cookie')).toBe('localhost-cookie=best'); + expect(await page.frames()[1].evaluate('document.cookie')).toBe('127-cookie=worst'); + + expect(await page.cookies()).toEqual([{ + name: 'localhost-cookie', + value: 'best', + domain: 'localhost', + path: '/', + expires: 0, + size: 20, + httpOnly: false, + secure: false, + session: true + }]); + + expect(await page.cookies(CROSS_PROCESS_PREFIX)).toEqual([{ + name: '127-cookie', + value: 'worst', + domain: '127.0.0.1', + path: '/', + expires: 0, + size: 15, + httpOnly: false, + secure: false, + session: true + }]); + + })); + }); }); if (process.env.COVERAGE) { diff --git a/third_party/phantomjs/test/module/webpage/cookies.js b/third_party/phantomjs/test/module/webpage/cookies.js index e1137a5d..344dffb1 100644 --- a/third_party/phantomjs/test/module/webpage/cookies.js +++ b/third_party/phantomjs/test/module/webpage/cookies.js @@ -1,23 +1,24 @@ -//! unsupported async_test(function () { var url = TEST_HTTP_BASE + "echo"; - var page = new WebPage(); + var webpage = require('webpage'); + + var page = webpage.create(); page.cookies = [{ 'name' : 'Valid-Cookie-Name', 'value' : 'Valid-Cookie-Value', 'domain' : 'localhost', 'path' : '/', - 'httponly' : true, + 'httpOnly' : true, 'secure' : false },{ 'name' : 'Valid-Cookie-Name-Sec', 'value' : 'Valid-Cookie-Value-Sec', 'domain' : 'localhost', 'path' : '/', - 'httponly' : true, + 'httpOnly' : true, 'secure' : false, - 'expires' : Date.now() + 3600 //< expires in 1h + 'expires': (Date.now()/1000) + 3600 }]; page.open(url, this.step_func(function (status) { @@ -41,7 +42,9 @@ async_test(function () { async_test(function () { var url = TEST_HTTP_BASE + "echo"; - var page = new WebPage(); + var webpage = require('webpage'); + + var page = webpage.create(); page.addCookie({ 'name' : 'Added-Cookie-Name', @@ -68,7 +71,9 @@ async_test(function () { async_test(function () { var url = TEST_HTTP_BASE + "echo"; - var page = new WebPage(); + var webpage = require('webpage'); + + var page = webpage.create(); page.cookies = [ { // domain mismatch. @@ -85,7 +90,7 @@ async_test(function () { 'name' : 'Invalid-Cookie-Name-3', 'value' : 'Invalid-Cookie-Value-3', 'domain' : 'localhost', - 'expires' : 'Sat, 01 Jan 2000 00:00:00 GMT' + 'expires' : 5 },{ // https only: the cookie will be set, // but won't be visible from the given URL (not https). 'name' : 'Invalid-Cookie-Name-4', @@ -96,12 +101,7 @@ async_test(function () { 'name' : 'Invalid-Cookie-Name-5', 'value' : 'Invalid-Cookie-Value-5', 'domain' : 'localhost', - 'expires' : new Date().getTime() - 1 //< date in the past - },{ // cookie expired (date in "sec since epoch" - using "expiry"). - 'name' : 'Invalid-Cookie-Name-6', - 'value' : 'Invalid-Cookie-Value-6', - 'domain' : 'localhost', - 'expiry' : new Date().getTime() - 1 //< date in the past + 'expires' : (Date.now()/1000) - 10 //< date in the past }]; page.open(url, this.step_func_done(function (status) {