chore(sessions): break out session utils and write some unit tests (#21048)

Co-authored-by: Matt Schile <mschile@gmail.com>
Co-authored-by: Bill Glesias <bglesias@gmail.com>
This commit is contained in:
Emily Rohrbough
2022-04-19 11:00:16 -05:00
committed by GitHub
parent afca88e7a9
commit 86f5b49d2c
3 changed files with 463 additions and 198 deletions

View File

@@ -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' })
})
})
})
})

View File

@@ -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<HTMLElement>[] = []
const $iframeContainer = $(`<div style="display:none"></div>`).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 src="${`${u}/__cypress/automation/setLocalStorage?${u}`}"></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<any[]> => {
const results = [] as any[]
const iframes: JQuery<HTMLElement>[] = []
let onPostMessage
const successOrigins = [] as string[]
const $iframeContainer = $(`<div style="display:none"></div>`).appendTo($('body', specWindow.document))
_.each(origins, (u) => {
const $iframe = $(`<iframe src="${`${u}/__cypress/automation/getLocalStorage`}"></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<void>
}

View File

@@ -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<HTMLElement>[] = []
const $iframeContainer = $(`<div style="display:none"></div>`).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 src="${`${u}/__cypress/automation/setLocalStorage?${u}`}"></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<any[]> => {
const results = [] as any[]
const iframes: JQuery<HTMLElement>[] = []
let onPostMessage
const successOrigins = [] as string[]
const $iframeContainer = $(`<div style="display:none"></div>`).appendTo($('body', specWindow.document))
_.each(origins, (u) => {
const $iframe = $(`<iframe src="${`${u}/__cypress/automation/getLocalStorage`}"></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<void>
}
export {
getSessionDetails,
getCurrentOriginStorage,
setPostMessageLocalStorage,
getConsoleProps,
getPostMessageLocalStorage,
navigateAboutBlank,
}