From 203b2d660304d557629e31bb146bb9be3d457bcc Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 25 Apr 2022 17:23:48 -0500 Subject: [PATCH 01/22] chore(sessions): break out sessions manager code --- .../driver/src/cy/commands/sessions/index.ts | 318 +---------------- .../src/cy/commands/sessions/sessions.ts | 334 ++++++++++++++++++ packages/driver/types/internal-types.d.ts | 7 + 3 files changed, 349 insertions(+), 310 deletions(-) create mode 100644 packages/driver/src/cy/commands/sessions/sessions.ts diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index e901955df5c6..e56453fa23a2 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -1,334 +1,32 @@ import _ from 'lodash' -import { $Location } from '../../../cypress/location' -import $errUtils from '../../../cypress/error_utils' import stringifyStable from 'json-stable-stringify' +import $errUtils from '../../../cypress/error_utils' import $stackUtils from '../../../cypress/stack_utils' +import SessionsManager from './sessions' import { getSessionDetails, - getCurrentOriginStorage, - setPostMessageLocalStorage, getConsoleProps, - getPostMessageLocalStorage, navigateAboutBlank, } from './utils' + const currentTestRegisteredSessions = new Map() -type ActiveSessions = Cypress.Commands.Session.ActiveSessions type SessionData = Cypress.Commands.Session.SessionData -/** - * rules for clearing session data: - * - if page reloads due to top navigation OR user hard reload, session data should NOT be cleared - * - if user relaunches the browser or launches a new spec, session data SHOULD be cleared - * - session data SHOULD be cleared between specs in run mode - * - * therefore session data should be cleared with spec browser launch - */ export default function (Commands, Cypress, cy) { - const { Promise } = Cypress - - const setActiveSession = (obj: ActiveSessions) => { - const currentSessions = cy.state('activeSessions') || {} - - const newSessions = { ...currentSessions, ...obj } - - cy.state('activeSessions', newSessions) - } - - const getActiveSession = (id: string): SessionData => { - const currentSessions = cy.state('activeSessions') || {} - - return currentSessions[id] - } - - const clearActiveSessions = () => { - const curSessions = cy.state('activeSessions') || {} - - cy.state('activeSessions', _.mapValues(curSessions, (v) => ({ ...v, hydrated: false }))) - } - - async function mapOrigins (origins) { - const currentOrigin = $Location.create(window.location.href).origin - - return _.uniq( - _.flatten(await Promise.map( - ([] as string[]).concat(origins), async (v) => { - if (v === '*') { - return _.keys(await Cypress.backend('get:rendered:html:origins')).concat([currentOrigin]) - } - - if (v === 'currentOrigin') return currentOrigin - - return $Location.create(v).origin - }, - )), - ) as string[] - } - - async function _setStorageOnOrigins (originOptions) { - const specWindow = cy.state('specWindow') - - const currentOrigin = $Location.create(window.location.href).origin - - const currentOriginIndex = _.findIndex(originOptions, { origin: currentOrigin }) - - if (currentOriginIndex !== -1) { - const opts = originOptions.splice(currentOriginIndex, 1)[0] - - if (!_.isEmpty(opts.localStorage)) { - if (opts.localStorage.clear) { - window.localStorage.clear() - } - - _.each(opts.localStorage.value, (val, key) => localStorage.setItem(key, val)) - } - - if (opts.sessionStorage) { - if (opts.sessionStorage.clear) { - window.sessionStorage.clear() - } - - _.each(opts.sessionStorage.value, (val, key) => sessionStorage.setItem(key, val)) - } - } - - if (_.isEmpty(originOptions)) { - return - } - - await setPostMessageLocalStorage(specWindow, originOptions) - } - - async function getAllHtmlOrigins () { - const currentOrigin = $Location.create(window.location.href).origin - - const origins = _.uniq([..._.keys(await Cypress.backend('get:rendered:html:origins')), currentOrigin]) as string[] - - return origins - } - function throwIfNoSessionSupport () { if (!Cypress.config('experimentalSessionAndOrigin')) { $errUtils.throwErrByPath('sessions.experimentNotEnabled', { args: { + // determine if using removed <9.6.0 experimental session opt-in flag to + // generate a coherent error message experimentalSessionSupport: Cypress.config('experimentalSessionSupport'), }, }) } } - const sessions = { - defineSession (options = {} as any): SessionData { - const sess_state: SessionData = { - id: options.id, - cookies: null, - localStorage: null, - setup: options.setup, - hydrated: false, - validate: options.validate, - } - - setActiveSession({ [sess_state.id]: sess_state }) - - return sess_state - }, - - async clearAllSavedSessions () { - clearActiveSessions() - - return Cypress.backend('clear:session', null) - }, - - async clearCurrentSessionData () { - window.localStorage.clear() - window.sessionStorage.clear() - - await Promise.all([ - sessions.clearStorage(), - sessions.clearCookies(), - ]) - }, - - async setSessionData (data) { - await sessions.clearCurrentSessionData() - const allHtmlOrigins = await getAllHtmlOrigins() - - let _localStorage = data.localStorage || [] - let _sessionStorage = data.sessionStorage || [] - - _.each(allHtmlOrigins, (v) => { - if (!_.find(_localStorage, v)) { - _localStorage = _localStorage.concat({ origin: v, clear: true }) - } - - if (!_.find(_sessionStorage, v)) { - _sessionStorage = _sessionStorage.concat({ origin: v, clear: true }) - } - }) - - await Promise.all([ - sessions.setStorage({ localStorage: _localStorage, sessionStorage: _sessionStorage }), - Cypress.automation('clear:cookies', null), - ]) - - await sessions.setCookies(data.cookies) - }, - - getCookies () { - return Cypress.automation('get:cookies', {}) - }, - - setCookies (data) { - return Cypress.automation('set:cookies', data) - }, - - async clearCookies () { - return Cypress.automation('clear:cookies', await sessions.getCookies()) - }, - - async getCurrentSessionData () { - const storage = await sessions.getStorage({ origin: '*' }) - - let cookies = [] as any[] - - cookies = await Cypress.automation('get:cookies', {}) - - const ses = { - ...storage, - cookies, - } - - return ses - }, - - getSession (id) { - return Cypress.backend('get:session', id) - }, - - /** - * 1) if we only need currentOrigin localStorage, access sync - * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* - * and postMessage() the localStorage value to us - * 3) if cross-origin https, since we pass-thru https conntections in the proxy, we need to - * send a message telling our proxy server to intercept the next req to the https domain, - * then follow 2) - */ - async getStorage (options = {}) { - const specWindow = cy.state('specWindow') - - if (!_.isObject(options)) { - throw new Error('getStorage() takes an object') - } - - const opts = _.defaults({}, options, { - origin: 'currentOrigin', - }) - - const currentOrigin = $Location.create(window.location.href).origin - - const origins = await mapOrigins(opts.origin) - - const getResults = () => { - return results - } - const results = { - localStorage: [] as any[], - sessionStorage: [] as any[], - } - - function pushValue (origin, value) { - if (!_.isEmpty(value.localStorage)) { - results.localStorage.push({ origin, value: value.localStorage }) - } - - if (!_.isEmpty(value.sessionStorage)) { - results.sessionStorage.push({ origin, value: value.sessionStorage }) - } - } - - const currentOriginIndex = origins.indexOf(currentOrigin) - - if (currentOriginIndex !== -1) { - origins.splice(currentOriginIndex, 1) - const currentOriginStorage = getCurrentOriginStorage() - - pushValue(currentOrigin, currentOriginStorage) - } - - if (_.isEmpty(origins)) { - return getResults() - } - - if (currentOrigin.startsWith('https:')) { - _.remove(origins, (v) => v.startsWith('http:')) - } - - const postMessageResults = await getPostMessageLocalStorage(specWindow, origins) - - postMessageResults.forEach((val) => { - pushValue(val[0], val[1]) - }) - - return getResults() - }, - - async clearStorage () { - const origins = await getAllHtmlOrigins() - - const originOptions = origins.map((v) => ({ origin: v, clear: true })) - - await sessions.setStorage({ - localStorage: originOptions, - sessionStorage: originOptions, - }) - }, - - async setStorage (options: any, clearAll = false) { - const currentOrigin = $Location.create(window.location.href).origin as string - - const mapToCurrentOrigin = (v) => ({ ...v, origin: (v.origin && v.origin !== 'currentOrigin') ? $Location.create(v.origin).origin : currentOrigin }) - - const mappedLocalStorage = _.map(options.localStorage, (v) => { - const mapped = { origin: v.origin, localStorage: _.pick(v, 'value', 'clear') } - - if (clearAll) { - mapped.localStorage.clear = true - } - - return mapped - }).map(mapToCurrentOrigin) - - const mappedSessionStorage = _.map(options.sessionStorage, (v) => { - const mapped = { origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') } - - if (clearAll) { - mapped.sessionStorage.clear = true - } - - return mapped - }).map(mapToCurrentOrigin) - - const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v)) - - await _setStorageOnOrigins(storageOptions) - }, - - registerSessionHooks () { - Cypress.on('test:before:run:async', () => { - if (Cypress.config('experimentalSessionAndOrigin')) { - currentTestRegisteredSessions.clear() - - return navigateAboutBlank(false) - .then(() => sessions.clearCurrentSessionData()) - .then(() => { - return Cypress.backend('reset:rendered:html:origins') - }) - } - - return - }) - }, - } + const sessions = new SessionsManager(Cypress, cy) Cypress.on('run:start', () => { sessions.registerSessionHooks() @@ -447,7 +145,7 @@ export default function (Commands, Cypress, cy) { _.extend(existingSession, data) existingSession.hydrated = true - setActiveSession({ [existingSession.id]: existingSession }) + sessions.setActiveSession({ [existingSession.id]: existingSession }) dataLog.set({ consoleProps: () => getConsoleProps(existingSession), @@ -688,5 +386,5 @@ export default function (Commands, Cypress, cy) { }, }) - Cypress.session = sessions + Cypress.session = sessions.publicAPI() } diff --git a/packages/driver/src/cy/commands/sessions/sessions.ts b/packages/driver/src/cy/commands/sessions/sessions.ts new file mode 100644 index 000000000000..d569bafe2134 --- /dev/null +++ b/packages/driver/src/cy/commands/sessions/sessions.ts @@ -0,0 +1,334 @@ +import _ from 'lodash' +import { $Location } from '../../../cypress/location' + +import { + getCurrentOriginStorage, + setPostMessageLocalStorage, + getPostMessageLocalStorage, + navigateAboutBlank, +} from './utils' + +const currentTestRegisteredSessions = new Map() + +type ActiveSessions = Cypress.Commands.Session.ActiveSessions +type SessionData = Cypress.Commands.Session.SessionData + +export default class SessionsManager { + Cypress + cy + + constructor (Cypress, cy) { + this.Cypress = Cypress + this.cy = cy + } + + setActiveSession (obj: ActiveSessions) { + const currentSessions = this.cy.state('activeSessions') || {} + + const newSessions = { ...currentSessions, ...obj } + + this.cy.state('activeSessions', newSessions) + } + + getActiveSession (id: string): SessionData { + const currentSessions = this.cy.state('activeSessions') || {} + + return currentSessions[id] + } + + clearActiveSessions () { + const curSessions = this.cy.state('activeSessions') || {} + + this.cy.state('activeSessions', _.mapValues(curSessions, (v) => ({ ...v, hydrated: false }))) + } + + async mapOrigins (origins: string) { + const currentOrigin = $Location.create(window.location.href).origin + + return _.uniq( + _.flatten(await this.Cypress.Promise.map( + ([] as string[]).concat(origins), async (v) => { + if (v === '*') { + return _.keys(await this.Cypress.backend('get:rendered:html:origins')).concat([currentOrigin]) + } + + if (v === 'currentOrigin') return currentOrigin + + return $Location.create(v).origin + }, + )), + ) as string[] + } + + async _setStorageOnOrigins (originOptions) { + const specWindow = this.cy.state('specWindow') + + const currentOrigin = $Location.create(window.location.href).origin + + const currentOriginIndex = _.findIndex(originOptions, { origin: currentOrigin }) + + if (currentOriginIndex !== -1) { + const opts = originOptions.splice(currentOriginIndex, 1)[0] + + if (!_.isEmpty(opts.localStorage)) { + if (opts.localStorage.clear) { + window.localStorage.clear() + } + + _.each(opts.localStorage.value, (val, key) => localStorage.setItem(key, val)) + } + + if (opts.sessionStorage) { + if (opts.sessionStorage.clear) { + window.sessionStorage.clear() + } + + _.each(opts.sessionStorage.value, (val, key) => sessionStorage.setItem(key, val)) + } + } + + if (_.isEmpty(originOptions)) { + return + } + + await setPostMessageLocalStorage(specWindow, originOptions) + } + + async getAllHtmlOrigins () { + const currentOrigin = $Location.create(window.location.href).origin + + const origins = _.uniq([..._.keys(await Cypress.backend('get:rendered:html:origins')), currentOrigin]) as string[] + + return origins + } + + defineSession (options = {} as any): SessionData { + const sess_state: SessionData = { + id: options.id, + cookies: null, + localStorage: null, + setup: options.setup, + hydrated: false, + validate: options.validate, + } + + this.setActiveSession({ [sess_state.id]: sess_state }) + + return sess_state + } + + async clearAllSavedSessions () { + this.clearActiveSessions() + + return this.Cypress.backend('clear:session', null) + } + + async clearCurrentSessionData () { + window.localStorage.clear() + window.sessionStorage.clear() + + await Promise.all([ + this.clearStorage(), + this.clearCookies(), + ]) + } + + async setSessionData (data) { + await this.clearCurrentSessionData() + const allHtmlOrigins = await this.getAllHtmlOrigins() + + let _localStorage = data.localStorage || [] + let _sessionStorage = data.sessionStorage || [] + + _.each(allHtmlOrigins, (v) => { + if (!_.find(_localStorage, v)) { + _localStorage = _localStorage.concat({ origin: v, clear: true }) + } + + if (!_.find(_sessionStorage, v)) { + _sessionStorage = _sessionStorage.concat({ origin: v, clear: true }) + } + }) + + await Promise.all([ + this.setStorage({ localStorage: _localStorage, sessionStorage: _sessionStorage }), + this.Cypress.automation('clear:cookies', null), + ]) + + await this.setCookies(data.cookies) + } + + async getCookies () { + return this.Cypress.automation('get:cookies', {}) + } + + setCookies (data) { + return this.Cypress.automation('set:cookies', data) + } + + async clearCookies () { + return this.Cypress.automation('clear:cookies', await this.getCookies()) + } + + async getCurrentSessionData () { + const storage = await this.getStorage({ origin: '*' }) + + let cookies = [] as any[] + + cookies = await this.Cypress.automation('get:cookies', {}) + + const ses = { + ...storage, + cookies, + } + + return ses + } + + getSession (id) { + return this.Cypress.backend('get:session', id) + } + + /** + * 1) if we only need currentOrigin localStorage, access sync + * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* + * and postMessage() the localStorage value to us + * 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to + * send a message telling our proxy server to intercept the next req to the https domain, + * then follow 2) + */ + async getStorage (options = {}) { + const specWindow = this.cy.state('specWindow') + + if (!_.isObject(options)) { + throw new Error('getStorage() takes an object') + } + + const opts = _.defaults({}, options, { + origin: 'currentOrigin', + }) + + const currentOrigin = $Location.create(window.location.href).origin + + const origins = await this.mapOrigins(opts.origin) + + const getResults = () => { + return results + } + const results = { + localStorage: [] as any[], + sessionStorage: [] as any[], + } + + function pushValue (origin, value) { + if (!_.isEmpty(value.localStorage)) { + results.localStorage.push({ origin, value: value.localStorage }) + } + + if (!_.isEmpty(value.sessionStorage)) { + results.sessionStorage.push({ origin, value: value.sessionStorage }) + } + } + + const currentOriginIndex = origins.indexOf(currentOrigin) + + if (currentOriginIndex !== -1) { + origins.splice(currentOriginIndex, 1) + const currentOriginStorage = getCurrentOriginStorage() + + pushValue(currentOrigin, currentOriginStorage) + } + + if (_.isEmpty(origins)) { + return getResults() + } + + if (currentOrigin.startsWith('https:')) { + _.remove(origins, (v) => v.startsWith('http:')) + } + + const postMessageResults = await getPostMessageLocalStorage(specWindow, origins) + + postMessageResults.forEach((val) => { + pushValue(val[0], val[1]) + }) + + return getResults() + } + + async clearStorage () { + const origins = await this.getAllHtmlOrigins() + + const originOptions = origins.map((v) => ({ origin: v, clear: true })) + + await this.setStorage({ + localStorage: originOptions, + sessionStorage: originOptions, + }) + } + + async setStorage (options: any, clearAll = false) { + const currentOrigin = $Location.create(window.location.href).origin as string + + const mapToCurrentOrigin = (v) => ({ ...v, origin: (v.origin && v.origin !== 'currentOrigin') ? $Location.create(v.origin).origin : currentOrigin }) + + const mappedLocalStorage = _.map(options.localStorage, (v) => { + const mapped = { origin: v.origin, localStorage: _.pick(v, 'value', 'clear') } + + if (clearAll) { + mapped.localStorage.clear = true + } + + return mapped + }).map(mapToCurrentOrigin) + + const mappedSessionStorage = _.map(options.sessionStorage, (v) => { + const mapped = { origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') } + + if (clearAll) { + mapped.sessionStorage.clear = true + } + + return mapped + }).map(mapToCurrentOrigin) + + const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v)) + + await this._setStorageOnOrigins(storageOptions) + } + + registerSessionHooks () { + this.Cypress.on('test:before:run:async', () => { + if (Cypress.config('experimentalSessionAndOrigin')) { + currentTestRegisteredSessions.clear() + + return navigateAboutBlank(false) + .then(() => this.clearCurrentSessionData()) + .then(() => { + return this.Cypress.backend('reset:rendered:html:origins') + }) + } + + return + }) + } + + publicAPI () { + return { + clearAllSavedSessions: this.clearAllSavedSessions, + clearActiveSessions: this.clearActiveSessions, + clearCookies: this.clearCookies, + clearCurrentSessionData: this.clearCurrentSessionData, + clearStorage: this.clearStorage, + defineSession: this.defineSession, + getCookies: this.getCookies, + getCurrentSessionData: this.getCurrentSessionData, + getSession: this.getSession, + getStorage: this.getStorage, + registerSessionHooks: this.registerSessionHooks, + setCookies: this.setCookies, + setSessionData: this.setSessionData, + setStorage: this.setStorage, + } + } +} diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index e310a6481e9f..d639fc1119fe 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -23,6 +23,12 @@ declare namespace Cypress { (action: 'after:screenshot', config: {}) } + interface Automation { + (eventName: 'clear:cookies', cookies?: Array): Bluebird.Promise + (eventName: 'get:cookies'): Bluebird.Promise> + (eventName: 'set:cookies', data: Array): Bluebird.Promise + } + interface Backend { (task: 'cross:origin:release:html'): boolean (task: 'cross:origin:bridge:ready', args: { originPolicy?: string }): boolean @@ -53,6 +59,7 @@ declare namespace Cypress { interface Cypress { backend: (eventName: string, ...args: any[]) => Promise + automation: Automation // TODO: how to pull this from proxy-logging.ts? can't import in a d.ts file... ProxyLogging: any // TODO: how to pull these from resolvers.ts? can't import in a d.ts file... From e9a1a8890ab32bcce16b4077b708fbd44dd876f4 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 26 Apr 2022 08:15:55 -0500 Subject: [PATCH 02/22] manager manages registered sessions --- packages/driver/src/cy/commands/sessions/index.ts | 11 +++++------ packages/driver/src/cy/commands/sessions/sessions.ts | 3 +-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index e56453fa23a2..7a2a1001caa4 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -9,8 +9,6 @@ import { navigateAboutBlank, } from './utils' -const currentTestRegisteredSessions = new Map() - type SessionData = Cypress.Commands.Session.SessionData export default function (Commands, Cypress, cy) { @@ -72,17 +70,18 @@ export default function (Commands, Cypress, cy) { }) } - let existingSession: SessionData = getActiveSession(id) + let existingSession: SessionData = sessions.getActiveSession(id) + const isRegisteredSessionForTest = sessions.currentTestRegisteredSessions.has(id) if (!setup) { - if (!existingSession || !currentTestRegisteredSessions.has(id)) { + if (!existingSession || !isRegisteredSessionForTest) { $errUtils.throwErrByPath('sessions.session.not_found', { args: { id } }) } } else { const isUniqSessionDefinition = !existingSession || existingSession.setup.toString().trim() !== setup.toString().trim() if (isUniqSessionDefinition) { - if (currentTestRegisteredSessions.has(id)) { + if (isRegisteredSessionForTest) { $errUtils.throwErrByPath('sessions.session.duplicateId', { args: { id: existingSession.id } }) } @@ -92,7 +91,7 @@ export default function (Commands, Cypress, cy) { validate: options.validate, }) - currentTestRegisteredSessions.set(id, true) + sessions.currentTestRegisteredSessions.set(id, true) } } diff --git a/packages/driver/src/cy/commands/sessions/sessions.ts b/packages/driver/src/cy/commands/sessions/sessions.ts index d569bafe2134..04575815b05e 100644 --- a/packages/driver/src/cy/commands/sessions/sessions.ts +++ b/packages/driver/src/cy/commands/sessions/sessions.ts @@ -8,14 +8,13 @@ import { navigateAboutBlank, } from './utils' -const currentTestRegisteredSessions = new Map() - type ActiveSessions = Cypress.Commands.Session.ActiveSessions type SessionData = Cypress.Commands.Session.SessionData export default class SessionsManager { Cypress cy + currentTestRegisteredSessions = new Map() constructor (Cypress, cy) { this.Cypress = Cypress From 7321cfbcd7de6dbecb4b7ecbff9d356306f89154 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 26 Apr 2022 11:12:57 -0500 Subject: [PATCH 03/22] some unit tests --- .../commands/sessions/manager_spec.ts | 228 ++++++++++++++++++ .../driver/src/cy/commands/sessions/index.ts | 4 +- .../sessions/{sessions.ts => manager.ts} | 17 +- 3 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 packages/driver/cypress/integration/commands/sessions/manager_spec.ts rename packages/driver/src/cy/commands/sessions/{sessions.ts => manager.ts} (94%) diff --git a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts new file mode 100644 index 000000000000..50ac831c0914 --- /dev/null +++ b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts @@ -0,0 +1,228 @@ +const SessionsManager = require('@packages/driver/src/cy/commands/sessions/manager').default + +describe('src/cy/commands/sessions/utils.ts', () => { + // @ts-ignore + const CypressInstance = Cypress.$Cypress.create({}) + + it('creates SessionsManager instance', () => { + const sessions = new SessionsManager(CypressInstance, () => {}) + + expect(sessions).to.haveOwnProperty('cy') + expect(sessions).to.haveOwnProperty('Cypress') + expect(sessions).to.haveOwnProperty('currentTestRegisteredSessions') + expect(sessions.currentTestRegisteredSessions).to.be.instanceOf(Map) + }) + + describe('.setActiveSession', () => { + it('adds session when non were previously added', () => { + const cySpy = cy.spy(cy, 'state').withArgs('activeSessions') + + const activeSession: Cypress.Commands.Session.ActiveSessions = { + 'session_1': { + id: 'session_1', + setup: () => {}, + hydrated: true, + }, + } + + const sessions = new SessionsManager(CypressInstance, cy) + + sessions.setActiveSession(activeSession) + const calls = cySpy.getCalls() + + expect(cySpy).to.be.calledTwice + expect(calls[0].args[1]).to.be.undefined + expect(calls[1].args[1]).to.haveOwnProperty('session_1') + }) + + it('adds session when other sessions had previously added', () => { + const existingSessions: Cypress.Commands.Session.ActiveSessions = { + 'session_1': { + id: 'session_1', + setup: () => {}, + hydrated: false, + }, + 'session_2': { + id: 'session_2', + setup: () => {}, + hydrated: true, + }, + } + + const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(existingSessions) + + const activeSession: Cypress.Commands.Session.ActiveSessions = { + 'session_3': { + id: 'session_3', + setup: () => {}, + hydrated: true, + }, + } + + const sessions = new SessionsManager(CypressInstance, cy) + + sessions.setActiveSession(activeSession) + const calls = cySpy.getCalls() + + expect(cySpy).to.be.calledTwice + expect(calls[0].args[1]).to.be.undefined + expect(calls[1].args[1]).to.haveOwnProperty('session_1') + expect(calls[1].args[1]).to.haveOwnProperty('session_2') + expect(calls[1].args[1]).to.haveOwnProperty('session_3') + }) + }) + + describe('.getActiveSession', () => { + it('returns undefined when no active sessions', () => { + const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions') + + const sessions = new SessionsManager(CypressInstance, cy) + + const activeSession = sessions.getActiveSession('session_1') + + expect(cySpy).to.be.calledOnce + expect(activeSession).to.be.undefined + }) + + it('returns session when found', () => { + const activeSessions: Cypress.Commands.Session.ActiveSessions = { + 'session_1': { + id: 'session_1', + setup: () => {}, + hydrated: false, + }, + 'session_2': { + id: 'session_2', + setup: () => {}, + hydrated: true, + }, + } + + const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(activeSessions) + + const sessions = new SessionsManager(CypressInstance, cy) + + let activeSession = sessions.getActiveSession('session_1') + + expect(cySpy).to.be.calledOnce + expect(activeSession).to.deep.eq(activeSessions['session_1']) + }) + }) + + describe('.clearActiveSessions', () => { + it('handles when no active sessions have been set', () => { + const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions') + + const sessions = new SessionsManager(CypressInstance, cy) + + sessions.clearActiveSessions() + const calls = cySpy.getCalls() + + expect(cySpy).to.be.calledTwice + expect(calls[1].args[1]).to.be.instanceOf(Object) + expect(calls[1].args[1]).to.deep.eq({}) + }) + + it('updates the existing active sessions to "hydrated: false"', () => { + const existingSessions: Cypress.Commands.Session.ActiveSessions = { + 'session_1': { + id: 'session_1', + setup: () => {}, + hydrated: false, + }, + 'session_2': { + id: 'session_2', + setup: () => {}, + hydrated: true, + }, + } + + const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(existingSessions) + + const sessions = new SessionsManager(CypressInstance, cy) + + sessions.clearActiveSessions() + const calls = cySpy.getCalls() + + expect(cySpy).to.be.calledTwice + expect(calls[1].args[1]).to.be.instanceOf(Object) + expect(calls[1].args[1]).to.haveOwnProperty('session_1') + expect(calls[1].args[1].session_1).to.haveOwnProperty('hydrated', false) + expect(calls[1].args[1]).to.haveOwnProperty('session_2') + expect(calls[1].args[1].session_2).to.haveOwnProperty('hydrated', false) + }) + }) + + describe('.mapOrigins', () => {}) + + describe('._setStorageOnOrigins', () => {}) + + describe('.getAllHtmlOrigins', () => {}) + + describe('.defineSession', () => {}) + + it('.clearAllSavedSessions', async () => { + const cypressSpy = cy.stub(CypressInstance, 'backend').withArgs('clear:session').resolves(null) + + const sessions = new SessionsManager(CypressInstance, () => {}) + const sessionsSpy = cy.stub(sessions, 'clearActiveSessions') + + await sessions.clearAllSavedSessions() + + expect(sessionsSpy).to.be.calledOnce + expect(cypressSpy).to.be.calledOnceWith('clear:session', null) + }) + + describe('.clearCurrentSessionData', () => {}) + + it('.getCookies', async () => { + const cookies = [{ id: 'cookie' }] + const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('get:cookies').resolves(cookies) + + const sessions = new SessionsManager(CypressInstance, () => {}) + + const sessionCookies = await sessions.getCookies() + + expect(cypressSpy).to.be.calledOnceWith('get:cookies', {}) + expect(sessionCookies).to.deep.eq(cookies) + }) + + it('.setCookies', async () => { + const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('set:cookies') + + const sessions = new SessionsManager(CypressInstance, () => {}) + + await sessions.setCookies({}) + + expect(cypressSpy).to.be.calledOnceWith('set:cookies', {}) + }) + + it('.clearCookies', async () => { + const cookies = [{ id: 'cookie' }] + const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('clear:cookies').resolves([]) + + const sessions = new SessionsManager(CypressInstance, () => {}) + const sessionsSpy = cy.stub(sessions, 'getCookies').resolves(cookies) + + await sessions.clearCookies() + + expect(sessionsSpy).to.be.calledOnce + expect(cypressSpy).to.be.calledOnceWith('clear:cookies', cookies) + }) + + it('.getSession', () => { + const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:session') + + const sessions = new SessionsManager(CypressInstance, () => {}) + + sessions.getSession('session_1') + + expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1') + }) + + describe('.getStorage', () => {}) + describe('.clearStorage', () => {}) + describe('.setStorage', () => {}) + describe('.registerSessionHooks', () => {}) + describe('.publicAPI', () => {}) +}) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 7a2a1001caa4..eea47bebc026 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -2,7 +2,7 @@ import _ from 'lodash' import stringifyStable from 'json-stable-stringify' import $errUtils from '../../../cypress/error_utils' import $stackUtils from '../../../cypress/stack_utils' -import SessionsManager from './sessions' +import SessionsManager from './manager' import { getSessionDetails, getConsoleProps, @@ -16,7 +16,7 @@ export default function (Commands, Cypress, cy) { if (!Cypress.config('experimentalSessionAndOrigin')) { $errUtils.throwErrByPath('sessions.experimentNotEnabled', { args: { - // determine if using removed <9.6.0 experimental session opt-in flag to + // determine if using experimental session opt-in flag (removed in 9.6.0) to // generate a coherent error message experimentalSessionSupport: Cypress.config('experimentalSessionSupport'), }, diff --git a/packages/driver/src/cy/commands/sessions/sessions.ts b/packages/driver/src/cy/commands/sessions/manager.ts similarity index 94% rename from packages/driver/src/cy/commands/sessions/sessions.ts rename to packages/driver/src/cy/commands/sessions/manager.ts index 04575815b05e..1a6b62f8bf9d 100644 --- a/packages/driver/src/cy/commands/sessions/sessions.ts +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -37,8 +37,9 @@ export default class SessionsManager { clearActiveSessions () { const curSessions = this.cy.state('activeSessions') || {} + const clearedSessions: ActiveSessions = _.mapValues(curSessions, (v) => ({ ...v, hydrated: false })) - this.cy.state('activeSessions', _.mapValues(curSessions, (v) => ({ ...v, hydrated: false }))) + this.cy.state('activeSessions', clearedSessions) } async mapOrigins (origins: string) { @@ -157,12 +158,12 @@ export default class SessionsManager { await this.setCookies(data.cookies) } - async getCookies () { + async getCookies (): Promise> { return this.Cypress.automation('get:cookies', {}) } - setCookies (data) { - return this.Cypress.automation('set:cookies', data) + setCookies (cookies: Array) { + return this.Cypress.automation('set:cookies', cookies) } async clearCookies () { @@ -172,9 +173,7 @@ export default class SessionsManager { async getCurrentSessionData () { const storage = await this.getStorage({ origin: '*' }) - let cookies = [] as any[] - - cookies = await this.Cypress.automation('get:cookies', {}) + const cookies: Array = await this.getCookies() const ses = { ...storage, @@ -184,7 +183,7 @@ export default class SessionsManager { return ses } - getSession (id) { + getSession (id: string) { return this.Cypress.backend('get:session', id) } @@ -299,7 +298,7 @@ export default class SessionsManager { registerSessionHooks () { this.Cypress.on('test:before:run:async', () => { if (Cypress.config('experimentalSessionAndOrigin')) { - currentTestRegisteredSessions.clear() + this.currentTestRegisteredSessions.clear() return navigateAboutBlank(false) .then(() => this.clearCurrentSessionData()) From 4002732d49b484140076f9d544dfab39e2ad92a4 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 26 Apr 2022 16:07:27 -0500 Subject: [PATCH 04/22] add more tests and some slight clean up --- .../commands/sessions/manager_spec.ts | 130 +++++++++++++++--- .../driver/src/cy/commands/sessions/index.ts | 14 +- .../src/cy/commands/sessions/manager.ts | 80 ++++------- 3 files changed, 154 insertions(+), 70 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts index 50ac831c0914..48523035ad10 100644 --- a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts +++ b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts @@ -1,8 +1,9 @@ const SessionsManager = require('@packages/driver/src/cy/commands/sessions/manager').default -describe('src/cy/commands/sessions/utils.ts', () => { +describe('src/cy/commands/sessions/manager.ts', () => { // @ts-ignore const CypressInstance = Cypress.$Cypress.create({}) + const baseUrl = Cypress.config('baseUrl') it('creates SessionsManager instance', () => { const sessions = new SessionsManager(CypressInstance, () => {}) @@ -13,7 +14,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { expect(sessions.currentTestRegisteredSessions).to.be.instanceOf(Map) }) - describe('.setActiveSession', () => { + describe('.setActiveSession()', () => { it('adds session when non were previously added', () => { const cySpy = cy.spy(cy, 'state').withArgs('activeSessions') @@ -72,7 +73,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { }) }) - describe('.getActiveSession', () => { + describe('.getActiveSession()', () => { it('returns undefined when no active sessions', () => { const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions') @@ -109,7 +110,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { }) }) - describe('.clearActiveSessions', () => { + describe('.clearActiveSessions()', () => { it('handles when no active sessions have been set', () => { const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions') @@ -153,15 +154,77 @@ describe('src/cy/commands/sessions/utils.ts', () => { }) }) - describe('.mapOrigins', () => {}) + describe('.mapOrigins()', () => { + it('maps when requesting all origins', async () => { + const sessions = new SessionsManager(CypressInstance, cy) + + const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com'] + const sessionsSpy = cy.stub(sessions, 'getAllHtmlOrigins').resolves(allOrigins) + + const origins = await sessions.mapOrigins('*') + + expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com']) + expect(sessionsSpy).to.be.calledOnce + }) + + it('maps when requesting the current origin', async () => { + const sessions = new SessionsManager(CypressInstance, cy) + const sessionsSpy = cy.stub(sessions, 'getAllHtmlOrigins') + const origins = await sessions.mapOrigins('currentOrigin') + + expect(origins).to.deep.eq([baseUrl]) + expect(sessionsSpy).not.to.be.called + }) - describe('._setStorageOnOrigins', () => {}) + it('maps when requesting a specific origin', async () => { + const sessions = new SessionsManager(CypressInstance, cy) + const sessionsSpy = cy.stub(sessions, 'getAllHtmlOrigins') + const origins = await sessions.mapOrigins('https://example.com/random_page?1') + + expect(origins).to.deep.eq(['https://example.com']) + expect(sessionsSpy).not.to.be.called + }) + }) + + describe('._setStorageOnOrigins()', () => {}) - describe('.getAllHtmlOrigins', () => {}) + it('.getAllHtmlOrigins()', async () => { + const storedOrigins = { + 'https://example.com': {}, + 'https://foobar.com': {}, + } - describe('.defineSession', () => {}) + storedOrigins[`${baseUrl}`] = {} + const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins').resolves(storedOrigins) + const sessions = new SessionsManager(CypressInstance, cy) - it('.clearAllSavedSessions', async () => { + const origins = await sessions.getAllHtmlOrigins() + + expect(cypressSpy).have.been.calledOnce + expect(origins).to.have.lengthOf(3) + expect(origins).to.deep.eq(['https://example.com', 'https://foobar.com', baseUrl]) + }) + + it('.defineSession()', () => { + const sessions = new SessionsManager(CypressInstance, cy) + const sessionsSpy = cy.stub(sessions, 'setActiveSession') + const setup = cy.stub() + const sess = sessions.defineSession({ id: '1', setup }) + + expect(sess).to.deep.eq({ + id: '1', + setup, + validate: undefined, + cookies: null, + localStorage: null, + hydrated: false, + }) + + expect(sessionsSpy).to.be.calledOnce + expect(sessionsSpy.getCall(0).args[0]).to.deep.eq({ 1: sess }) + }) + + it('.clearAllSavedSessions()', async () => { const cypressSpy = cy.stub(CypressInstance, 'backend').withArgs('clear:session').resolves(null) const sessions = new SessionsManager(CypressInstance, () => {}) @@ -173,9 +236,25 @@ describe('src/cy/commands/sessions/utils.ts', () => { expect(cypressSpy).to.be.calledOnceWith('clear:session', null) }) - describe('.clearCurrentSessionData', () => {}) + it('.clearCurrentSessionData()', async () => { + const windowLocalStorageStub = cy.stub(window.localStorage, 'clear') + const windowSessionStorageStub = cy.stub(window.sessionStorage, 'clear') - it('.getCookies', async () => { + const sessions = new SessionsManager(CypressInstance, () => {}) + const clearStorageSpy = cy.stub(sessions, 'clearStorage') + const clearCookiesSpy = cy.stub(sessions, 'clearCookies') + + await sessions.clearCurrentSessionData() + expect(clearStorageSpy).to.be.calledOnce + expect(clearCookiesSpy).to.be.calledOnce + expect(windowLocalStorageStub).to.be.calledOnce + expect(windowSessionStorageStub).to.be.calledOnce + }) + + // TODO + describe('.setSessionData', () => {}) + + it('.getCookies()', async () => { const cookies = [{ id: 'cookie' }] const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('get:cookies').resolves(cookies) @@ -187,7 +266,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { expect(sessionCookies).to.deep.eq(cookies) }) - it('.setCookies', async () => { + it('.setCookies()', async () => { const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('set:cookies') const sessions = new SessionsManager(CypressInstance, () => {}) @@ -197,7 +276,7 @@ describe('src/cy/commands/sessions/utils.ts', () => { expect(cypressSpy).to.be.calledOnceWith('set:cookies', {}) }) - it('.clearCookies', async () => { + it('.clearCookies()', async () => { const cookies = [{ id: 'cookie' }] const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('clear:cookies').resolves([]) @@ -210,7 +289,23 @@ describe('src/cy/commands/sessions/utils.ts', () => { expect(cypressSpy).to.be.calledOnceWith('clear:cookies', cookies) }) - it('.getSession', () => { + it('.getCurrentSessionData', async () => { + const sessions = new SessionsManager(CypressInstance, () => {}) + const getStorageSpy = cy.stub(sessions, 'getStorage').resolves({ localStorage: [] }) + const cookiesSpy = cy.stub(sessions, 'getCookies').resolves([{ id: 'cookie' }]) + + const sessData = await sessions.getCurrentSessionData() + + expect(sessData).to.deep.eq({ + localStorage: [], + cookies: [{ id: 'cookie' }], + }) + + expect(getStorageSpy).to.be.calledOnce + expect(cookiesSpy).to.be.calledOnce + }) + + it('.getSession()', () => { const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:session') const sessions = new SessionsManager(CypressInstance, () => {}) @@ -220,9 +315,12 @@ describe('src/cy/commands/sessions/utils.ts', () => { expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1') }) + // TODO describe('.getStorage', () => {}) + + // TODO describe('.clearStorage', () => {}) + + // TODO describe('.setStorage', () => {}) - describe('.registerSessionHooks', () => {}) - describe('.publicAPI', () => {}) }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index eea47bebc026..8a13b939f1c1 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -27,7 +27,19 @@ export default function (Commands, Cypress, cy) { const sessions = new SessionsManager(Cypress, cy) Cypress.on('run:start', () => { - sessions.registerSessionHooks() + Cypress.on('test:before:run:async', () => { + if (Cypress.config('experimentalSessionAndOrigin')) { + this.currentTestRegisteredSessions.clear() + + return navigateAboutBlank(false) + .then(() => this.clearCurrentSessionData()) + .then(() => { + return this.Cypress.backend('reset:rendered:html:origins') + }) + } + + return + }) }) Commands.addAll({ diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts index 1a6b62f8bf9d..5ae5b199982d 100644 --- a/packages/driver/src/cy/commands/sessions/manager.ts +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -5,7 +5,6 @@ import { getCurrentOriginStorage, setPostMessageLocalStorage, getPostMessageLocalStorage, - navigateAboutBlank, } from './utils' type ActiveSessions = Cypress.Commands.Session.ActiveSessions @@ -43,21 +42,17 @@ export default class SessionsManager { } async mapOrigins (origins: string) { - const currentOrigin = $Location.create(window.location.href).origin - - return _.uniq( - _.flatten(await this.Cypress.Promise.map( - ([] as string[]).concat(origins), async (v) => { - if (v === '*') { - return _.keys(await this.Cypress.backend('get:rendered:html:origins')).concat([currentOrigin]) - } - - if (v === 'currentOrigin') return currentOrigin + let renderedOrigins + + if (origins === '*') { + renderedOrigins = await this.getAllHtmlOrigins() + } else if (origins === 'currentOrigin') { + renderedOrigins = [$Location.create(window.location.href).origin] + } else { + renderedOrigins = [$Location.create(origins).origin] + } - return $Location.create(v).origin - }, - )), - ) as string[] + return _.uniq(renderedOrigins) as string[] } async _setStorageOnOrigins (originOptions) { @@ -96,10 +91,10 @@ export default class SessionsManager { async getAllHtmlOrigins () { const currentOrigin = $Location.create(window.location.href).origin + const storedOrigins = await this.Cypress.backend('get:rendered:html:origins') + const origins = [..._.keys(storedOrigins), currentOrigin] - const origins = _.uniq([..._.keys(await Cypress.backend('get:rendered:html:origins')), currentOrigin]) as string[] - - return origins + return _.uniq(origins) } defineSession (options = {} as any): SessionData { @@ -171,16 +166,15 @@ export default class SessionsManager { } async getCurrentSessionData () { - const storage = await this.getStorage({ origin: '*' }) - - const cookies: Array = await this.getCookies() + const [storage, cookies] = await Promise.all([ + this.getStorage({ origin: '*' }), + this.getCookies(), + ]) - const ses = { + return { ...storage, cookies, } - - return ses } getSession (id: string) { @@ -188,13 +182,13 @@ export default class SessionsManager { } /** - * 1) if we only need currentOrigin localStorage, access sync - * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* - * and postMessage() the localStorage value to us - * 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to - * send a message telling our proxy server to intercept the next req to the https domain, - * then follow 2) - */ + * 1) if we only need currentOrigin localStorage, access sync + * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* + * and postMessage() the localStorage value to us + * 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to + * send a message telling our proxy server to intercept the next req to the https domain, + * then follow 2) + */ async getStorage (options = {}) { const specWindow = this.cy.state('specWindow') @@ -210,9 +204,6 @@ export default class SessionsManager { const origins = await this.mapOrigins(opts.origin) - const getResults = () => { - return results - } const results = { localStorage: [] as any[], sessionStorage: [] as any[], @@ -238,7 +229,7 @@ export default class SessionsManager { } if (_.isEmpty(origins)) { - return getResults() + return results } if (currentOrigin.startsWith('https:')) { @@ -251,7 +242,7 @@ export default class SessionsManager { pushValue(val[0], val[1]) }) - return getResults() + return results } async clearStorage () { @@ -295,22 +286,6 @@ export default class SessionsManager { await this._setStorageOnOrigins(storageOptions) } - registerSessionHooks () { - this.Cypress.on('test:before:run:async', () => { - if (Cypress.config('experimentalSessionAndOrigin')) { - this.currentTestRegisteredSessions.clear() - - return navigateAboutBlank(false) - .then(() => this.clearCurrentSessionData()) - .then(() => { - return this.Cypress.backend('reset:rendered:html:origins') - }) - } - - return - }) - } - publicAPI () { return { clearAllSavedSessions: this.clearAllSavedSessions, @@ -323,7 +298,6 @@ export default class SessionsManager { getCurrentSessionData: this.getCurrentSessionData, getSession: this.getSession, getStorage: this.getStorage, - registerSessionHooks: this.registerSessionHooks, setCookies: this.setCookies, setSessionData: this.setSessionData, setStorage: this.setStorage, From 248b3625b336d64be16c4997559d908bc746462f Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 26 Apr 2022 16:19:30 -0500 Subject: [PATCH 05/22] . --- packages/driver/src/cy/commands/sessions/index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 8a13b939f1c1..8b1e585df46c 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -11,6 +11,15 @@ import { type SessionData = Cypress.Commands.Session.SessionData +/** + * Session data should be cleared with spec browser launch. + * + * Rules for clearing session data: + * - if page reloads due to top navigation OR user hard reload, session data should NOT be cleared + * - if user relaunches the browser or launches a new spec, session data SHOULD be cleared + * - session data SHOULD be cleared between specs in run mode + */ + export default function (Commands, Cypress, cy) { function throwIfNoSessionSupport () { if (!Cypress.config('experimentalSessionAndOrigin')) { @@ -29,12 +38,12 @@ export default function (Commands, Cypress, cy) { Cypress.on('run:start', () => { Cypress.on('test:before:run:async', () => { if (Cypress.config('experimentalSessionAndOrigin')) { - this.currentTestRegisteredSessions.clear() + sessions.currentTestRegisteredSessions.clear() return navigateAboutBlank(false) - .then(() => this.clearCurrentSessionData()) + .then(() => sessions.clearCurrentSessionData()) .then(() => { - return this.Cypress.backend('reset:rendered:html:origins') + return Cypress.backend('reset:rendered:html:origins') }) } From 690e8894dd1ed03256e4366475e9851c409fe9f4 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 26 Apr 2022 16:52:24 -0500 Subject: [PATCH 06/22] fix run mode issue. --- .../integration/commands/sessions/manager_spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts index 48523035ad10..57fbfbd4b761 100644 --- a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts +++ b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts @@ -1,9 +1,15 @@ -const SessionsManager = require('@packages/driver/src/cy/commands/sessions/manager').default +const SessionsManager = require('../../../../src/cy/commands/sessions/manager').default +const $Cypress = require('../../../../src/cypress').default describe('src/cy/commands/sessions/manager.ts', () => { + let CypressInstance + let baseUrl + + beforeEach(function () { // @ts-ignore - const CypressInstance = Cypress.$Cypress.create({}) - const baseUrl = Cypress.config('baseUrl') + CypressInstance = new $Cypress() + baseUrl = Cypress.config('baseUrl') + }) it('creates SessionsManager instance', () => { const sessions = new SessionsManager(CypressInstance, () => {}) From 2f6062cba4d9814a0d88cfbd50bfe5daaaee3144 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 28 Apr 2022 14:31:14 -0500 Subject: [PATCH 07/22] bind correctly for spies --- .../commands/sessions/manager_spec.ts | 240 +++++++------ .../driver/src/cy/commands/sessions/index.ts | 15 +- .../src/cy/commands/sessions/manager.ts | 337 +++++++++--------- 3 files changed, 298 insertions(+), 294 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts index 57fbfbd4b761..8a5a492f01ec 100644 --- a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts +++ b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts @@ -12,12 +12,12 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) it('creates SessionsManager instance', () => { - const sessions = new SessionsManager(CypressInstance, () => {}) + const sessionsManager = new SessionsManager(CypressInstance, () => {}) - expect(sessions).to.haveOwnProperty('cy') - expect(sessions).to.haveOwnProperty('Cypress') - expect(sessions).to.haveOwnProperty('currentTestRegisteredSessions') - expect(sessions.currentTestRegisteredSessions).to.be.instanceOf(Map) + expect(sessionsManager).to.haveOwnProperty('cy') + expect(sessionsManager).to.haveOwnProperty('Cypress') + expect(sessionsManager).to.haveOwnProperty('currentTestRegisteredSessions') + expect(sessionsManager.currentTestRegisteredSessions).to.be.instanceOf(Map) }) describe('.setActiveSession()', () => { @@ -32,9 +32,9 @@ describe('src/cy/commands/sessions/manager.ts', () => { }, } - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) - sessions.setActiveSession(activeSession) + sessionsManager.setActiveSession(activeSession) const calls = cySpy.getCalls() expect(cySpy).to.be.calledTwice @@ -66,9 +66,9 @@ describe('src/cy/commands/sessions/manager.ts', () => { }, } - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) - sessions.setActiveSession(activeSession) + sessionsManager.setActiveSession(activeSession) const calls = cySpy.getCalls() expect(cySpy).to.be.calledTwice @@ -83,9 +83,9 @@ describe('src/cy/commands/sessions/manager.ts', () => { it('returns undefined when no active sessions', () => { const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions') - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) - const activeSession = sessions.getActiveSession('session_1') + const activeSession = sessionsManager.getActiveSession('session_1') expect(cySpy).to.be.calledOnce expect(activeSession).to.be.undefined @@ -107,9 +107,9 @@ describe('src/cy/commands/sessions/manager.ts', () => { const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(activeSessions) - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) - let activeSession = sessions.getActiveSession('session_1') + let activeSession = sessionsManager.getActiveSession('session_1') expect(cySpy).to.be.calledOnce expect(activeSession).to.deep.eq(activeSessions['session_1']) @@ -120,9 +120,9 @@ describe('src/cy/commands/sessions/manager.ts', () => { it('handles when no active sessions have been set', () => { const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions') - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) - sessions.clearActiveSessions() + sessionsManager.clearActiveSessions() const calls = cySpy.getCalls() expect(cySpy).to.be.calledTwice @@ -146,9 +146,9 @@ describe('src/cy/commands/sessions/manager.ts', () => { const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(existingSessions) - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) - sessions.clearActiveSessions() + sessionsManager.clearActiveSessions() const calls = cySpy.getCalls() expect(cySpy).to.be.calledTwice @@ -162,34 +162,46 @@ describe('src/cy/commands/sessions/manager.ts', () => { describe('.mapOrigins()', () => { it('maps when requesting all origins', async () => { - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com'] - const sessionsSpy = cy.stub(sessions, 'getAllHtmlOrigins').resolves(allOrigins) + const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins) - const origins = await sessions.mapOrigins('*') + const origins = await sessionsManager.mapOrigins('*') expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com']) expect(sessionsSpy).to.be.calledOnce }) it('maps when requesting the current origin', async () => { - const sessions = new SessionsManager(CypressInstance, cy) - const sessionsSpy = cy.stub(sessions, 'getAllHtmlOrigins') - const origins = await sessions.mapOrigins('currentOrigin') + const sessionsManager = new SessionsManager(CypressInstance, cy) + const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins') + const origins = await sessionsManager.mapOrigins('currentOrigin') expect(origins).to.deep.eq([baseUrl]) expect(sessionsSpy).not.to.be.called }) it('maps when requesting a specific origin', async () => { - const sessions = new SessionsManager(CypressInstance, cy) - const sessionsSpy = cy.stub(sessions, 'getAllHtmlOrigins') - const origins = await sessions.mapOrigins('https://example.com/random_page?1') + const sessionsManager = new SessionsManager(CypressInstance, cy) + const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins') + const origins = await sessionsManager.mapOrigins('https://example.com/random_page?1') expect(origins).to.deep.eq(['https://example.com']) expect(sessionsSpy).not.to.be.called }) + + it('maps when requesting a list of origins', async () => { + const sessionsManager = new SessionsManager(CypressInstance, cy) + + const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com'] + const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins) + + const origins = await sessionsManager.mapOrigins(['*', 'https://other.com']) + + expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com', 'https://other.com']) + expect(sessionsSpy).to.be.calledOnce + }) }) describe('._setStorageOnOrigins()', () => {}) @@ -202,131 +214,133 @@ describe('src/cy/commands/sessions/manager.ts', () => { storedOrigins[`${baseUrl}`] = {} const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins').resolves(storedOrigins) - const sessions = new SessionsManager(CypressInstance, cy) + const sessionsManager = new SessionsManager(CypressInstance, cy) - const origins = await sessions.getAllHtmlOrigins() + const origins = await sessionsManager.getAllHtmlOrigins() expect(cypressSpy).have.been.calledOnce expect(origins).to.have.lengthOf(3) expect(origins).to.deep.eq(['https://example.com', 'https://foobar.com', baseUrl]) }) - it('.defineSession()', () => { - const sessions = new SessionsManager(CypressInstance, cy) - const sessionsSpy = cy.stub(sessions, 'setActiveSession') - const setup = cy.stub() - const sess = sessions.defineSession({ id: '1', setup }) - - expect(sess).to.deep.eq({ - id: '1', - setup, - validate: undefined, - cookies: null, - localStorage: null, - hydrated: false, + describe('.sessions', () => { + it('sessions.defineSession()', () => { + const sessionsManager = new SessionsManager(CypressInstance, cy) + const sessionsSpy = cy.stub(sessionsManager, 'setActiveSession') + const setup = cy.stub() + const sess = sessionsManager.sessions.defineSession({ id: '1', setup }) + + expect(sess).to.deep.eq({ + id: '1', + setup, + validate: undefined, + cookies: null, + localStorage: null, + hydrated: false, + }) + + expect(sessionsSpy).to.be.calledOnce + expect(sessionsSpy.getCall(0).args[0]).to.deep.eq({ 1: sess }) }) - expect(sessionsSpy).to.be.calledOnce - expect(sessionsSpy.getCall(0).args[0]).to.deep.eq({ 1: sess }) - }) + it('sessions.clearAllSavedSessions()', async () => { + const cypressSpy = cy.stub(CypressInstance, 'backend').withArgs('clear:session').resolves(null) - it('.clearAllSavedSessions()', async () => { - const cypressSpy = cy.stub(CypressInstance, 'backend').withArgs('clear:session').resolves(null) + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + const sessionsSpy = cy.stub(sessionsManager, 'clearActiveSessions') - const sessions = new SessionsManager(CypressInstance, () => {}) - const sessionsSpy = cy.stub(sessions, 'clearActiveSessions') + await sessionsManager.sessions.clearAllSavedSessions() - await sessions.clearAllSavedSessions() + expect(sessionsSpy).to.be.calledOnce + expect(cypressSpy).to.be.calledOnceWith('clear:session', null) + }) - expect(sessionsSpy).to.be.calledOnce - expect(cypressSpy).to.be.calledOnceWith('clear:session', null) - }) + it('.clearCurrentSessionData()', async () => { + const windowLocalStorageStub = cy.stub(window.localStorage, 'clear') + const windowSessionStorageStub = cy.stub(window.sessionStorage, 'clear') - it('.clearCurrentSessionData()', async () => { - const windowLocalStorageStub = cy.stub(window.localStorage, 'clear') - const windowSessionStorageStub = cy.stub(window.sessionStorage, 'clear') + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + const clearStorageSpy = cy.stub(sessionsManager.sessions, 'clearStorage') + const clearCookiesSpy = cy.stub(sessionsManager.sessions, 'clearCookies') - const sessions = new SessionsManager(CypressInstance, () => {}) - const clearStorageSpy = cy.stub(sessions, 'clearStorage') - const clearCookiesSpy = cy.stub(sessions, 'clearCookies') + await sessionsManager.sessions.clearCurrentSessionData() + expect(clearStorageSpy).to.be.calledOnce + expect(clearCookiesSpy).to.be.calledOnce + expect(windowLocalStorageStub).to.be.calledOnce + expect(windowSessionStorageStub).to.be.calledOnce + }) - await sessions.clearCurrentSessionData() - expect(clearStorageSpy).to.be.calledOnce - expect(clearCookiesSpy).to.be.calledOnce - expect(windowLocalStorageStub).to.be.calledOnce - expect(windowSessionStorageStub).to.be.calledOnce - }) + // TODO + describe('sessions.setSessionData', () => {}) - // TODO - describe('.setSessionData', () => {}) + it('sessions.getCookies()', async () => { + const cookies = [{ id: 'cookie' }] + const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('get:cookies').resolves(cookies) - it('.getCookies()', async () => { - const cookies = [{ id: 'cookie' }] - const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('get:cookies').resolves(cookies) + const sessionsManager = new SessionsManager(CypressInstance, () => {}) - const sessions = new SessionsManager(CypressInstance, () => {}) + const sessionCookies = await sessionsManager.sessions.getCookies() - const sessionCookies = await sessions.getCookies() + expect(cypressSpy).to.be.calledOnceWith('get:cookies', {}) + expect(sessionCookies).to.deep.eq(cookies) + }) - expect(cypressSpy).to.be.calledOnceWith('get:cookies', {}) - expect(sessionCookies).to.deep.eq(cookies) - }) + it('sessions.setCookies()', async () => { + const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('set:cookies') - it('.setCookies()', async () => { - const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('set:cookies') + const sessionsManager = new SessionsManager(CypressInstance, () => {}) - const sessions = new SessionsManager(CypressInstance, () => {}) + await sessionsManager.sessions.setCookies({}) - await sessions.setCookies({}) + expect(cypressSpy).to.be.calledOnceWith('set:cookies', {}) + }) - expect(cypressSpy).to.be.calledOnceWith('set:cookies', {}) - }) + it('sessions.clearCookies()', async () => { + const cookies = [{ id: 'cookie' }] + const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('clear:cookies').resolves([]) - it('.clearCookies()', async () => { - const cookies = [{ id: 'cookie' }] - const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('clear:cookies').resolves([]) + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + const sessionsSpy = cy.stub(sessionsManager.sessions, 'getCookies').resolves(cookies) - const sessions = new SessionsManager(CypressInstance, () => {}) - const sessionsSpy = cy.stub(sessions, 'getCookies').resolves(cookies) + await sessionsManager.sessions.clearCookies() - await sessions.clearCookies() + expect(sessionsSpy).to.be.calledOnce + expect(cypressSpy).to.be.calledOnceWith('clear:cookies', cookies) + }) - expect(sessionsSpy).to.be.calledOnce - expect(cypressSpy).to.be.calledOnceWith('clear:cookies', cookies) - }) + it('sessions.getCurrentSessionData', async () => { + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + const getStorageSpy = cy.stub(sessionsManager.sessions, 'getStorage').resolves({ localStorage: [] }) + const cookiesSpy = cy.stub(sessionsManager.sessions, 'getCookies').resolves([{ id: 'cookie' }]) - it('.getCurrentSessionData', async () => { - const sessions = new SessionsManager(CypressInstance, () => {}) - const getStorageSpy = cy.stub(sessions, 'getStorage').resolves({ localStorage: [] }) - const cookiesSpy = cy.stub(sessions, 'getCookies').resolves([{ id: 'cookie' }]) + const sessData = await sessionsManager.sessions.getCurrentSessionData() - const sessData = await sessions.getCurrentSessionData() + expect(sessData).to.deep.eq({ + localStorage: [], + cookies: [{ id: 'cookie' }], + }) - expect(sessData).to.deep.eq({ - localStorage: [], - cookies: [{ id: 'cookie' }], + expect(getStorageSpy).to.be.calledOnce + expect(cookiesSpy).to.be.calledOnce }) - expect(getStorageSpy).to.be.calledOnce - expect(cookiesSpy).to.be.calledOnce - }) - - it('.getSession()', () => { - const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:session') + it('sessions.getSession()', () => { + const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:session') - const sessions = new SessionsManager(CypressInstance, () => {}) + const sessionsManager = new SessionsManager(CypressInstance, () => {}) - sessions.getSession('session_1') + sessionsManager.sessions.getSession('session_1') - expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1') - }) + expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1') + }) - // TODO - describe('.getStorage', () => {}) + // TODO + describe('sessions.getStorage', () => {}) - // TODO - describe('.clearStorage', () => {}) + // TODO + describe('sessions.clearStorage', () => {}) - // TODO - describe('.setStorage', () => {}) + // TODO + describe('sessions.setStorage', () => {}) + }) }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 8b1e585df46c..1c291d14d55f 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -33,12 +33,13 @@ export default function (Commands, Cypress, cy) { } } - const sessions = new SessionsManager(Cypress, cy) + const sessionsManager = new SessionsManager(Cypress, cy) + const sessions = sessionsManager.sessions Cypress.on('run:start', () => { Cypress.on('test:before:run:async', () => { if (Cypress.config('experimentalSessionAndOrigin')) { - sessions.currentTestRegisteredSessions.clear() + sessionsManager.currentTestRegisteredSessions.clear() return navigateAboutBlank(false) .then(() => sessions.clearCurrentSessionData()) @@ -91,8 +92,8 @@ export default function (Commands, Cypress, cy) { }) } - let existingSession: SessionData = sessions.getActiveSession(id) - const isRegisteredSessionForTest = sessions.currentTestRegisteredSessions.has(id) + let existingSession: SessionData = sessionsManager.getActiveSession(id) + const isRegisteredSessionForTest = sessionsManager.currentTestRegisteredSessions.has(id) if (!setup) { if (!existingSession || !isRegisteredSessionForTest) { @@ -112,7 +113,7 @@ export default function (Commands, Cypress, cy) { validate: options.validate, }) - sessions.currentTestRegisteredSessions.set(id, true) + sessionsManager.currentTestRegisteredSessions.set(id, true) } } @@ -165,7 +166,7 @@ export default function (Commands, Cypress, cy) { _.extend(existingSession, data) existingSession.hydrated = true - sessions.setActiveSession({ [existingSession.id]: existingSession }) + sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) dataLog.set({ consoleProps: () => getConsoleProps(existingSession), @@ -406,5 +407,5 @@ export default function (Commands, Cypress, cy) { }, }) - Cypress.session = sessions.publicAPI() + Cypress.session = sessions } diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts index 5ae5b199982d..950217371481 100644 --- a/packages/driver/src/cy/commands/sessions/manager.ts +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -20,7 +20,7 @@ export default class SessionsManager { this.cy = cy } - setActiveSession (obj: ActiveSessions) { + setActiveSession = (obj: ActiveSessions) => { const currentSessions = this.cy.state('activeSessions') || {} const newSessions = { ...currentSessions, ...obj } @@ -28,34 +28,38 @@ export default class SessionsManager { this.cy.state('activeSessions', newSessions) } - getActiveSession (id: string): SessionData { + getActiveSession = (id: string): SessionData => { const currentSessions = this.cy.state('activeSessions') || {} return currentSessions[id] } - clearActiveSessions () { + clearActiveSessions = () => { const curSessions = this.cy.state('activeSessions') || {} const clearedSessions: ActiveSessions = _.mapValues(curSessions, (v) => ({ ...v, hydrated: false })) this.cy.state('activeSessions', clearedSessions) } - async mapOrigins (origins: string) { - let renderedOrigins + mapOrigins = async (origins: string | Array) => { + const getOrigins = this.Cypress.Promise.map( + ([] as string[]).concat(origins), async (v) => { + if (v === '*') { + return await this.getAllHtmlOrigins() + } - if (origins === '*') { - renderedOrigins = await this.getAllHtmlOrigins() - } else if (origins === 'currentOrigin') { - renderedOrigins = [$Location.create(window.location.href).origin] - } else { - renderedOrigins = [$Location.create(origins).origin] - } + if (v === 'currentOrigin') { + return $Location.create(window.location.href).origin + } + + return $Location.create(v).origin + }, + ) - return _.uniq(renderedOrigins) as string[] + return _.uniq(_.flatten(await getOrigins)) } - async _setStorageOnOrigins (originOptions) { + _setStorageOnOrigins = async (originOptions) => { const specWindow = this.cy.state('specWindow') const currentOrigin = $Location.create(window.location.href).origin @@ -89,7 +93,7 @@ export default class SessionsManager { await setPostMessageLocalStorage(specWindow, originOptions) } - async getAllHtmlOrigins () { + getAllHtmlOrigins = async () => { const currentOrigin = $Location.create(window.location.href).origin const storedOrigins = await this.Cypress.backend('get:rendered:html:origins') const origins = [..._.keys(storedOrigins), currentOrigin] @@ -97,210 +101,195 @@ export default class SessionsManager { return _.uniq(origins) } - defineSession (options = {} as any): SessionData { - const sess_state: SessionData = { - id: options.id, - cookies: null, - localStorage: null, - setup: options.setup, - hydrated: false, - validate: options.validate, - } + // this the public api exposed to consumers as Cypress.session + sessions = { + defineSession: (options = {} as any): SessionData => { + const sess_state: SessionData = { + id: options.id, + cookies: null, + localStorage: null, + setup: options.setup, + hydrated: false, + validate: options.validate, + } - this.setActiveSession({ [sess_state.id]: sess_state }) + this.setActiveSession({ [sess_state.id]: sess_state }) - return sess_state - } + return sess_state + }, - async clearAllSavedSessions () { - this.clearActiveSessions() + clearAllSavedSessions: async () => { + this.clearActiveSessions() - return this.Cypress.backend('clear:session', null) - } + return this.Cypress.backend('clear:session', null) + }, - async clearCurrentSessionData () { - window.localStorage.clear() - window.sessionStorage.clear() + clearCurrentSessionData: async () => { + window.localStorage.clear() + window.sessionStorage.clear() - await Promise.all([ - this.clearStorage(), - this.clearCookies(), - ]) - } + await Promise.all([ + this.sessions.clearStorage(), + this.sessions.clearCookies(), + ]) + }, - async setSessionData (data) { - await this.clearCurrentSessionData() - const allHtmlOrigins = await this.getAllHtmlOrigins() + setSessionData: async (data) => { + await this.sessions.clearCurrentSessionData() + const allHtmlOrigins = await this.getAllHtmlOrigins() - let _localStorage = data.localStorage || [] - let _sessionStorage = data.sessionStorage || [] + let _localStorage = data.localStorage || [] + let _sessionStorage = data.sessionStorage || [] - _.each(allHtmlOrigins, (v) => { - if (!_.find(_localStorage, v)) { - _localStorage = _localStorage.concat({ origin: v, clear: true }) - } + _.each(allHtmlOrigins, (v) => { + if (!_.find(_localStorage, v)) { + _localStorage = _localStorage.concat({ origin: v, clear: true }) + } - if (!_.find(_sessionStorage, v)) { - _sessionStorage = _sessionStorage.concat({ origin: v, clear: true }) - } - }) + if (!_.find(_sessionStorage, v)) { + _sessionStorage = _sessionStorage.concat({ origin: v, clear: true }) + } + }) - await Promise.all([ - this.setStorage({ localStorage: _localStorage, sessionStorage: _sessionStorage }), - this.Cypress.automation('clear:cookies', null), - ]) + await Promise.all([ + this.sessions.setStorage({ localStorage: _localStorage, sessionStorage: _sessionStorage }), + this.Cypress.automation('clear:cookies', null), + ]) - await this.setCookies(data.cookies) - } + await this.sessions.setCookies(data.cookies) + }, - async getCookies (): Promise> { - return this.Cypress.automation('get:cookies', {}) - } + getCookies: async () => { + return this.Cypress.automation('get:cookies', {}) + }, - setCookies (cookies: Array) { - return this.Cypress.automation('set:cookies', cookies) - } + setCookies: async (cookies) => { + return this.Cypress.automation('set:cookies', cookies) + }, - async clearCookies () { - return this.Cypress.automation('clear:cookies', await this.getCookies()) - } + clearCookies: async () => { + return this.Cypress.automation('clear:cookies', await this.sessions.getCookies()) + }, - async getCurrentSessionData () { - const [storage, cookies] = await Promise.all([ - this.getStorage({ origin: '*' }), - this.getCookies(), - ]) + getCurrentSessionData: async () => { + const [storage, cookies] = await Promise.all([ + this.sessions.getStorage({ origin: '*' }), + this.sessions.getCookies(), + ]) - return { - ...storage, - cookies, - } - } + return { + ...storage, + cookies, + } + }, + + getSession: (id: string) => { + return this.Cypress.backend('get:session', id) + }, + + /** + * 1) if we only need currentOrigin localStorage, access sync + * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* + * and postMessage() the localStorage value to us + * 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to + * send a message telling our proxy server to intercept the next req to the https domain, + * then follow 2) + */ + getStorage: async (options = {}) => { + const specWindow = this.cy.state('specWindow') + + if (!_.isObject(options)) { + throw new Error('getStorage() takes an object') + } - getSession (id: string) { - return this.Cypress.backend('get:session', id) - } + const opts = _.defaults({}, options, { + origin: 'currentOrigin', + }) - /** - * 1) if we only need currentOrigin localStorage, access sync - * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* - * and postMessage() the localStorage value to us - * 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to - * send a message telling our proxy server to intercept the next req to the https domain, - * then follow 2) - */ - async getStorage (options = {}) { - const specWindow = this.cy.state('specWindow') + const currentOrigin = $Location.create(window.location.href).origin - if (!_.isObject(options)) { - throw new Error('getStorage() takes an object') - } + const origins = await this.mapOrigins(opts.origin) + + const results = { + localStorage: [] as any[], + sessionStorage: [] as any[], + } - const opts = _.defaults({}, options, { - origin: 'currentOrigin', - }) + function pushValue (origin, value) { + if (!_.isEmpty(value.localStorage)) { + results.localStorage.push({ origin, value: value.localStorage }) + } - const currentOrigin = $Location.create(window.location.href).origin + if (!_.isEmpty(value.sessionStorage)) { + results.sessionStorage.push({ origin, value: value.sessionStorage }) + } + } - const origins = await this.mapOrigins(opts.origin) + const currentOriginIndex = origins.indexOf(currentOrigin) - const results = { - localStorage: [] as any[], - sessionStorage: [] as any[], - } + if (currentOriginIndex !== -1) { + origins.splice(currentOriginIndex, 1) + const currentOriginStorage = getCurrentOriginStorage() - function pushValue (origin, value) { - if (!_.isEmpty(value.localStorage)) { - results.localStorage.push({ origin, value: value.localStorage }) + pushValue(currentOrigin, currentOriginStorage) } - if (!_.isEmpty(value.sessionStorage)) { - results.sessionStorage.push({ origin, value: value.sessionStorage }) + if (_.isEmpty(origins)) { + return results } - } - const currentOriginIndex = origins.indexOf(currentOrigin) + if (currentOrigin.startsWith('https:')) { + _.remove(origins, (v) => v.startsWith('http:')) + } - if (currentOriginIndex !== -1) { - origins.splice(currentOriginIndex, 1) - const currentOriginStorage = getCurrentOriginStorage() + const postMessageResults = await getPostMessageLocalStorage(specWindow, origins) - pushValue(currentOrigin, currentOriginStorage) - } + postMessageResults.forEach((val) => { + pushValue(val[0], val[1]) + }) - if (_.isEmpty(origins)) { return results - } + }, - if (currentOrigin.startsWith('https:')) { - _.remove(origins, (v) => v.startsWith('http:')) - } + clearStorage: async () => { + const origins = await this.getAllHtmlOrigins() - const postMessageResults = await getPostMessageLocalStorage(specWindow, origins) + const originOptions = origins.map((v) => ({ origin: v, clear: true })) - postMessageResults.forEach((val) => { - pushValue(val[0], val[1]) - }) + await this.sessions.setStorage({ + localStorage: originOptions, + sessionStorage: originOptions, + }) + }, - return results - } + setStorage: async (options: any, clearAll = false) => { + const currentOrigin = $Location.create(window.location.href).origin as string - async clearStorage () { - const origins = await this.getAllHtmlOrigins() + const mapToCurrentOrigin = (v) => ({ ...v, origin: (v.origin && v.origin !== 'currentOrigin') ? $Location.create(v.origin).origin : currentOrigin }) - const originOptions = origins.map((v) => ({ origin: v, clear: true })) + const mappedLocalStorage = _.map(options.localStorage, (v) => { + const mapped = { origin: v.origin, localStorage: _.pick(v, 'value', 'clear') } - await this.setStorage({ - localStorage: originOptions, - sessionStorage: originOptions, - }) - } - - async setStorage (options: any, clearAll = false) { - const currentOrigin = $Location.create(window.location.href).origin as string - - const mapToCurrentOrigin = (v) => ({ ...v, origin: (v.origin && v.origin !== 'currentOrigin') ? $Location.create(v.origin).origin : currentOrigin }) - - const mappedLocalStorage = _.map(options.localStorage, (v) => { - const mapped = { origin: v.origin, localStorage: _.pick(v, 'value', 'clear') } - - if (clearAll) { - mapped.localStorage.clear = true - } - - return mapped - }).map(mapToCurrentOrigin) + if (clearAll) { + mapped.localStorage.clear = true + } - const mappedSessionStorage = _.map(options.sessionStorage, (v) => { - const mapped = { origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') } + return mapped + }).map(mapToCurrentOrigin) - if (clearAll) { - mapped.sessionStorage.clear = true - } + const mappedSessionStorage = _.map(options.sessionStorage, (v) => { + const mapped = { origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') } - return mapped - }).map(mapToCurrentOrigin) + if (clearAll) { + mapped.sessionStorage.clear = true + } - const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v)) + return mapped + }).map(mapToCurrentOrigin) - await this._setStorageOnOrigins(storageOptions) - } + const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v)) - publicAPI () { - return { - clearAllSavedSessions: this.clearAllSavedSessions, - clearActiveSessions: this.clearActiveSessions, - clearCookies: this.clearCookies, - clearCurrentSessionData: this.clearCurrentSessionData, - clearStorage: this.clearStorage, - defineSession: this.defineSession, - getCookies: this.getCookies, - getCurrentSessionData: this.getCurrentSessionData, - getSession: this.getSession, - getStorage: this.getStorage, - setCookies: this.setCookies, - setSessionData: this.setSessionData, - setStorage: this.setStorage, - } + await this._setStorageOnOrigins(storageOptions) + }, } } From d5e91374f18afbd23699a5425d56e1d8a66a11de Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 28 Apr 2022 14:33:40 -0500 Subject: [PATCH 08/22] remove types. not sure on the return values. --- packages/driver/types/internal-types.d.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index d639fc1119fe..e310a6481e9f 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -23,12 +23,6 @@ declare namespace Cypress { (action: 'after:screenshot', config: {}) } - interface Automation { - (eventName: 'clear:cookies', cookies?: Array): Bluebird.Promise - (eventName: 'get:cookies'): Bluebird.Promise> - (eventName: 'set:cookies', data: Array): Bluebird.Promise - } - interface Backend { (task: 'cross:origin:release:html'): boolean (task: 'cross:origin:bridge:ready', args: { originPolicy?: string }): boolean @@ -59,7 +53,6 @@ declare namespace Cypress { interface Cypress { backend: (eventName: string, ...args: any[]) => Promise - automation: Automation // TODO: how to pull this from proxy-logging.ts? can't import in a d.ts file... ProxyLogging: any // TODO: how to pull these from resolvers.ts? can't import in a d.ts file... From 845f80bdc5a10aa7cf12fad5222df430eb8baa68 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 29 Apr 2022 08:30:23 -0500 Subject: [PATCH 09/22] fix tests --- .../commands/sessions/manager_spec.ts | 16 ++++++++++++---- .../driver/src/cy/commands/sessions/manager.ts | 4 ++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts index 8a5a492f01ec..dd0907dc251a 100644 --- a/packages/driver/cypress/integration/commands/sessions/manager_spec.ts +++ b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts @@ -256,18 +256,26 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) it('.clearCurrentSessionData()', async () => { - const windowLocalStorageStub = cy.stub(window.localStorage, 'clear') - const windowSessionStorageStub = cy.stub(window.sessionStorage, 'clear') + // Unable to cleanly mock localStorage or sessionStorage on Firefox, + // so add dummy values and ensure they are cleared as expected. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1141698 + window.localStorage.foo = 'bar' + window.sessionStorage.jazzy = 'music' + + expect(window.localStorage).of.have.lengthOf(1) + expect(window.sessionStorage).of.have.lengthOf(1) const sessionsManager = new SessionsManager(CypressInstance, () => {}) + const clearStorageSpy = cy.stub(sessionsManager.sessions, 'clearStorage') const clearCookiesSpy = cy.stub(sessionsManager.sessions, 'clearCookies') await sessionsManager.sessions.clearCurrentSessionData() + expect(clearStorageSpy).to.be.calledOnce expect(clearCookiesSpy).to.be.calledOnce - expect(windowLocalStorageStub).to.be.calledOnce - expect(windowSessionStorageStub).to.be.calledOnce + expect(window.localStorage).of.have.lengthOf(0) + expect(window.sessionStorage).of.have.lengthOf(0) }) // TODO diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts index 950217371481..3b63c3ad3a1c 100644 --- a/packages/driver/src/cy/commands/sessions/manager.ts +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -41,7 +41,7 @@ export default class SessionsManager { this.cy.state('activeSessions', clearedSessions) } - mapOrigins = async (origins: string | Array) => { + mapOrigins = async (origins: string | Array): Promise> => { const getOrigins = this.Cypress.Promise.map( ([] as string[]).concat(origins), async (v) => { if (v === '*') { @@ -208,7 +208,7 @@ export default class SessionsManager { const currentOrigin = $Location.create(window.location.href).origin - const origins = await this.mapOrigins(opts.origin) + const origins: Array = await this.mapOrigins(opts.origin) const results = { localStorage: [] as any[], From 56e3394b65552f130bdec639f15385e824f7a24a Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Fri, 29 Apr 2022 15:40:04 -0500 Subject: [PATCH 10/22] check in dump --- .../driver/src/cy/commands/sessions/index.ts | 453 +++++++++--------- packages/driver/src/cy/logGroup.ts | 1 + packages/driver/types/cy/logGroup.d.ts | 2 + .../cypress/integration/sessions.ui.spec.js | 268 ++++++++--- 4 files changed, 412 insertions(+), 312 deletions(-) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index ad998d43dd4b..668c21c6c730 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -2,6 +2,7 @@ import _ from 'lodash' import stringifyStable from 'json-stable-stringify' import $errUtils from '../../../cypress/error_utils' import $stackUtils from '../../../cypress/stack_utils' +import group from '../../logGroup' import SessionsManager from './manager' import { getSessionDetails, @@ -92,317 +93,295 @@ export default function (Commands, Cypress, cy) { }) } - let existingSession: SessionData = sessionsManager.getActiveSession(id) - const isRegisteredSessionForTest = sessionsManager.currentTestRegisteredSessions.has(id) - - if (!setup) { - if (!existingSession || !isRegisteredSessionForTest) { - $errUtils.throwErrByPath('sessions.session.not_found', { args: { id } }) - } - } else { - const isUniqSessionDefinition = !existingSession || existingSession.setup.toString().trim() !== setup.toString().trim() + function createSession (existingSession, isRecreatingSession = false) { + return group(Cypress, { + name: 'Create New Session', + message: '', + event: true, + type: 'system', + }, (log) => { + return cy.then(async () => { + await navigateAboutBlank() + await sessions.clearCurrentSessionData() - if (isUniqSessionDefinition) { - if (isRegisteredSessionForTest) { - $errUtils.throwErrByPath('sessions.session.duplicateId', { args: { id: existingSession.id } }) - } + await existingSession.setup() + await navigateAboutBlank() + const data = await sessions.getCurrentSessionData() - existingSession = sessions.defineSession({ - id, - setup, - validate: options.validate, - }) - - sessionsManager.currentTestRegisteredSessions.set(id, true) - } - } + _.extend(existingSession, data) + existingSession.hydrated = true - const _log = Cypress.log({ - name: 'session', - message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, - groupStart: true, - snapshot: false, - }) + log.set({ state: 'passed' }) + _log.set({ + renderProps: () => { + if (isRecreatingSession) { + return { + indicator: 'bad', + message: `(recreated) ${_log.get().message}`, + } + } + + return { + indicator: 'successful', + message: `(new) ${_log.get().message}`, + } + }, + }) - const dataLog = Cypress.log({ - name: 'session', - sessionInfo: getSessionDetails(existingSession), - message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, - }) + sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) - function runSetup (existingSession) { - Cypress.log({ - name: 'Create New Session', - state: 'passed', - event: true, - type: 'system', - message: ``, - groupStart: true, - }) + dataLog.set({ + consoleProps: () => getConsoleProps(existingSession), + }) - if (!hadValidationError) { - _log.set({ - renderProps: () => { - return { - indicator: 'successful', - message: `(new) ${_log.get().message}`, - } - }, + // persist the session to the server. Only matters in openMode OR if there's a top navigation on a future test. + // eslint-disable-next-line no-console + return Cypress.backend('save:session', { ...existingSession, setup: existingSession.setup.toString() }).catch(console.error) }) - } - - return cy.then(async () => { - await navigateAboutBlank() - await sessions.clearCurrentSessionData() - - return existingSession.setup() }) - .then(async () => { - await navigateAboutBlank() - const data = await sessions.getCurrentSessionData() + } - Cypress.log({ groupEnd: true, emitOnly: true }) + function restoreSession (existingSession) { + return group(Cypress, { + name: 'Restore Saved Session', + message: '', + event: true, + type: 'system', + }, (log) => { + return cy.then(async () => { + await navigateAboutBlank() + + log.set({ state: 'passed' }) + _log.set({ + renderProps: () => { + return { + indicator: 'pending', + message: `(saved) ${_log.get().message}`, + } + }, + }) - _.extend(existingSession, data) - existingSession.hydrated = true + sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) - sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) + dataLog.set({ + consoleProps: () => getConsoleProps(existingSession), + }) - dataLog.set({ - consoleProps: () => getConsoleProps(existingSession), + await sessions.setSessionData(existingSession) }) - - // persist the session to the server. Only matters in openMode OR if there's a top navigation on a future test. - // eslint-disable-next-line no-console - return Cypress.backend('save:session', { ...existingSession, setup: existingSession.setup.toString() }).catch(console.error) }) } // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. - function validateSession (existingSession, _onFail) { - const validatingLog = Cypress.log({ + function validateSession (existingSession, shouldFailOnValidation = true) { + if (!existingSession.validate) { + return + } + + return group(Cypress, { name: 'Validate Session', message: '', - snapshot: false, type: 'system', - state: 'passed', event: true, - groupStart: true, - }) + }, (log) => { + const onSuccess = () => { + log.set({ message: 'valid', state: 'passed' }) - const onSuccess = () => { - validatingLog.set({ - name: 'Validate Session: valid', - message: '', - type: 'system', - event: true, - state: 'warning', - }) + Cypress.log({ groupEnd: true, emitOnly: true }) + } - Cypress.log({ groupEnd: true, emitOnly: true }) - } + const onFail = (err) => { + // @ts-ignore + log.set({ message: 'invalid', state: 'warning' }) - const onFail = (err) => { - _onFail(err, validatingLog) - } + if (shouldFailOnValidation) { + throwValidationError(err) + } - let _commandToResume: any = null + Cypress.log({ showError: true }).error(err) - let _didThrow = false + const shouldRecreateSession = true - let returnVal + return cy.wrap(shouldRecreateSession, { log: false }) + } - try { - returnVal = existingSession.validate() - } catch (e) { - onFail(e) + let _errThrownFromCypressCommand = null + let returnVal - return - } + try { + returnVal = existingSession.validate() + } catch (e) { + onFail(e) - if (typeof returnVal === 'object' && typeof returnVal.catch === 'function' && typeof returnVal.then === 'function') { - return returnVal - .then((val) => { - if (val === false) { - // set current command to cy.session for more accurate codeFrame - cy.state('current', sessionCommand) - $errUtils.throwErrByPath('sessions.validate_callback_false', { args: { reason: 'resolved false' } }) - } + return + } - onSuccess() - }) - .catch((err) => { - onFail(err) - }) - } + // if returned value is a promise + if (typeof returnVal === 'object' && typeof returnVal.catch === 'function' && typeof returnVal.then === 'function') { + return returnVal + .then((val) => { + if (val === false) { + // set current command to cy.session for more accurate codeFrame + cy.state('current', sessionCommand) - cy.state('onCommandFailed', (err, queue, next) => { - const index = _.findIndex(queue.get(), (command: any) => { - return ( - _commandToResume - && command.attributes.chainerId === _commandToResume.chainerId - ) - }) + onFail($errUtils.errByPath('sessions.validate_callback_false', { args: { reason: 'resolved false' } })) + } - // attach codeframe and cleanse the stack trace since we will not hit the cy.fail callback - // if this is the first time validate fails - if (typeof err === 'string') { - err = new Error(err) + onSuccess() + }) + .catch((err) => { + onFail(err) + }) } - err.stack = $stackUtils.normalizedStack(err) + // Inject onCommandFailed override to handle when a Cypress Command leveraged in the + // validation calling fails. This callback is executed instead of the standard command + // failure to prevent the Command queue from stop on this command failure and allow the + // sessions command to to update the validation session log message as failed + // and provide a meaningful error message related to failed session validation. + cy.state('onCommandFailed', (err, queue, next) => { + const index = _.findIndex(queue.get(), (command: any) => { + return (_parseValidationResult && command.attributes.chainerId === _parseValidationResult.chainerId) + }) - err = $errUtils.enhanceStack({ - err, - userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - projectRoot: Cypress.config('projectRoot'), - }) + // attach codeframe and cleanse the stack trace since we will not hit the cy.fail callback + // if this is the first time validate fails + if (typeof err === 'string') { + err = new Error(err) + } - cy.state('index', index) + err.stack = $stackUtils.normalizedStack(err) - cy.state('onCommandFailed', null) + err = $errUtils.enhanceStack({ + err, + userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), + projectRoot: Cypress.config('projectRoot'), + }) - _didThrow = err + // forward command queue index to the _parseValidationResult command to correctly handle + // the validation failure + cy.state('index', index) + cy.state('onCommandFailed', null) - return next() - }) + _errThrownFromCypressCommand = err - const _catchCommand = cy.then(async () => { - cy.state('onCommandFailed', null) - if (_didThrow) return onFail((_didThrow)) + return next() + }) - if (returnVal === false) { - // set current command to cy.session for more accurate codeframe - cy.state('current', sessionCommand) + const _parseValidationResult = cy.then(async () => { + cy.state('onCommandFailed', null) - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) - } + if (_errThrownFromCypressCommand) { + console.log('did throw') + + return onFail(_errThrownFromCypressCommand) + } - if (returnVal === undefined || Cypress.isCy(returnVal)) { - const val = cy.state('current').get('prev')?.attributes?.subject + if (returnVal === false) { + // set current command to cy.session for more accurate codeframe + cy.state('current', sessionCommand) - if (val === false) { - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) + return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) } - } - onSuccess() + if (returnVal === undefined || Cypress.isCy(returnVal)) { + const val = cy.state('current').get('prev')?.attributes?.subject + + if (val === false) { + return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) + } + } + + onSuccess() + }) + + return _parseValidationResult }) + } - _commandToResume = _catchCommand + const throwValidationError = (err) => { + $errUtils.modifyErrMsg(err, `\n\nThis error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.`, _.add) - return _catchCommand + _log.error(err) + cy.fail(err, { log: false }) } - let hadValidationError = false - let onValidationError: Function = (err, log) => { - log.set({ - name: 'Validate Session: invalid', - message: '', - type: 'system', - event: true, - state: 'warning', + const createSessionWorkflow = (shouldRecreateSession = false) => { + return cy.then(() => { + createSession(existingSession, shouldRecreateSession) }) - - const errorLog = Cypress.log({ - showError: true, - type: 'system', - event: true, - name: '', - message: '', + .then(() => { + validateSession(existingSession) }) + } - errorLog.error(err) - errorLog.set({ - state: 'warn', + const restoreSavedSessionWorkflow = () => { + return cy.then(async () => { + restoreSession(existingSession) + }) + .then(() => { + const shouldFailOnValidation = false + validateSession(existingSession, shouldFailOnValidation) }) + .then((hadValidationError) => { + if (hadValidationError) { + const shouldRecreateSession = true - _log.set({ - renderProps: () => { - return { - indicator: 'bad', - message: `(recreated) ${_log.get().message}`, - } - }, + createSessionWorkflow(shouldRecreateSession) + } }) + } - Cypress.log({ groupEnd: true, emitOnly: true }) + let existingSession: SessionData = sessionsManager.getActiveSession(id) + const isRegisteredSessionForTest = sessionsManager.currentTestRegisteredSessions.has(id) - hadValidationError = true + if (!setup) { + if (!existingSession || !isRegisteredSessionForTest) { + $errUtils.throwErrByPath('sessions.session.not_found', { args: { id } }) + } + } else { + const isUniqSessionDefinition = !existingSession || existingSession.setup.toString().trim() !== setup.toString().trim() - return runSetup(existingSession) - .then(() => { - cy.then(() => { - return validateSession(existingSession, throwValidationError) - }) - .then(() => { - cy.then(async () => { - await navigateAboutBlank() - Cypress.log({ groupEnd: true, name: '', message: '', emitOnly: true }) - }) + if (isUniqSessionDefinition) { + if (isRegisteredSessionForTest) { + $errUtils.throwErrByPath('sessions.session.duplicateId', { args: { id: existingSession.id } }) + } + + existingSession = sessions.defineSession({ + id, + setup, + validate: options.validate, }) - }) + + sessionsManager.currentTestRegisteredSessions.set(id, true) + } } - const throwValidationError = (err) => { - $errUtils.modifyErrMsg(err, `\n\nThis error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.`, _.add) + const _log = Cypress.log({ + name: 'session', + message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, + groupStart: true, + snapshot: false, + }) - cy.fail(err) - } + const dataLog = Cypress.log({ + name: 'session', + sessionInfo: getSessionDetails(existingSession), + message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, + }) return cy.then(async () => { if (!existingSession.hydrated) { const serverStoredSession = await sessions.getSession(existingSession.id).catch(_.noop) - // we have a saved session on the server AND setup matches - if (serverStoredSession && serverStoredSession.setup === existingSession.setup.toString()) { - _.extend(existingSession, _.omit(serverStoredSession, 'setup')) - existingSession.hydrated = true - } else { - onValidationError = throwValidationError - - return runSetup(existingSession) + // create new session if we do not have a saved session on the server OR setup functions do not match + if (!serverStoredSession || serverStoredSession.setup !== existingSession.setup.toString()) { + return createSessionWorkflow() } } - Cypress.log({ - name: 'Restore Saved Session', - event: true, - state: 'passed', - type: 'system', - message: ``, - groupStart: true, - }) - - await navigateAboutBlank() - - _log.set({ - renderProps: () => { - return { - indicator: 'pending', - message: `(saved) ${_log.get().message}`, - } - }, - }) - - dataLog.set({ - consoleProps: () => getConsoleProps(existingSession), - }) - - await sessions.setSessionData(existingSession) - }) - .then(async () => { - Cypress.log({ groupEnd: true, emitOnly: true }) - if (existingSession.validate) { - await validateSession(existingSession, onValidationError) - } - }) - .then(async () => { - if (!hadValidationError) { - await navigateAboutBlank() - Cypress.log({ groupEnd: true, emitOnly: true }) - } + return restoreSavedSessionWorkflow() }) }, }) diff --git a/packages/driver/src/cy/logGroup.ts b/packages/driver/src/cy/logGroup.ts index 908ea08dad12..2ec78c583cab 100644 --- a/packages/driver/src/cy/logGroup.ts +++ b/packages/driver/src/cy/logGroup.ts @@ -12,6 +12,7 @@ export default (Cypress, userOptions: Cypress.LogGroup.Config, fn: Cypress.LogGr instrument: 'command', groupStart: true, emitOnly: !shouldEmitLog, + event: false, } const log = Cypress.log(options) diff --git a/packages/driver/types/cy/logGroup.d.ts b/packages/driver/types/cy/logGroup.d.ts index 9d496bbd2520..5ec525f4c3bc 100644 --- a/packages/driver/types/cy/logGroup.d.ts +++ b/packages/driver/types/cy/logGroup.d.ts @@ -8,6 +8,8 @@ declare namespace Cypress { // the JQuery element for the command. This will highlight the command // in the main window when debugging $el?: JQuery + // whether or not the generated log was an event or command + event?: boolean // whether or not to emit a log to the UI // when disabled, child logs will not be nested in the UI log?: boolean diff --git a/packages/runner/cypress/integration/sessions.ui.spec.js b/packages/runner/cypress/integration/sessions.ui.spec.js index 3c9d4561ace5..0e3b2316ceed 100644 --- a/packages/runner/cypress/integration/sessions.ui.spec.js +++ b/packages/runner/cypress/integration/sessions.ui.spec.js @@ -2,38 +2,119 @@ const helpers = require('../support/helpers') const { runIsolatedCypress } = helpers.createCypress({ config: { experimentalSessionAndOrigin: true } }) -describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 660 }, () => { - it('empty session with no data', () => { +describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 1000 }, () => { + it.only('create new session', () => { + cy.then(() => {}) + // runIsolatedCypress(() => { + // it('t1', () => { + // const setupFn = cy.stub() + + // // cy.session('blank_session', setupFn) + + // // cy.then(() => { + // // expect(setupFn).to.be.calledOnce + // // }) + // }) + // }) + + // cy.get('.sessions-container') + // .should('contain', 'Sessions (1)') + // .click() + // .should('contain', 'blank_session') + + // cy.get('.command-name-session') + // .first() + // .find('i.successful') + // .siblings() + // .should('contain', '(new) blank_session') + + // cy.get('.command-name-session') + // .last() + // .contains('blank_session') + // .click() + + // cy.percySnapshot() + }) + + it('create new session with validation', () => { runIsolatedCypress(() => { it('t1', () => { - cy.session('blank_session', () => {}) - assert(true) + const setupFn = cy.stub() + const validateFn = cy.stub() + + cy.session('blank_session', setupFn, { + validate: validateFn, + }) + + cy.then(() => { + expect(setupFn).to.be.calledOnce + expect(validateFn).to.be.calledOnce + }) }) }) - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'blank_session') + // cy.get('.sessions-container') + // .should('contain', 'Sessions (1)') + // .click() + // .should('contain', 'blank_session') - cy.get('.command-name-session') - .first() - .find('i.successful') - .siblings() - .should('contain', '(new) blank_session') + // cy.get('.command-name-session') + // .first() + // .find('i.successful') + // .siblings() + // .should('contain', '(new) blank_session') - cy.get('.command-name-session') - .last() - .contains('blank_session') - .click() + // cy.get('.command-name-session') + // .last() + // .contains('blank_session') + // .click() - cy.percySnapshot() + // cy.percySnapshot() + }) + + it('create new session and fails validation', () => { + runIsolatedCypress(() => { + it('t1', () => { + const setupFn = cy.stub() + const validateFn = cy.stub().returns(false) + + cy.on('fail', (err) => { + expect(setupFn).to.be.calledOnce + expect(validateFn).to.be.calledOnce + }) + + cy.session('blank_session', setupFn, { + validate: validateFn, + }) + }) + }) + + // cy.get('.sessions-container') + // .should('contain', 'Sessions (1)') + // .click() + // .should('contain', 'blank_session') + + // cy.get('.command-name-session') + // .first() + // .find('i.successful') + // .siblings() + // .should('contain', '(new) blank_session') + + // cy.get('.command-name-session') + // .last() + // .contains('blank_session') + // .click() + + // cy.percySnapshot() }) it('shows message for new, saved, and recreated session', () => { runIsolatedCypress(() => { const stub = Cypress.sinon.stub().callsFake(() => { - if (stub.callCount === 3) { + console.log(stub.callCount) + if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { + throw new Error('false') + return false } }) @@ -47,7 +128,8 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) it('t1', () => { - expect(window.localStorage.foo).to.eq('val') + expect(true).to.be.true + // expect(window.localStorage.foo).to.eq('val') }) it('t2', () => { @@ -57,69 +139,105 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh it('t3', () => { expect(window.localStorage.foo).to.eq('val') }) + + it('t4', () => { + expect(window.localStorage.foo).to.eq('val') + }) }) cy.get('.test').each(($el) => cy.wrap($el).click()) - cy.log('validating new session was created') - cy.get('.test').eq(0).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') - - cy.get('.command-name-session') - .first() - .find('i.successful') - .siblings() - .should('contain', '(new) user1') - - cy.get('.command-name-session') - .last() - .contains('user1') - .click() - - cy.get('.command-name-assert') - .should('have.class', 'command-state-passed') - }) + // cy.log('validating new session was created') + // cy.get('.test').eq(0).within(() => { + // cy.get('.sessions-container') + // .should('contain', 'Sessions (1)') + // .click() + // .should('contain', 'user1') - cy.log('validating previous session was used') - cy.get('.test').eq(1).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') - - cy.get('.command-name-session') - .first() - .find('i.pending') - .siblings() - .should('contain', '(saved) user1') - - cy.get('.command-name-session') - .last() - .contains('user1') - }) + // cy.get('.command-name-session') + // .first() + // .find('i.successful') + // .siblings() + // .should('contain', '(new) user1') - cy.log('validating session was recreated after it failed to verify') - cy.get('.test').eq(2).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') - - cy.get('.command-name-session') - .first() - .find('i.bad') - .siblings() - .should('contain', '(recreated) user1') - - cy.get('.command-name-session') - .last() - .contains('user1') + // cy.get('.command-name-session') + // .last() + // .contains('user1') + // .click() + + // cy.get('.command-name-assert') + // .should('have.class', 'command-state-passed') + // }) + + // cy.log('validating previous session was used') + // cy.get('.test').eq(1).within(() => { + // cy.get('.sessions-container') + // .should('contain', 'Sessions (1)') + // .click() + // .should('contain', 'user1') + + // cy.get('.command-name-session') + // .first() + // .find('i.pending') + // .siblings() + // .should('contain', '(saved) user1') + + // cy.get('.command-name-session') + // .last() + // .contains('user1') + // }) + + // cy.log('validating session was recreated after it failed to verify') + // cy.get('.test').eq(2).within(() => { + // cy.get('.sessions-container') + // .should('contain', 'Sessions (1)') + // .click() + // .should('contain', 'user1') + + // cy.get('.command-name-session') + // .first() + // .find('i.bad') + // .siblings() + // .should('contain', '(recreated) user1') + + // cy.get('.command-name-session') + // .last() + // .contains('user1') + // }) + + // cy.percySnapshot() + }) + + it.only('shows message for new, saved, and recreated session', () => { + runIsolatedCypress(() => { + const stub = Cypress.sinon.stub().callsFake(() => { + console.log(stub.callCount) + if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { + throw new Error('false') + + return false + } + }) + + beforeEach(() => { + + }) + + it('t1', () => { + cy.session('user1', () => { + window.localStorage.foo = 'val' + }) + + cy.session('user1') + cy.session('user2') + }) + + it('t2', () => { + expect(window.localStorage.foo).to.eq('val') + }) }) - cy.percySnapshot() + cy.get('.test').each(($el) => cy.wrap($el).click()) }) it('multiple sessions in a test', () => { From d43630cf7eb9e28cbc623dd0e5f7e626520e0329 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 2 May 2022 18:13:29 -0500 Subject: [PATCH 11/22] add some command tests --- .../commands/sessions/sessions.spec.js | 186 ++++++++++++++++++ packages/driver/types/cy/logGroup.d.ts | 2 +- 2 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 packages/driver/cypress/integration/commands/sessions/sessions.spec.js diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js new file mode 100644 index 000000000000..58d1c12bce25 --- /dev/null +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -0,0 +1,186 @@ +// const baseUrl = Cypress.config('baseUrl') +// const iframeBaseUrl = Cypress.env('iframeBaseUrl') + +// const expectCurrentSessionData = (obj) => { +// cy.then(async () => { +// return await Cypress.session.getCurrentSessionData() +// .then((result) => { +// expect(result.cookies.map((v) => v.name)).members(obj.cookies || []) +// expect(result.localStorage).deep.members(obj.localStorage || []) +// expect(result.sessionStorage).deep.members(obj.sessionStorage || []) +// }) +// }) +// } + +beforeEach(() => { + if (top.doNotClearSessions) { + top.doNotClearSessions = false + + return + } + + cy.wrap(Cypress.session.clearAllSavedSessions(), { log: false }) +}) + +// const sessionUser = (name = 'user0') => { +// return cy.session(name, () => { +// cy.visit(`/cross_origin_iframe/${name}`) +// cy.window().then((win) => { +// win.localStorage.username = name +// }) +// }) +// } + +describe('cy.session', () => { + describe('args', () => { + it('accepts string as id', () => { + cy.session('some-name', () => {}) + cy.session({ name: 'some-name', zkey: 'val' }, () => {}) + }) + + it('accepts array as id', () => { + cy.session('some-name', () => {}) + }) + + it('accepts object as id', () => { + cy.session('some-name', () => {}) + }) + + it('uses sorted stringify and rejects duplicate registrations', (done) => { + cy.on('fail', (err) => { + expect(err.message).contain('previously used name') + expect(err.message).contain('{"key":"val"') + done() + }) + + cy.session({ name: 'bob', key: 'val' }, () => { + // foo + }) + + cy.session({ key: 'val', name: 'bob' }, () => { + // bar + }) + }) + }) + + describe('.log', () => { + beforeEach(function () { + this.logs = [] + + cy.on('log:added', (attrs, log) => { + this.lastLog = log + this.logs.push(log) + }) + + return null + }) + }) + + describe('errors', () => { + let lastLog = null + let logs = [] + + beforeEach(() => { + cy.on('log:added', (attrs, log) => { + if (attrs.name === 'session') { + lastLog = log + logs.push(log) + } + }) + + return null + }) + + it('throws when sessionId argument was not provided', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid argument. The first argument `id` must be an string or serializable object.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session() + }) + + it('throws when sessionId argument is not an object', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid argument. The first argument `id` must be an string or serializable object.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session(1) + }) + + it('throws when options argument is provided and is not an object', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid argument. The optional third argument `options` must be an object.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session', () => {}, 'invalid_arg') + }) + + it('throws when options argument has an invalid option', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid option: **invalid_key**\nAvailable options are: `validate`') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session', () => {}, { invalid_key: 2 }) + }) + + it('throws when options argument has an option with an invalid type', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` was passed an invalid option value. **validate** must be of type **function** but was **number**.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session', () => {}, { validate: 2 }) + }) + + it('throws when setup function is not provided and existing session is not found', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('No session is defined with the name\n **some-session**\nIn order to use `cy.session()`, provide a `setup` as the second argument:\n\n`cy.session(id, setup)`') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session') + }) + + it('throws when sessionId is duplicated with different setup functions', function (done) { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('You may not call `cy.session()` with a previously used name and different options. If you want to specify different options, please use a unique name other than **some-session**.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('some-session', () => true) + cy.session('some-session', () => false) + }) + }) +}) diff --git a/packages/driver/types/cy/logGroup.d.ts b/packages/driver/types/cy/logGroup.d.ts index 5ec525f4c3bc..e905a43c8fc5 100644 --- a/packages/driver/types/cy/logGroup.d.ts +++ b/packages/driver/types/cy/logGroup.d.ts @@ -1,7 +1,7 @@ // The type declarations for Cypress Log Group & the corresponding configuration permutations declare namespace Cypress { declare namespace LogGroup { - type ApiCallback = (log: Cypress.Log) => Chainable + type ApiCallback = (log: Cypress.Log) => Chainable | void type LogGroup = (cypress: Cypress.Cypress, options: Partial, callback: LogGroupCallback) => Chainable interface Config { From 28d6d46871dca65fe8429a39c573abc6449da8dd Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 3 May 2022 08:33:09 -0500 Subject: [PATCH 12/22] more driver tests and fix session config error --- .../commands/sessions/sessions.spec.js | 120 +++-- .../driver/src/cy/commands/sessions/index.ts | 459 +++++++++--------- .../e2e/cypress/integration/session_spec.js | 101 ---- 3 files changed, 328 insertions(+), 352 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index 58d1c12bce25..a3861069f065 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -1,26 +1,24 @@ -// const baseUrl = Cypress.config('baseUrl') +const baseUrl = Cypress.config('baseUrl') // const iframeBaseUrl = Cypress.env('iframeBaseUrl') -// const expectCurrentSessionData = (obj) => { -// cy.then(async () => { -// return await Cypress.session.getCurrentSessionData() -// .then((result) => { -// expect(result.cookies.map((v) => v.name)).members(obj.cookies || []) -// expect(result.localStorage).deep.members(obj.localStorage || []) -// expect(result.sessionStorage).deep.members(obj.sessionStorage || []) -// }) -// }) -// } - -beforeEach(() => { - if (top.doNotClearSessions) { - top.doNotClearSessions = false +const expectCurrentSessionData = (obj) => { + return Cypress.session.getCurrentSessionData() + .then((result) => { + cy.log(result) + expect(result.cookies.map((v) => v.name)).members(obj.cookies || []) + expect(result.localStorage).deep.members(obj.localStorage || []) + expect(result.sessionStorage).deep.members(obj.sessionStorage || []) + }) +} +// beforeEach(() => { +// if (top.doNotClearSessions) { +// top.doNotClearSessions = false - return - } +// return +// } - cy.wrap(Cypress.session.clearAllSavedSessions(), { log: false }) -}) +// cy.wrap(Cypress.session.clearAllSavedSessions(), { log: false }) +// }) // const sessionUser = (name = 'user0') => { // return cy.session(name, () => { @@ -34,24 +32,30 @@ beforeEach(() => { describe('cy.session', () => { describe('args', () => { it('accepts string as id', () => { - cy.session('some-name', () => {}) - cy.session({ name: 'some-name', zkey: 'val' }, () => {}) + cy.session('session-id', () => {}) + cy.session({ name: 'session-id', zkey: 'val' }, () => {}) }) it('accepts array as id', () => { - cy.session('some-name', () => {}) + cy.session('session-id', () => {}) }) it('accepts object as id', () => { - cy.session('some-name', () => {}) + cy.session('session-id', () => {}) }) - it('uses sorted stringify and rejects duplicate registrations', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('previously used name') - expect(err.message).contain('{"key":"val"') - done() + // redundant? + it('accepts options as third argument', () => { + const setup = cy.stub().as('setupSession') + const validate = cy.stub().as('validateSession') + + cy.session('session-id', setup, { validate }) + cy.then(() => { + expect(setup).to.be.calledOnce + expect(validate).to.be.calledOnce }) + }) + }) cy.session({ name: 'bob', key: 'val' }, () => { // foo @@ -91,6 +95,47 @@ describe('cy.session', () => { return null }) + it('throws error when experimentalSessionAndOrigin not enabled', { experimentalSessionAndOrigin: false, experimentalSessionSupport: false }, (done) => { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('`cy.session()` requires enabling the `experimentalSessionAndOrigin` flag.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('sessions-not-enabled') + }) + + it('throws error when experimentalSessionSupport is enabled through test config', { experimentalSessionAndOrigin: false, experimentalSessionSupport: true }, (done) => { + cy.on('fail', (err) => { + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + + done() + }) + + cy.session('sessions-not-enabled') + }) + + it('throws error when experimentalSessionSupport is enabled through Cypress.config', { experimentalSessionAndOrigin: false }, (done) => { + Cypress.config('experimentalSessionSupport', true) + + cy.on('fail', (err) => { + Cypress.config('experimentalSessionSupport', false) + expect(lastLog.get('error')).to.eq(err) + expect(lastLog.get('state')).to.eq('failed') + expect(err.message).to.eq('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') + expect(err.docsUrl).to.eq('https://on.cypress.io/session') + done() + }) + + cy.session('sessions-not-enabled') + }) + it('throws when sessionId argument was not provided', function (done) { cy.on('fail', (err) => { expect(lastLog.get('error')).to.eq(err) @@ -169,18 +214,29 @@ describe('cy.session', () => { cy.session('some-session') }) - it('throws when sessionId is duplicated with different setup functions', function (done) { + it('throws when multiple session calls with same sessionId but different options', function (done) { cy.on('fail', (err) => { expect(lastLog.get('error')).to.eq(err) expect(lastLog.get('state')).to.eq('failed') - expect(err.message).to.eq('You may not call `cy.session()` with a previously used name and different options. If you want to specify different options, please use a unique name other than **some-session**.') + expect(err.message).to.eq('You may not call `cy.session()` with a previously used name and different options. If you want to specify different options, please use a unique name other than **duplicate-session**.') expect(err.docsUrl).to.eq('https://on.cypress.io/session') + expectCurrentSessionData({ + localStorage: [{ origin: baseUrl, value: { one: 'value' } }], + }) + done() }) - cy.session('some-session', () => true) - cy.session('some-session', () => false) + cy.session('duplicate-session', () => { + // function content + window.localStorage.one = 'value' + }) + + cy.session('duplicate-session', () => { + // different function content + window.localStorage.two = 'value' + }) }) }) }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 668c21c6c730..d57dfdb87fce 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -2,7 +2,6 @@ import _ from 'lodash' import stringifyStable from 'json-stable-stringify' import $errUtils from '../../../cypress/error_utils' import $stackUtils from '../../../cypress/stack_utils' -import group from '../../logGroup' import SessionsManager from './manager' import { getSessionDetails, @@ -67,7 +66,7 @@ export default function (Commands, Cypress, cy) { const sessionCommand = cy.state('current') // stringify deterministically if we were given an object - id = typeof id === 'string' ? id : stringifyStable(id) + id = _.isString(id) ? id : stringifyStable(id) if (options) { if (!_.isObject(options)) { @@ -78,14 +77,14 @@ export default function (Commands, Cypress, cy) { 'validate': 'function', } - Object.keys(options).forEach((key) => { + Object.entries(options).forEach(([key, value]) => { const expectedType = validOpts[key] if (!expectedType) { $errUtils.throwErrByPath('sessions.session.wrongArgOptionUnexpected', { args: { key } }) } - const actualType = typeof options[key] + const actualType = typeof value if (actualType !== expectedType) { $errUtils.throwErrByPath('sessions.session.wrongArgOptionInvalid', { args: { key, expected: expectedType, actual: actualType } }) @@ -93,295 +92,317 @@ export default function (Commands, Cypress, cy) { }) } - function createSession (existingSession, isRecreatingSession = false) { - return group(Cypress, { - name: 'Create New Session', - message: '', - event: true, - type: 'system', - }, (log) => { - return cy.then(async () => { - await navigateAboutBlank() - await sessions.clearCurrentSessionData() + let existingSession: SessionData = sessionsManager.getActiveSession(id) + const isRegisteredSessionForTest = sessionsManager.currentTestRegisteredSessions.has(id) - await existingSession.setup() - await navigateAboutBlank() - const data = await sessions.getCurrentSessionData() + if (!setup) { + if (!existingSession || !isRegisteredSessionForTest) { + $errUtils.throwErrByPath('sessions.session.not_found', { args: { id } }) + } + } else { + const isUniqSessionDefinition = !existingSession || existingSession.setup.toString().trim() !== setup.toString().trim() - _.extend(existingSession, data) - existingSession.hydrated = true + if (isUniqSessionDefinition) { + if (isRegisteredSessionForTest) { + $errUtils.throwErrByPath('sessions.session.duplicateId', { args: { id: existingSession.id } }) + } - log.set({ state: 'passed' }) - _log.set({ - renderProps: () => { - if (isRecreatingSession) { - return { - indicator: 'bad', - message: `(recreated) ${_log.get().message}`, - } - } - - return { - indicator: 'successful', - message: `(new) ${_log.get().message}`, - } - }, - }) + existingSession = sessions.defineSession({ + id, + setup, + validate: options.validate, + }) - sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) + sessionsManager.currentTestRegisteredSessions.set(id, true) + } + } - dataLog.set({ - consoleProps: () => getConsoleProps(existingSession), - }) + const _log = Cypress.log({ + name: 'session', + message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, + groupStart: true, + snapshot: false, + }) - // persist the session to the server. Only matters in openMode OR if there's a top navigation on a future test. - // eslint-disable-next-line no-console - return Cypress.backend('save:session', { ...existingSession, setup: existingSession.setup.toString() }).catch(console.error) - }) - }) - } + const dataLog = Cypress.log({ + name: 'session', + sessionInfo: getSessionDetails(existingSession), + message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, + }) - function restoreSession (existingSession) { - return group(Cypress, { - name: 'Restore Saved Session', - message: '', + function runSetup (existingSession) { + Cypress.log({ + name: 'Create New Session', + state: 'passed', event: true, type: 'system', - }, (log) => { - return cy.then(async () => { - await navigateAboutBlank() - - log.set({ state: 'passed' }) - _log.set({ - renderProps: () => { - return { - indicator: 'pending', - message: `(saved) ${_log.get().message}`, - } - }, - }) + message: ``, + groupStart: true, + }) - sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) + if (!hadValidationError) { + _log.set({ + renderProps: () => { + return { + indicator: 'successful', + message: `(new) ${_log.get().message}`, + } + }, + }) + } - dataLog.set({ - consoleProps: () => getConsoleProps(existingSession), - }) + return cy.then(async () => { + await navigateAboutBlank() + await sessions.clearCurrentSessionData() + + return existingSession.setup() + }) + .then(async () => { + await navigateAboutBlank() + const data = await sessions.getCurrentSessionData() + + Cypress.log({ groupEnd: true, emitOnly: true }) - await sessions.setSessionData(existingSession) + _.extend(existingSession, data) + existingSession.hydrated = true + + sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) + + dataLog.set({ + consoleProps: () => getConsoleProps(existingSession), }) + + // persist the session to the server. Only matters in openMode OR if there's a top navigation on a future test. + // eslint-disable-next-line no-console + return Cypress.backend('save:session', { ...existingSession, setup: existingSession.setup.toString() }).catch(console.error) }) } // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. - function validateSession (existingSession, shouldFailOnValidation = true) { - if (!existingSession.validate) { - return - } - - return group(Cypress, { + function validateSession (existingSession, _onFail) { + const validatingLog = Cypress.log({ name: 'Validate Session', message: '', + snapshot: false, type: 'system', + state: 'passed', event: true, - }, (log) => { - const onSuccess = () => { - log.set({ message: 'valid', state: 'passed' }) + groupStart: true, + }) - Cypress.log({ groupEnd: true, emitOnly: true }) - } + const onSuccess = () => { + validatingLog.set({ + name: 'Validate Session: valid', + message: '', + type: 'system', + event: true, + state: 'warning', + }) - const onFail = (err) => { - // @ts-ignore - log.set({ message: 'invalid', state: 'warning' }) + Cypress.log({ groupEnd: true, emitOnly: true }) + } - if (shouldFailOnValidation) { - throwValidationError(err) - } + const onFail = (err) => { + _onFail(err, validatingLog) + } - Cypress.log({ showError: true }).error(err) + let _commandToResume: any = null - const shouldRecreateSession = true + let _didThrow = false - return cy.wrap(shouldRecreateSession, { log: false }) - } + let returnVal - let _errThrownFromCypressCommand = null - let returnVal + try { + returnVal = existingSession.validate() + } catch (e) { + onFail(e) - try { - returnVal = existingSession.validate() - } catch (e) { - onFail(e) + return + } - return - } + if (typeof returnVal === 'object' && typeof returnVal.catch === 'function' && typeof returnVal.then === 'function') { + return returnVal + .then((val) => { + if (val === false) { + // set current command to cy.session for more accurate codeFrame + cy.state('current', sessionCommand) + $errUtils.throwErrByPath('sessions.validate_callback_false', { args: { reason: 'resolved false' } }) + } - // if returned value is a promise - if (typeof returnVal === 'object' && typeof returnVal.catch === 'function' && typeof returnVal.then === 'function') { - return returnVal - .then((val) => { - if (val === false) { - // set current command to cy.session for more accurate codeFrame - cy.state('current', sessionCommand) + onSuccess() + }) + .catch((err) => { + onFail(err) + }) + } - onFail($errUtils.errByPath('sessions.validate_callback_false', { args: { reason: 'resolved false' } })) - } + cy.state('onCommandFailed', (err, queue, next) => { + const index = _.findIndex(queue.get(), (command: any) => { + return ( + _commandToResume + && command.attributes.chainerId === _commandToResume.chainerId + ) + }) - onSuccess() - }) - .catch((err) => { - onFail(err) - }) + // attach codeframe and cleanse the stack trace since we will not hit the cy.fail callback + // if this is the first time validate fails + if (typeof err === 'string') { + err = new Error(err) } - // Inject onCommandFailed override to handle when a Cypress Command leveraged in the - // validation calling fails. This callback is executed instead of the standard command - // failure to prevent the Command queue from stop on this command failure and allow the - // sessions command to to update the validation session log message as failed - // and provide a meaningful error message related to failed session validation. - cy.state('onCommandFailed', (err, queue, next) => { - const index = _.findIndex(queue.get(), (command: any) => { - return (_parseValidationResult && command.attributes.chainerId === _parseValidationResult.chainerId) - }) - - // attach codeframe and cleanse the stack trace since we will not hit the cy.fail callback - // if this is the first time validate fails - if (typeof err === 'string') { - err = new Error(err) - } - - err.stack = $stackUtils.normalizedStack(err) - - err = $errUtils.enhanceStack({ - err, - userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), - projectRoot: Cypress.config('projectRoot'), - }) + err.stack = $stackUtils.normalizedStack(err) - // forward command queue index to the _parseValidationResult command to correctly handle - // the validation failure - cy.state('index', index) - cy.state('onCommandFailed', null) + err = $errUtils.enhanceStack({ + err, + userInvocationStack: $errUtils.getUserInvocationStack(err, Cypress.state), + projectRoot: Cypress.config('projectRoot'), + }) - _errThrownFromCypressCommand = err + cy.state('index', index) - return next() - }) + cy.state('onCommandFailed', null) - const _parseValidationResult = cy.then(async () => { - cy.state('onCommandFailed', null) + _didThrow = err - if (_errThrownFromCypressCommand) { - console.log('did throw') + return next() + }) - return onFail(_errThrownFromCypressCommand) - } + const _catchCommand = cy.then(async () => { + cy.state('onCommandFailed', null) + if (_didThrow) return onFail((_didThrow)) - if (returnVal === false) { - // set current command to cy.session for more accurate codeframe - cy.state('current', sessionCommand) + if (returnVal === false) { + // set current command to cy.session for more accurate codeframe + cy.state('current', sessionCommand) - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) - } + return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'returned false' })) + } - if (returnVal === undefined || Cypress.isCy(returnVal)) { - const val = cy.state('current').get('prev')?.attributes?.subject + if (returnVal === undefined || Cypress.isCy(returnVal)) { + const val = cy.state('current').get('prev')?.attributes?.subject - if (val === false) { - return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) - } + if (val === false) { + return onFail($errUtils.errByPath('sessions.validate_callback_false', { reason: 'resolved false' })) } + } - onSuccess() - }) - - return _parseValidationResult + onSuccess() }) - } - const throwValidationError = (err) => { - $errUtils.modifyErrMsg(err, `\n\nThis error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.`, _.add) + _commandToResume = _catchCommand - _log.error(err) - cy.fail(err, { log: false }) + return _catchCommand } - const createSessionWorkflow = (shouldRecreateSession = false) => { - return cy.then(() => { - createSession(existingSession, shouldRecreateSession) - }) - .then(() => { - validateSession(existingSession) + let hadValidationError = false + let onValidationError: Function = (err, log) => { + log.set({ + name: 'Validate Session: invalid', + message: '', + type: 'system', + event: true, + state: 'warning', }) - } - const restoreSavedSessionWorkflow = () => { - return cy.then(async () => { - restoreSession(existingSession) + const errorLog = Cypress.log({ + showError: true, + type: 'system', + event: true, + name: '', + message: '', }) - .then(() => { - const shouldFailOnValidation = false - validateSession(existingSession, shouldFailOnValidation) + errorLog.error(err) + errorLog.set({ + state: 'warn', + }) - .then((hadValidationError) => { - if (hadValidationError) { - const shouldRecreateSession = true - createSessionWorkflow(shouldRecreateSession) - } + _log.set({ + renderProps: () => { + return { + indicator: 'bad', + message: `(recreated) ${_log.get().message}`, + } + }, }) - } - let existingSession: SessionData = sessionsManager.getActiveSession(id) - const isRegisteredSessionForTest = sessionsManager.currentTestRegisteredSessions.has(id) + Cypress.log({ groupEnd: true, emitOnly: true }) - if (!setup) { - if (!existingSession || !isRegisteredSessionForTest) { - $errUtils.throwErrByPath('sessions.session.not_found', { args: { id } }) - } - } else { - const isUniqSessionDefinition = !existingSession || existingSession.setup.toString().trim() !== setup.toString().trim() - - if (isUniqSessionDefinition) { - if (isRegisteredSessionForTest) { - $errUtils.throwErrByPath('sessions.session.duplicateId', { args: { id: existingSession.id } }) - } + hadValidationError = true - existingSession = sessions.defineSession({ - id, - setup, - validate: options.validate, + return runSetup(existingSession) + .then(() => { + cy.then(() => { + return validateSession(existingSession, throwValidationError) }) - - sessionsManager.currentTestRegisteredSessions.set(id, true) - } + .then(() => { + cy.then(async () => { + await navigateAboutBlank() + Cypress.log({ groupEnd: true, name: '', message: '', emitOnly: true }) + }) + }) + }) } - const _log = Cypress.log({ - name: 'session', - message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, - groupStart: true, - snapshot: false, - }) + const throwValidationError = (err) => { + $errUtils.modifyErrMsg(err, `\n\nThis error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.`, _.add) - const dataLog = Cypress.log({ - name: 'session', - sessionInfo: getSessionDetails(existingSession), - message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, - }) + cy.fail(err) + } return cy.then(async () => { if (!existingSession.hydrated) { const serverStoredSession = await sessions.getSession(existingSession.id).catch(_.noop) - // create new session if we do not have a saved session on the server OR setup functions do not match - if (!serverStoredSession || serverStoredSession.setup !== existingSession.setup.toString()) { - return createSessionWorkflow() + // we have a saved session on the server AND setup matches + if (serverStoredSession && serverStoredSession.setup === existingSession.setup.toString()) { + _.extend(existingSession, _.omit(serverStoredSession, 'setup')) + existingSession.hydrated = true + } else { + onValidationError = throwValidationError + + return runSetup(existingSession) } } - return restoreSavedSessionWorkflow() + Cypress.log({ + name: 'Restore Saved Session', + event: true, + state: 'passed', + type: 'system', + message: ``, + groupStart: true, + }) + + await navigateAboutBlank() + + _log.set({ + renderProps: () => { + return { + indicator: 'pending', + message: `(saved) ${_log.get().message}`, + } + }, + }) + + dataLog.set({ + consoleProps: () => getConsoleProps(existingSession), + }) + + await sessions.setSessionData(existingSession) + }) + .then(async () => { + Cypress.log({ groupEnd: true, emitOnly: true }) + if (existingSession.validate) { + await validateSession(existingSession, onValidationError) + } + }) + .then(async () => { + if (!hadValidationError) { + await navigateAboutBlank() + Cypress.log({ groupEnd: true, emitOnly: true }) + } }) }, }) diff --git a/system-tests/projects/e2e/cypress/integration/session_spec.js b/system-tests/projects/e2e/cypress/integration/session_spec.js index a5de24bbeb1d..c6d1bc67bdfb 100644 --- a/system-tests/projects/e2e/cypress/integration/session_spec.js +++ b/system-tests/projects/e2e/cypress/integration/session_spec.js @@ -24,10 +24,7 @@ before(() => { }) const sessionUser = (name = 'user0') => { - console.log('session User') - return cy.session(name, () => { - console.log('cyvisit') cy.visit(`https://localhost:4466/cross_origin_iframe/${name}`) cy.window().then((win) => { win.localStorage.username = name @@ -131,29 +128,6 @@ describe('cross origin automations', function () { }) }) -describe('args', () => { - it('accepts string or object as id', () => { - cy.session('some-name', () => {}) - cy.session({ name: 'some-name', zkey: 'val' }, () => {}) - }) - - it('uses sorted stringify and rejects duplicate registrations', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('previously used name') - expect(err.message).contain('{"key":"val"') - done() - }) - - cy.session({ name: 'bob', key: 'val' }, () => { - // foo - }) - - cy.session({ key: 'val', name: 'bob' }, () => { - // bar - }) - }) -}) - describe('with a blank session', () => { beforeEach(() => { cy.session('sess1', @@ -181,7 +155,6 @@ describe('with a blank session', () => { expectCurrentSessionData({ cookies: ['/form'], - }) }) }) @@ -760,77 +733,3 @@ describe('ignores setting insecure context data when on secure context', () => { }) }) }) - -describe('errors', () => { - it('throws error when experimentalSessionAndOrigin not enabled', { experimentalSessionAndOrigin: false }, (done) => { - cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag') - done() - }) - - cy.session('sessions-not-enabled') - }) - - it('throws error when experimentalSessionSupport is enabled through test config', { experimentalSessionAndOrigin: false, experimentalSessionSupport: true }, (done) => { - cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') - done() - }) - - cy.session('sessions-not-enabled') - }) - - it('throws error when experimentalSessionSupport is enabled through Cypress.config', { experimentalSessionAndOrigin: false }, (done) => { - Cypress.config('experimentalSessionSupport', true) - - cy.on('fail', ({ message }) => { - expect(message).contain('\`cy.session()\` requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.') - done() - }) - - cy.session('sessions-not-enabled') - }) - - it('throws if session has not been defined during current test', (done) => { - cy.on('fail', (err) => { - expect(err.message) - .contain('session') - .contain('No session is defined with') - .contain('**bob**') - - expect(err.docsUrl).eq('https://on.cypress.io/session') - expect(err.codeFrame.frame, 'has accurate codeframe').contain('session') - - done() - }) - - cy.session('bob') - }) - - it('throws if multiple session calls with same name but different options', (done) => { - cy.on('fail', (err) => { - expect(err.message) - expect(err.message).contain('previously used name') - .contain('**duplicate-session**') - - expect(err.docsUrl).eq('https://on.cypress.io/session') - expect(err.codeFrame.frame, 'has accurate codeframe').contain('session') - - done() - }) - - cy.session('duplicate-session', () => { - // function content - window.localStorage.one = 'value' - }) - - cy.session('duplicate-session', () => { - // different function content - window.localStorage.two = 'value' - }) - - expectCurrentSessionData({ - localStorage: [{ origin: 'https://localhost:4466', value: { two: 'value' } }], - }) - }) -}) From 51824638d3a9f6903512ca25585398112ad316ce Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 3 May 2022 08:35:07 -0500 Subject: [PATCH 13/22] Fix parsing error argument --- packages/driver/src/cypress/error_messages.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index 4c41358263e4..c5e67d8307a2 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1653,16 +1653,17 @@ export default { validate_callback_false: { message: 'Your `cy.session` **validate** callback {{reason}}', }, - experimentNotEnabled (experimentalSessionSupport) { + experimentNotEnabled ({ experimentalSessionSupport }) { if (experimentalSessionSupport) { return { message: stripIndent` ${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag. The \`experimentalSessionSupport\` flag was enabled but was removed in Cypress version 9.6.0.`, + docsUrl: 'https://on.cypress.io/session', } } return { - message: `${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag`, + message: `${cmd('session')} requires enabling the \`experimentalSessionAndOrigin\` flag.`, docsUrl: 'https://on.cypress.io/session', } }, From 2e8136a8f63bd623bcd92dec6c8aef1ddf5ccea2 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 3 May 2022 13:53:47 -0500 Subject: [PATCH 14/22] test for failed validation error messges --- .../commands/sessions/sessions.spec.js | 113 +++++++++++++++++ .../e2e/cypress/integration/session_spec.js | 117 ------------------ 2 files changed, 113 insertions(+), 117 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index a3861069f065..17c0498e6db2 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -238,5 +238,118 @@ describe('cy.session', () => { window.localStorage.two = 'value' }) }) + + describe('options.validate failures', () => { + const errorHookMessage = 'This error occurred in a session validate hook after initializing the session. Because validation failed immediately after session setup we failed the test.' + + it('throws when options.validate has a failing Cypress command', (done) => { + cy.on('fail', (err) => { + expect(err.message).contain('Expected to find element: `#does_not_exist`') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + + done() + }) + + cy.session(['mock-session', 'command'], () => { + cy.log('setup') + }, { + validate () { + cy.get('#does_not_exist', { timeout: 20 }) + }, + }) + }) + + it('throws when options.validate throws an error', (done) => { + cy.on('fail', (err) => { + expect(err.message).contain('validate error') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + done() + }) + + cy.session(['mock-session', 'throws'], () => { + cy.log('setup') + }, { + validate () { + throw new Error('validate error') + }, + }) + }) + + it('throws when options.validate rejects', (done) => { + cy.on('fail', (err) => { + expect(err.message).contain('validate error') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + + done() + }) + + cy.session(['mock-session', 'rejects'], () => { + cy.log('setup') + }, { + validate () { + return Promise.reject(new Error('validate error')) + }, + }) + }) + + it('throws when options.validate returns false', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contain('Your `cy.session` **validate** callback returned false.') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + + done() + }) + + cy.session(['mock-session', 'return false'], () => { + cy.log('setup') + }, { + validate () { + return false + }, + }) + }) + + it('throws when options.validate resolves false', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contain('Your `cy.session` **validate** callback resolved false.') + expect(err.message).contain(errorHookMessage) + expect(err.codeFrame).exist + done() + }) + + cy.session(['mock-session', 'resolves false'], () => { + cy.log('setup') + }, { + validate () { + return Promise.resolve(false) + }, + }) + }) + + // TODO: emilyrohrbough - 4/3/2022 - figure out what the below comment means + // TODO: cy.validate that will fail, hook into event, soft-reload inside and test everything is halted + // Look at other tests for cancellation + // make error collapsible by default + + it('throws when options.validate returns Chainer', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.contain('Your `cy.session` **validate** callback resolved false.') + expect(err.message).contain(errorHookMessage) + done() + }) + + cy.session(['mock-session', 'Chainer'], () => { + cy.log('setup') + }, { + validate () { + return cy.wrap(false) + }, + }) + }) + }) }) }) diff --git a/system-tests/projects/e2e/cypress/integration/session_spec.js b/system-tests/projects/e2e/cypress/integration/session_spec.js index c6d1bc67bdfb..565e23912876 100644 --- a/system-tests/projects/e2e/cypress/integration/session_spec.js +++ b/system-tests/projects/e2e/cypress/integration/session_spec.js @@ -453,123 +453,6 @@ describe('options.validate reruns steps when failing cy.request', () => { }) }) -describe('options.validate failing test', () => { - it('test fails when options.validate after setup fails command', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('foo') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_1', () => { - cy.log('setup') - }, { - validate () { - cy.wrap('foo', { timeout: 30 }).should('eq', 'bar') - }, - }) - }) - - it('test fails when options.validate after setup throws', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_2', () => { - cy.log('setup') - }, { - validate () { - throw new Error('validate error') - }, - }) - }) - - it('test fails when options.validate after setup rejects', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('validate error') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_3', () => { - cy.log('setup') - }, { - validate () { - return Promise.reject(new Error('validate error')) - }, - }) - }) - - it('test fails when options.validate after setup returns false', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('returned false') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_4', () => { - cy.log('setup') - }, { - validate () { - return false - }, - }) - }) - - it('test fails when options.validate after setup resolves false', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('callback resolved false') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - expect(err.codeFrame).exist - - done() - }) - - cy.session('user_validate_fails_after_setup_5', () => { - cy.log('setup') - }, { - validate () { - return Promise.resolve(false) - }, - }) - }) - - // TODO: cy.validate that will fail, hook into event, soft-reload inside and test everything is halted - // Look at other tests for cancellation - // make error collapsible by default - - it('test fails when options.validate after setup returns Chainer', (done) => { - cy.on('fail', (err) => { - expect(err.message).contain('callback resolved false') - expect(err.message).contain('in a session validate hook') - expect(err.message).not.contain('not from Cypress') - done() - }) - - cy.session('user_validate_fails_after_setup', () => { - cy.log('setup') - }, { - validate () { - return cy.wrap(false) - }, - }) - }) -}) - describe('can wait for login redirect automatically', () => { it('t1', () => { cy.session('redirect-login', () => { From 01ea0ff4e686eb1d6d4dd600b4e6eab1331d6815 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 3 May 2022 14:17:38 -0500 Subject: [PATCH 15/22] wait for diff pr --- packages/driver/src/cy/logGroup.ts | 1 - packages/driver/types/cy/logGroup.d.ts | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/driver/src/cy/logGroup.ts b/packages/driver/src/cy/logGroup.ts index 2ec78c583cab..908ea08dad12 100644 --- a/packages/driver/src/cy/logGroup.ts +++ b/packages/driver/src/cy/logGroup.ts @@ -12,7 +12,6 @@ export default (Cypress, userOptions: Cypress.LogGroup.Config, fn: Cypress.LogGr instrument: 'command', groupStart: true, emitOnly: !shouldEmitLog, - event: false, } const log = Cypress.log(options) diff --git a/packages/driver/types/cy/logGroup.d.ts b/packages/driver/types/cy/logGroup.d.ts index e905a43c8fc5..9d496bbd2520 100644 --- a/packages/driver/types/cy/logGroup.d.ts +++ b/packages/driver/types/cy/logGroup.d.ts @@ -1,15 +1,13 @@ // The type declarations for Cypress Log Group & the corresponding configuration permutations declare namespace Cypress { declare namespace LogGroup { - type ApiCallback = (log: Cypress.Log) => Chainable | void + type ApiCallback = (log: Cypress.Log) => Chainable type LogGroup = (cypress: Cypress.Cypress, options: Partial, callback: LogGroupCallback) => Chainable interface Config { // the JQuery element for the command. This will highlight the command // in the main window when debugging $el?: JQuery - // whether or not the generated log was an event or command - event?: boolean // whether or not to emit a log to the UI // when disabled, child logs will not be nested in the UI log?: boolean From bed8cf3a603ded8ce33c49d7b268c8af9e986e69 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 3 May 2022 15:44:33 -0500 Subject: [PATCH 16/22] update ui tests --- .../cypress/integration/sessions.ui.spec.js | 460 ++++++++++-------- 1 file changed, 259 insertions(+), 201 deletions(-) diff --git a/packages/runner/cypress/integration/sessions.ui.spec.js b/packages/runner/cypress/integration/sessions.ui.spec.js index 0e3b2316ceed..a42c59b6474d 100644 --- a/packages/runner/cypress/integration/sessions.ui.spec.js +++ b/packages/runner/cypress/integration/sessions.ui.spec.js @@ -2,244 +2,306 @@ const helpers = require('../support/helpers') const { runIsolatedCypress } = helpers.createCypress({ config: { experimentalSessionAndOrigin: true } }) -describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 1000 }, () => { - it.only('create new session', () => { - cy.then(() => {}) - // runIsolatedCypress(() => { - // it('t1', () => { - // const setupFn = cy.stub() - - // // cy.session('blank_session', setupFn) - - // // cy.then(() => { - // // expect(setupFn).to.be.calledOnce - // // }) - // }) - // }) - - // cy.get('.sessions-container') - // .should('contain', 'Sessions (1)') - // .click() - // .should('contain', 'blank_session') - - // cy.get('.command-name-session') - // .first() - // .find('i.successful') - // .siblings() - // .should('contain', '(new) blank_session') - - // cy.get('.command-name-session') - // .last() - // .contains('blank_session') - // .click() - - // cy.percySnapshot() +const validateSessionsInstrumentPanel = (sessionIds = []) => { + cy.get('.sessions-container') + .should('contain', `Sessions (${sessionIds.length})`) + .click() + + sessionIds.forEach((id) => { + cy.contains('.sessions-container', id) }) +} - it('create new session with validation', () => { +describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 1000 }, () => { + it('creates new session', () => { runIsolatedCypress(() => { it('t1', () => { - const setupFn = cy.stub() - const validateFn = cy.stub() - - cy.session('blank_session', setupFn, { - validate: validateFn, - }) + const setupFn = cy.stub().as('runSetup') - cy.then(() => { - expect(setupFn).to.be.calledOnce - expect(validateFn).to.be.calledOnce - }) + cy.session('blank_session', setupFn) + cy.log('after') }) }) - // cy.get('.sessions-container') - // .should('contain', 'Sessions (1)') - // .click() - // .should('contain', 'blank_session') + validateSessionsInstrumentPanel(['blank_session']) + + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') + + cy.get('.command-name-session').contains('blank_session') + cy.contains('Create New Session') + .closest('.command') + .within(() => { + cy.contains('runSetup') + }) + }) - // cy.get('.command-name-session') - // .first() - // .find('i.successful') - // .siblings() - // .should('contain', '(new) blank_session') + cy.percySnapshot() - // cy.get('.command-name-session') - // .last() - // .contains('blank_session') - // .click() + cy.get('.command-name-session').first().click('top') - // cy.percySnapshot() + // FIXME: this should be length 2, not 3 + // the 'Clear Page' log should be nested under session group + cy.get('.command').should('have.length', 3) }) - it('create new session and fails validation', () => { + it('creates new session with validation', () => { runIsolatedCypress(() => { it('t1', () => { - const setupFn = cy.stub() - const validateFn = cy.stub().returns(false) - - cy.on('fail', (err) => { - expect(setupFn).to.be.calledOnce - expect(validateFn).to.be.calledOnce - }) + const setupFn = cy.stub().as('runSetup') + const validateFn = cy.stub().as('runValidation') cy.session('blank_session', setupFn, { validate: validateFn, }) + + cy.log('after') }) }) - // cy.get('.sessions-container') - // .should('contain', 'Sessions (1)') - // .click() - // .should('contain', 'blank_session') + validateSessionsInstrumentPanel(['blank_session']) - // cy.get('.command-name-session') - // .first() - // .find('i.successful') - // .siblings() - // .should('contain', '(new) blank_session') + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') - // cy.get('.command-name-session') - // .last() - // .contains('blank_session') - // .click() + cy.get('.command-name-session').contains('blank_session') + cy.contains('Create New Session') + .closest('.command') + .contains('runSetup') + }) - // cy.percySnapshot() - }) + cy.percySnapshot() - it('shows message for new, saved, and recreated session', () => { - runIsolatedCypress(() => { - const stub = Cypress.sinon.stub().callsFake(() => { - console.log(stub.callCount) - if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { - throw new Error('false') + // FIXME: this should be nested within the session command + cy.contains('Validate Session: valid') + .closest('.command') + .should('not.have.class', 'command-is-open') + .click() + .contains('runValidation') - return false - } - }) + cy.get('.command-name-session').first().click('top') - beforeEach(() => { - cy.session('user1', () => { - window.localStorage.foo = 'val' - }, { - validate: stub, - }) - }) + // FIXME: this should be length 2, not 5 + // the validate session group should be nested under session group + cy.get('.command').should('have.length', 5) + }) + it('creates new session and fails validation', () => { + runIsolatedCypress(() => { it('t1', () => { - expect(true).to.be.true - // expect(window.localStorage.foo).to.eq('val') - }) - - it('t2', () => { - expect(window.localStorage.foo).to.eq('val') - }) + const setupFn = cy.stub().as('runSetup') + const validateFn = cy.stub().returns(false).as('runValidation') - it('t3', () => { - expect(window.localStorage.foo).to.eq('val') - }) - - it('t4', () => { - expect(window.localStorage.foo).to.eq('val') + cy.session('blank_session', setupFn, { + validate: validateFn, + }) }) }) - cy.get('.test').each(($el) => cy.wrap($el).click()) - - // cy.log('validating new session was created') - // cy.get('.test').eq(0).within(() => { - // cy.get('.sessions-container') - // .should('contain', 'Sessions (1)') - // .click() - // .should('contain', 'user1') - - // cy.get('.command-name-session') - // .first() - // .find('i.successful') - // .siblings() - // .should('contain', '(new) user1') - - // cy.get('.command-name-session') - // .last() - // .contains('user1') - // .click() - - // cy.get('.command-name-assert') - // .should('have.class', 'command-state-passed') - // }) - - // cy.log('validating previous session was used') - // cy.get('.test').eq(1).within(() => { - // cy.get('.sessions-container') - // .should('contain', 'Sessions (1)') - // .click() - // .should('contain', 'user1') - - // cy.get('.command-name-session') - // .first() - // .find('i.pending') - // .siblings() - // .should('contain', '(saved) user1') - - // cy.get('.command-name-session') - // .last() - // .contains('user1') - // }) - - // cy.log('validating session was recreated after it failed to verify') - // cy.get('.test').eq(2).within(() => { - // cy.get('.sessions-container') - // .should('contain', 'Sessions (1)') - // .click() - // .should('contain', 'user1') - - // cy.get('.command-name-session') - // .first() - // .find('i.bad') - // .siblings() - // .should('contain', '(recreated) user1') - - // cy.get('.command-name-session') - // .last() - // .contains('user1') - // }) - - // cy.percySnapshot() - }) - - it.only('shows message for new, saved, and recreated session', () => { - runIsolatedCypress(() => { - const stub = Cypress.sinon.stub().callsFake(() => { - console.log(stub.callCount) - if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { - throw new Error('false') - - return false - } - }) + validateSessionsInstrumentPanel(['blank_session']) - beforeEach(() => { + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') - }) + cy.get('.command-name-session').contains('blank_session') + cy.contains('Create New Session') + .closest('.command') + .contains('runSetup') + }) - it('t1', () => { - cy.session('user1', () => { - window.localStorage.foo = 'val' - }) + cy.contains('Validate Session') - cy.session('user1') - cy.session('user2') - }) + // FIXME: this should be nested within the session command + // FIXME: this should be Validate Session: invalid + cy.contains('Validate Session') + .closest('.command') + .should('have.class', 'command-is-open') + .contains('runValidation') - it('t2', () => { - expect(window.localStorage.foo).to.eq('val') - }) - }) + cy.contains('CypressError') - cy.get('.test').each(($el) => cy.wrap($el).click()) + cy.percySnapshot() }) + // it('restores saved session', () => { + // runIsolatedCypress(() => { + // const stub = Cypress.sinon.stub().callsFake(() => { + // console.log(stub.callCount) + // if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { + // throw new Error('false') + + // return false + // } + // }) + + // beforeEach(() => { + // cy.session('user1', () => { + // window.localStorage.foo = 'val' + // }, { + // validate: stub, + // }) + // }) + + // it('t1', () => { + // expect(true).to.be.true + // // expect(window.localStorage.foo).to.eq('val') + // }) + + // it('t2', () => { + // expect(window.localStorage.foo).to.eq('val') + // }) + + // it('t3', () => { + // expect(window.localStorage.foo).to.eq('val') + // }) + + // it('t4', () => { + // expect(window.localStorage.foo).to.eq('val') + // }) + // }) + + // cy.get('.test').each(($el) => cy.wrap($el).click()) + // cy.get('.test').eq(0).within(() => { + // validateSessionsInstrumentPanel(['blank_session']) + // }) + + // // cy.log('validating new session was created') + // // cy.get('.test').eq(0).within(() => { + // // cy.get('.sessions-container') + // // .should('contain', 'Sessions (1)') + // // .click() + // // .should('contain', 'user1') + + // // cy.get('.command-name-session') + // // .first() + // // .find('i.successful') + // // .siblings() + // // .should('contain', '(new) user1') + + // // cy.get('.command-name-session') + // // .last() + // // .contains('user1') + // // .click() + + // // cy.get('.command-name-assert') + // // .should('have.class', 'command-state-passed') + // // }) + + // // cy.log('validating previous session was used') + // // cy.get('.test').eq(1).within(() => { + // // cy.get('.sessions-container') + // // .should('contain', 'Sessions (1)') + // // .click() + // // .should('contain', 'user1') + + // // cy.get('.command-name-session') + // // .first() + // // .find('i.pending') + // // .siblings() + // // .should('contain', '(saved) user1') + + // // cy.get('.command-name-session') + // // .last() + // // .contains('user1') + // // }) + + // // cy.log('validating session was recreated after it failed to verify') + // // cy.get('.test').eq(2).within(() => { + // // cy.get('.sessions-container') + // // .should('contain', 'Sessions (1)') + // // .click() + // // .should('contain', 'user1') + + // // cy.get('.command-name-session') + // // .first() + // // .find('i.bad') + // // .siblings() + // // .should('contain', '(recreated) user1') + + // // cy.get('.command-name-session') + // // .last() + // // .contains('user1') + // // }) + + // // cy.percySnapshot() + // }) + + // it('recreated session', () => { + // runIsolatedCypress(() => { + // const stub = Cypress.sinon.stub().callsFake(() => { + // console.log(stub.callCount) + // if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { + // throw new Error('false') + + // return false + // } + // }) + + // beforeEach(() => { + + // }) + + // it('t1', () => { + // cy.session('user1', () => { + // window.localStorage.foo = 'val' + // }) + + // cy.session('user1') + // cy.session('user2') + // }) + + // it('t2', () => { + // expect(window.localStorage.foo).to.eq('val') + // }) + // }) + + // cy.get('.test').each(($el) => cy.wrap($el).click()) + // }) + + // it('recreated session and fails validation', () => { + // runIsolatedCypress(() => { + // const stub = Cypress.sinon.stub().callsFake(() => { + // console.log(stub.callCount) + // if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { + // throw new Error('false') + + // return false + // } + // }) + + // beforeEach(() => { + + // }) + + // it('t1', () => { + // cy.session('user1', () => { + // window.localStorage.foo = 'val' + // }) + + // cy.session('user1') + // cy.session('user2') + // }) + + // it('t2', () => { + // expect(window.localStorage.foo).to.eq('val') + // }) + // }) + + // cy.get('.test').each(($el) => cy.wrap($el).click()) + // }) + it('multiple sessions in a test', () => { runIsolatedCypress(() => { it('t1', () => { @@ -254,11 +316,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) }) - cy.get('.sessions-container').first().click() - .should('contain', 'Sessions (2)') - .should('contain', 'user1') - .should('contain', 'user2') - + validateSessionsInstrumentPanel(['user1', 'user2']) cy.percySnapshot() }) }) From a0a87b9eae28fd760e92d21407bf2cd329ab9cc0 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 3 May 2022 17:49:53 -0500 Subject: [PATCH 17/22] add more ui tests --- packages/reporter/src/attempts/attempts.tsx | 12 +- packages/reporter/src/test/test.tsx | 2 +- .../cypress/integration/sessions.ui.spec.js | 432 +++++++++++------- 3 files changed, 266 insertions(+), 180 deletions(-) diff --git a/packages/reporter/src/attempts/attempts.tsx b/packages/reporter/src/attempts/attempts.tsx index 7a8b6f13798c..fe7f05882305 100644 --- a/packages/reporter/src/attempts/attempts.tsx +++ b/packages/reporter/src/attempts/attempts.tsx @@ -41,7 +41,7 @@ const StudioError = () => ( ) -function renderAttemptContent (model: AttemptModel) { +function renderAttemptContent (model: AttemptModel, studioActive: boolean) { // performance optimization - don't render contents if not open return ( @@ -55,7 +55,7 @@ function renderAttemptContent (model: AttemptModel) {
- + {studioActive && }
) @@ -64,6 +64,7 @@ function renderAttemptContent (model: AttemptModel) { interface AttemptProps { model: AttemptModel scrollIntoView: Function + studioActive: boolean } @observer @@ -73,7 +74,7 @@ class Attempt extends Component { } render () { - const { model } = this.props + const { model, studioActive } = this.props // HACK: causes component update when command log is added model.commands.length @@ -91,14 +92,14 @@ class Attempt extends Component { headerClass='attempt-name' isOpen={model.isOpen} > - {renderAttemptContent(model)} + {renderAttemptContent(model, studioActive)} ) } } -const Attempts = observer(({ test, scrollIntoView }: {test: TestModel, scrollIntoView: Function}) => { +const Attempts = observer(({ test, scrollIntoView, studioActive }: {test: TestModel, scrollIntoView: Function, studioActive: boolean}) => { return (
    @@ -107,6 +108,7 @@ const Attempts = observer(({ test, scrollIntoView }: {test: TestModel, scrollInt ) diff --git a/packages/reporter/src/test/test.tsx b/packages/reporter/src/test/test.tsx index bc3ec513e306..4345ddcfe021 100644 --- a/packages/reporter/src/test/test.tsx +++ b/packages/reporter/src/test/test.tsx @@ -182,7 +182,7 @@ class Test extends Component { return (
    - this._scrollIntoView()} /> + this._scrollIntoView()} /> { appState.studioActive && }
    ) diff --git a/packages/runner/cypress/integration/sessions.ui.spec.js b/packages/runner/cypress/integration/sessions.ui.spec.js index a42c59b6474d..890933f92dc8 100644 --- a/packages/runner/cypress/integration/sessions.ui.spec.js +++ b/packages/runner/cypress/integration/sessions.ui.spec.js @@ -12,6 +12,17 @@ const validateSessionsInstrumentPanel = (sessionIds = []) => { }) } +const validateNewSessionGroup = () => { + cy.contains('Create New Session') + .closest('.command') + .contains('runSetup') + + cy.contains('Create New Session') + .closest('.command') + .find('.command-name-Clear-Page') + .should('have.length', 2) +} + describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 1000 }, () => { it('creates new session', () => { runIsolatedCypress(() => { @@ -73,9 +84,8 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh .should('contain', '(new) blank_session') cy.get('.command-name-session').contains('blank_session') - cy.contains('Create New Session') - .closest('.command') - .contains('runSetup') + + validateNewSessionGroup() }) cy.percySnapshot() @@ -116,12 +126,9 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh .should('contain', '(new) blank_session') cy.get('.command-name-session').contains('blank_session') - cy.contains('Create New Session') - .closest('.command') - .contains('runSetup') - }) - cy.contains('Validate Session') + validateNewSessionGroup() + }) // FIXME: this should be nested within the session command // FIXME: this should be Validate Session: invalid @@ -135,172 +142,249 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.percySnapshot() }) - // it('restores saved session', () => { - // runIsolatedCypress(() => { - // const stub = Cypress.sinon.stub().callsFake(() => { - // console.log(stub.callCount) - // if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { - // throw new Error('false') - - // return false - // } - // }) - - // beforeEach(() => { - // cy.session('user1', () => { - // window.localStorage.foo = 'val' - // }, { - // validate: stub, - // }) - // }) - - // it('t1', () => { - // expect(true).to.be.true - // // expect(window.localStorage.foo).to.eq('val') - // }) - - // it('t2', () => { - // expect(window.localStorage.foo).to.eq('val') - // }) - - // it('t3', () => { - // expect(window.localStorage.foo).to.eq('val') - // }) - - // it('t4', () => { - // expect(window.localStorage.foo).to.eq('val') - // }) - // }) - - // cy.get('.test').each(($el) => cy.wrap($el).click()) - // cy.get('.test').eq(0).within(() => { - // validateSessionsInstrumentPanel(['blank_session']) - // }) - - // // cy.log('validating new session was created') - // // cy.get('.test').eq(0).within(() => { - // // cy.get('.sessions-container') - // // .should('contain', 'Sessions (1)') - // // .click() - // // .should('contain', 'user1') - - // // cy.get('.command-name-session') - // // .first() - // // .find('i.successful') - // // .siblings() - // // .should('contain', '(new) user1') - - // // cy.get('.command-name-session') - // // .last() - // // .contains('user1') - // // .click() - - // // cy.get('.command-name-assert') - // // .should('have.class', 'command-state-passed') - // // }) - - // // cy.log('validating previous session was used') - // // cy.get('.test').eq(1).within(() => { - // // cy.get('.sessions-container') - // // .should('contain', 'Sessions (1)') - // // .click() - // // .should('contain', 'user1') - - // // cy.get('.command-name-session') - // // .first() - // // .find('i.pending') - // // .siblings() - // // .should('contain', '(saved) user1') - - // // cy.get('.command-name-session') - // // .last() - // // .contains('user1') - // // }) - - // // cy.log('validating session was recreated after it failed to verify') - // // cy.get('.test').eq(2).within(() => { - // // cy.get('.sessions-container') - // // .should('contain', 'Sessions (1)') - // // .click() - // // .should('contain', 'user1') - - // // cy.get('.command-name-session') - // // .first() - // // .find('i.bad') - // // .siblings() - // // .should('contain', '(recreated) user1') - - // // cy.get('.command-name-session') - // // .last() - // // .contains('user1') - // // }) - - // // cy.percySnapshot() - // }) - - // it('recreated session', () => { - // runIsolatedCypress(() => { - // const stub = Cypress.sinon.stub().callsFake(() => { - // console.log(stub.callCount) - // if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { - // throw new Error('false') - - // return false - // } - // }) - - // beforeEach(() => { - - // }) - - // it('t1', () => { - // cy.session('user1', () => { - // window.localStorage.foo = 'val' - // }) - - // cy.session('user1') - // cy.session('user2') - // }) - - // it('t2', () => { - // expect(window.localStorage.foo).to.eq('val') - // }) - // }) - - // cy.get('.test').each(($el) => cy.wrap($el).click()) - // }) - - // it('recreated session and fails validation', () => { - // runIsolatedCypress(() => { - // const stub = Cypress.sinon.stub().callsFake(() => { - // console.log(stub.callCount) - // if (stub.callCount === 3 || stub.callCount === 5 || stub.callCount === 6) { - // throw new Error('false') - - // return false - // } - // }) - - // beforeEach(() => { - - // }) - - // it('t1', () => { - // cy.session('user1', () => { - // window.localStorage.foo = 'val' - // }) - - // cy.session('user1') - // cy.session('user2') - // }) - - // it('t2', () => { - // expect(window.localStorage.foo).to.eq('val') - // }) - // }) - - // cy.get('.test').each(($el) => cy.wrap($el).click()) - // }) + it('restores saved session', () => { + runIsolatedCypress(() => { + let setupFn + let validateFn + + before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().as('runValidation') + }) + + it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') + }) + + it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') + }) + }) + + cy.get('.test').each(($el) => cy.wrap($el).click()) + + cy.log('validate new session was created in first test') + cy.get('.test').eq(0).within(() => { + validateSessionsInstrumentPanel(['user1']) + validateNewSessionGroup() + }) + + cy.log('validate saved session was used in second test') + cy.get('.test').eq(1).within(() => { + validateSessionsInstrumentPanel(['user1']) + + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.pending').siblings().should('contain', '(saved) user1') + + cy.get('.command-name-session').contains('user1') + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('Clear Page') + .should('have.length', 1) + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('runSetup') + .should('not.exist') + + cy.contains('Validate Session: valid') + .closest('.command') + // FIXME: this validation group does not align with the + // with Create New Session's validation group behavior + // should be 'not.have.class' to align + .should('have.class', 'command-is-open') + .contains('runValidation') + }) + + cy.get('.command-name-session').first().click('top') + + cy.get('.command').should('have.length', 2) + }) + }) + + it('recreates session', () => { + runIsolatedCypress(() => { + let setupFn + let validateFn + + before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().callsFake(() => { + if (validateFn.callCount === 2) { + return false + } + }).as('runValidation') + }) + + it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') + }) + + it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') + }) + }) + + cy.get('.test').each(($el) => cy.wrap($el).click()) + + cy.log('validate new session was created in first test') + cy.get('.test').eq(0).within(() => { + validateSessionsInstrumentPanel(['user1']) + + cy.contains('Create New Session') + }) + + cy.log('validate saved session was used in second test') + cy.get('.test').eq(1).within(() => { + validateSessionsInstrumentPanel(['user1']) + + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.bad').siblings().should('contain', '(recreated) user1') + + cy.get('.command-name-session').contains('user1') + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('Clear Page') + .should('have.length', 1) + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('runSetup') + .should('not.exist') + + cy.contains('Validate Session: invalid') + + cy.contains('Create New Session') + .closest('.command') + .should('have.class', 'command-is-open') + + cy.contains('Validate Session: valid') + .closest('.command') + // FIXME: this validation group does not align with the + // with Create New Session's validation group behavior + // should be 'not.have.class' to align + .should('have.class', 'command-is-open') + .contains('runValidation') + }) + .percySnapshot() + + cy.get('.runnable-err').should('have.length', 1) + + cy.get('.command-name-session').first().click('top') + + cy.get('.command').should('have.length', 2) + }) + }) + + it('recreated session and fails validation', () => { + runIsolatedCypress(() => { + let setupFn + let validateFn + + before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().callsFake(() => { + if (validateFn.callCount >= 2) { + return false + } + }).as('runValidation') + }) + + it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') + }) + + it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') + }) + }) + + cy.get('.test').each(($el) => cy.wrap($el).click()) + + cy.log('validate new session was created in first test') + cy.get('.test').eq(0).within(() => { + validateSessionsInstrumentPanel(['user1']) + + cy.contains('Create New Session') + }) + + cy.log('validate saved session was used in second test') + cy.get('.test').eq(1).within(() => { + validateSessionsInstrumentPanel(['user1']) + + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.bad').siblings().should('contain', '(recreated) user1') + + cy.get('.command-name-session').contains('user1') + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('Clear Page') + .should('have.length', 1) + + cy.contains('Restore Saved Session') + .closest('.command') + .contains('runSetup') + .should('not.exist') + + cy.contains('Validate Session: invalid') + + // FIXME: this validation group should say 'Validate Session: valid' + cy.contains('Validate Session') + .closest('.command') + // FIXME: this validation group does not align with the + // with Create New Session's validation group behavior + //' should be 'not.have.class' to align + .should('have.class', 'command-is-open') + .contains('runValidation') + }) + .percySnapshot() + + cy.contains('Create New Session') + .closest('.command') + // FIXME: this 'Create New Session' group's collapsed behavior + // does not align with behavior observed in other 'Create New Session' + // groups should be 'not.have.class' to align + .should('not.have.class', 'command-is-open') + .click() + + validateNewSessionGroup() + + cy.get('.runnable-err').should('have.length', 2) + }) + }) it('multiple sessions in a test', () => { runIsolatedCypress(() => { From faf137523dcade333376abc3ef077f7e56821793 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 4 May 2022 11:18:55 -0500 Subject: [PATCH 18/22] align with 10.0 test setup --- .../cypress/integration/sessions.ui.spec.js | 166 ++---------------- .../e2e/sessions/multiple_sessions.cy.js | 10 ++ .../cypress/e2e/sessions/new_session.cy.js | 6 + .../new_session_and_fails_validation.cy.js | 8 + .../new_session_with_validation.cy.js | 10 ++ .../e2e/sessions/recreates_session.cy.js | 27 +++ ...creates_session_and_fails_validation.cy.js | 27 +++ .../e2e/sessions/restores_saved_session.cy.js | 23 +++ 8 files changed, 129 insertions(+), 148 deletions(-) create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js diff --git a/packages/runner/cypress/integration/sessions.ui.spec.js b/packages/runner/cypress/integration/sessions.ui.spec.js index 890933f92dc8..67c516799d61 100644 --- a/packages/runner/cypress/integration/sessions.ui.spec.js +++ b/packages/runner/cypress/integration/sessions.ui.spec.js @@ -1,4 +1,5 @@ const helpers = require('../support/helpers') +const path = require('path') const { runIsolatedCypress } = helpers.createCypress({ config: { experimentalSessionAndOrigin: true } }) @@ -12,9 +13,10 @@ const validateSessionsInstrumentPanel = (sessionIds = []) => { }) } -const validateNewSessionGroup = () => { +const validateCreateNewSessionGroup = () => { cy.contains('Create New Session') .closest('.command') + .should('have.class', 'command-is-open') .contains('runSetup') cy.contains('Create New Session') @@ -25,14 +27,7 @@ const validateNewSessionGroup = () => { describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 1000 }, () => { it('creates new session', () => { - runIsolatedCypress(() => { - it('t1', () => { - const setupFn = cy.stub().as('runSetup') - - cy.session('blank_session', setupFn) - cy.log('after') - }) - }) + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy')) validateSessionsInstrumentPanel(['blank_session']) @@ -44,11 +39,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh .should('contain', '(new) blank_session') cy.get('.command-name-session').contains('blank_session') - cy.contains('Create New Session') - .closest('.command') - .within(() => { - cy.contains('runSetup') - }) + validateCreateNewSessionGroup() }) cy.percySnapshot() @@ -61,18 +52,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) it('creates new session with validation', () => { - runIsolatedCypress(() => { - it('t1', () => { - const setupFn = cy.stub().as('runSetup') - const validateFn = cy.stub().as('runValidation') - - cy.session('blank_session', setupFn, { - validate: validateFn, - }) - - cy.log('after') - }) - }) + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy')) validateSessionsInstrumentPanel(['blank_session']) @@ -85,7 +65,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.get('.command-name-session').contains('blank_session') - validateNewSessionGroup() + validateCreateNewSessionGroup() }) cy.percySnapshot() @@ -105,16 +85,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) it('creates new session and fails validation', () => { - runIsolatedCypress(() => { - it('t1', () => { - const setupFn = cy.stub().as('runSetup') - const validateFn = cy.stub().returns(false).as('runValidation') - - cy.session('blank_session', setupFn, { - validate: validateFn, - }) - }) - }) + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy')) validateSessionsInstrumentPanel(['blank_session']) @@ -127,7 +98,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.get('.command-name-session').contains('blank_session') - validateNewSessionGroup() + validateCreateNewSessionGroup() }) // FIXME: this should be nested within the session command @@ -143,38 +114,14 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) it('restores saved session', () => { - runIsolatedCypress(() => { - let setupFn - let validateFn - - before(() => { - setupFn = cy.stub().as('runSetup') - validateFn = cy.stub().as('runValidation') - }) - - it('t1', () => { - cy.session('user1', setupFn, { - validate: validateFn, - }) - - cy.log('after') - }) - - it('t2', () => { - cy.session('user1', setupFn, { - validate: validateFn, - }) - - cy.log('after') - }) - }) + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy')) cy.get('.test').each(($el) => cy.wrap($el).click()) cy.log('validate new session was created in first test') cy.get('.test').eq(0).within(() => { validateSessionsInstrumentPanel(['user1']) - validateNewSessionGroup() + validateCreateNewSessionGroup() }) cy.log('validate saved session was used in second test') @@ -214,35 +161,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) it('recreates session', () => { - runIsolatedCypress(() => { - let setupFn - let validateFn - - before(() => { - setupFn = cy.stub().as('runSetup') - validateFn = cy.stub().callsFake(() => { - if (validateFn.callCount === 2) { - return false - } - }).as('runValidation') - }) - - it('t1', () => { - cy.session('user1', setupFn, { - validate: validateFn, - }) - - cy.log('after') - }) - - it('t2', () => { - cy.session('user1', setupFn, { - validate: validateFn, - }) - - cy.log('after') - }) - }) + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy')) cy.get('.test').each(($el) => cy.wrap($el).click()) @@ -276,9 +195,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.contains('Validate Session: invalid') - cy.contains('Create New Session') - .closest('.command') - .should('have.class', 'command-is-open') + validateCreateNewSessionGroup() cy.contains('Validate Session: valid') .closest('.command') @@ -298,36 +215,8 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) }) - it('recreated session and fails validation', () => { - runIsolatedCypress(() => { - let setupFn - let validateFn - - before(() => { - setupFn = cy.stub().as('runSetup') - validateFn = cy.stub().callsFake(() => { - if (validateFn.callCount >= 2) { - return false - } - }).as('runValidation') - }) - - it('t1', () => { - cy.session('user1', setupFn, { - validate: validateFn, - }) - - cy.log('after') - }) - - it('t2', () => { - cy.session('user1', setupFn, { - validate: validateFn, - }) - - cy.log('after') - }) - }) + it('recreates session and fails validation', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy')) cy.get('.test').each(($el) => cy.wrap($el).click()) @@ -361,6 +250,8 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.contains('Validate Session: invalid') + validateCreateNewSessionGroup() + // FIXME: this validation group should say 'Validate Session: valid' cy.contains('Validate Session') .closest('.command') @@ -372,33 +263,12 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh }) .percySnapshot() - cy.contains('Create New Session') - .closest('.command') - // FIXME: this 'Create New Session' group's collapsed behavior - // does not align with behavior observed in other 'Create New Session' - // groups should be 'not.have.class' to align - .should('not.have.class', 'command-is-open') - .click() - - validateNewSessionGroup() - cy.get('.runnable-err').should('have.length', 2) }) }) it('multiple sessions in a test', () => { - runIsolatedCypress(() => { - it('t1', () => { - cy.session('user1', () => { - window.localStorage.foo = 'val' - }) - - cy.session('user2', () => { - window.localStorage.foo = 'val' - window.localStorage.bar = 'val' - }) - }) - }) + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy')) validateSessionsInstrumentPanel(['user1', 'user2']) cy.percySnapshot() diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js new file mode 100644 index 000000000000..347eab320df9 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/multiple_sessions.cy.js @@ -0,0 +1,10 @@ +it('t1', () => { + cy.session('user1', () => { + window.localStorage.foo = 'val' + }) + + cy.session('user2', () => { + window.localStorage.foo = 'val' + window.localStorage.bar = 'val' + }) +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js new file mode 100644 index 000000000000..c8acc81bfe82 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy.js @@ -0,0 +1,6 @@ +it('t1', () => { + const setupFn = cy.stub().as('runSetup') + + cy.session('blank_session', setupFn) + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js new file mode 100644 index 000000000000..27db500fa423 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy.js @@ -0,0 +1,8 @@ +it('t1', () => { + const setupFn = cy.stub().as('runSetup') + const validateFn = cy.stub().returns(false).as('runValidation') + + cy.session('blank_session', setupFn, { + validate: validateFn, + }) +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js new file mode 100644 index 000000000000..9e16375511ed --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy.js @@ -0,0 +1,10 @@ +it('t1', () => { + const setupFn = cy.stub().as('runSetup') + const validateFn = cy.stub().as('runValidation') + + cy.session('blank_session', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js new file mode 100644 index 000000000000..3a5389920b16 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session.cy.js @@ -0,0 +1,27 @@ +let setupFn +let validateFn + +before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().callsFake(() => { + if (validateFn.callCount === 2) { + return false + } + }).as('runValidation') +}) + +it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) + +it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js new file mode 100644 index 000000000000..11201aa6b3d1 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/recreates_session_and_fails_validation.cy.js @@ -0,0 +1,27 @@ +let setupFn +let validateFn + +before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().callsFake(() => { + if (validateFn.callCount >= 2) { + return false + } + }).as('runValidation') +}) + +it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) + +it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js new file mode 100644 index 000000000000..4640c687c030 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/restores_saved_session.cy.js @@ -0,0 +1,23 @@ +let setupFn +let validateFn + +before(() => { + setupFn = cy.stub().as('runSetup') + validateFn = cy.stub().as('runValidation') +}) + +it('t1', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) + +it('t2', () => { + cy.session('user1', setupFn, { + validate: validateFn, + }) + + cy.log('after') +}) From 6be18664e71680f7ca1db65ac7059aa7ac946859 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 4 May 2022 11:26:49 -0500 Subject: [PATCH 19/22] clean up --- .../commands/sessions/sessions.spec.js | 36 ++++--------------- 1 file changed, 7 insertions(+), 29 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index 17c0498e6db2..40c788d40276 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -1,5 +1,11 @@ const baseUrl = Cypress.config('baseUrl') -// const iframeBaseUrl = Cypress.env('iframeBaseUrl') + +if (!Cypress.config('experimentalSessionsAndOrigin')) { + // eslint-disable-next-line + it.only('skip tests since the `experimentalSessionsAndOrigin` configuration is disabled', () => { + cy.log('Run `cypress:open-experimentalSessionAndOrigin` or `cypress:run-experimentalSessionAndOrigin` to run these tests.') + }) +} const expectCurrentSessionData = (obj) => { return Cypress.session.getCurrentSessionData() @@ -10,24 +16,6 @@ const expectCurrentSessionData = (obj) => { expect(result.sessionStorage).deep.members(obj.sessionStorage || []) }) } -// beforeEach(() => { -// if (top.doNotClearSessions) { -// top.doNotClearSessions = false - -// return -// } - -// cy.wrap(Cypress.session.clearAllSavedSessions(), { log: false }) -// }) - -// const sessionUser = (name = 'user0') => { -// return cy.session(name, () => { -// cy.visit(`/cross_origin_iframe/${name}`) -// cy.window().then((win) => { -// win.localStorage.username = name -// }) -// }) -// } describe('cy.session', () => { describe('args', () => { @@ -57,16 +45,6 @@ describe('cy.session', () => { }) }) - cy.session({ name: 'bob', key: 'val' }, () => { - // foo - }) - - cy.session({ key: 'val', name: 'bob' }, () => { - // bar - }) - }) - }) - describe('.log', () => { beforeEach(function () { this.logs = [] From 36f63190670c94bc1579fda8cb23fbd27e53deee Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 4 May 2022 11:29:13 -0500 Subject: [PATCH 20/22] will add later --- .../integration/commands/sessions/sessions.spec.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index 40c788d40276..b489dad4452b 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -45,19 +45,6 @@ describe('cy.session', () => { }) }) - describe('.log', () => { - beforeEach(function () { - this.logs = [] - - cy.on('log:added', (attrs, log) => { - this.lastLog = log - this.logs.push(log) - }) - - return null - }) - }) - describe('errors', () => { let lastLog = null let logs = [] From 0ba9a8d91bddb7b9c53968edfe3eb9ac5490bfeb Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 4 May 2022 12:01:27 -0500 Subject: [PATCH 21/22] fix --- .../cypress/integration/commands/sessions/sessions.spec.js | 7 ------- packages/driver/package.json | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index b489dad4452b..596601f08719 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -1,12 +1,5 @@ const baseUrl = Cypress.config('baseUrl') -if (!Cypress.config('experimentalSessionsAndOrigin')) { - // eslint-disable-next-line - it.only('skip tests since the `experimentalSessionsAndOrigin` configuration is disabled', () => { - cy.log('Run `cypress:open-experimentalSessionAndOrigin` or `cypress:run-experimentalSessionAndOrigin` to run these tests.') - }) -} - const expectCurrentSessionData = (obj) => { return Cypress.session.getCurrentSessionData() .then((result) => { diff --git a/packages/driver/package.json b/packages/driver/package.json index 3f6bc03f24eb..fa8541de4439 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -5,7 +5,7 @@ "scripts": { "clean-deps": "rm -rf node_modules", "cypress:open": "node ../../scripts/cypress open", - "cypress:run": "node ../../scripts/cypress run --spec \"cypress/integration/*/*\",\"cypress/integration/*/!(origin)/**/*\"", + "cypress:run": "node ../../scripts/cypress run --spec \"cypress/integration/*/*\",\"cypress/integration/*/!(origin|sessions)/**/*\"", "cypress:open-experimentalSessionAndOrigin": "node ../../scripts/cypress open --config experimentalSessionAndOrigin=true", "cypress:run-experimentalSessionAndOrigin": "node ../../scripts/cypress run --config experimentalSessionAndOrigin=true", "postinstall": "patch-package", From 96184ad90c252a4b5c1bce442e6ddceacb495395 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 4 May 2022 13:55:16 -0500 Subject: [PATCH 22/22] fix tests --- .../commands/sessions/sessions.spec.js | 13 ++++++--- packages/driver/src/cypress.ts | 2 +- packages/driver/src/cypress/error_messages.ts | 2 +- .../cypress/integration/test_errors_spec.ts | 4 +-- .../cypress/integration/retries.ui.spec.js | 2 +- system-tests/__snapshots__/session_spec.ts.js | 29 ++++--------------- system-tests/package.json | 3 +- 7 files changed, 21 insertions(+), 34 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index 596601f08719..f1b93ac82295 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -1,6 +1,11 @@ const baseUrl = Cypress.config('baseUrl') -const expectCurrentSessionData = (obj) => { +before(() => { + // sessions has logic built in to persists sessions on UI refresh + Cypress.session.clearAllSavedSessions() +}) + +const expectCurrentSessionData = async (obj) => { return Cypress.session.getCurrentSessionData() .then((result) => { cy.log(result) @@ -10,7 +15,7 @@ const expectCurrentSessionData = (obj) => { }) } -describe('cy.session', () => { +describe('cy.session', { retries: 0 }, () => { describe('args', () => { it('accepts string as id', () => { cy.session('session-id', () => {}) @@ -173,13 +178,13 @@ describe('cy.session', () => { }) it('throws when multiple session calls with same sessionId but different options', function (done) { - cy.on('fail', (err) => { + cy.on('fail', async (err) => { expect(lastLog.get('error')).to.eq(err) expect(lastLog.get('state')).to.eq('failed') expect(err.message).to.eq('You may not call `cy.session()` with a previously used name and different options. If you want to specify different options, please use a unique name other than **duplicate-session**.') expect(err.docsUrl).to.eq('https://on.cypress.io/session') - expectCurrentSessionData({ + await expectCurrentSessionData({ localStorage: [{ origin: baseUrl, value: { one: 'value' } }], }) diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index cfb8d1319f49..79857ab0bc6a 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -672,7 +672,7 @@ class $Cypress { // clone the error object // and set stack cleaned // to prevent bluebird from - // attaching long stace traces + // attaching long stack traces // which otherwise make this err // unusably long const err = $errUtils.makeErrFromObj(e) as BackendError diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index c5e67d8307a2..3c8d21ef1ae3 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1651,7 +1651,7 @@ export default { sessions: { validate_callback_false: { - message: 'Your `cy.session` **validate** callback {{reason}}', + message: 'Your `cy.session` **validate** callback {{reason}}.', }, experimentNotEnabled ({ experimentalSessionSupport }) { if (experimentalSessionSupport) { diff --git a/packages/reporter/cypress/integration/test_errors_spec.ts b/packages/reporter/cypress/integration/test_errors_spec.ts index 0c1f6337421b..9cede7b50ef3 100644 --- a/packages/reporter/cypress/integration/test_errors_spec.ts +++ b/packages/reporter/cypress/integration/test_errors_spec.ts @@ -295,13 +295,13 @@ describe('test errors', () => { }) it('is not visible by default', () => { - cy.get('.studio-err-wrapper').should('not.be.visible') + cy.get('.studio-err-wrapper').should('not.exist') }) it('is visible when studio is active', () => { runner.emit('reporter:start', { studioActive: true }) - cy.get('.studio-err-wrapper').should('be.visible') + cy.get('.studio-err-wrapper').should('exist').should('be.visible') cy.percySnapshot() }) diff --git a/packages/runner/cypress/integration/retries.ui.spec.js b/packages/runner/cypress/integration/retries.ui.spec.js index b7cb5802afbf..a64c1bd18374 100644 --- a/packages/runner/cypress/integration/retries.ui.spec.js +++ b/packages/runner/cypress/integration/retries.ui.spec.js @@ -148,7 +148,7 @@ describe('runner/cypress retries.ui.spec', { viewportWidth: 600, viewportHeight: cy.get(attemptTag(3)).parentsUntil('.collapsible').last().parent().within(() => { cy.get('.instruments-container').should('contain', 'Spies / Stubs (2)') cy.get('.instruments-container').should('contain', 'Routes (2)') - cy.get('.runnable-err').should('not.be.visible') + cy.get('.runnable-err').should('not.exist') }) }) diff --git a/system-tests/__snapshots__/session_spec.ts.js b/system-tests/__snapshots__/session_spec.ts.js index 4535f63e09b6..4c69d23d5025 100644 --- a/system-tests/__snapshots__/session_spec.ts.js +++ b/system-tests/__snapshots__/session_spec.ts.js @@ -25,10 +25,6 @@ exports['e2e sessions / session tests'] = ` ✓ get localStorage from all origins ✓ only gets localStorage from origins visited in test - args - ✓ accepts string or object as id - ✓ uses sorted stringify and rejects duplicate registrations - with a blank session ✓ t1 ✓ t2 @@ -87,14 +83,6 @@ exports['e2e sessions / session tests'] = ` ✓ t1 ✓ t2 - options.validate failing test - ✓ test fails when options.validate after setup fails command - ✓ test fails when options.validate after setup throws - ✓ test fails when options.validate after setup rejects - ✓ test fails when options.validate after setup returns false - ✓ test fails when options.validate after setup resolves false - ✓ test fails when options.validate after setup returns Chainer - can wait for login redirect automatically ✓ t1 @@ -118,23 +106,16 @@ exports['e2e sessions / session tests'] = ` ✓ switches to secure context - clears only secure context data - 1/2 ✓ clears only secure context data - 2/2 - errors - ✓ throws error when experimentalSessionAndOrigin not enabled - ✓ throws error when experimentalSessionSupport is enabled through test config - ✓ throws error when experimentalSessionSupport is enabled through Cypress.config - ✓ throws if session has not been defined during current test - ✓ throws if multiple session calls with same name but different options - - 56 passing + 43 passing 1 pending (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 57 │ - │ Passing: 56 │ + │ Tests: 44 │ + │ Passing: 43 │ │ Failing: 0 │ │ Pending: 1 │ │ Skipped: 0 │ @@ -152,9 +133,9 @@ exports['e2e sessions / session tests'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ session_spec.js XX:XX 57 56 - 1 - │ + │ ✔ session_spec.js XX:XX 44 43 - 1 - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 57 56 - 1 - + ✔ All specs passed! XX:XX 44 43 - 1 - ` diff --git a/system-tests/package.json b/system-tests/package.json index 41c1298afd62..0595544821f3 100644 --- a/system-tests/package.json +++ b/system-tests/package.json @@ -8,7 +8,8 @@ "type-check": "tsc --project .", "projects:yarn:install": "node ./scripts/projects-yarn-install.js", "test": "node ./scripts/run.js --glob-in-dir='{test,test-binary}'", - "test:ci": "node ./scripts/run.js" + "test:ci": "node ./scripts/run.js", + "update:snapshots": "SNAPSHOT_UPDATE=1 npm run test" }, "devDependencies": { "@babel/core": "7.9.0",