diff --git a/packages/driver/cypress/integration/commands/sessions/utils_spec.js b/packages/driver/cypress/integration/commands/sessions/utils_spec.js new file mode 100644 index 0000000000..dcf0705a11 --- /dev/null +++ b/packages/driver/cypress/integration/commands/sessions/utils_spec.js @@ -0,0 +1,244 @@ +const { + getSessionDetails, + getConsoleProps, + navigateAboutBlank, +} = require('@packages/driver/src/cy/commands/sessions/utils') + +describe('src/cy/commands/sessions/utils.ts', () => { + describe('.getSessionDetails', () => { + it('for one domain with neither cookies or local storage set', () => { + const sessionState = { + id: 'session1', + } + + const details = getSessionDetails(sessionState) + + expect(details.id).to.eq('session1') + expect(Object.keys(details.data)).to.have.length(0) + }) + + it('for one domain with only cookies set', () => { + const sessionState = { + id: 'session1', + cookies: [ + { name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 }, + ], + } + + const details = getSessionDetails(sessionState) + + expect(details.id).to.eq('session1') + expect(Object.keys(details.data)).to.have.length(1) + expect(details.data).to.have.property('localhost') + expect(details.data.localhost).to.deep.eq({ + cookies: 1, + }) + }) + + it('for one domain with only local storage set', () => { + const sessionState = { + id: 'session1', + localStorage: [ + { origin: 'localhost', value: { 'stor-foo': 's-f' } }, + ], + } + + const details = getSessionDetails(sessionState) + + expect(details.id).to.eq('session1') + expect(Object.keys(details.data)).to.have.length(1) + expect(details.data).to.have.property('localhost') + expect(details.data.localhost).to.deep.eq({ + localStorage: 1, + }) + }) + + it('for one domain with both cookies and localStorage', () => { + const sessionState = { + id: 'session1', + cookies: [ + { name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 }, + ], + localStorage: [ + { origin: 'localhost', value: { 'stor-foo': 's-f' } }, + ], + } + + const details = getSessionDetails(sessionState) + + expect(details.id).to.eq('session1') + expect(Object.keys(details.data)).to.have.length(1) + expect(details.data).to.have.property('localhost') + expect(details.data.localhost).to.deep.eq({ + cookies: 1, + localStorage: 1, + }) + }) + + it('for multiple domains', () => { + const sessionState = { + id: 'session1', + cookies: [ + { name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 }, + { name: 'bar', value: 'b', path: '/', domain: 'localhost', secure: false, httpOnly: false, expiry: 456 }, + ], + localStorage: [ + { origin: 'localhost', value: { 'stor-foo': 's-f' } }, + { origin: 'http://example.com', value: { 'random': 'hi' } }, + ], + } + + const details = getSessionDetails(sessionState) + + expect(details.id).to.eq('session1') + expect(Object.keys(details.data)).to.have.length(2) + expect(details.data).to.have.property('localhost') + expect(details.data.localhost).to.deep.eq({ + cookies: 2, + localStorage: 1, + }) + + expect(details.data).to.have.property('example.com') + expect(details.data['example.com']).to.deep.eq({ + localStorage: 1, + }) + }) + }) + + describe('.getConsoleProps', () => { + it('for one domain with neither cookies or localStorage set', () => { + const sessionState = { + id: 'session1', + } + + const consoleProps = getConsoleProps(sessionState) + + expect(consoleProps.id).to.eq('session1') + expect(consoleProps.table).to.have.length(0) + }) + + it('for one domain with only cookies set', () => { + const sessionState = { + id: 'session1', + cookies: [ + { name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 }, + ], + } + + const consoleProps = getConsoleProps(sessionState) + + expect(consoleProps.id).to.eq('session1') + expect(consoleProps.table).to.have.length(1) + const cookiesTable = consoleProps.table[0]() + + expect(cookiesTable.name).to.contain('Cookies - localhost (1)') + expect(cookiesTable.data).to.deep.eq(sessionState.cookies) + }) + + it('for one domain with only localStorage set', () => { + const sessionState = { + id: 'session1', + localStorage: [ + { origin: 'localhost', value: { 'stor-foo': 's-f' } }, + ], + } + const consoleProps = getConsoleProps(sessionState) + + expect(consoleProps.id).to.eq('session1') + expect(consoleProps.table).to.have.length(1) + const localStorageTable = consoleProps.table[0]() + + expect(localStorageTable.name).to.contain('Storage - localhost (1)') + expect(localStorageTable.data).to.have.length(1) + expect(localStorageTable.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }]) + }) + + it('for one domain with both cookies and localStorage set', () => { + const sessionState = { + id: 'session1', + cookies: [ + { name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 }, + ], + localStorage: [ + { origin: 'localhost', value: { 'stor-foo': 's-f' } }, + ], + } + + const consoleProps = getConsoleProps(sessionState) + + expect(consoleProps.id).to.eq('session1') + expect(consoleProps.table).to.have.length(2) + let table = consoleProps.table[0]() + + expect(table.name).to.contain('Cookies - localhost (1)') + expect(table.data).to.have.length(1) + expect(table.data).to.deep.eq(sessionState.cookies) + + table = consoleProps.table[1]() + expect(table.name).to.contain('Storage - localhost (1)') + expect(table.data).to.have.length(1) + expect(table.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }]) + }) + + it('for multiple domains', () => { + const sessionState = { + id: 'session1', + cookies: [ + { name: 'foo', value: 'f', path: '/', domain: 'localhost', secure: true, httpOnly: true, expiry: 123 }, + { name: 'bar', value: 'b', path: '/', domain: 'localhost', secure: false, httpOnly: false, expiry: 456 }, + ], + localStorage: [ + { origin: 'localhost', value: { 'stor-foo': 's-f' } }, + { origin: 'http://example.com', value: { 'random': 'hi' } }, + ], + } + + const consoleProps = getConsoleProps(sessionState) + + expect(consoleProps.id).to.eq('session1') + expect(consoleProps.table).to.have.length(3) + let table = consoleProps.table[0]() + + expect(table.name).to.contain('Cookies - localhost (2)') + expect(table.data).to.have.length(2) + expect(table.data).to.deep.eq(sessionState.cookies) + + table = consoleProps.table[1]() + expect(table.name).to.contain('Storage - localhost (1)') + expect(table.data).to.have.length(1) + expect(table.data).to.deep.eq([{ key: 'stor-foo', value: 's-f' }]) + + table = consoleProps.table[2]() + expect(table.name).to.contain('Storage - example.com (1)') + expect(table.data).to.have.length(1) + expect(table.data).to.deep.eq([{ key: 'random', value: 'hi' }]) + }) + }) + + describe('.navigateAboutBlank', () => { + it('triggers session blank page visit', () => { + const stub = cy.stub(Cypress, 'action').log(false) + .callThrough() + .withArgs('cy:visit:blank') + + cy.then(() => { + navigateAboutBlank() + navigateAboutBlank(true) + expect(stub).to.have.been.calledTwice + expect(stub.args[0]).to.deep.eq(['cy:visit:blank', { type: 'session' }]) + expect(stub.args[1]).to.deep.eq(['cy:visit:blank', { type: 'session' }]) + }) + }) + + it('triggers session-lifecycle blank page visit', () => { + const stub = cy.stub(Cypress, 'action').log(false) + .callThrough() + .withArgs('cy:visit:blank') + + cy.then(() => { + navigateAboutBlank(false) + expect(stub).to.have.been.calledWith('cy:visit:blank', { type: 'session-lifecycle' }) + }) + }) + }) +}) diff --git a/packages/driver/src/cy/commands/sessions.ts b/packages/driver/src/cy/commands/sessions/index.ts similarity index 76% rename from packages/driver/src/cy/commands/sessions.ts rename to packages/driver/src/cy/commands/sessions/index.ts index 0f526910b6..4aaedbc4cd 100644 --- a/packages/driver/src/cy/commands/sessions.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -1,10 +1,16 @@ import _ from 'lodash' -import $ from 'jquery' -import { $Location } from '../../cypress/location' -import $errUtils from '../../cypress/error_utils' +import { $Location } from '../../../cypress/location' +import $errUtils from '../../../cypress/error_utils' import stringifyStable from 'json-stable-stringify' -import $stackUtils from '../../cypress/stack_utils' -import Bluebird from 'bluebird' +import $stackUtils from '../../../cypress/stack_utils' +import { + getSessionDetails, + getCurrentOriginStorage, + setPostMessageLocalStorage, + getConsoleProps, + getPostMessageLocalStorage, + navigateAboutBlank, +} from './utils' const currentTestRegisteredSessions = new Map() type ActiveSessions = Cypress.Commands.Session.ActiveSessions @@ -18,193 +24,6 @@ type SessionData = Cypress.Commands.Session.SessionData * therefore session data should be cleared with spec browser launch */ -const getSessionDetails = (sessState: SessionData) => { - return { - id: sessState.id, - data: _.merge( - _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v.length })), - ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: Object.keys(v.value).length } })), - ) } -} - -const getSessionDetailsForTable = (sessState: SessionData) => { - return _.merge( - _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v })), - ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: v } })), - ) -} - -const isSecureContext = (url: string) => url.startsWith('https:') - -const getCurrentOriginStorage = () => { - // localStorage.length property is not always accurate, we must stringify to check for entries - // for ex) try setting localStorage.key = 'val' and reading localStorage.length, may be 0. - const _localStorageStr = JSON.stringify(window.localStorage) - const _localStorage = _localStorageStr.length > 2 && JSON.parse(_localStorageStr) - const _sessionStorageStr = JSON.stringify(window.sessionStorage) - const _sessionStorage = _sessionStorageStr.length > 2 && JSON.parse(JSON.stringify(window.sessionStorage)) - - const value = {} as any - - if (_localStorage) { - value.localStorage = _localStorage - } - - if (_sessionStorage) { - value.sessionStorage = _sessionStorage - } - - return value -} - -const setPostMessageLocalStorage = async (specWindow, originOptions) => { - const origins = originOptions.map((v) => v.origin) as string[] - - const iframes: JQuery[] = [] - - const $iframeContainer = $(`
`).appendTo($('body', specWindow.document)) - - // if we're on an https domain, there is no way for the secure context to access insecure origins from iframes - // since there is no way for the app to access localStorage on insecure contexts, we don't have to clear any localStorage on http domains. - if (isSecureContext(specWindow.location.href)) { - _.remove(origins, (v) => !isSecureContext(v)) - } - - if (!origins.length) return [] - - _.each(origins, (u) => { - const $iframe = $(``) - - $iframe.appendTo($iframeContainer) - iframes.push($iframe) - }) - - let onPostMessage - - const successOrigins = [] as string[] - - return new Bluebird((resolve) => { - onPostMessage = (event) => { - const data = event.data - - if (data.type === 'set:storage:load') { - if (!event.source) { - throw new Error('failed to get localStorage') - } - - const opts = _.find(originOptions, { origin: event.origin })! - - event.source.postMessage({ type: 'set:storage:data', data: opts }, '*') - } else if (data.type === 'set:storage:complete') { - successOrigins.push(event.origin) - if (successOrigins.length === origins.length) { - resolve() - } - } - } - - specWindow.addEventListener('message', onPostMessage) - }) - // timeout just in case something goes wrong and the iframe never loads in - .timeout(2000) - .finally(() => { - specWindow.removeEventListener('message', onPostMessage) - $iframeContainer.remove() - }) - .catch(() => { - Cypress.log({ - name: 'warning', - message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, - }) - }) -} - -const getConsoleProps = (sessState: SessionData) => { - const sessionDetails = getSessionDetailsForTable(sessState) - - const tables = _.flatMap(sessionDetails, (val, domain) => { - const cookiesTable = () => { - return { - name: `🍪 Cookies - ${domain} (${val.cookies.length})`, - data: val.cookies, - } - } - - const localStorageTable = () => { - return { - name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`, - data: _.map(val.localStorage.value, (value, key) => { - return { - key, - value, - } - }), - } - } - - return [ - val.cookies && cookiesTable, - val.localStorage && localStorageTable, - ] - }) - - return { - id: sessState.id, - table: _.compact(tables), - } -} - -const getPostMessageLocalStorage = (specWindow, origins): Promise => { - const results = [] as any[] - const iframes: JQuery[] = [] - let onPostMessage - const successOrigins = [] as string[] - - const $iframeContainer = $(`
`).appendTo($('body', specWindow.document)) - - _.each(origins, (u) => { - const $iframe = $(``) - - $iframe.appendTo($iframeContainer) - iframes.push($iframe) - }) - - return new Bluebird((resolve) => { - // when the cross-domain iframe for each domain is loaded - // we can only communicate through postmessage - onPostMessage = ((event) => { - const data = event.data - - if (data.type !== 'localStorage') return - - const value = data.value - - results.push([event.origin, value]) - - successOrigins.push(event.origin) - if (successOrigins.length === origins.length) { - resolve(results) - } - }) - - specWindow.addEventListener('message', onPostMessage) - }) - // timeout just in case something goes wrong and the iframe never loads in - .timeout(2000) - .finally(() => { - specWindow.removeEventListener('message', onPostMessage) - $iframeContainer.remove() - }) - .catch((err) => { - Cypress.log({ - name: 'warning', - message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, - }) - - return [] - }) -} - export default function (Commands, Cypress, cy) { const { Promise } = Cypress @@ -867,9 +686,3 @@ export default function (Commands, Cypress, cy) { Cypress.session = sessions } - -function navigateAboutBlank (session = true) { - Cypress.action('cy:url:changed', '') - - return Cypress.action('cy:visit:blank', { type: session ? 'session' : 'session-lifecycle' }) as unknown as Promise -} diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts new file mode 100644 index 0000000000..9f20ebea50 --- /dev/null +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -0,0 +1,208 @@ +import _ from 'lodash' +import $ from 'jquery' +import { $Location } from '../../../cypress/location' +import Bluebird from 'bluebird' + +type SessionData = Cypress.Commands.Session.SessionData + +const getSessionDetails = (sessState: SessionData) => { + return { + id: sessState.id, + data: _.merge( + _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v.length })), + ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: Object.keys(v.value).length } })), + ) } +} + +const getSessionDetailsForTable = (sessState: SessionData) => { + return _.merge( + _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v })), + ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: v } })), + ) +} + +const isSecureContext = (url: string) => url.startsWith('https:') + +const getCurrentOriginStorage = () => { + // localStorage.length property is not always accurate, we must stringify to check for entries + // for ex) try setting localStorage.key = 'val' and reading localStorage.length, may be 0. + const _localStorageStr = JSON.stringify(window.localStorage) + const _localStorage = _localStorageStr.length > 2 && JSON.parse(_localStorageStr) + const _sessionStorageStr = JSON.stringify(window.sessionStorage) + const _sessionStorage = _sessionStorageStr.length > 2 && JSON.parse(JSON.stringify(window.sessionStorage)) + + const value = {} as any + + if (_localStorage) { + value.localStorage = _localStorage + } + + if (_sessionStorage) { + value.sessionStorage = _sessionStorage + } + + return value +} + +const setPostMessageLocalStorage = async (specWindow, originOptions) => { + const origins = originOptions.map((v) => v.origin) as string[] + + const iframes: JQuery[] = [] + + const $iframeContainer = $(`
`).appendTo($('body', specWindow.document)) + + // if we're on an https domain, there is no way for the secure context to access insecure origins from iframes + // since there is no way for the app to access localStorage on insecure contexts, we don't have to clear any localStorage on http domains. + if (isSecureContext(specWindow.location.href)) { + _.remove(origins, (v) => !isSecureContext(v)) + } + + if (!origins.length) return [] + + _.each(origins, (u) => { + const $iframe = $(``) + + $iframe.appendTo($iframeContainer) + iframes.push($iframe) + }) + + let onPostMessage + + const successOrigins = [] as string[] + + return new Bluebird((resolve) => { + onPostMessage = (event) => { + const data = event.data + + if (data.type === 'set:storage:load') { + if (!event.source) { + throw new Error('failed to get localStorage') + } + + const opts = _.find(originOptions, { origin: event.origin })! + + event.source.postMessage({ type: 'set:storage:data', data: opts }, '*') + } else if (data.type === 'set:storage:complete') { + successOrigins.push(event.origin) + if (successOrigins.length === origins.length) { + resolve() + } + } + } + + specWindow.addEventListener('message', onPostMessage) + }) + // timeout just in case something goes wrong and the iframe never loads in + .timeout(2000) + .finally(() => { + specWindow.removeEventListener('message', onPostMessage) + $iframeContainer.remove() + }) + .catch(() => { + Cypress.log({ + name: 'warning', + message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, + }) + }) +} + +const getConsoleProps = (sessState: SessionData) => { + const sessionDetails = getSessionDetailsForTable(sessState) + + const tables = _.flatMap(sessionDetails, (val, domain) => { + const cookiesTable = () => { + return { + name: `🍪 Cookies - ${domain} (${val.cookies.length})`, + data: val.cookies, + } + } + + const localStorageTable = () => { + return { + name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`, + data: _.map(val.localStorage.value, (value, key) => { + return { + key, + value, + } + }), + } + } + + return [ + val.cookies && cookiesTable, + val.localStorage && localStorageTable, + ] + }) + + return { + id: sessState.id, + table: _.compact(tables), + } +} + +const getPostMessageLocalStorage = (specWindow, origins): Promise => { + const results = [] as any[] + const iframes: JQuery[] = [] + let onPostMessage + const successOrigins = [] as string[] + + const $iframeContainer = $(`
`).appendTo($('body', specWindow.document)) + + _.each(origins, (u) => { + const $iframe = $(``) + + $iframe.appendTo($iframeContainer) + iframes.push($iframe) + }) + + return new Bluebird((resolve) => { + // when the cross-domain iframe for each domain is loaded + // we can only communicate through postmessage + onPostMessage = ((event) => { + const data = event.data + + if (data.type !== 'localStorage') return + + const value = data.value + + results.push([event.origin, value]) + + successOrigins.push(event.origin) + if (successOrigins.length === origins.length) { + resolve(results) + } + }) + + specWindow.addEventListener('message', onPostMessage) + }) + // timeout just in case something goes wrong and the iframe never loads in + .timeout(2000) + .finally(() => { + specWindow.removeEventListener('message', onPostMessage) + $iframeContainer.remove() + }) + .catch((err) => { + Cypress.log({ + name: 'warning', + message: `failed to access session localStorage data on origin(s): ${_.xor(origins, successOrigins).join(', ')}`, + }) + + return [] + }) +} + +function navigateAboutBlank (session: boolean = true) { + Cypress.action('cy:url:changed', '') + + return Cypress.action('cy:visit:blank', { type: session ? 'session' : 'session-lifecycle' }) as unknown as Promise +} + +export { + getSessionDetails, + getCurrentOriginStorage, + setPostMessageLocalStorage, + getConsoleProps, + getPostMessageLocalStorage, + navigateAboutBlank, +}